diff --git a/.github/workflows/hypothesis.yaml b/.github/workflows/hypothesis.yaml index e836f30a5b..e374138f49 100644 --- a/.github/workflows/hypothesis.yaml +++ b/.github/workflows/hypothesis.yaml @@ -41,6 +41,7 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + fetch-depth: 0 # grab all tags so hatch-vcs derives real versions for zarr-python and the in-tree zarr-metadata persist-credentials: false - name: Set HYPOTHESIS_PROFILE based on trigger env: diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index fe0d09f300..5e2211500c 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -63,9 +63,119 @@ jobs: ls ls dist + # --------------------------------------------------------------------------- + # Pre-publish gate: confirm zarr-metadata's required floor is on PyPI. + # + # zarr-python and zarr-metadata co-develop in this monorepo. During local + # development zarr-metadata is resolved from packages/zarr-metadata/ via the + # uv workspace (see [tool.uv.sources] in pyproject.toml). The wheel we are + # about to publish, however, only carries a version-range requirement + # (e.g. `zarr-metadata>=0.1.1,<0.2`); end users will resolve that against + # PyPI. + # + # The failure mode this job catches: a zarr-python PR added code that + # depends on a zarr-metadata feature that has been merged into + # packages/zarr-metadata/ but not yet released to PyPI. CI passed because + # the workspace override resolved to the in-tree copy, but a user installing + # the resulting zarr-python wheel would get a published zarr-metadata that + # lacks the feature, and zarr-python would fail at import or first use. + # + # The mitigation here is a presence check on PyPI: extract the floor of + # zarr-python's zarr-metadata requirement from the wheel's METADATA file, + # and refuse to upload if that exact version is not yet on PyPI. This is + # analogous to what `cargo publish` does automatically against crates.io, + # but expressed as a CI step because twine has no built-in equivalent. + # + # When you bump zarr-metadata to a new version that zarr-python depends on, + # the required release order is: + # 1. release zarr-metadata to PyPI; + # 2. bump the floor in zarr-python's [project.dependencies]; + # 3. release zarr-python. + # This job will fail at step 3 if step 1 was skipped. + # --------------------------------------------------------------------------- + verify_pypi_dependency: + name: Verify zarr-metadata floor is on PyPI + needs: [build_artifacts] + runs-on: ubuntu-latest + # Run only on actual releases. Pull-request and push-to-main runs go + # through CI without this gate, since their wheels are never uploaded. + if: github.event_name == 'release' + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: releases + path: dist + + - name: Check zarr-metadata floor is published on PyPI + run: | + # The wheel's METADATA file lives at zarr-*.dist-info/METADATA inside + # the wheel. `unzip -p` writes a file's contents to stdout without + # extracting; the glob matches whichever dist-info dir is inside. + metadata="$(unzip -p dist/zarr-*.whl '*.dist-info/METADATA')" + + # Pick the Requires-Dist line for zarr-metadata. The wheel may have + # several Requires-Dist lines for different extras; we want the one + # that applies unconditionally (no `; extra == "..."` marker). + # Match `Requires-Dist: zarr-metadata` followed by anything that + # ends a project name in PEP 508: a version operator (<, >, =, !, + # ~), whitespace, `[` (extras), `;` (markers), `(` (legacy + # parenthesized version), or end-of-line. The character class + # excludes letters/digits/underscore/hyphen, so a hypothetical + # `zarr-metadata-ext` dep would not match. + req_line="$(printf '%s' "$metadata" \ + | grep -E '^Requires-Dist: zarr-metadata([^A-Za-z0-9_-]|$)' \ + | grep -v 'extra ==' \ + || true)" + + if [ -z "$req_line" ]; then + echo "::error::Could not find an unconditional Requires-Dist line for zarr-metadata in the built wheel." + echo "Wheel METADATA Requires-Dist lines:" + printf '%s' "$metadata" | grep '^Requires-Dist:' || true + exit 1 + fi + echo "Requires-Dist line: $req_line" + + # Extract the floor: the version after `>=`. Version specifiers in + # PEP 440 are comma-separated (e.g. `>=0.1.1, <0.2`); the floor is + # the bound after the first `>=`. `grep -oE '>=[^,]+'` captures + # `>=0.1.1` (everything up to the comma), then we strip the + # operator and surrounding whitespace. + floor="$(printf '%s' "$req_line" \ + | grep -oE '>=[[:space:]]*[^,]+' \ + | sed 's/^>=[[:space:]]*//; s/[[:space:]]*$//' \ + | head -1)" + + if [ -z "$floor" ]; then + echo '::error::Could not extract a >= floor from:' "$req_line" + echo "zarr-python's zarr-metadata requirement must include a >= bound so this gate has something to check." + exit 1 + fi + echo "zarr-metadata floor: $floor" + + # PyPI's JSON API returns 200 if the named version exists and 404 + # if it doesn't. -s silences progress output; -o /dev/null discards + # the body; -w %%{http_code} prints just the status. Any non-200 + # response means the floor has not been published yet. + status="$(curl -s -o /dev/null -w '%{http_code}' \ + "https://pypi.org/pypi/zarr-metadata/${floor}/json")" + + if [ "$status" != "200" ]; then + echo "::error::zarr-metadata ${floor} is not available on PyPI (HTTP ${status})." + echo "" + echo "The wheel about to be uploaded declares it requires zarr-metadata ${floor} or later," + echo "but no such release exists on PyPI. Publish zarr-metadata ${floor} first, then" + echo "re-run this release workflow." + exit 1 + fi + echo "OK: zarr-metadata ${floor} is on PyPI; safe to upload zarr-python." + upload_pypi: name: Upload to PyPI - needs: [build_artifacts, test_dist_pypi] + # Depend on the new gate so the upload step does not run if the floor + # is missing from PyPI. The gate runs only on releases (see its `if:` + # condition); on PR / push runs it is skipped, and skipped jobs in a + # `needs:` list are treated as satisfied by GitHub Actions. + needs: [build_artifacts, test_dist_pypi, verify_pypi_dependency] runs-on: ubuntu-latest if: github.event_name == 'release' environment: diff --git a/.github/workflows/zarr-metadata.yml b/.github/workflows/zarr-metadata.yml index b5f56dd508..c3fda272d8 100644 --- a/.github/workflows/zarr-metadata.yml +++ b/.github/workflows/zarr-metadata.yml @@ -1,7 +1,10 @@ name: zarr-metadata -# Job steps delegate to packages/zarr-metadata/justfile, the single source of -# truth for this package's verbs; CI owns only the python matrix and caching. +# The justfile's `uv run` recipes resolve against the repo-root uv workspace, +# whose `requires-python = ">=3.12"` blocks the 3.11 floor tested here, so +# most jobs install into standalone venvs instead of delegating to +# packages/zarr-metadata/justfile (only workspace-independent recipes like +# `just lint` are shared). Keep pins in sync with the justfile. on: push: @@ -23,6 +26,13 @@ concurrency: cancel-in-progress: true jobs: + # zarr-metadata CI installs zarr-metadata standalone, not as a uv + # workspace member. The workspace at the repo root forces uv to honor + # `requires-python = ">=3.12"` from zarr-python's pyproject.toml, which + # blocks Python 3.11 even though zarr-metadata itself supports 3.11+. + # Using `uv venv` + `uv pip install` from a tmp directory bypasses + # workspace resolution and tests zarr-metadata the way downstream users + # actually install it: as a standalone package from PyPI. test: name: pytest py=${{ matrix.python-version }} runs-on: ubuntu-latest @@ -42,14 +52,18 @@ jobs: uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 with: enable-cache: true - - name: Install just - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - - name: Sync test dependency group - run: uv sync --group test --python ${{ matrix.python-version }} + - name: Create standalone Python ${{ matrix.python-version }} venv + # Place the venv outside the workspace tree so uv doesn't try + # to resolve workspace-wide requirements. + run: uv venv "$RUNNER_TEMP/zm-venv" --python ${{ matrix.python-version }} --seed + - name: Install zarr-metadata and test deps + run: | + uv pip install \ + --python "$RUNNER_TEMP/zm-venv/bin/python" \ + --group pyproject.toml:test \ + . - name: Run pytest - run: just test + run: '"$RUNNER_TEMP/zm-venv/bin/python" -m pytest tests' ruff: name: ruff @@ -84,11 +98,19 @@ jobs: uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 with: enable-cache: true - - name: Install just - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 + - name: Create standalone Python 3.11 venv + run: uv venv "$RUNNER_TEMP/zm-venv" --python 3.11 --seed + - name: Install zarr-metadata and test deps + run: | + uv pip install \ + --python "$RUNNER_TEMP/zm-venv/bin/python" \ + --group pyproject.toml:test \ + . - name: Run pyright - # The pyright version and interpreter pins live in the justfile. - run: just typecheck + # Pinned to the last pyright that types PEP 661 sentinels in class + # attributes correctly; 1.1.405+ regressed (microsoft/pyright#11115). + # Keep in sync with the pin in packages/zarr-metadata/justfile. + run: uvx --python "$RUNNER_TEMP/zm-venv/bin/python" pyright==1.1.404 src docs: name: docs @@ -105,10 +127,18 @@ jobs: uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 with: enable-cache: true - - name: Install just - uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4 + - name: Create standalone Python 3.12 venv + run: uv venv "$RUNNER_TEMP/zm-venv" --python 3.12 --seed + - name: Install zarr-metadata and docs deps + run: | + uv pip install \ + --python "$RUNNER_TEMP/zm-venv/bin/python" \ + --group pyproject.toml:docs \ + . - name: Build docs - run: just docs-check + run: | + DISABLE_MKDOCS_2_WARNING=true "$RUNNER_TEMP/zm-venv/bin/python" \ + -m mkdocs build --strict zarr-metadata-complete: name: zarr-metadata complete diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57a1d0d4f7..75bb45c59b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,6 +67,17 @@ repos: entry: "\\.(lstrip|rstrip)\\([\"'][^\"']{2,}[\"']\\)" types: [python] files: ^(src|tests)/ + - id: check-min-deps-floor + name: check min_deps zarr-metadata pin matches the project floor + # language: python (not system) so pre-commit provisions an + # interpreter; the script is stdlib-only, so no extra deps are + # needed. Avoids assuming a bare `python` is on PATH. + language: python + entry: python ci/check_min_deps_floor.py + # Run whenever pyproject.toml changes; pass_filenames is False because + # the script reads the file directly rather than processing argv. + pass_filenames: false + files: ^pyproject\.toml$ - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.26.1 hooks: diff --git a/changes/3961.feature.md b/changes/3961.feature.md new file mode 100644 index 0000000000..00e42cfc7b --- /dev/null +++ b/changes/3961.feature.md @@ -0,0 +1,3 @@ +``zarr-python`` now depends on the [``zarr-metadata``](https://pypi.org/project/zarr-metadata/) package, which provides spec-defined TypedDicts and literal types for Zarr v2 and v3 metadata documents. Several internal types previously defined in ``zarr-python`` are now aliases that re-export their canonical definitions from ``zarr-metadata``: ``zarr.codecs.blosc.BloscShuffleLiteral``, ``zarr.codecs.blosc.BloscCnameLiteral``, ``zarr.codecs.blosc.BloscConfigV3``, ``zarr.codecs.blosc.BloscJSON_V3``, ``zarr.codecs.cast_value.RoundingMode``, ``zarr.codecs.cast_value.OutOfRangeMode``, ``zarr.core.metadata.v2.ArrayV2MetadataDict``, ``zarr.core.metadata.v3.AllowedExtraField``, and ``zarr.core.metadata.v3.ArrayMetadataJSON_V3``. + +The version requirement (``zarr-metadata>=0.3.0,<0.4``) caps the major version so a future breaking change in ``zarr-metadata`` cannot silently break installed ``zarr-python``. During local development, ``zarr-metadata`` is resolved from the in-tree copy under ``packages/zarr-metadata/`` via a uv workspace; see [the contributing guide](https://zarr.readthedocs.io/en/stable/contributing.html) for details. diff --git a/changes/3961.misc.md b/changes/3961.misc.md new file mode 100644 index 0000000000..0180c88d39 --- /dev/null +++ b/changes/3961.misc.md @@ -0,0 +1,13 @@ +`Struct.to_json` now emits the `configuration.fields` array as a tuple rather +than a list. The serialized JSON is unchanged (a JSON array is produced either +way), but the in-memory dict returned by `to_json(zarr_format=3)` now holds a +tuple, matching the `tuple[StructField, ...]` shape that `zarr-metadata` models +for this field. + +Internal: zarr-python now sources its codec, dtype, and chunk-grid name +constants and the `Endianness`, `BloscShuffle`, `BloscCname`, sharding +`IndexLocation`, and `DateTimeUnit` literal types from `zarr-metadata`'s +top-level exports rather than re-defining them. The historical zarr-python +names (e.g. `zarr.codecs.bytes.EndianLiteral`, +`zarr.codecs.sharding.IndexLocation`) are retained as re-exports, so existing +imports keep working. No user-facing behavior changes. diff --git a/ci/check_min_deps_floor.py b/ci/check_min_deps_floor.py new file mode 100644 index 0000000000..461e1d0e47 --- /dev/null +++ b/ci/check_min_deps_floor.py @@ -0,0 +1,111 @@ +""" +Enforce the invariant: `min_deps` pins zarr-metadata to the floor of +zarr-python's declared zarr-metadata range. + +zarr-python declares `zarr-metadata>=X.Y.Z,<...>` in `[project.dependencies]`. +The `min_deps` hatch env tests against the *minimum* supported deps, so it +must pin zarr-metadata to exactly that floor (e.g. `zarr-metadata==X.Y.Z`). +Without this script the two declarations can drift silently — the project's +floor could rise without `min_deps` noticing, and `min_deps` would no longer +verify what its name claims. + +Run: + python ci/check_min_deps_floor.py + +Exits 0 if floors agree; non-zero with a clear message if not. +""" + +from __future__ import annotations + +import re +import sys +import tomllib +from pathlib import Path + +ROOT = Path(__file__).parent.parent.resolve() +PYPROJECT = ROOT / "pyproject.toml" + +# Match `>=X.Y.Z` (with or without surrounding whitespace) inside a PEP 440 +# version specifier set. Captures just the version number. +_FLOOR_RE = re.compile(r">=\s*([^,\s]+)") +# Match `==X.Y.Z` likewise. Captures the version number. +_PIN_RE = re.compile(r"==\s*([^,\s]+)") + + +def find_zarr_metadata_floor(deps: list[str]) -> str: + """Return the >= floor of zarr-metadata declared in `deps`. + + `deps` is a list of PEP 508 strings, e.g. as found in + `[project.dependencies]`. Raises if zarr-metadata is not present, or + if its specifier set has no `>=` bound. + """ + for dep in deps: + # Project name is everything up to the first non-name character. + # Quick split: package name terminates at the first occurrence of a + # version operator, whitespace, `[`, `;`, or `(`. + name = re.split(r"[<>=!~\s\[;(]", dep, maxsplit=1)[0].strip() + if name == "zarr-metadata": + match = _FLOOR_RE.search(dep) + if not match: + raise SystemExit( + f"zarr-metadata dependency has no `>=` floor: {dep!r}\n" + "Floor verification requires an explicit lower bound." + ) + return match.group(1) + raise SystemExit( + "zarr-metadata not found in [project.dependencies]. " + "This script assumes zarr-python depends on zarr-metadata." + ) + + +def find_zarr_metadata_pin(deps: list[str]) -> str: + """Return the `==` pin of zarr-metadata declared in `deps`. + + `deps` is a list of PEP 508 strings, e.g. as found in + `[tool.hatch.envs.min_deps.extra-dependencies]`. Raises if + zarr-metadata is not present, or if its specifier is not a `==` pin. + """ + for dep in deps: + name = re.split(r"[<>=!~\s\[;(]", dep, maxsplit=1)[0].strip() + if name == "zarr-metadata": + match = _PIN_RE.search(dep) + if not match: + raise SystemExit( + f"min_deps zarr-metadata entry is not an `==` pin: {dep!r}\n" + "The min_deps env must pin zarr-metadata exactly to the floor." + ) + return match.group(1) + raise SystemExit( + "zarr-metadata not found in [tool.hatch.envs.min_deps.extra-dependencies].\n" + "Add `'zarr-metadata=='` to keep min_deps testing the declared floor." + ) + + +def main() -> int: + data = tomllib.loads(PYPROJECT.read_text()) + + project_deps = data["project"]["dependencies"] + floor = find_zarr_metadata_floor(project_deps) + + min_deps_extra = data["tool"]["hatch"]["envs"]["min_deps"]["extra-dependencies"] + pin = find_zarr_metadata_pin(min_deps_extra) + + if floor != pin: + print( + f"floor / min_deps pin mismatch for zarr-metadata:\n" + f" [project.dependencies] floor: >={floor}\n" + f" [tool.hatch.envs.min_deps] pin: =={pin}\n" + f"\n" + f"These must agree. Either update the floor in " + f"[project.dependencies] or the pin in min_deps so both name " + f"the same zarr-metadata version.", + file=sys.stderr, + ) + return 1 + + print(f"OK: zarr-metadata floor {floor} matches min_deps pin {pin}.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/contributing.md b/docs/contributing.md index dea7256c36..7d74eed511 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -93,6 +93,44 @@ To verify that your development environment is working, you can run the unit tes hatch env run --env test.py3.12-optional run ``` +#### The zarr-metadata package and the workspace + +zarr-python depends on [`zarr-metadata`](https://pypi.org/project/zarr-metadata/), a small package of TypedDicts and literals describing the JSON shape of Zarr v2 and v3 metadata documents. Both packages live in this repository: + +- zarr-python: the project root. +- zarr-metadata: [`packages/zarr-metadata/`](https://github.com/zarr-developers/zarr-python/tree/main/packages/zarr-metadata) — its own `pyproject.toml`, source tree, and tests. + +This is configured as a workspace in two places, because the project supports both [`uv`](https://docs.astral.sh/uv/) and [`hatch`](https://hatch.pypa.io/) as front-ends. + +**uv workspace declaration** (consumed by `uv sync`, `uv run`, and anything reading uv's project metadata): + +```toml +[tool.uv.workspace] +members = ["packages/zarr-metadata"] + +[tool.uv.sources] +zarr-metadata = { workspace = true } +``` + +**Hatch workspace declaration** (consumed by `hatch env run`, including the CI test matrix in `test.yml`): + +```toml +[tool.hatch.envs.test] +workspace.members = ["packages/zarr-metadata"] +``` + +Both mechanisms point at the same in-tree path. They have to be declared separately because uv and hatch don't share configuration. The `dev` env, the `test` matrix, the inherited `gputest` and `upstream` envs all use the in-tree source. The `min_deps` env explicitly opts out (`workspace.members = []`) so it tests against the minimum supported zarr-metadata from PyPI — the floor of the version range in `[project.dependencies]`. + +What this means in practice: + +- **During local development** (whether you invoke `uv run pytest` or `hatch env run --env test.py3.12-optional run`), zarr-python resolves `zarr-metadata` from the in-tree source under `packages/zarr-metadata/`. Changes you make there are immediately visible to zarr-python without reinstalling. +- **In the published wheel**, only the `[project.dependencies]` version requirement (`zarr-metadata>=0.3.0,<0.4`) is carried. The workspace declarations are development-only configuration. Users installing zarr-python from PyPI get the published zarr-metadata wheel. +- **In CI**, the primary test matrix (`test.yml`) runs `hatch env run` against the in-tree zarr-metadata. A change in `packages/zarr-metadata/` that breaks zarr-python surfaces immediately, before zarr-metadata is released to PyPI. The `min_deps` job additionally exercises the published floor on every PR, so a change in zarr-python that *requires* an unreleased zarr-metadata feature also gets caught. + +If you change zarr-metadata, also run zarr-python's test suite. The workspace setup makes this transparent — your usual `uv run pytest` or `hatch env run` picks up the in-tree source automatically. + +When releasing a new zarr-metadata version that contains a breaking change, also bump zarr-python's version cap on zarr-metadata (currently `<0.3`) in the same release cycle. See [Releasing zarr-python when zarr-metadata has changed](#releasing-zarr-python-when-zarr-metadata-has-changed) below for the full procedure. + ### Creating a branch Before you do any new work or submit a pull request, please open an issue on GitHub to report the bug or propose the feature you'd like to add. @@ -412,6 +450,32 @@ We aim to either **promote** or **remove** experimental features within **6 mont Features in `zarr.experimental` carry no stability guarantees. They may be changed or removed in any release, including patch releases. If you depend on an experimental feature, pin your `zarr-python` version accordingly. +## Release procedure + +Open an issue on GitHub announcing the release using the release checklist template: +[https://github.com/zarr-developers/zarr-python/issues/new?template=release-checklist.md](https://github.com/zarr-developers/zarr-python/issues/new?template=release-checklist.md). The release checklist includes all steps necessary for the release. + +### Preparing a release + +Releases are prepared using the ["Prepare release notes"](https://github.com/zarr-developers/zarr-python/actions/workflows/prepare_release.yml) workflow. To run it: + +1. Go to the [workflow page](https://github.com/zarr-developers/zarr-python/actions/workflows/prepare_release.yml) and click "Run workflow". +2. Enter the release version (e.g. `3.2.0`) and the target branch (defaults to `main`). +3. The workflow will run `towncrier build` to render the changelog, remove consumed fragments from `changes/`, and open a pull request on the `release/v` branch. +4. The release PR is automatically labeled `run-downstream`, which triggers the [downstream test workflow](https://github.com/zarr-developers/zarr-python/actions/workflows/downstream.yml) to run Xarray and numcodecs integration tests against the release branch. +5. Review the rendered changelog in `docs/release-notes.md` and verify downstream tests pass before merging. + +### Releasing zarr-python when zarr-metadata has changed + +zarr-python depends on the [`zarr-metadata`](https://pypi.org/project/zarr-metadata/) package, which is developed in the same monorepo (see [The zarr-metadata package and the workspace](#the-zarr-metadata-package-and-the-workspace) above). When a zarr-python release depends on a zarr-metadata change that has not yet been published to PyPI, the release must follow this order: + +1. **Bump zarr-metadata's version** in `packages/zarr-metadata/pyproject.toml` and `packages/zarr-metadata/src/zarr_metadata/__init__.py` (the version literal). Use semver: bump the minor for breaking type changes, the patch for additive changes. +2. **Release zarr-metadata to PyPI.** Tag and publish from `packages/zarr-metadata/`. +3. **Bump zarr-python's floor** on zarr-metadata in `[project.dependencies]` (e.g. `zarr-metadata>=0.2.0,<0.3` → `zarr-metadata>=0.3.0,<0.4`). Update `[tool.uv.workspace]` and `[tool.uv.sources]` only if necessary. +4. **Release zarr-python.** + +If steps 1 and 2 are skipped (or step 3's bumped floor names a version that does not yet exist on PyPI), the `verify_pypi_dependency` job in [`releases.yml`](https://github.com/zarr-developers/zarr-python/blob/main/.github/workflows/releases.yml) will fail before the upload step runs. This gate exists because the wheel ships only a version-range requirement; pip resolves that against PyPI on the user's machine, and there is no built-in equivalent of `cargo publish`'s automatic check that the declared dependency is actually available in the registry. + ## Benchmarks Zarr uses [pytest-benchmark](https://pytest-benchmark.readthedocs.io/en/latest/) for running diff --git a/pyproject.toml b/pyproject.toml index 684ac80b77..f95a0b2690 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ 'google-crc32c>=1.5', 'typing_extensions>=4.14', 'donfig>=0.8', + 'zarr-metadata>=0.4.0,<0.5', ] dynamic = [ @@ -156,6 +157,31 @@ omit = [ "bench/compress_normal.py", ] +# When developing zarr-python locally, resolve zarr-metadata from the in-tree +# package under packages/zarr-metadata/. The `[project.dependencies]` version +# requirement is what propagates to consumers installing from PyPI. +[tool.uv.workspace] +members = ["packages/zarr-metadata"] + +[tool.uv.sources] +zarr-metadata = { workspace = true } + +# zarr-metadata's standalone docs site pins its own mkdocs stack, which need +# not resolve jointly with zarr-python's docs pins. The group is only ever +# installed standalone (see packages/zarr-metadata/justfile and the +# zarr-metadata CI workflow), so exclude the pairings from the workspace +# lock. One mutual-exclusion set (rather than two pairs) because zarr:dev +# already includes zarr:docs, so the two are never co-requested, and +# overlapping pair declarations trip uv's conflict inference. +[tool.uv] +conflicts = [ + [ + { package = "zarr", group = "dev" }, + { package = "zarr", group = "docs" }, + { package = "zarr-metadata", group = "docs" }, + ], +] + [tool.hatch] version.source = "vcs" # Only consider zarr-python's own `v*` tags when deriving the version. Without @@ -170,9 +196,18 @@ hooks.vcs.version-file = "src/zarr/_version.py" [tool.hatch.envs.dev] dependency-groups = ["dev"] +# Resolve zarr-metadata from the in-tree workspace member, not PyPI. See +# `[tool.uv.sources]` above for the equivalent for `uv run` invocations. +workspace.members = ["packages/zarr-metadata"] [tool.hatch.envs.test] dependency-groups = ["test"] +# Resolve zarr-metadata from the in-tree workspace member, not PyPI, so CI +# in `test.yml` exercises the integration between the two packages on every +# PR. Envs that inherit via `template = "test"` (gputest, upstream) pick +# this up automatically; min_deps overrides it (see below) to test against +# the published floor. +workspace.members = ["packages/zarr-metadata"] [tool.hatch.envs.test.env-vars] @@ -251,6 +286,15 @@ PIP_EXTRA_INDEX_URL = "https://pypi.org/simple/" PIP_PRE = "1" [tool.hatch.envs.min_deps] +# Use pip rather than the inherited uv installer. This env must resolve +# zarr-metadata from PyPI (the published floor pinned below), not the in-tree +# workspace member. uv would honor the root `[tool.uv.sources] zarr-metadata = +# { workspace = true }` and substitute the workspace copy (whose hatch-vcs dev +# version can never equal the `==` floor), producing "No solution found" — see +# https://github.com/pypa/hatch/issues/1639. pip ignores `[tool.uv.sources]` +# entirely, so the `zarr-metadata==` pin below resolves against the +# published wheel and keeps the "minimum supported deps" guarantee honest. +installer = "pip" description = """Test environment for minimum supported dependencies See Spec 0000 for details and drop schedule: https://scientific-python.org/specs/spec-0000/ @@ -269,6 +313,10 @@ extra-dependencies = [ 'typing_extensions==4.14.*', 'donfig==0.8.*', 'obstore==0.5.*', + # Pin to the floor of zarr-python's declared zarr-metadata range. Must + # match the >= bound in [project.dependencies] above; the + # `check_min_deps_floor.py` pre-commit hook enforces this invariant. + 'zarr-metadata==0.4.0', ] [tool.hatch.envs.default] diff --git a/src/zarr/codecs/__init__.py b/src/zarr/codecs/__init__.py index 9a1b47b351..a9b0fadc4e 100644 --- a/src/zarr/codecs/__init__.py +++ b/src/zarr/codecs/__init__.py @@ -1,5 +1,17 @@ from __future__ import annotations +from zarr_metadata import ( + BLOSC_CODEC_NAME, + BYTES_CODEC_NAME, + CAST_VALUE_CODEC_NAME, + CRC32C_CODEC_NAME, + GZIP_CODEC_NAME, + SCALE_OFFSET_CODEC_NAME, + SHARDING_INDEXED_CODEC_NAME, + TRANSPOSE_CODEC_NAME, + ZSTD_CODEC_NAME, +) + from zarr.codecs.blosc import BloscCname, BloscCodec, BloscShuffle from zarr.codecs.bytes import BytesCodec, Endian from zarr.codecs.cast_value import CastValue @@ -54,20 +66,20 @@ "ZstdCodec", ] -register_codec("blosc", BloscCodec) -register_codec("cast_value", CastValue) -register_codec("bytes", BytesCodec) +register_codec(BLOSC_CODEC_NAME, BloscCodec) +register_codec(CAST_VALUE_CODEC_NAME, CastValue) +register_codec(BYTES_CODEC_NAME, BytesCodec) # compatibility with earlier versions of ZEP1 register_codec("endian", BytesCodec) -register_codec("crc32c", Crc32cCodec) -register_codec("gzip", GzipCodec) -register_codec("scale_offset", ScaleOffset) -register_codec("sharding_indexed", ShardingCodec) -register_codec("zstd", ZstdCodec) +register_codec(CRC32C_CODEC_NAME, Crc32cCodec) +register_codec(GZIP_CODEC_NAME, GzipCodec) +register_codec(SCALE_OFFSET_CODEC_NAME, ScaleOffset) +register_codec(SHARDING_INDEXED_CODEC_NAME, ShardingCodec) +register_codec(ZSTD_CODEC_NAME, ZstdCodec) register_codec("vlen-utf8", VLenUTF8Codec) register_codec("vlen-bytes", VLenBytesCodec) -register_codec("transpose", TransposeCodec) +register_codec(TRANSPOSE_CODEC_NAME, TransposeCodec) # Register all the codecs formerly contained in numcodecs.zarr3 diff --git a/src/zarr/codecs/blosc.py b/src/zarr/codecs/blosc.py index 087de716fc..f10114553c 100644 --- a/src/zarr/codecs/blosc.py +++ b/src/zarr/codecs/blosc.py @@ -3,16 +3,24 @@ import asyncio from dataclasses import dataclass, field, replace from functools import cached_property -from typing import TYPE_CHECKING, ClassVar, Final, Literal, NotRequired, TypedDict +from typing import TYPE_CHECKING, ClassVar, Literal, NotRequired, TypedDict import numcodecs +import zarr_metadata from numcodecs.blosc import Blosc from packaging.version import Version +from zarr_metadata import BLOSC_CODEC_NAME +from zarr_metadata.v3.codec.blosc import ( + BloscCodecConfiguration as _BloscCodecConfiguration, +) +from zarr_metadata.v3.codec.blosc import ( + BloscCodecObject as _BloscCodecObject, +) from zarr.abc.codec import BytesBytesCodec from zarr.codecs._deprecated_enum import _coerce_enum_input, _DeprecatedStrEnumMeta from zarr.core.buffer.cpu import as_numpy_array_wrapper -from zarr.core.common import JSON, NamedRequiredConfig, parse_named_configuration +from zarr.core.common import JSON, parse_named_configuration from zarr.core.dtype.common import HasItemSize if TYPE_CHECKING: @@ -21,19 +29,24 @@ from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer -BloscShuffleLiteral = Literal["noshuffle", "shuffle", "bitshuffle"] +# Re-exported under zarr-python's historical names; canonical definitions live +# in `zarr_metadata`. Plain assignments (not `import as`) so these remain +# explicitly importable from this module. +BloscShuffleLiteral = zarr_metadata.BloscShuffle """The shuffle values permitted for the blosc codec""" -BLOSC_SHUFFLE: Final = ("noshuffle", "shuffle", "bitshuffle") +BLOSC_SHUFFLE = zarr_metadata.BLOSC_SHUFFLE -BloscCnameLiteral = Literal["lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd"] +BloscCnameLiteral = zarr_metadata.BloscCName """The codec identifiers used in the blosc codec""" -BLOSC_CNAME: Final = ("lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd") +BLOSC_CNAME = zarr_metadata.BLOSC_CNAME class BloscConfigV2(TypedDict): - """Configuration for the V2 Blosc codec""" + """Configuration for the V2 Blosc codec. + + v2 codec shapes predate zarr-metadata, which models only v3 codecs.""" cname: BloscCnameLiteral clevel: int @@ -42,20 +55,8 @@ class BloscConfigV2(TypedDict): typesize: NotRequired[int] -class BloscConfigV3(TypedDict): - """Configuration for the V3 Blosc codec""" - - cname: BloscCnameLiteral - clevel: int - shuffle: BloscShuffleLiteral - blocksize: int - typesize: int - - -class BloscJSON_V3(NamedRequiredConfig[Literal["blosc"], BloscConfigV3]): - """ - The JSON form of the Blosc codec in Zarr V3. - """ +BloscConfigV3 = _BloscCodecConfiguration +BloscJSON_V3 = _BloscCodecObject class BloscShuffle(metaclass=_DeprecatedStrEnumMeta): @@ -264,12 +265,12 @@ def __init__( @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: - _, configuration_parsed = parse_named_configuration(data, "blosc") + _, configuration_parsed = parse_named_configuration(data, BLOSC_CODEC_NAME) return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: result: BloscJSON_V3 = { - "name": "blosc", + "name": BLOSC_CODEC_NAME, "configuration": { "typesize": self.typesize, "cname": self.cname, diff --git a/src/zarr/codecs/bytes.py b/src/zarr/codecs/bytes.py index fae762fd08..53ed7fdd91 100644 --- a/src/zarr/codecs/bytes.py +++ b/src/zarr/codecs/bytes.py @@ -3,7 +3,10 @@ import sys import warnings from dataclasses import dataclass, replace -from typing import TYPE_CHECKING, ClassVar, Final, Literal +from typing import TYPE_CHECKING, ClassVar, Final + +import zarr_metadata +from zarr_metadata import BYTES_CODEC_NAME from zarr.abc.codec import ArrayBytesCodec from zarr.codecs._deprecated_enum import _coerce_enum_input, _DeprecatedStrEnumMeta @@ -17,11 +20,13 @@ from zarr.core.array_spec import ArraySpec - -EndianLiteral = Literal["little", "big"] +# Re-exported under zarr-python's historical names; canonical definitions live +# in `zarr_metadata`. Plain assignments (not `import as`) so these remain +# explicitly importable from this module. +EndianLiteral = zarr_metadata.Endianness """Byte order of multi-byte numeric data.""" -ENDIAN: Final = ("little", "big") +ENDIAN: Final = zarr_metadata.ENDIANNESS class Endian(metaclass=_DeprecatedStrEnumMeta): @@ -59,7 +64,7 @@ def __init__(self, *, endian: Endian | EndianLiteral | None = sys.byteorder) -> @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( - data, "bytes", require_configuration=False + data, BYTES_CODEC_NAME, require_configuration=False ) configuration_parsed = configuration_parsed or {} configuration_parsed.setdefault("endian", None) @@ -67,9 +72,9 @@ def from_dict(cls, data: dict[str, JSON]) -> Self: def to_dict(self) -> dict[str, JSON]: if self.endian is None: - return {"name": "bytes"} + return {"name": BYTES_CODEC_NAME} else: - return {"name": "bytes", "configuration": {"endian": self.endian}} + return {"name": BYTES_CODEC_NAME, "configuration": {"endian": self.endian}} def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: if isinstance(array_spec.dtype, Struct): diff --git a/src/zarr/codecs/cast_value.py b/src/zarr/codecs/cast_value.py index eb8a4de248..dad7dd90a9 100644 --- a/src/zarr/codecs/cast_value.py +++ b/src/zarr/codecs/cast_value.py @@ -12,9 +12,10 @@ from collections.abc import Mapping from dataclasses import dataclass, replace -from typing import TYPE_CHECKING, Final, Literal, TypedDict, cast +from typing import TYPE_CHECKING, Final, TypedDict, cast import numpy as np +from zarr_metadata import CAST_VALUE_CODEC_NAME from zarr.abc.codec import ArrayArrayCodec from zarr.core.common import JSON, parse_named_configuration @@ -23,6 +24,13 @@ if TYPE_CHECKING: from typing import NotRequired, Self + from zarr_metadata.v3.codec.cast_value import ( + CastOutOfRangeMode as OutOfRangeMode, + ) + from zarr_metadata.v3.codec.cast_value import ( + CastRoundingMode as RoundingMode, + ) + from zarr.core.array_spec import ArraySpec from zarr.core.buffer import NDBuffer from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType @@ -33,17 +41,6 @@ class ScalarMapJSON(TypedDict): decode: NotRequired[list[tuple[object, object]]] -RoundingMode = Literal[ - "nearest-even", - "towards-zero", - "towards-positive", - "towards-negative", - "nearest-away", -] - -OutOfRangeMode = Literal["clamp", "wrap"] - - class ScalarMap(TypedDict, total=False): """ The normalized, in-memory form of a scalar map. @@ -230,7 +227,7 @@ def __init__( @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( - data, "cast_value", require_configuration=True + data, CAST_VALUE_CODEC_NAME, require_configuration=True ) return cls(**configuration_parsed) # type: ignore[arg-type] @@ -241,12 +238,18 @@ def to_dict(self) -> dict[str, JSON]: if self.out_of_range is not None: config["out_of_range"] = self.out_of_range if self.scalar_map is not None: - json_map: dict[str, list[tuple[object, object]]] = {} + # Emit ScalarMap entries as a tuple of 2-tuples. JSON Arrays are + # typed fixed-length containers at the spec level; the + # in-memory canonical shape is `tuple[tuple[object, object], ...]` + # to match `zarr_metadata.v3.codec.cast_value.ScalarMap`. + json_map: dict[str, tuple[tuple[object, object], ...]] = {} for direction in ("encode", "decode"): if direction in self.scalar_map: - json_map[direction] = [(k, v) for k, v in self.scalar_map[direction].items()] + json_map[direction] = tuple( + (k, v) for k, v in self.scalar_map[direction].items() + ) config["scalar_map"] = cast("JSON", json_map) - return {"name": "cast_value", "configuration": config} + return {"name": CAST_VALUE_CODEC_NAME, "configuration": config} def validate( self, diff --git a/src/zarr/codecs/crc32c_.py b/src/zarr/codecs/crc32c_.py index 7d41e11637..928af3a61b 100644 --- a/src/zarr/codecs/crc32c_.py +++ b/src/zarr/codecs/crc32c_.py @@ -6,6 +6,7 @@ import google_crc32c import numpy as np +from zarr_metadata import CRC32C_CODEC_NAME from zarr.abc.codec import BytesBytesCodec from zarr.core.common import JSON, parse_named_configuration @@ -25,11 +26,11 @@ class Crc32cCodec(BytesBytesCodec): @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: - parse_named_configuration(data, "crc32c", require_configuration=False) + parse_named_configuration(data, CRC32C_CODEC_NAME, require_configuration=False) return cls() def to_dict(self) -> dict[str, JSON]: - return {"name": "crc32c"} + return {"name": CRC32C_CODEC_NAME} def _decode_sync( self, diff --git a/src/zarr/codecs/gzip.py b/src/zarr/codecs/gzip.py index b8591748f7..66e1aa0d03 100644 --- a/src/zarr/codecs/gzip.py +++ b/src/zarr/codecs/gzip.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING from numcodecs.gzip import GZip +from zarr_metadata import GZIP_CODEC_NAME from zarr.abc.codec import BytesBytesCodec from zarr.core.buffer.cpu import as_numpy_array_wrapper @@ -43,11 +44,11 @@ def __init__(self, *, level: int = 5) -> None: @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: - _, configuration_parsed = parse_named_configuration(data, "gzip") + _, configuration_parsed = parse_named_configuration(data, GZIP_CODEC_NAME) return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: - return {"name": "gzip", "configuration": {"level": self.level}} + return {"name": GZIP_CODEC_NAME, "configuration": {"level": self.level}} @cached_property def _gzip_codec(self) -> GZip: diff --git a/src/zarr/codecs/scale_offset.py b/src/zarr/codecs/scale_offset.py index c96e177c6b..bfa887407a 100644 --- a/src/zarr/codecs/scale_offset.py +++ b/src/zarr/codecs/scale_offset.py @@ -5,6 +5,7 @@ import numpy as np import numpy.typing as npt +from zarr_metadata import SCALE_OFFSET_CODEC_NAME from zarr.abc.codec import ArrayArrayCodec from zarr.core.common import JSON, parse_named_configuration @@ -327,20 +328,20 @@ def __init__(self, *, offset: object = 0, scale: object = 1) -> None: @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( - data, "scale_offset", require_configuration=False + data, SCALE_OFFSET_CODEC_NAME, require_configuration=False ) configuration_parsed = configuration_parsed or {} return cls(**configuration_parsed) def to_dict(self) -> dict[str, JSON]: if self.offset == 0 and self.scale == 1: - return {"name": "scale_offset"} + return {"name": SCALE_OFFSET_CODEC_NAME} config: dict[str, JSON] = {} if self.offset != 0: config["offset"] = self.offset if self.scale != 1: config["scale"] = self.scale - return {"name": "scale_offset", "configuration": config} + return {"name": SCALE_OFFSET_CODEC_NAME, "configuration": config} def validate( self, diff --git a/src/zarr/codecs/sharding.py b/src/zarr/codecs/sharding.py index d8ca8bdf62..0b37db1b79 100644 --- a/src/zarr/codecs/sharding.py +++ b/src/zarr/codecs/sharding.py @@ -7,6 +7,8 @@ import numpy as np import numpy.typing as npt +import zarr_metadata +from zarr_metadata import SHARDING_INDEXED_CODEC_NAME from zarr.abc.codec import ( ArrayBytesCodec, @@ -87,10 +89,13 @@ ShardMutableMapping = MutableMapping[tuple[int, ...], Buffer | None] -IndexLocation = Literal["start", "end"] +# Re-exported under zarr-python's historical names; canonical definitions live +# in `zarr_metadata`. Plain assignments (not `import as`) so these remain +# explicitly importable from this module. +IndexLocation = zarr_metadata.ShardingIndexLocation """Position of the shard index within the encoded shard.""" -INDEX_LOCATION: Final = ("start", "end") +INDEX_LOCATION: Final = zarr_metadata.SHARDING_INDEX_LOCATION class ShardingCodecIndexLocation(metaclass=_DeprecatedStrEnumMeta): @@ -485,7 +490,7 @@ def __setstate__(self, state: dict[str, Any]) -> None: @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: - _, configuration_parsed = parse_named_configuration(data, "sharding_indexed") + _, configuration_parsed = parse_named_configuration(data, SHARDING_INDEXED_CODEC_NAME) return cls(**configuration_parsed) # type: ignore[arg-type] @property @@ -529,7 +534,7 @@ def _get_inner_pipeline(self, shard_spec: ArraySpec) -> CodecPipeline: def to_dict(self) -> dict[str, JSON]: return { - "name": "sharding_indexed", + "name": SHARDING_INDEXED_CODEC_NAME, "configuration": { "chunk_shape": self.chunk_shape, "codecs": tuple(s.to_dict() for s in self.codecs), diff --git a/src/zarr/codecs/transpose.py b/src/zarr/codecs/transpose.py index 5756fba2b4..098155710f 100644 --- a/src/zarr/codecs/transpose.py +++ b/src/zarr/codecs/transpose.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, cast import numpy as np +from zarr_metadata import TRANSPOSE_CODEC_NAME from zarr.abc.codec import ArrayArrayCodec from zarr.core.array_spec import ArraySpec @@ -41,11 +42,11 @@ def __init__(self, *, order: Iterable[int]) -> None: @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: - _, configuration_parsed = parse_named_configuration(data, "transpose") + _, configuration_parsed = parse_named_configuration(data, TRANSPOSE_CODEC_NAME) return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: - return {"name": "transpose", "configuration": {"order": tuple(self.order)}} + return {"name": TRANSPOSE_CODEC_NAME, "configuration": {"order": tuple(self.order)}} def validate( self, diff --git a/src/zarr/codecs/zstd.py b/src/zarr/codecs/zstd.py index f93c25a3c7..198bfa47bb 100644 --- a/src/zarr/codecs/zstd.py +++ b/src/zarr/codecs/zstd.py @@ -8,6 +8,7 @@ import numcodecs from numcodecs.zstd import Zstd from packaging.version import Version +from zarr_metadata import ZSTD_CODEC_NAME from zarr.abc.codec import BytesBytesCodec from zarr.core.buffer.cpu import as_numpy_array_wrapper @@ -60,11 +61,14 @@ def __init__(self, *, level: int = 0, checksum: bool = False) -> None: @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: - _, configuration_parsed = parse_named_configuration(data, "zstd") + _, configuration_parsed = parse_named_configuration(data, ZSTD_CODEC_NAME) return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: - return {"name": "zstd", "configuration": {"level": self.level, "checksum": self.checksum}} + return { + "name": ZSTD_CODEC_NAME, + "configuration": {"level": self.level, "checksum": self.checksum}, + } @cached_property def _zstd_codec(self) -> Zstd: diff --git a/src/zarr/core/dtype/common.py b/src/zarr/core/dtype/common.py index 76d763d267..179c2bee29 100644 --- a/src/zarr/core/dtype/common.py +++ b/src/zarr/core/dtype/common.py @@ -11,13 +11,17 @@ TypeGuard, ) +import zarr_metadata from typing_extensions import ReadOnly from zarr.core.common import NamedConfig from zarr.errors import UnstableSpecificationWarning -EndiannessStr = Literal["little", "big"] -ENDIANNESS_STR: Final = "little", "big" +# Re-exported under zarr-python's historical names; canonical definitions live +# in `zarr_metadata`. Plain assignments (not `import as`) so these remain +# explicitly importable from this module. +EndiannessStr = zarr_metadata.Endianness +ENDIANNESS_STR: Final = zarr_metadata.ENDIANNESS SpecialFloatStrings = Literal["NaN", "Infinity", "-Infinity"] SPECIAL_FLOAT_STRINGS: Final = ("NaN", "Infinity", "-Infinity") diff --git a/src/zarr/core/dtype/npy/common.py b/src/zarr/core/dtype/npy/common.py index f413f5f678..9382d40693 100644 --- a/src/zarr/core/dtype/npy/common.py +++ b/src/zarr/core/dtype/npy/common.py @@ -18,6 +18,7 @@ ) import numpy as np +import zarr_metadata from zarr.core.dtype.common import ( ENDIANNESS_STR, @@ -33,26 +34,11 @@ IntLike = SupportsInt | SupportsIndex | bytes | str FloatLike = SupportsIndex | SupportsFloat | bytes | str ComplexLike = SupportsFloat | SupportsIndex | SupportsComplex | bytes | str | None -DateTimeUnit = Literal[ - "Y", "M", "W", "D", "h", "m", "s", "ms", "us", "μs", "ns", "ps", "fs", "as", "generic" -] -DATETIME_UNIT: Final = ( - "Y", - "M", - "W", - "D", - "h", - "m", - "s", - "ms", - "us", - "μs", - "ns", - "ps", - "fs", - "as", - "generic", -) +# Re-exported under zarr-python's historical names; canonical definitions live +# in `zarr_metadata`. Plain assignments (not `import as`) so these remain +# explicitly importable from this module. +DateTimeUnit = zarr_metadata.NumpyTimeUnit +DATETIME_UNIT: Final = zarr_metadata.NUMPY_TIME_UNIT IntishFloat = NewType("IntishFloat", float) """A type for floats that represent integers, like 1.0 (but not 1.1).""" diff --git a/src/zarr/core/dtype/npy/structured.py b/src/zarr/core/dtype/npy/structured.py index dcc523d1d2..10e4a621f3 100644 --- a/src/zarr/core/dtype/npy/structured.py +++ b/src/zarr/core/dtype/npy/structured.py @@ -592,10 +592,14 @@ def to_json(self, zarr_format: ZarrFormat) -> StructuredJSON_V2 | StructJSON_V3: # The "struct" data type has a stable Zarr V3 specification # (https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/struct), # so unlike the legacy "structured" alias it does not emit an unstable-spec warning. - fields_v3 = [ + # `fields` is emitted as a tuple, not a list: a JSON array is a + # typed fixed-length container, which `tuple` models faithfully. + # This matches zarr-metadata's `StructConfiguration.fields` type. + # `json.dumps` serializes tuple and list identically. + fields_v3 = tuple( {"name": f_name, "data_type": f_dtype.to_json(zarr_format=zarr_format)} for f_name, f_dtype in self.fields - ] + ) return cast( "StructJSON_V3", {"name": self._zarr_v3_name, "configuration": {"fields": fields_v3}}, diff --git a/src/zarr/core/dtype/npy/time.py b/src/zarr/core/dtype/npy/time.py index 4efa0be7bb..d0c5eaa9c6 100644 --- a/src/zarr/core/dtype/npy/time.py +++ b/src/zarr/core/dtype/npy/time.py @@ -16,6 +16,8 @@ import numpy as np from typing_extensions import ReadOnly +from zarr_metadata import NUMPY_TIME_UNIT as DATETIME_UNIT +from zarr_metadata import NumpyTimeUnit as DateTimeUnit from zarr.core.common import NamedRequiredConfig from zarr.core.dtype.common import ( @@ -26,8 +28,6 @@ check_dtype_spec_v2, ) from zarr.core.dtype.npy.common import ( - DATETIME_UNIT, - DateTimeUnit, check_json_int, endianness_to_numpy_str, get_endianness_from_numpy_dtype, diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index 65f7767a29..869381884b 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import logging import unicodedata import warnings @@ -73,6 +74,8 @@ ) from typing import Any + from zarr_metadata.v2 import ZarrV2ConsolidatedMetadataJSON + from zarr.core.array_spec import ArrayConfigLike from zarr.core.buffer import Buffer, BufferPrototype from zarr.core.chunk_key_encodings import ChunkKeyEncodingLike @@ -436,6 +439,12 @@ def to_dict(self) -> dict[str, Any]: else: # Leave consolidated metadata unset if it's None result.pop("consolidated_metadata") + # `node_type` is a v3-only field. v2 group metadata (.zgroup) has + # only `zarr_format`; attributes live in a sibling .zattrs file. + # The dataclass carries `node_type` for in-memory use; strip it + # from the serialized v2 form. + if self.zarr_format == 2: + result.pop("node_type", None) return result @@ -626,8 +635,14 @@ def _from_bytes_v2( group_metadata: dict[str, Any] = {**zgroup, "attributes": zattrs} if consolidated_metadata_bytes is not None: - v2_consolidated_doc = buffer_to_json_object(consolidated_metadata_bytes) - v2_consolidated_metadata = cast("dict[str, Any]", v2_consolidated_doc["metadata"]) + # The parsed file has the shape of `ZarrV2ConsolidatedMetadataJSON` from + # zarr-metadata (keys like `foo/.zarray`, `foo/.zgroup`, + # `foo/.zattrs`). Mutate it below to strip and reorganize + # entries, so convert to a mutable `dict` after parsing. + parsed: ZarrV2ConsolidatedMetadataJSON = json.loads( + consolidated_metadata_bytes.to_bytes() + ) + v2_consolidated_metadata = dict(parsed["metadata"]) # We already read zattrs and zgroup. Should we ignore these? v2_consolidated_metadata.pop(".zattrs", None) v2_consolidated_metadata.pop(".zgroup", None) diff --git a/src/zarr/core/metadata/v2.py b/src/zarr/core/metadata/v2.py index 91515d87b9..32a03910ab 100644 --- a/src/zarr/core/metadata/v2.py +++ b/src/zarr/core/metadata/v2.py @@ -4,7 +4,7 @@ import warnings from collections.abc import Iterable, Sequence from functools import cached_property -from typing import TYPE_CHECKING, Any, TypedDict, cast +from typing import TYPE_CHECKING, Any, cast from zarr.abc.metadata import Metadata from zarr.abc.numcodec import Numcodec, _is_numcodec @@ -29,6 +29,7 @@ from dataclasses import dataclass, field, fields, replace import numpy as np +from zarr_metadata.v2.array import ZarrV2ArrayMetadataJSON as _ZarrV2ArrayMetadataJSON from zarr.core._json import json_to_buffer from zarr.core.array_spec import ArrayConfig, ArraySpec @@ -43,18 +44,10 @@ from zarr.core.config import config, parse_indexing_order from zarr.core.metadata.common import parse_attributes - -class ArrayV2MetadataDict(TypedDict): - """ - A typed dictionary model for Zarr format 2 metadata. - """ - - zarr_format: Literal[2] - attributes: dict[str, JSON] - - # Union of acceptable types for v2 compressors type CompressorLikev2 = dict[str, JSON] | Numcodec | None +# Re-export the v2 array metadata JSON shape under zarr-python's historical name. +ArrayV2MetadataDict = _ZarrV2ArrayMetadataJSON @dataclass(frozen=True, kw_only=True) diff --git a/src/zarr/core/metadata/v3.py b/src/zarr/core/metadata/v3.py index 9eaccc5076..50f6dd17cd 100644 --- a/src/zarr/core/metadata/v3.py +++ b/src/zarr/core/metadata/v3.py @@ -3,9 +3,16 @@ import json from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field, replace -from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypeGuard, cast +from typing import TYPE_CHECKING, Any, Final, Literal, TypeGuard, cast from typing_extensions import TypedDict +from zarr_metadata import ( + RECTILINEAR_CHUNK_GRID_NAME, + REGULAR_CHUNK_GRID_NAME, + RectilinearChunkGridName, + RegularChunkGridName, +) +from zarr_metadata.v3.array import ZarrV3ArrayMetadataJSON, ZarrV3ExtensionField from zarr.abc.codec import ArrayArrayCodec, ArrayBytesCodec, BytesBytesCodec, Codec from zarr.abc.metadata import Metadata @@ -140,14 +147,12 @@ def parse_storage_transformers(data: object) -> tuple[dict[str, JSON], ...]: ) -class AllowedExtraField(TypedDict, extra_items=JSON): # type: ignore[call-arg] - """ - This class models allowed extra fields in array metadata. - They must have ``must_understand`` set to ``False``, and may contain - arbitrary additional JSON data. - """ +AllowedExtraField = ZarrV3ExtensionField +"""Alias for `zarr_metadata.v3.array.ZarrV3ExtensionField`. - must_understand: Literal[False] +`must_understand` is typed as `bool` to match the spec (extension authors that +*understand* a field may produce `True`); the runtime guard +`check_allowed_extra_field` enforces that zarr-python only accepts `False`.""" def check_allowed_extra_field(data: object) -> TypeGuard[AllowedExtraField]: @@ -192,10 +197,10 @@ class RectilinearChunkGridMetadataConfig(TypedDict): RegularChunkGridMetadataJSON = NamedRequiredConfig[ - Literal["regular"], RegularChunkGridMetadataConfig + RegularChunkGridName, RegularChunkGridMetadataConfig ] RectilinearChunkGridMetadataJSON = NamedRequiredConfig[ - Literal["rectilinear"], RectilinearChunkGridMetadataConfig + RectilinearChunkGridName, RectilinearChunkGridMetadataConfig ] @@ -260,13 +265,13 @@ def ndim(self) -> int: def to_dict(self) -> RegularChunkGridMetadataJSON: # type: ignore[override] return { - "name": "regular", + "name": REGULAR_CHUNK_GRID_NAME, "configuration": {"chunk_shape": self.chunk_shape}, } @classmethod def from_dict(cls, data: RegularChunkGridMetadataJSON) -> Self: # type: ignore[override] - parse_named_configuration(data, "regular") # validate name + parse_named_configuration(data, REGULAR_CHUNK_GRID_NAME) # validate name configuration = data["configuration"] return cls(chunk_shape=_parse_chunk_shape(configuration["chunk_shape"])) @@ -316,7 +321,7 @@ def to_dict(self) -> RectilinearChunkGridMetadataJSON: # type: ignore[override] else: serialized_dims.append(list(dim_spec)) return { - "name": "rectilinear", + "name": RECTILINEAR_CHUNK_GRID_NAME, "configuration": { "kind": "inline", "chunk_shapes": tuple(serialized_dims), @@ -349,7 +354,7 @@ def update_shape( @classmethod def from_dict(cls, data: RectilinearChunkGridMetadataJSON) -> Self: # type: ignore[override] - parse_named_configuration(data, "rectilinear") # validate name + parse_named_configuration(data, RECTILINEAR_CHUNK_GRID_NAME) # validate name configuration = data["configuration"] validate_rectilinear_kind(configuration.get("kind")) raw_shapes = configuration["chunk_shapes"] @@ -413,32 +418,20 @@ def parse_chunk_grid( return data name, _ = parse_named_configuration(data) - if name == "regular": + if name == REGULAR_CHUNK_GRID_NAME: return RegularChunkGridMetadata.from_dict(data) # type: ignore[arg-type] - if name == "rectilinear": + if name == RECTILINEAR_CHUNK_GRID_NAME: return RectilinearChunkGridMetadata.from_dict(data) # type: ignore[arg-type] raise ValueError(f"Unknown chunk grid name: {name!r}") -class ArrayMetadataJSON_V3(TypedDict, extra_items=AllowedExtraField): # type: ignore[call-arg] - """ - A typed dictionary model for zarr v3 array metadata. +ArrayMetadataJSON_V3 = ZarrV3ArrayMetadataJSON +"""Alias for `zarr_metadata.v3.array.ZarrV3ArrayMetadataJSON`, the TypedDict modeling +the v3 array metadata document. - Extra keys are permitted if they conform to ``AllowedExtraField`` - (i.e. they are mappings with ``must_understand: false``). - """ - - zarr_format: Literal[3] - node_type: Literal["array"] - data_type: str | NamedConfig[str, Mapping[str, JSON]] - shape: tuple[int, ...] - chunk_grid: str | NamedConfig[str, Mapping[str, JSON]] - chunk_key_encoding: str | NamedConfig[str, Mapping[str, JSON]] - fill_value: JSON - codecs: tuple[str | NamedConfig[str, Mapping[str, JSON]], ...] - attributes: NotRequired[Mapping[str, JSON]] - storage_transformers: NotRequired[tuple[str | NamedConfig[str, Mapping[str, JSON]], ...]] - dimension_names: NotRequired[tuple[str | None, ...]] +Used throughout zarr-python under this name to avoid visual collision with +the `ArrayV3Metadata` dataclass — the two differ only in word order. Extra +keys are permitted on this dict if they conform to `ZarrV3ExtensionField`.""" """ @@ -665,6 +658,12 @@ def from_dict(cls, data: dict[str, JSON]) -> Self: ) def to_dict(self) -> dict[str, JSON]: + """Serialize as a JSON-shaped dict matching `ArrayMetadataJSON_V3`. + + Return type is `dict[str, JSON]` rather than `ArrayMetadataJSON_V3` so + the result composes with other zarr-python metadata serialisation + paths that traffic in `dict[str, JSON]` (notably consolidated metadata). + """ out_dict = super().to_dict() extra_fields = out_dict.pop("extra_fields") out_dict = out_dict | extra_fields # type: ignore[operator] diff --git a/tests/test_array.py b/tests/test_array.py index b1a7a3c0f2..8824c9bc27 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -7,7 +7,7 @@ import re import sys from itertools import accumulate, starmap -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Literal, cast from unittest import mock import numcodecs @@ -85,7 +85,11 @@ from .test_dtype.conftest import zdtype_examples if TYPE_CHECKING: + from zarr_metadata import ZarrV2ArrayMetadataJSON + from zarr_metadata.v3.codec.bytes import BytesCodecMetadata + from zarr.abc.codec import CodecJSON_V3 + from zarr.core.metadata import ArrayMetadataJSON_V3 @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @@ -325,47 +329,47 @@ def test_serializable_sync_array(store: LocalStore, zarr_format: ZarrFormat) -> @pytest.mark.parametrize("store", ["memory"], indirect=True) -@pytest.mark.parametrize("zarr_format", [2, 3, "invalid"]) -def test_storage_transformers(store: MemoryStore, zarr_format: ZarrFormat | str) -> None: +@pytest.mark.parametrize("zarr_format", [2, 3]) +def test_storage_transformers(store: MemoryStore, zarr_format: ZarrFormat) -> None: """ - Test that providing an actual storage transformer produces a warning and otherwise passes through + storage_transformers is a v3-only field; passing a populated one to v3 + array construction raises, while v2 (where the field has no spec + meaning) is unaffected. """ - metadata_dict: dict[str, JSON] if zarr_format == 3: - metadata_dict = { + v3_metadata: ArrayMetadataJSON_V3 = { "zarr_format": 3, "node_type": "array", "shape": (10,), "chunk_grid": {"name": "regular", "configuration": {"chunk_shape": (1,)}}, "data_type": "uint8", "chunk_key_encoding": {"name": "v2", "configuration": {"separator": "/"}}, - "codecs": (BytesCodec().to_dict(),), + "codecs": (BytesCodec().to_dict(),), # type: ignore[typeddict-item] "fill_value": 0, - "storage_transformers": ({"test": "should_raise"}), + # Deliberately invalid: the test asserts that any non-empty + # storage_transformers value triggers the "not supported" + # error path, regardless of its inner shape. + "storage_transformers": ({"test": "should_raise"},), # type: ignore[typeddict-item] } + match = "Arrays with storage transformers are not supported in zarr-python at this time." + with pytest.raises(ValueError, match=match): + # cast: from_dict accepts the wider `dict[str, JSON]`. + Array.from_dict(StorePath(store), data=cast("dict[str, JSON]", v3_metadata)) else: - metadata_dict = { - "zarr_format": zarr_format, + # Plain v2 array metadata; no v3-only fields (no codecs, + # storage_transformers, chunk_grid, etc.). + v2_metadata: ZarrV2ArrayMetadataJSON = { + "zarr_format": 2, "shape": (10,), "chunks": (1,), "dtype": "|u1", "dimension_separator": ".", - "codecs": (BytesCodec().to_dict(),), + "compressor": None, "fill_value": 0, "order": "C", - "storage_transformers": ({"test": "should_raise"}), + "filters": None, } - if zarr_format == 3: - match = "Arrays with storage transformers are not supported in zarr-python at this time." - with pytest.raises(ValueError, match=match): - Array.from_dict(StorePath(store), data=metadata_dict) - elif zarr_format == 2: - # no warning - Array.from_dict(StorePath(store), data=metadata_dict) - else: - match = f"Invalid zarr_format: {zarr_format}. Expected 2 or 3" - with pytest.raises(ValueError, match=match): - Array.from_dict(StorePath(store), data=metadata_dict) + Array.from_dict(StorePath(store), data=cast("dict[str, JSON]", v2_metadata)) @pytest.mark.parametrize("test_cls", [AnyArray, AnyAsyncArray]) @@ -1888,7 +1892,7 @@ def test_roundtrip_numcodecs() -> None: dimension_names=["lat", "lon"], ) - BYTES_CODEC = {"name": "bytes", "configuration": {"endian": "little"}} + BYTES_CODEC: BytesCodecMetadata = {"name": "bytes", "configuration": {"endian": "little"}} # Read in the array again and check compressor config root = zarr.open_group(store) metadata = root["test"].metadata.to_dict() diff --git a/tests/test_codecs/test_cast_value.py b/tests/test_codecs/test_cast_value.py index c43edb76e8..d682234ace 100644 --- a/tests/test_codecs/test_cast_value.py +++ b/tests/test_codecs/test_cast_value.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any, cast import numpy as np import pytest @@ -9,6 +9,11 @@ from tests.conftest import Expect, ExpectFail from zarr.codecs.cast_value import CastValue +if TYPE_CHECKING: + from zarr_metadata.v3.codec.cast_value import CastValueCodecObject + + from zarr.core.common import JSON + try: import cast_value_rs # noqa: F401 @@ -26,14 +31,25 @@ # --------------------------------------------------------------------------- +_CAST_VALUE_MINIMAL: CastValueCodecObject = { + "name": "cast_value", + "configuration": {"data_type": "uint8"}, +} +_CAST_VALUE_FULL: CastValueCodecObject = { + "name": "cast_value", + "configuration": { + "data_type": "uint8", + "rounding": "towards-zero", + "out_of_range": "clamp", + "scalar_map": {"encode": (("NaN", 0),)}, + }, +} + + @pytest.mark.parametrize( "case", [ - Expect( - input=CastValue(data_type="uint8"), - output={"name": "cast_value", "configuration": {"data_type": "uint8"}}, - id="minimal", - ), + Expect(input=CastValue(data_type="uint8"), output=_CAST_VALUE_MINIMAL, id="minimal"), Expect( input=CastValue( data_type="uint8", @@ -41,51 +57,53 @@ out_of_range="clamp", scalar_map={"encode": [("NaN", 0)]}, ), - output={ - "name": "cast_value", - "configuration": { - "data_type": "uint8", - "rounding": "towards-zero", - "out_of_range": "clamp", - "scalar_map": {"encode": [("NaN", 0)]}, - }, - }, + output=_CAST_VALUE_FULL, id="full", ), ], ids=lambda c: c.id, ) -def test_to_dict(case: Expect[CastValue, dict[str, Any]]) -> None: +def test_to_dict(case: Expect[CastValue, CastValueCodecObject]) -> None: """to_dict produces the expected JSON structure.""" assert case.input.to_dict() == case.output +_CAST_VALUE_FROM_DICT_DEFAULTS: CastValueCodecObject = { + "name": "cast_value", + "configuration": {"data_type": "float32"}, +} +_CAST_VALUE_FROM_DICT_EXPLICIT: CastValueCodecObject = { + "name": "cast_value", + "configuration": { + "data_type": "int16", + "rounding": "towards-zero", + "out_of_range": "clamp", + }, +} + + @pytest.mark.parametrize( "case", [ Expect( - input={"name": "cast_value", "configuration": {"data_type": "float32"}}, + input=_CAST_VALUE_FROM_DICT_DEFAULTS, output=("float32", "nearest-even", None), id="defaults", ), Expect( - input={ - "name": "cast_value", - "configuration": { - "data_type": "int16", - "rounding": "towards-zero", - "out_of_range": "clamp", - }, - }, + input=_CAST_VALUE_FROM_DICT_EXPLICIT, output=("int16", "towards-zero", "clamp"), id="explicit", ), ], ids=lambda c: c.id, ) -def test_from_dict(case: Expect[dict[str, Any], tuple[str, str, str | None]]) -> None: +def test_from_dict( + case: Expect[CastValueCodecObject, tuple[str, str, str | None]], +) -> None: """from_dict deserializes configuration with correct values and defaults.""" - codec = CastValue.from_dict(case.input) + # cast: from_dict accepts the wider `dict[str, JSON]`. + codec = CastValue.from_dict(cast("dict[str, JSON]", case.input)) dtype_name, rounding, out_of_range = case.output assert codec.dtype.to_native_dtype() == np.dtype(dtype_name) assert codec.rounding == rounding diff --git a/tests/test_codecs/test_scale_offset.py b/tests/test_codecs/test_scale_offset.py index 513caf463a..89df06a80c 100644 --- a/tests/test_codecs/test_scale_offset.py +++ b/tests/test_codecs/test_scale_offset.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any, cast import numpy as np import pytest @@ -16,53 +16,67 @@ from zarr.core.buffer.core import default_buffer_prototype from zarr.storage._memory import MemoryStore +if TYPE_CHECKING: + from zarr_metadata.v3.codec.scale_offset import ScaleOffsetCodecObject + + from zarr.core.common import JSON + # --------------------------------------------------------------------------- # Serialization # --------------------------------------------------------------------------- +_SCALE_OFFSET_DEFAULT: ScaleOffsetCodecObject = {"name": "scale_offset"} +_SCALE_OFFSET_OFFSET_ONLY: ScaleOffsetCodecObject = { + "name": "scale_offset", + "configuration": {"offset": 5}, +} +_SCALE_OFFSET_SCALE_ONLY: ScaleOffsetCodecObject = { + "name": "scale_offset", + "configuration": {"scale": 0.1}, +} +_SCALE_OFFSET_BOTH: ScaleOffsetCodecObject = { + "name": "scale_offset", + "configuration": {"offset": 5, "scale": 0.1}, +} + + @pytest.mark.parametrize( "case", [ - Expect(input=ScaleOffset(), output={"name": "scale_offset"}, id="default"), - Expect( - input=ScaleOffset(offset=5), - output={"name": "scale_offset", "configuration": {"offset": 5}}, - id="offset-only", - ), - Expect( - input=ScaleOffset(scale=0.1), - output={"name": "scale_offset", "configuration": {"scale": 0.1}}, - id="scale-only", - ), - Expect( - input=ScaleOffset(offset=5, scale=0.1), - output={"name": "scale_offset", "configuration": {"offset": 5, "scale": 0.1}}, - id="both", - ), + Expect(input=ScaleOffset(), output=_SCALE_OFFSET_DEFAULT, id="default"), + Expect(input=ScaleOffset(offset=5), output=_SCALE_OFFSET_OFFSET_ONLY, id="offset-only"), + Expect(input=ScaleOffset(scale=0.1), output=_SCALE_OFFSET_SCALE_ONLY, id="scale-only"), + Expect(input=ScaleOffset(offset=5, scale=0.1), output=_SCALE_OFFSET_BOTH, id="both"), ], ids=lambda c: c.id, ) -def test_to_dict(case: Expect[ScaleOffset, dict[str, Any]]) -> None: +def test_to_dict(case: Expect[ScaleOffset, ScaleOffsetCodecObject]) -> None: """to_dict produces the expected JSON structure.""" assert case.input.to_dict() == case.output +_SCALE_OFFSET_FROM_DICT_NO_CONFIG: ScaleOffsetCodecObject = {"name": "scale_offset"} +_SCALE_OFFSET_FROM_DICT_WITH_CONFIG: ScaleOffsetCodecObject = { + "name": "scale_offset", + "configuration": {"offset": 3, "scale": 2}, +} + + @pytest.mark.parametrize( "case", [ - Expect(input={"name": "scale_offset"}, output=(0, 1), id="no-config"), - Expect( - input={"name": "scale_offset", "configuration": {"offset": 3, "scale": 2}}, - output=(3, 2), - id="with-config", - ), + Expect(input=_SCALE_OFFSET_FROM_DICT_NO_CONFIG, output=(0, 1), id="no-config"), + Expect(input=_SCALE_OFFSET_FROM_DICT_WITH_CONFIG, output=(3, 2), id="with-config"), ], ids=lambda c: c.id, ) -def test_from_dict(case: Expect[dict[str, Any], tuple[int | float, int | float]]) -> None: +def test_from_dict( + case: Expect[ScaleOffsetCodecObject, tuple[int | float, int | float]], +) -> None: """from_dict deserializes configuration with correct values and defaults.""" - codec = ScaleOffset.from_dict(case.input) + # cast: from_dict accepts the wider `dict[str, JSON]`. + codec = ScaleOffset.from_dict(cast("dict[str, JSON]", case.input)) expected_offset, expected_scale = case.output assert codec.offset == expected_offset assert codec.scale == expected_scale diff --git a/tests/test_dtype/test_npy/test_bool.py b/tests/test_dtype/test_npy/test_bool.py index da30214b3b..ff48b26189 100644 --- a/tests/test_dtype/test_npy/test_bool.py +++ b/tests/test_dtype/test_npy/test_bool.py @@ -1,10 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING, ClassVar + import numpy as np from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.bool import Bool +if TYPE_CHECKING: + from zarr_metadata.v3.data_type.bool import BoolDataTypeName + class TestBool(BaseTestZDType): test_cls = Bool @@ -16,7 +21,7 @@ class TestBool(BaseTestZDType): np.dtype(np.uint16), ) valid_json_v2 = ({"name": "|b1", "object_codec_id": None},) - valid_json_v3 = ("bool",) + valid_json_v3: ClassVar[tuple[BoolDataTypeName, ...]] = ("bool",) invalid_json_v2 = ( "|b1", "bool", diff --git a/tests/test_dtype/test_npy/test_complex.py b/tests/test_dtype/test_npy/test_complex.py index b4ce42be58..dda60585f8 100644 --- a/tests/test_dtype/test_npy/test_complex.py +++ b/tests/test_dtype/test_npy/test_complex.py @@ -1,12 +1,17 @@ from __future__ import annotations import math +from typing import TYPE_CHECKING, ClassVar import numpy as np from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.complex import Complex64, Complex128 +if TYPE_CHECKING: + from zarr_metadata.v3.data_type.complex64 import Complex64DataTypeName + from zarr_metadata.v3.data_type.complex128 import Complex128DataTypeName + class _BaseTestFloat(BaseTestZDType): def scalar_equals(self, scalar1: object, scalar2: object) -> bool: @@ -27,7 +32,7 @@ class TestComplex64(_BaseTestFloat): {"name": ">c8", "object_codec_id": None}, {"name": "c16", "object_codec_id": None}, {"name": " bool: @@ -36,7 +43,7 @@ class TestFloat16(_BaseTestFloat): {"name": ">f2", "object_codec_id": None}, {"name": "f4", "object_codec_id": None}, {"name": "f8", "object_codec_id": None}, {"name": "i1", "int8", @@ -51,7 +63,7 @@ class TestInt16(BaseTestZDType): {"name": ">i2", "object_codec_id": None}, {"name": "i4", "object_codec_id": None}, {"name": "i8", "object_codec_id": None}, {"name": "u2", "object_codec_id": None}, {"name": "u4", "object_codec_id": None}, {"name": "u8", "object_codec_id": None}, {"name": "i4"], ["field2", ">f8"]], "object_codec_id": None}, {"name": [["field1", ">i8"], ["field2", ">i4"]], "object_codec_id": None}, ) - valid_json_v3 = ( + # `StructConfiguration.fields` is a `tuple[StructField, ...]` (a JSON array + # is a typed fixed-length container), and `Struct.to_json` emits a tuple to + # match, so the field entries are written as tuples here. + valid_json_v3: ClassVar[tuple[StructMetadata, ...]] = ( { "name": "struct", "configuration": { - "fields": [ + "fields": ( {"name": "field1", "data_type": "int32"}, {"name": "field2", "data_type": "float64"}, - ] + ) }, }, { "name": "struct", "configuration": { - "fields": [ + "fields": ( { "name": "field1", "data_type": { @@ -62,7 +68,7 @@ class TestStruct(BaseTestZDType): "configuration": {"length_bytes": 32}, }, }, - ] + ) }, }, ) diff --git a/tests/test_dtype/test_npy/test_time.py b/tests/test_dtype/test_npy/test_time.py index 67ba3bd130..14b6000999 100644 --- a/tests/test_dtype/test_npy/test_time.py +++ b/tests/test_dtype/test_npy/test_time.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import get_args +from typing import TYPE_CHECKING, ClassVar, get_args import numpy as np import pytest @@ -10,6 +10,10 @@ from zarr.core.dtype.npy.common import DateTimeUnit from zarr.core.dtype.npy.time import DateTime64, TimeDelta64, datetime_from_int +if TYPE_CHECKING: + from zarr_metadata.v3.data_type.numpy_datetime64 import NumpyDatetime64 + from zarr_metadata.v3.data_type.numpy_timedelta64 import NumpyTimedelta64 + class _TestTimeBase(BaseTestZDType): def json_scalar_equals(self, scalar1: object, scalar2: object) -> bool: @@ -40,7 +44,7 @@ class TestDateTime64(_TestTimeBase): {"name": " None: """ Test that we can create an AsyncGroup from a dict diff --git a/tests/test_metadata/conftest.py b/tests/test_metadata/conftest.py index 24f2417fce..2a0765b9a4 100644 --- a/tests/test_metadata/conftest.py +++ b/tests/test_metadata/conftest.py @@ -5,7 +5,10 @@ from zarr.codecs.bytes import BytesCodec if TYPE_CHECKING: - from zarr.core.metadata.v3 import ArrayMetadataJSON_V3 + from zarr_metadata.v3.chunk_grid.regular import RegularChunkGridMetadata + from zarr_metadata.v3.chunk_key_encoding.default import DefaultChunkKeyEncodingMetadata + + from zarr.core.metadata import ArrayMetadataJSON_V3 def minimal_metadata_dict_v3( @@ -23,13 +26,29 @@ def minimal_metadata_dict_v3( **overrides Override any of the standard metadata fields. """ + # Bind chunk-grid and chunk-key-encoding subdicts to their precise + # zarr-metadata types so structural shape errors surface here rather + # than downstream. + chunk_grid: RegularChunkGridMetadata = { + "name": "regular", + "configuration": {"chunk_shape": (4, 4)}, + } + chunk_key_encoding: DefaultChunkKeyEncodingMetadata = { + "name": "default", + "configuration": {"separator": "/"}, + } d: ArrayMetadataJSON_V3 = { "zarr_format": 3, "node_type": "array", "shape": (4, 4), "data_type": "uint8", - "chunk_grid": {"name": "regular", "configuration": {"chunk_shape": (4, 4)}}, - "chunk_key_encoding": {"name": "default", "configuration": {"separator": "/"}}, + # mypy does not recognize structural subtyping between TypedDicts, + # so `RegularChunkGridMetadata` is not seen as assignable to the + # outer `str | NamedConfig` field type even though it is. The + # bound variables above are correct; suppress the spurious + # `typeddict-item` rejections here. + "chunk_grid": chunk_grid, # type: ignore[typeddict-item] + "chunk_key_encoding": chunk_key_encoding, # type: ignore[typeddict-item] "fill_value": 0, "codecs": (BytesCodec().to_dict(),), # type: ignore[typeddict-item] "attributes": {}, diff --git a/tests/test_metadata/test_consolidated.py b/tests/test_metadata/test_consolidated.py index cd0fd92d74..11f8b484c2 100644 --- a/tests/test_metadata/test_consolidated.py +++ b/tests/test_metadata/test_consolidated.py @@ -1,7 +1,7 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import numpy as np import pytest @@ -26,6 +26,10 @@ from zarr.storage import StorePath if TYPE_CHECKING: + from zarr_metadata.v2 import ZarrV2ConsolidatedMetadataJSON, ZarrV2ZAttrsJSON, ZarrV2ZGroupJSON + from zarr_metadata.v3.array import ZarrV3ArrayMetadataJSONPartial + from zarr_metadata.v3.group import ZarrV3GroupMetadataJSON + from zarr.abc.store import Store from zarr.core.common import JSON, ZarrFormat @@ -63,14 +67,22 @@ async def test_getitem_consolidated_empty_leaf_group( # # field on the leaf group nodes. if zarr_format == 2: - zmetadata: dict[str, JSON] = { + # Bind each value to a typed variable so the outer TypedDict's + # value-union (ZarrV2ZArrayJSON | ZarrV2ZGroupJSON | ZarrV2ZAttrsJSON) + # resolves unambiguously to the correct arm — inline literals + # do not narrow because mypy can't structurally disambiguate + # `{}` between `ZarrV2ZAttrsJSON` (Mapping[str, object]) and an + # empty TypedDict variant. + empty_attrs: ZarrV2ZAttrsJSON = {} + empty_group: ZarrV2ZGroupJSON = {"zarr_format": 2} + zmetadata: ZarrV2ConsolidatedMetadataJSON = { "metadata": { - ".zattrs": {}, - ".zgroup": {"zarr_format": 2}, - "raw/.zattrs": {}, - "raw/.zgroup": {"zarr_format": 2}, - "raw/varm/.zattrs": {}, - "raw/varm/.zgroup": {"zarr_format": 2}, + ".zattrs": empty_attrs, + ".zgroup": empty_group, + "raw/.zattrs": empty_attrs, + "raw/.zgroup": empty_group, + "raw/varm/.zattrs": empty_attrs, + "raw/varm/.zgroup": empty_group, }, "zarr_consolidated_format": 1, } @@ -83,7 +95,15 @@ async def test_getitem_consolidated_empty_leaf_group( ) else: - zmetadata = { + # The v3 shape is a group metadata document with an inline + # `consolidated_metadata` extension field; not a + # `ZarrV2ConsolidatedMetadataJSON` shape, so use a separately-named + # variable. + # Complete v3 group document with an inline `consolidated_metadata` + # extension field. mypy does not honor PEP 728 `extra_items=`, so + # the extension key needs a `typeddict-unknown-key` suppression even + # though `ZarrV3GroupMetadataJSON` permits conforming extension fields. + zarr_json: ZarrV3GroupMetadataJSON = { # type: ignore[typeddict-unknown-key] "attributes": {}, "zarr_format": 3, "consolidated_metadata": { @@ -105,7 +125,7 @@ async def test_getitem_consolidated_empty_leaf_group( "node_type": "group", } await memory_store.set( - "zarr.json", cpu.Buffer.from_bytes(json.dumps(zmetadata).encode()) + "zarr.json", cpu.Buffer.from_bytes(json.dumps(zarr_json).encode()) ) group = await zarr.api.asynchronous.open_consolidated( @@ -141,7 +161,10 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: await consolidate_metadata(memory_store_with_hierarchy) group2 = await AsyncGroup.open(memory_store_with_hierarchy) - array_metadata: dict[str, JSON] = { + # Partial v3 array document: `shape` and `chunk_grid` are intentionally + # omitted and supplied per-array via spread below. `ZarrV3ArrayMetadataJSONPartial` + # is the `total=False` form that types exactly this kind of fragment. + array_metadata: ZarrV3ArrayMetadataJSONPartial = { "attributes": {}, "chunk_key_encoding": { "configuration": {"separator": "/"}, @@ -171,7 +194,7 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: "configuration": {"chunk_shape": (1, 2, 3)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "lat": ArrayV3Metadata.from_dict( @@ -181,7 +204,7 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: "configuration": {"chunk_shape": (1,)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "lon": ArrayV3Metadata.from_dict( @@ -191,7 +214,7 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: "configuration": {"chunk_shape": (2,)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "time": ArrayV3Metadata.from_dict( @@ -201,7 +224,7 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: "configuration": {"chunk_shape": (3,)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "child": GroupMetadata( @@ -210,7 +233,7 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: metadata={ "array": ArrayV3Metadata.from_dict( { - **array_metadata, + **array_metadata, # type: ignore[dict-item] "attributes": {"key": "child"}, "shape": (4, 4), "chunk_grid": { @@ -232,7 +255,7 @@ async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: ), "array": ArrayV3Metadata.from_dict( { - **array_metadata, + **array_metadata, # type: ignore[dict-item] "attributes": {"key": "grandchild"}, "shape": (4, 4), "chunk_grid": { @@ -292,7 +315,9 @@ def test_consolidated_sync(self, memory_store: Store) -> None: zarr.api.synchronous.consolidate_metadata(memory_store) group2 = zarr.Group.open(memory_store) - array_metadata: dict[str, JSON] = { + # Partial v3 array document (see `test_consolidated_metadata`): `shape` + # and `chunk_grid` are supplied per-array via the spreads below. + array_metadata: ZarrV3ArrayMetadataJSONPartial = { "attributes": {}, "chunk_key_encoding": { "configuration": {"separator": "/"}, @@ -322,7 +347,7 @@ def test_consolidated_sync(self, memory_store: Store) -> None: "configuration": {"chunk_shape": (1, 2, 3)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "lat": ArrayV3Metadata.from_dict( @@ -332,7 +357,7 @@ def test_consolidated_sync(self, memory_store: Store) -> None: "configuration": {"chunk_shape": (1,)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "lon": ArrayV3Metadata.from_dict( @@ -342,7 +367,7 @@ def test_consolidated_sync(self, memory_store: Store) -> None: "configuration": {"chunk_shape": (2,)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "time": ArrayV3Metadata.from_dict( @@ -352,7 +377,7 @@ def test_consolidated_sync(self, memory_store: Store) -> None: "configuration": {"chunk_shape": (3,)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), }, @@ -409,7 +434,9 @@ def test_consolidated_metadata_from_dict(self) -> None: ConsolidatedMetadata.from_dict(data) def test_flatten(self) -> None: - array_metadata: dict[str, Any] = { + # Partial v3 array document (see `test_consolidated_metadata`): `shape` + # and `chunk_grid` are supplied per-array via the spreads below. + array_metadata: ZarrV3ArrayMetadataJSONPartial = { "attributes": {}, "chunk_key_encoding": { "configuration": {"separator": "/"}, @@ -434,7 +461,7 @@ def test_flatten(self) -> None: "configuration": {"chunk_shape": (1, 2, 3)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "lat": ArrayV3Metadata.from_dict( @@ -444,7 +471,7 @@ def test_flatten(self) -> None: "configuration": {"chunk_shape": (1,)}, "name": "regular", }, - **array_metadata, + **array_metadata, # type: ignore[dict-item] } ), "child": GroupMetadata( @@ -453,7 +480,7 @@ def test_flatten(self) -> None: metadata={ "array": ArrayV3Metadata.from_dict( { - **array_metadata, + **array_metadata, # type: ignore[dict-item] "attributes": {"key": "child"}, "shape": (4, 4), "chunk_grid": { @@ -468,7 +495,7 @@ def test_flatten(self) -> None: metadata={ "array": ArrayV3Metadata.from_dict( { - **array_metadata, + **array_metadata, # type: ignore[dict-item] "attributes": {"key": "grandchild"}, "shape": (4, 4), "chunk_grid": { diff --git a/tests/test_metadata/test_v2.py b/tests/test_metadata/test_v2.py index 0f280f0401..3990933684 100644 --- a/tests/test_metadata/test_v2.py +++ b/tests/test_metadata/test_v2.py @@ -21,6 +21,8 @@ from pathlib import Path from typing import Any + from zarr_metadata import ZarrV2ConsolidatedMetadataJSON + from zarr.abc.codec import Codec from zarr.core.common import JSON @@ -108,7 +110,7 @@ class TestConsolidated: async def v2_consolidated_metadata( self, memory_store: zarr.storage.MemoryStore ) -> zarr.storage.MemoryStore: - zmetadata: dict[str, JSON] = { + zmetadata: ZarrV2ConsolidatedMetadataJSON = { "metadata": { ".zattrs": { "Conventions": "COARDS", @@ -171,19 +173,19 @@ async def v2_consolidated_metadata( await store.set(".zmetadata", cpu.Buffer.from_bytes(json.dumps(zmetadata).encode())) await store.set( "air/.zarray", - cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["air/.zarray"]).encode()), # type: ignore[index, call-overload] + cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["air/.zarray"]).encode()), ) await store.set( "air/.zattrs", - cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["air/.zattrs"]).encode()), # type: ignore[index, call-overload] + cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["air/.zattrs"]).encode()), ) await store.set( "time/.zarray", - cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["time/.zarray"]).encode()), # type: ignore[index, call-overload] + cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["time/.zarray"]).encode()), ) await store.set( "time/.zattrs", - cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["time/.zattrs"]).encode()), # type: ignore[index, call-overload] + cpu.Buffer.from_bytes(json.dumps(zmetadata["metadata"]["time/.zattrs"]).encode()), ) # and a nested group for fun @@ -196,13 +198,13 @@ async def v2_consolidated_metadata( await store.set( "nested/array/.zarray", cpu.Buffer.from_bytes( - json.dumps(zmetadata["metadata"]["nested/array/.zarray"]).encode() # type: ignore[index, call-overload] + json.dumps(zmetadata["metadata"]["nested/array/.zarray"]).encode() ), ) await store.set( "nested/array/.zattrs", cpu.Buffer.from_bytes( - json.dumps(zmetadata["metadata"]["nested/array/.zattrs"]).encode() # type: ignore[index, call-overload] + json.dumps(zmetadata["metadata"]["nested/array/.zattrs"]).encode() ), ) diff --git a/tests/test_metadata/test_v3.py b/tests/test_metadata/test_v3.py index d1e156e500..703ac1b7ba 100644 --- a/tests/test_metadata/test_v3.py +++ b/tests/test_metadata/test_v3.py @@ -34,6 +34,9 @@ if TYPE_CHECKING: from typing import Any + from zarr_metadata import ZarrV3GroupMetadataJSON + from zarr_metadata.v3.codec.bytes import BytesCodecObject + # --------------------------------------------------------------------------- # Parsing helpers @@ -178,8 +181,13 @@ def test_array_metadata_keys_matches_typeddict() -> None: # --------------------------------------------------------------------------- # Codecs after evolution for single-byte (uint8) and multi-byte (float64) types. +# The uint8 case omits `configuration`; floor-pinned zarr-metadata 0.1.1 +# marks that field as required, so the annotation is dropped until the +# relaxed shape ships. _UINT8_CODECS = ({"name": "bytes"},) -_FLOAT64_CODECS = ({"name": "bytes", "configuration": {"endian": "little"}},) +_FLOAT64_CODECS: tuple[BytesCodecObject, ...] = ( + {"name": "bytes", "configuration": {"endian": "little"}}, +) @pytest.mark.parametrize( @@ -448,7 +456,11 @@ def test_group_metadata_to_dict_consolidated(attributes: dict[str, Any] | None) ): group = consolidate_metadata(store) - assert group.metadata.to_dict() == { + # `consolidated_metadata` is an `ZarrV3ExtensionField` (extra key allowed + # on `ZarrV3GroupMetadataJSON` via PEP 728 extra_items=ZarrV3ExtensionField). mypy + # doesn't honor PEP 728 yet and reports `typeddict-unknown-key`; the + # annotation is correct, so the error code is ignored at the literal. + expected: ZarrV3GroupMetadataJSON = { # type: ignore[typeddict-unknown-key] "zarr_format": 3, "node_type": "group", "attributes": attributes or {}, @@ -469,3 +481,4 @@ def test_group_metadata_to_dict_consolidated(attributes: dict[str, Any] | None) }, }, } + assert group.metadata.to_dict() == expected diff --git a/uv.lock b/uv.lock index 8eac71caa7..0129b5a91e 100644 --- a/uv.lock +++ b/uv.lock @@ -2,8 +2,25 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "extra == 'group-13-zarr-metadata-docs' and extra != 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", + "extra != 'group-13-zarr-metadata-docs' and extra != 'group-4-zarr-dev' and extra == 'group-4-zarr-docs'", + "python_full_version >= '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", + "python_full_version < '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", + "extra != 'group-13-zarr-metadata-docs' and extra != 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", +] +conflicts = [[ + { package = "zarr", group = "dev" }, + { package = "zarr", group = "docs" }, + { package = "zarr-metadata", group = "docs" }, +], [ + { package = "zarr", group = "dev" }, + { package = "zarr-metadata", group = "docs" }, +]] + +[manifest] +members = [ + "zarr", + "zarr-metadata", ] [[package]] @@ -44,7 +61,7 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, { 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" } @@ -148,7 +165,7 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ @@ -261,7 +278,7 @@ wheels = [ [[package]] name = "aws-sam-translator" -version = "1.109.0" +version = "1.111.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, @@ -269,9 +286,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/09/f62aa8d076f6ba85080ec6291e61af345e9be0daf8a4094101555e054ec7/aws_sam_translator-1.109.0.tar.gz", hash = "sha256:0c5e60223ae8434ce0c6bdb9a491d69ba3ec97e15c0d825d3803f7806382d804", size = 369016, upload-time = "2026-04-08T23:34:32.535Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/31/4e6d6f0b9d4ead8eaa1c13a14d86834e7691acf5726fb49de98f8e195028/aws_sam_translator-1.111.0.tar.gz", hash = "sha256:6884d94e28dc20384e5e0396e9386a456fe59303d706924deb2646329b4d97d3", size = 374368, upload-time = "2026-07-02T00:31:39.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/29/db13205af6bbebdc8dae9dd603ef97ee10a23cd8a3e26d9de728948b2e33/aws_sam_translator-1.109.0-py3-none-any.whl", hash = "sha256:9a6376e7c6d4fee173342b8b557035a8e3ec36e795e175e870411c8e4238873d", size = 432447, upload-time = "2026-04-08T23:34:30.881Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e3/505c9db9c4a4270ad12bac621f370cb02eabb224886c1b81960c658d9baa/aws_sam_translator-1.111.0-py3-none-any.whl", hash = "sha256:510d0ad8cd40b245a62004f2dc974ddbb6ff0d496bdec0bc7d9cd209b46d5fad", size = 440579, upload-time = "2026-07-02T00:31:38.077Z" }, ] [[package]] @@ -455,7 +472,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -603,7 +620,7 @@ name = "click" version = "8.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } wheels = [ @@ -693,7 +710,7 @@ name = "cryptography" version = "48.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } wheels = [ @@ -798,7 +815,7 @@ name = "docker" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, { name = "requests" }, { name = "urllib3" }, ] @@ -1501,10 +1518,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, ] +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + [[package]] name = "mkdocs-material" version = "9.7.7" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "extra != 'group-13-zarr-metadata-docs' and extra != 'group-4-zarr-dev' and extra == 'group-4-zarr-docs'", + "python_full_version >= '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", + "python_full_version < '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", +] dependencies = [ { name = "babel" }, { name = "backrefs" }, @@ -1551,17 +1595,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/90/871b1cddc01d2ba1637b858eeeabc2e3013dc8df591306b5567b98ef0870/mkdocs_redirects-1.2.3-py3-none-any.whl", hash = "sha256:ec7312fff462d03ec16395d0c001006a418f8d0c21cdf2b47ff11cf839dc3ce0", size = 6245, upload-time = "2026-03-28T13:57:40.466Z" }, ] +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2", marker = "extra == 'group-13-zarr-metadata-docs' or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, + { name = "markdown", marker = "extra == 'group-13-zarr-metadata-docs' or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, + { name = "markupsafe", marker = "extra == 'group-13-zarr-metadata-docs' or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, + { name = "mkdocs", marker = "extra == 'group-13-zarr-metadata-docs' or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, + { name = "mkdocs-autorefs", marker = "extra == 'group-13-zarr-metadata-docs' or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, + { name = "pymdown-extensions", marker = "extra == 'group-13-zarr-metadata-docs' or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + [[package]] name = "mkdocstrings" version = "1.0.6" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "extra != 'group-13-zarr-metadata-docs' and extra != 'group-4-zarr-dev' and extra == 'group-4-zarr-docs'", + "python_full_version >= '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", + "python_full_version < '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", +] dependencies = [ - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "pymdown-extensions" }, + { name = "jinja2", marker = "extra == 'group-4-zarr-dev' or extra == 'group-4-zarr-docs'" }, + { name = "markdown", marker = "extra == 'group-4-zarr-dev' or extra == 'group-4-zarr-docs'" }, + { name = "markupsafe", marker = "extra == 'group-4-zarr-dev' or extra == 'group-4-zarr-docs'" }, + { name = "mkdocs", marker = "extra == 'group-4-zarr-dev' or extra == 'group-4-zarr-docs'" }, + { name = "mkdocs-autorefs", marker = "extra == 'group-4-zarr-dev' or extra == 'group-4-zarr-docs'" }, + { name = "pymdown-extensions", marker = "extra == 'group-4-zarr-dev' or extra == 'group-4-zarr-docs'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } wheels = [ @@ -1575,7 +1641,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, + { name = "mkdocstrings", version = "1.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-13-zarr-metadata-docs' or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, + { name = "mkdocstrings", version = "1.0.6", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-4-zarr-dev' or extra == 'group-4-zarr-docs'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } wheels = [ @@ -1945,7 +2012,7 @@ name = "obstore" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/2f/f83afaab7945509d72245b2b00af0b4834ce78fdd2d9ae9f0ad1a3036a91/obstore-0.11.0.tar.gz", hash = "sha256:a2f55163bcd348b4a60d12e6893eac50eddc742bad8032a1705d49140b992204", size = 130565, upload-time = "2026-06-25T18:29:49.405Z" } wheels = [ @@ -2291,7 +2358,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2299,80 +2366,84 @@ dependencies = [ { 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" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" 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" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] @@ -2437,7 +2508,7 @@ name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, @@ -2467,7 +2538,7 @@ version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] 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 = [ @@ -2647,7 +2718,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -2886,10 +2957,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] + [[package]] name = "ruff" version = "0.15.22" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "extra != 'group-13-zarr-metadata-docs' and extra != 'group-4-zarr-dev' and extra == 'group-4-zarr-docs'", + "python_full_version >= '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", + "python_full_version < '3.15' and extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev' and extra != 'group-4-zarr-docs'", +] sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, @@ -2989,7 +3090,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alabaster" }, { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, { name = "docutils" }, { name = "imagesize" }, { name = "jinja2" }, @@ -3116,7 +3217,7 @@ version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, { name = "rich" }, { name = "shellingham" }, ] @@ -3435,6 +3536,7 @@ dependencies = [ { name = "numpy" }, { name = "packaging" }, { name = "typing-extensions" }, + { name = "zarr-metadata" }, ] [package.optional-dependencies] @@ -3445,7 +3547,7 @@ cli = [ { name = "typer" }, ] gpu = [ - { name = "cupy-cuda12x", marker = "sys_platform != 'darwin'" }, + { name = "cupy-cuda12x", marker = "sys_platform != 'darwin' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs') or (extra == 'group-4-zarr-dev' and extra == 'group-4-zarr-docs')" }, ] optional = [ { name = "universal-pathlib" }, @@ -3463,16 +3565,16 @@ dev = [ { name = "fsspec" }, { name = "griffe-inherited-docstrings" }, { name = "hypothesis" }, - { name = "markdown-exec", extra = ["ansi"] }, + { name = "markdown-exec", extra = ["ansi"], marker = "extra == 'group-4-zarr-dev' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs')" }, { name = "mike" }, { name = "mkdocs" }, - { name = "mkdocs-material", extra = ["imaging"] }, + { name = "mkdocs-material", version = "9.7.7", source = { registry = "https://pypi.org/simple" }, extra = ["imaging"], marker = "extra == 'group-4-zarr-dev' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs')" }, { name = "mkdocs-redirects" }, - { name = "mkdocstrings" }, + { name = "mkdocstrings", version = "1.0.6", source = { registry = "https://pypi.org/simple" } }, { name = "mkdocstrings-python" }, - { name = "moto", extra = ["s3", "server"] }, + { name = "moto", extra = ["s3", "server"], marker = "extra == 'group-4-zarr-dev' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs')" }, { name = "mypy" }, - { name = "numcodecs", extra = ["msgpack"] }, + { name = "numcodecs", extra = ["msgpack"], marker = "extra == 'group-4-zarr-dev' or (extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs')" }, { name = "numpydoc" }, { name = "obstore" }, { name = "pytest" }, @@ -3483,7 +3585,7 @@ dev = [ { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "requests" }, - { name = "ruff" }, + { name = "ruff", version = "0.15.22", source = { registry = "https://pypi.org/simple" } }, { name = "s3fs" }, { name = "tomlkit" }, { name = "towncrier" }, @@ -3493,16 +3595,16 @@ dev = [ docs = [ { name = "astroid" }, { name = "griffe-inherited-docstrings" }, - { name = "markdown-exec", extra = ["ansi"] }, + { name = "markdown-exec", extra = ["ansi"], marker = "(extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra != 'group-4-zarr-dev' and extra == 'group-4-zarr-docs') or (extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs')" }, { name = "mike" }, { name = "mkdocs" }, - { name = "mkdocs-material", extra = ["imaging"] }, + { name = "mkdocs-material", version = "9.7.7", source = { registry = "https://pypi.org/simple" }, extra = ["imaging"], marker = "(extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra != 'group-4-zarr-dev' and extra == 'group-4-zarr-docs') or (extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs')" }, { name = "mkdocs-redirects" }, - { name = "mkdocstrings" }, + { name = "mkdocstrings", version = "1.0.6", source = { registry = "https://pypi.org/simple" } }, { name = "mkdocstrings-python" }, - { name = "numcodecs", extra = ["msgpack"] }, + { name = "numcodecs", extra = ["msgpack"], marker = "(extra == 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-dev') or (extra != 'group-4-zarr-dev' and extra == 'group-4-zarr-docs') or (extra != 'group-13-zarr-metadata-docs' and extra == 'group-4-zarr-docs')" }, { name = "pytest" }, - { name = "ruff" }, + { name = "ruff", version = "0.15.22", source = { registry = "https://pypi.org/simple" } }, { name = "s3fs" }, { name = "towncrier" }, ] @@ -3558,6 +3660,7 @@ requires-dist = [ { name = "typer", marker = "extra == 'cli'" }, { name = "typing-extensions", specifier = ">=4.14" }, { name = "universal-pathlib", marker = "extra == 'optional'" }, + { name = "zarr-metadata", editable = "packages/zarr-metadata" }, ] provides-extras = ["cast-value-rs", "cli", "gpu", "optional", "remote"] @@ -3647,3 +3750,43 @@ test = [ { name = "tomlkit", specifier = "==0.15.1" }, { name = "uv", specifier = "==0.11.31" }, ] + +[[package]] +name = "zarr-metadata" +source = { editable = "packages/zarr-metadata" } +dependencies = [ + { name = "typing-extensions" }, +] + +[package.dev-dependencies] +docs = [ + { name = "griffe-inherited-docstrings" }, + { name = "mkdocs" }, + { name = "mkdocs-material", version = "9.7.6", source = { registry = "https://pypi.org/simple" } }, + { name = "mkdocstrings", version = "1.0.4", source = { registry = "https://pypi.org/simple" } }, + { name = "mkdocstrings-python" }, + { name = "ruff", version = "0.15.20", source = { registry = "https://pypi.org/simple" } }, +] +test = [ + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [{ name = "typing-extensions", specifier = ">=4.16" }] + +[package.metadata.requires-dev] +docs = [ + { name = "griffe-inherited-docstrings", specifier = "==1.1.3" }, + { name = "mkdocs", specifier = "==1.6.1" }, + { name = "mkdocs-material", specifier = "==9.7.6" }, + { name = "mkdocstrings", specifier = "==1.0.4" }, + { name = "mkdocstrings-python", specifier = "==2.0.5" }, + { name = "ruff", specifier = "==0.15.20" }, +] +test = [ + { name = "jsonschema" }, + { name = "pydantic", specifier = ">=2.13" }, + { name = "pytest" }, +]