pax_global_header00006660000000000000000000000064151763574300014525gustar00rootroot0000000000000052 comment=85890b3bb404fd1d401267c508a2694f5734559e zarr-python-3.2.1/000077500000000000000000000000001517635743000140255ustar00rootroot00000000000000zarr-python-3.2.1/.git-blame-ignore-revs000066400000000000000000000002321517635743000201220ustar00rootroot00000000000000# lint codebase with black and ruff 4e348d6b80c96da461fd866576c971b8a659ba15 # migrate from black to ruff format 22cea005629913208a85799372e045f353744add zarr-python-3.2.1/.git_archival.txt000066400000000000000000000002011517635743000172710ustar00rootroot00000000000000node: 85890b3bb404fd1d401267c508a2694f5734559e node-date: 2026-05-05T08:14:16-04:00 describe-name: v3.2.1 ref-names: tag: v3.2.1 zarr-python-3.2.1/.gitattributes000066400000000000000000000001341517635743000167160ustar00rootroot00000000000000*.py linguist-language=python *.ipynb linguist-documentation .git_archival.txt export-subst zarr-python-3.2.1/.github/000077500000000000000000000000001517635743000153655ustar00rootroot00000000000000zarr-python-3.2.1/.github/CODEOWNERS000066400000000000000000000000701517635743000167550ustar00rootroot00000000000000zarr/_storage/absstore.py @zarr-developers/azure-team zarr-python-3.2.1/.github/CONTRIBUTING.md000066400000000000000000000002621517635743000176160ustar00rootroot00000000000000Contributing ============ Please see the [project documentation](https://zarr.readthedocs.io/en/stable/developers/contributing.html) for information about contributing to Zarr. zarr-python-3.2.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001517635743000175505ustar00rootroot00000000000000zarr-python-3.2.1/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000051331517635743000224450ustar00rootroot00000000000000name: Bug Report description: Report incorrect behaviour in the library. labels: ["bug"] body: - type: markdown attributes: value: | Please provide the following information. - type: input id: Zarr-version attributes: label: Zarr version description: Value of ``zarr.__version__`` placeholder: v2.10.2, v2.11.3, v2.12.0, etc. validations: required: true - type: input id: Numcodecs-version attributes: label: Numcodecs version description: Value of ``numcodecs.__version__`` placeholder: v0.8.1, v0.9.0, v0.10.0, etc. validations: required: true - type: input id: Python-version attributes: label: Python Version description: Version of Python interpreter placeholder: 3.10, 3.11, 3.12 etc. validations: required: true - type: input id: OS attributes: label: Operating System description: Operating System placeholder: (Linux/Windows/Mac) validations: required: true - type: input id: installation attributes: label: Installation description: How was Zarr installed? placeholder: e.g., "using pip into virtual environment", or "using conda" validations: required: true - type: textarea id: description attributes: label: Description description: Explain why the current behavior is a problem, what the expected output/behaviour is, and why the expected output/behaviour is a better solution. validations: required: true - type: textarea id: reproduce attributes: label: Steps to reproduce description: Minimal, reproducible code sample. Must list dependencies in [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/#example). When put in a file named `issue.py` calling `uv run issue.py` should show the issue. value: | ```python # /// script # requires-python = ">=3.12" # dependencies = [ # "zarr@git+https://github.com/zarr-developers/zarr-python.git@main", # ] # /// # # This script automatically imports the development branch of zarr to check for issues import zarr # your reproducer code # zarr.print_debug_info() ``` validations: required: true - type: textarea id: additional-output attributes: label: Additional output description: If you think it might be relevant, please provide the output from ``pip freeze`` or ``conda env export`` depending on which was used to install Zarr. zarr-python-3.2.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000012221517635743000215350ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: Propose a new Zarr specification feature url: https://github.com/zarr-developers/zarr-specs about: A new feature for the Zarr storage specification should be opened on the zarr-specs repository. - name: Discuss something on ZulipChat url: https://ossci.zulipchat.com/ about: For questions like "How do I do X with Zarr?", consider posting your question to our developer chat. - name: Discuss something on GitHub Discussions url: https://github.com/zarr-developers/zarr-python/discussions about: For questions like "How do I do X with Zarr?", you can move to GitHub Discussions. zarr-python-3.2.1/.github/ISSUE_TEMPLATE/documentation.yml000066400000000000000000000012021517635743000231370ustar00rootroot00000000000000name: Documentation Improvement description: Report missing or wrong documentation. Alternatively, you can just open a pull request with the suggested change. title: "DOC: " labels: [documentation, help wanted] body: - type: textarea attributes: label: Describe the issue linked to the documentation description: > Please provide a description of what documentation you believe needs to be fixed/improved. validations: required: true - type: textarea attributes: label: Suggested fix for documentation description: > Please explain the suggested fix and why it's better than the existing documentation. zarr-python-3.2.1/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000005111517635743000234730ustar00rootroot00000000000000name: Feature Request description: Request a new feature for zarr-python # labels: [] body: - type: textarea attributes: label: Describe the new feature you'd like description: > Please provide a description of what new feature or functionality you'd like to see in zarr-python. validations: required: true zarr-python-3.2.1/.github/ISSUE_TEMPLATE/release-checklist.md000066400000000000000000000056101517635743000234630ustar00rootroot00000000000000--- name: Zarr-Python release checklist about: Checklist for a new Zarr-Python release. [For project maintainers only!] title: Release Zarr-Python vX.Y.Z labels: release-checklist assignees: '' --- **Release**: [v3.x.x](https://github.com/zarr-developers/zarr-python/milestones/?) **Scheduled Date**: 20YY/MM/DD **Priority PRs/issues to complete prior to release** - [ ] Priority pull request #X **Before release**: - [ ] Check [SPEC 0](https://scientific-python.org/specs/spec-0000/#support-window) to see if the minimum supported version of Python or NumPy needs bumping. - [ ] Verify that the latest CI workflows on `main` are passing: [Tests](https://github.com/zarr-developers/zarr-python/actions/workflows/test.yml), [GPU Tests](https://github.com/zarr-developers/zarr-python/actions/workflows/gpu_test.yml), [Hypothesis](https://github.com/zarr-developers/zarr-python/actions/workflows/hypothesis.yaml), [Docs](https://github.com/zarr-developers/zarr-python/actions/workflows/docs.yml), [Lint](https://github.com/zarr-developers/zarr-python/actions/workflows/lint.yml), [Wheels](https://github.com/zarr-developers/zarr-python/actions/workflows/releases.yml). - [ ] Run the ["Prepare release" workflow](https://github.com/zarr-developers/zarr-python/actions/workflows/prepare_release.yml) with the target version. This will build the changelog and open a release PR with the `run-downstream` label. - [ ] Verify that the [downstream tests](https://github.com/zarr-developers/zarr-python/actions/workflows/downstream.yml) (triggered automatically by the `run-downstream` label) pass on the release PR. - [ ] Review the release PR and verify the changelog in `docs/release-notes.md` looks correct. - [ ] Merge the release PR. **Release**: - [ ] [Draft a new GitHub Release](https://github.com/zarr-developers/zarr-python/releases/new) with tag `vX.Y.Z` targeting `main`. Use "Generate release notes" for the description. - [ ] Verify the release is published on [PyPI](https://pypi.org/project/zarr/) and [ReadTheDocs](https://zarr.readthedocs.io/en/stable/). **After release**: - [ ] Review and merge the pull request on the conda-forge [zarr-feedstock](https://github.com/conda-forge/zarr-feedstock) that will be automatically generated. --- - [ ] Party :tada: ---
Releasing from a branch other than main In rare cases (e.g. patch releases for an older minor version), you may need to release from a dedicated release branch (e.g. `3.1.x`): - Create the release branch from the appropriate tag if it doesn't already exist. - Cherry-pick or backport the necessary commits onto the branch. - Run `towncrier build --version x.y.z` and commit the result to the release branch instead of `main`. - When drafting the GitHub Release, set the target to the release branch instead of `main`. - After the release, ensure any relevant changelog updates are also reflected on `main`.
zarr-python-3.2.1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000005571517635743000211750ustar00rootroot00000000000000[Description of PR] TODO: * [ ] Add unit tests and/or doctests in docstrings * [ ] Add docstrings and API docs for any new/modified user-facing classes and functions * [ ] New/modified features documented in `docs/user-guide/*.md` * [ ] Changes documented as a new file in `changes/` * [ ] GitHub Actions have all passed * [ ] Test coverage is 100% (Codecov passes) zarr-python-3.2.1/.github/dependabot.yml000066400000000000000000000007231517635743000202170ustar00rootroot00000000000000--- version: 2 updates: # Updates for main - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" groups: actions: patterns: - "*" cooldown: default-days: 7 - package-ecosystem: "github-actions" directory: "/" target-branch: "support/v2" schedule: interval: "weekly" groups: actions: patterns: - "*" cooldown: default-days: 7 zarr-python-3.2.1/.github/labeler.yml000066400000000000000000000001431517635743000175140ustar00rootroot00000000000000needs release notes: - all: - changed-files: - all-globs-to-all-files: '!changes/*.md' zarr-python-3.2.1/.github/workflows/000077500000000000000000000000001517635743000174225ustar00rootroot00000000000000zarr-python-3.2.1/.github/workflows/check_changelogs.yml000066400000000000000000000012021517635743000234070ustar00rootroot00000000000000name: Check changelog entries on: pull_request: workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check-changelogs: name: Check changelog entries runs-on: ubuntu-latest steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 - name: Check changelog entries run: uv run --no-sync python ci/check_changelog_entries.py zarr-python-3.2.1/.github/workflows/codspeed.yml000066400000000000000000000020641517635743000217350ustar00rootroot00000000000000name: CodSpeed Benchmarks on: schedule: - cron: '0 9 * * 1' # Every Monday at 9am UTC pull_request: types: [labeled] workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: benchmarks: name: Run benchmarks runs-on: codspeed-macro if: | github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'benchmark')) steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Run the benchmarks uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4.12.1 with: mode: walltime run: hatch run test.py3.12-minimal:pytest tests/benchmarks --codspeed zarr-python-3.2.1/.github/workflows/docs.yml000066400000000000000000000013711517635743000210770ustar00rootroot00000000000000name: Docs on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: docs: name: Check docs runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 - run: uv sync --group docs - run: uv run mkdocs build env: DISABLE_MKDOCS_2_WARNING: "true" NO_MKDOCS_2_WARNING: "true" - run: uv run python ci/check_unlinked_types.py continue-on-error: true zarr-python-3.2.1/.github/workflows/downstream.yml000066400000000000000000000065521517635743000223400ustar00rootroot00000000000000name: Downstream on: workflow_dispatch: pull_request: types: [labeled] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: xarray: name: Xarray zarr backend tests if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'run-downstream' runs-on: ubuntu-latest steps: - name: Check out zarr-python uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Check out xarray uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: pydata/xarray path: xarray persist-credentials: false - name: Set up pixi uses: prefix-dev/setup-pixi@19eac09b398e3d0c747adc7921926a6d802df4da # v0.8.8 with: manifest-path: xarray/pixi.toml - name: Install zarr-python from branch working-directory: xarray run: pixi run -e test-py313 -- pip install --no-deps .. - name: Show versions working-directory: xarray run: | pixi run -e test-py313 -- python -c " import zarr; print(f'zarr {zarr.__version__}') import xarray; print(f'xarray {xarray.__version__}') " - name: Run xarray zarr backend tests working-directory: xarray run: | pixi run -e test-py313 -- python -m pytest -x --no-header -q \ xarray/tests/test_backends.py -k zarr \ xarray/tests/test_backends_api.py -k zarr \ xarray/tests/test_backends_datatree.py -k zarr numcodecs: name: numcodecs zarr3 codec tests if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'run-downstream' runs-on: ubuntu-latest steps: - name: Check out zarr-python uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Check out numcodecs uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: zarr-developers/numcodecs fetch-depth: 0 path: numcodecs submodules: recursive persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.13' - name: Install uv uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - name: Install numcodecs with test-zarr-main group working-directory: numcodecs run: | uv venv uv pip install --group dev uv sync --group dev --group test-zarr-main uv pip install --no-build-isolation -e . - name: Override zarr-python with branch version working-directory: numcodecs run: uv pip install --no-deps .. - name: Show versions working-directory: numcodecs run: | uv run python -c " import zarr; print(f'zarr {zarr.__version__}') import numcodecs; print(f'numcodecs {numcodecs.__version__}') " - name: Run numcodecs zarr3 tests working-directory: numcodecs run: uv run python -m pytest -x --no-header -q tests/test_zarr3.py zarr-python-3.2.1/.github/workflows/gpu_test.yml000066400000000000000000000046301517635743000220020ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: GPU Test on: push: branches: [ main, 3.1.x ] pull_request: branches: [ main, 3.1.x ] workflow_dispatch: env: LD_LIBRARY_PATH: /usr/local/cuda/extras/CUPTI/lib64:/usr/local/cuda/lib64 permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: name: py=${{ matrix.python-version }} environment: name: codecov-upload deployment: false runs-on: gpu-runner strategy: matrix: python-version: ['3.12'] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # grab all branches and tags persist-credentials: false # - name: cuda-toolkit # uses: Jimver/cuda-toolkit@v0.2.16 # id: cuda-toolkit # with: # cuda: '12.4.1' - name: Set up CUDA run: | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-keyring_1.1-1_all.deb sudo dpkg -i cuda-keyring_1.1-1_all.deb sudo apt-get update sudo apt-get -y install cuda-toolkit-12-6 echo "/usr/local/cuda/bin" >> $GITHUB_PATH - name: GPU check run: | nvidia-smi echo $PATH echo $LD_LIBRARY_PATH nvcc -V - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Set Up Hatch Env env: HATCH_ENV: gputest.py${{ matrix.python-version }} run: | hatch env create "$HATCH_ENV" hatch env run -e "$HATCH_ENV" list-env - name: Run Tests env: HATCH_ENV: gputest.py${{ matrix.python-version }} run: | hatch env run --env "$HATCH_ENV" run-coverage - name: Upload coverage uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 with: token: ${{ secrets.CODECOV_TOKEN }} flags: gpu verbose: true # optional (default = false) zarr-python-3.2.1/.github/workflows/hypothesis.yaml000066400000000000000000000070421517635743000225100ustar00rootroot00000000000000name: Slow Hypothesis CI on: push: branches: [main, 3.1.x] pull_request: branches: [main, 3.1.x] types: [opened, reopened, synchronize, labeled] schedule: - cron: "0 0 * * *" # Daily “At 00:00” UTC workflow_dispatch: # allows you to trigger manually permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: FORCE_COLOR: 3 jobs: hypothesis: name: Slow Hypothesis Tests environment: name: codecov-upload deployment: false runs-on: "ubuntu-latest" defaults: run: shell: bash -l {0} strategy: matrix: python-version: ['3.12'] dependency-set: ["optional"] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set HYPOTHESIS_PROFILE based on trigger env: EVENT_NAME: ${{ github.event_name }} run: | if [[ "$EVENT_NAME" == "schedule" || "$EVENT_NAME" == "workflow_dispatch" ]]; then echo "HYPOTHESIS_PROFILE=nightly" >> $GITHUB_ENV else echo "HYPOTHESIS_PROFILE=ci" >> $GITHUB_ENV fi - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Set Up Hatch Env env: HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }} run: | hatch env create "$HATCH_ENV" hatch env run -e "$HATCH_ENV" list-env # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#update-a-cache - name: Restore cached hypothesis directory id: restore-hypothesis-cache uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: .hypothesis/ key: cache-hypothesis-${{ runner.os }}-${{ github.run_id }} restore-keys: | cache-hypothesis- - name: Run slow Hypothesis tests if: success() id: status env: HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }} run: | echo "Using Hypothesis profile: $HYPOTHESIS_PROFILE" hatch env run --env "$HATCH_ENV" run-hypothesis # explicitly save the cache so it gets updated, also do this even if it fails. - name: Save cached hypothesis directory id: save-hypothesis-cache if: always() && steps.status.outcome != 'skipped' uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: .hypothesis/ key: cache-hypothesis-${{ runner.os }}-${{ github.run_id }} - name: Upload coverage uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests verbose: true # optional (default = false) - name: Generate and publish the report if: | failure() && steps.status.outcome == 'failure' && github.event_name == 'schedule' && github.repository_owner == 'zarr-developers' uses: scientific-python/issue-from-pytest-log-action@8e905db353437cda1d6a773de245343fbfc940dd # v1.5.0 with: log-path: output-${{ matrix.python-version }}-log.jsonl issue-title: "Nightly Hypothesis tests failed" issue-label: "topic-hypothesis" zarr-python-3.2.1/.github/workflows/issue-metrics.yml000066400000000000000000000026511517635743000227450ustar00rootroot00000000000000name: Monthly issue metrics on: workflow_dispatch: schedule: - cron: '3 2 1 * *' permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: name: issue metrics runs-on: ubuntu-latest permissions: issues: write # Required to create the metrics report issue pull-requests: read # Required to read PR metrics steps: - name: Get dates for last month shell: bash run: | # Calculate the first day of the previous month first_day=$(date -d "last month" +%Y-%m-01) # Calculate the last day of the previous month last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) #Set an environment variable with the date range echo "$first_day..$last_day" echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - name: Run issue-metrics tool uses: github/issue-metrics@67526e7bd8100b870f10b1c120780a8375777b43 # v3.25.5 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: 'repo:zarr-developers/zarr-python is:issue created:${{ env.last_month }} -reason:"not planned"' - name: Create issue uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0 with: title: Monthly issue metrics report token: ${{ secrets.GITHUB_TOKEN }} content-filepath: ./issue_metrics.md zarr-python-3.2.1/.github/workflows/lint.yml000066400000000000000000000010101517635743000211030ustar00rootroot00000000000000name: Lint on: push: branches: [main, 3.1.x] pull_request: branches: [main, 3.1.x] workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: j178/prek-action@0bb87d7f00b0c99306c8bcb8b8beba1eb581c037 # v1.1.1 zarr-python-3.2.1/.github/workflows/needs_release_notes.yml000066400000000000000000000016371517635743000241620ustar00rootroot00000000000000name: "Pull Request Labeler" on: # pull_request_target is needed to label PRs from forks. # This workflow only runs actions/labeler (no code checkout), so it's safe. pull_request_target: # zizmor: ignore[dangerous-triggers] types: [opened, reopened, synchronize] permissions: {} concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: labeler: name: Label pull request if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'pre-commit-ci[bot]' }} permissions: contents: read # Required to read label configuration pull-requests: write # Required to add labels to PRs runs-on: ubuntu-latest steps: - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} sync-labels: true zarr-python-3.2.1/.github/workflows/nightly_wheels.yml000066400000000000000000000023151517635743000231730ustar00rootroot00000000000000name: Nightly Wheels on: schedule: # Run nightly at 2 AM UTC - cron: '0 2 * * *' workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build_and_upload_nightly: name: Build and upload nightly wheels environment: name: nightly-wheel-upload deployment: false runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 name: Install Python with: python-version: '3.14' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Build wheel and sdist run: hatch build - name: Upload nightly wheels uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} zarr-python-3.2.1/.github/workflows/prepare_release.yml000066400000000000000000000052241517635743000233060ustar00rootroot00000000000000name: Prepare release notes on: workflow_dispatch: inputs: version: description: 'Release version notes (e.g. 3.2.0)' required: true type: string target_branch: description: 'Branch to target' required: false default: 'main' type: string permissions: contents: write pull-requests: write jobs: prepare: name: Build changelog and open PR runs-on: ubuntu-latest steps: - name: Validate inputs run: | if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-\.][a-zA-Z0-9]+)*$ ]]; then echo "::error::Invalid version format: '$VERSION'" exit 1 fi if [[ ! "$TARGET_BRANCH" =~ ^[a-zA-Z0-9._/-]+$ ]]; then echo "::error::Invalid branch name: '$TARGET_BRANCH'" exit 1 fi env: VERSION: ${{ inputs.version }} TARGET_BRANCH: ${{ inputs.target_branch }} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_branch }} fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' - name: Install towncrier run: pip install towncrier - name: Build changelog run: towncrier build --version "$VERSION" --yes env: VERSION: ${{ inputs.version }} - name: Create pull request uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: branch: release/v${{ inputs.version }} base: ${{ inputs.target_branch }} title: "Release v${{ inputs.version }}" body: | Automated release preparation for v${{ inputs.version }}. This PR was generated by the "Prepare release" workflow. It includes: - Rendered changelog via `towncrier build --version ${{ inputs.version }}` - Removal of consumed changelog fragments from `changes/` ## Checklist - [ ] Review the rendered changelog in `docs/release-notes.md` - [ ] Downstream tests pass (see [downstream workflow](https://github.com/zarr-developers/zarr-python/actions/workflows/downstream.yml)) - [ ] Merge this PR, then [draft a GitHub Release](https://github.com/zarr-developers/zarr-python/releases/new) targeting `${{ inputs.target_branch }}` with tag `v${{ inputs.version }}` commit-message: "chore: build changelog for v${{ inputs.version }}" labels: run-downstream delete-branch: true zarr-python-3.2.1/.github/workflows/releases.yml000066400000000000000000000044561517635743000217610ustar00rootroot00000000000000name: Wheels on: release: types: - published push: branches: [main] pull_request: branches: [main] workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build_artifacts: name: Build wheel on ubuntu-latest runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: submodules: true fetch-depth: 0 persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 name: Install Python with: python-version: '3.12' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Build wheel and sdist run: hatch build - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: releases path: dist test_dist_pypi: name: Test distribution artifacts needs: [build_artifacts] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: releases path: dist - name: test run: | ls ls dist upload_pypi: name: Upload to PyPI needs: [build_artifacts, test_dist_pypi] runs-on: ubuntu-latest if: github.event_name == 'release' environment: name: releases url: https://pypi.org/p/zarr permissions: id-token: write # Required for OIDC trusted publishing to PyPI attestations: write # Required for artifact attestation artifact-metadata: write # Required for artifact attestation metadata steps: - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: releases path: dist - name: Generate artifact attestation uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 with: subject-path: dist/* - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 zarr-python-3.2.1/.github/workflows/test.yml000066400000000000000000000134561517635743000211350ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Test on: push: branches: [ main, 3.1.x ] pull_request: branches: [ main, 3.1.x ] workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: name: os=${{ matrix.os }}, py=${{ matrix.python-version }}, deps=${{ matrix.dependency-set }} environment: name: codecov-upload deployment: false defaults: run: shell: bash strategy: matrix: python-version: ['3.12', '3.13', '3.14'] dependency-set: ["minimal", "optional"] os: ["ubuntu-latest"] include: - python-version: '3.12' dependency-set: 'optional' os: 'macos-latest' - python-version: '3.14' dependency-set: 'optional' os: 'macos-latest' - python-version: '3.12' dependency-set: 'optional' os: 'windows-latest' - python-version: '3.14' dependency-set: 'optional' os: 'windows-latest' runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # grab all branches and tags persist-credentials: false - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Set Up Hatch Env env: HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }} run: | hatch env create "$HATCH_ENV" hatch env run -e "$HATCH_ENV" list-env - name: Run Tests env: HYPOTHESIS_PROFILE: ci HATCH_ENV: test.py${{ matrix.python-version }}-${{ matrix.dependency-set }} run: | hatch env run --env "$HATCH_ENV" run-coverage - name: Upload coverage if: ${{ matrix.dependency-set == 'optional' && matrix.os == 'ubuntu-latest' }} uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests verbose: true # optional (default = false) test-upstream-and-min-deps: name: py=${{ matrix.python-version }}-${{ matrix.dependency-set }} environment: name: codecov-upload deployment: false runs-on: ubuntu-latest strategy: matrix: python-version: ['3.12', "3.14"] dependency-set: ["upstream", "min_deps"] exclude: - python-version: "3.14" dependency-set: min_deps - python-version: "3.12" dependency-set: upstream steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Set Up Hatch Env env: HATCH_ENV: ${{ matrix.dependency-set }} run: | hatch env create "$HATCH_ENV" hatch env run -e "$HATCH_ENV" list-env - name: Run Tests env: HATCH_ENV: ${{ matrix.dependency-set }} run: | hatch env run --env "$HATCH_ENV" run-coverage - name: Upload coverage uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 with: token: ${{ secrets.CODECOV_TOKEN }} flags: tests verbose: true # optional (default = false) doctests: name: doctests runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # required for hatch version discovery, which is needed for numcodecs.zarr3 persist-credentials: false - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.13' cache: 'pip' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Set Up Hatch Env run: | hatch run doctest:pip list - name: Run Tests run: | hatch run doctest:test benchmarks: name: Benchmark smoke test runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.13' cache: 'pip' - name: Install Hatch uses: pypa/hatch@257e27e51a6a5616ed08a39a408a21c35c9931bc with: version: '1.16.5' - name: Run Benchmarks run: | hatch env run --env "test.py3.13-minimal" run-benchmark test-complete: name: Test complete needs: [ test, test-upstream-and-min-deps, doctests, benchmarks ] if: always() runs-on: ubuntu-latest steps: - name: Check failure if: | contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') run: exit 1 - name: Success run: echo Success! zarr-python-3.2.1/.github/workflows/zarr-metadata.yml000066400000000000000000000053331517635743000227050ustar00rootroot00000000000000name: zarr-metadata on: push: branches: [main] paths: - 'packages/zarr-metadata/**' - '.github/workflows/zarr-metadata.yml' pull_request: paths: - 'packages/zarr-metadata/**' - '.github/workflows/zarr-metadata.yml' workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: name: pytest py=${{ matrix.python-version }} runs-on: ubuntu-latest defaults: run: shell: bash working-directory: packages/zarr-metadata strategy: fail-fast: false matrix: python-version: ['3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true - 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: Run pytest run: uv run --group test pytest tests ruff: name: ruff runs-on: ubuntu-latest defaults: run: shell: bash working-directory: packages/zarr-metadata steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - name: Run ruff run: uvx ruff check . pyright: name: pyright runs-on: ubuntu-latest defaults: run: shell: bash working-directory: packages/zarr-metadata steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true - name: Set up Python run: uv python install 3.11 - name: Sync test dependency group run: uv sync --group test --python 3.11 - name: Run pyright run: uv run --group test --with pyright pyright src zarr-metadata-complete: name: zarr-metadata complete needs: [test, ruff, pyright] if: always() runs-on: ubuntu-latest steps: - name: Check failure if: | contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') run: exit 1 - name: Success run: echo Success! zarr-python-3.2.1/.github/workflows/zizmor.yml000066400000000000000000000015021517635743000214750ustar00rootroot00000000000000name: GitHub Actions Security Analysis on: push: branches: [main] paths: - '.github/workflows/**' - '.github/actions/**' pull_request: branches: ["**"] paths: - '.github/workflows/**' - '.github/actions/**' workflow_dispatch: permissions: {} concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: zizmor: name: Run zizmor runs-on: ubuntu-latest permissions: security-events: write # Required by zizmor-action to upload SARIF files steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Run zizmor uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 zarr-python-3.2.1/.gitignore000066400000000000000000000021101517635743000160070ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ .venv/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .coverage .coverage.* .cache coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Documentation site/ docs/_build/ docs/data data data.zip # PyBuilder target/ # PyCharm .idea # Jupyter .ipynb_checkpoints/ # VCS versioning src/zarr/_version.py # emacs *~ # VSCode .vscode/ # test data #*.zarr #*.zip #example* #doesnotexist #test_sync* data/* src/fixture/ fixture/ junit.xml .DS_Store tests/.hypothesis .hypothesis/ zarr/version.py zarr.egg-info/ # zarr-metadata package lockfile (a library, not an app) packages/zarr-metadata/uv.lock zarr-python-3.2.1/.pre-commit-config.yaml000066400000000000000000000041111517635743000203030ustar00rootroot00000000000000ci: autoupdate_commit_msg: "chore: update pre-commit hooks" autoupdate_schedule: "monthly" autofix_prs: false skip: [] # pre-commit.ci only checks for updates, prek runs hooks locally default_stages: [pre-commit, pre-push] default_language_version: python: python3.12 repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.4 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell args: ["-L", "fo,ihs,kake,te", "-S", "fixture"] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-yaml exclude: mkdocs.yml - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.19.1 hooks: - id: mypy files: ^(src|tests)/ additional_dependencies: # Package dependencies - packaging - donfig - numcodecs - google-crc32c>=1.5 - numpy==2.1 # https://github.com/zarr-developers/zarr-python/issues/3780 + https://github.com/zarr-developers/zarr-python/issues/3688 - typing_extensions - universal-pathlib - obstore>=0.5.1 # Tests - pytest - hypothesis - s3fs - repo: https://github.com/scientific-python/cookie rev: 2026.03.02 hooks: - id: sp-repo-review - repo: https://github.com/numpy/numpydoc rev: v1.10.0 hooks: - id: numpydoc-validation - repo: local hooks: - id: ban-lstrip-rstrip name: ban lstrip/rstrip language: pygrep # Matches .lstrip() or .rstrip() where the string argument is 2+ characters. entry: "\\.(lstrip|rstrip)\\([\"'][^\"']{2,}[\"']\\)" types: [python] files: ^(src|tests)/ - repo: https://github.com/zizmorcore/zizmor-pre-commit rev: v1.23.1 hooks: - id: zizmor - repo: https://github.com/twisted/towncrier rev: 25.8.0 hooks: - id: towncrier-check zarr-python-3.2.1/.pyup.yml000066400000000000000000000005131517635743000156220ustar00rootroot00000000000000# pyup.io config file # see https://pyup.io/docs/configuration/ for all available options schedule: every month requirements: - requirements_dev_minimal.txt: pin: True update: all - requirements_dev_numpy.txt: pin: True update: all - requirements_dev_optional.txt: pin: True update: all zarr-python-3.2.1/.readthedocs.yaml000066400000000000000000000007031517635743000172540ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.12" jobs: install: - pip install --upgrade pip - pip install .[remote] --group docs pre_build: - | if [ "$READTHEDOCS_VERSION_TYPE" != "tag" ]; then towncrier build --version Unreleased --yes; fi build: html: - mkdocs build --strict --site-dir $READTHEDOCS_OUTPUT/html mkdocs: configuration: mkdocs.yml zarr-python-3.2.1/FUNDING.yml000066400000000000000000000001031517635743000156340ustar00rootroot00000000000000github: [numfocus] custom: ['https://numfocus.org/donate-to-zarr'] zarr-python-3.2.1/LICENSE.txt000066400000000000000000000021441517635743000156510ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2025 Zarr Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. zarr-python-3.2.1/README.md000066400000000000000000000145101517635743000153050ustar00rootroot00000000000000

# Zarr
Latest Release latest release
latest release
Package Status status
License license
Build Status build status
Pre-commit Status pre-commit status
Coverage coverage
Downloads pypi downloads
Developer Chat
Funding CZI's Essential Open Source Software for Science
Citation DOI
## What is it? Zarr is a Python package providing an implementation of compressed, chunked, N-dimensional arrays, designed for use in parallel computing. See the [documentation](https://zarr.readthedocs.io) for more information. ## Main Features - [**Create**](https://zarr.readthedocs.io/en/stable/user-guide/arrays.html#creating-an-array) N-dimensional arrays with any NumPy `dtype`. - [**Chunk arrays**](https://zarr.readthedocs.io/en/stable/user-guide/performance.html#chunk-optimizations) along any dimension. - [**Compress**](https://zarr.readthedocs.io/en/stable/user-guide/arrays.html#compressors) and/or filter chunks using any NumCodecs codec. - [**Store arrays**](https://zarr.readthedocs.io/en/stable/user-guide/storage.html) in memory, on disk, inside a zip file, on S3, etc... - [**Read**](https://zarr.readthedocs.io/en/stable/user-guide/arrays.html#reading-and-writing-data) an array [**concurrently**](https://zarr.readthedocs.io/en/stable/user-guide/performance.html#parallel-computing-and-synchronization) from multiple threads or processes. - [**Write**](https://zarr.readthedocs.io/en/stable/user-guide/arrays.html#reading-and-writing-data) to an array concurrently from multiple threads or processes. - Organize arrays into hierarchies via [**groups**](https://zarr.readthedocs.io/en/stable/quickstart.html#hierarchical-groups). ## Where to get it Zarr can be installed from PyPI using `pip`: ```bash pip install zarr ``` or via `conda`: ```bash conda install -c conda-forge zarr ``` For more details, including how to install from source, see the [installation documentation](https://zarr.readthedocs.io/en/stable/index.html#installation). zarr-python-3.2.1/TEAM.md000066400000000000000000000013521517635743000150760ustar00rootroot00000000000000## Active core-developers - @joshmoore (Josh Moore) - @jni (Juan Nunez-Iglesias) - @rabernat (Ryan Abernathey) - @jhamman (Joe Hamman) - @d-v-b (Davis Bennett) - @jakirkham (jakirkham) - @martindurant (Martin Durant) - @normanrz (Norman Rzepka) - @dstansby (David Stansby) - @dcherian (Deepak Cherian) - @TomAugspurger (Tom Augspurger) - @maxrjones (Max Jones) ## Emeritus core-developers - @alimanfoo (Alistair Miles) - @shoyer (Stephan Hoyer) - @ryan-williams (Ryan Williams) - @jrbourbeau (James Bourbeau) - @mzjp2 (Zain Patel) - @grlee77 (Gregory Lee) ## Former core-developers - @jeromekelleher (Jerome Kelleher) - @tjcrone (Tim Crone) - @funkey (Jan Funke) - @shikharsg - @Carreau (Matthias Bussonnier) - @dazzag24 - @WardF (Ward Fisher) zarr-python-3.2.1/bench/000077500000000000000000000000001517635743000151045ustar00rootroot00000000000000zarr-python-3.2.1/bench/compress_normal.py000066400000000000000000000016551517635743000206700ustar00rootroot00000000000000import sys import timeit import blosc import line_profiler import numpy as np import zarr if __name__ == "__main__": sys.path.insert(0, "..") # setup a = np.random.normal(2000, 1000, size=200000000).astype("u2") z = zarr.empty_like( a, chunks=1000000, compression="blosc", compression_opts={"cname": "lz4", "clevel": 5, "shuffle": 2}, ) print(z) print("*" * 79) # time t = timeit.repeat("z[:] = a", repeat=10, number=1, globals=globals()) print(t) print(min(t)) print(z) # profile profile = line_profiler.LineProfiler(blosc.compress) profile.run("z[:] = a") profile.print_stats() print("*" * 79) # time t = timeit.repeat("z[:]", repeat=10, number=1, globals=globals()) print(t) print(min(t)) # profile profile = line_profiler.LineProfiler(blosc.decompress) profile.run("z[:]") profile.print_stats() zarr-python-3.2.1/bench/compress_normal.txt000066400000000000000000000252341517635743000210560ustar00rootroot00000000000000zarr.core.Array((200000000,), uint16, chunks=(1000000,), order=C) compression: blosc; compression_opts: {'clevel': 5, 'cname': 'lz4', 'shuffle': 2} nbytes: 381.5M; nbytes_stored: 294; ratio: 1360544.2; initialized: 0/200 store: builtins.dict ******************************************************************************* [0.27119584499996563, 0.2855067059999783, 0.2887747180002407, 0.3058794240005227, 0.3139041080003153, 0.3021271820007314, 0.31543190899992624, 0.31403100900024583, 0.3272544129995367, 0.31834129100025166] 0.27119584499996563 zarr.core.Array((200000000,), uint16, chunks=(1000000,), order=C) compression: blosc; compression_opts: {'clevel': 5, 'cname': 'lz4', 'shuffle': 2} nbytes: 381.5M; nbytes_stored: 314.1M; ratio: 1.2; initialized: 200/200 store: builtins.dict Timer unit: 1e-06 s Total time: 0.297223 s File: /home/aliman/code/github/alimanfoo/zarr/zarr/blosc.pyx Function: compress at line 137 Line # Hits Time Per Hit % Time Line Contents ============================================================== 137 def compress(source, char* cname, int clevel, int shuffle): 138 """Compress data in a numpy array. 139 140 Parameters 141 ---------- 142 source : array-like 143 Data to be compressed. 144 cname : bytes 145 Name of compression library to use. 146 clevel : int 147 Compression level. 148 shuffle : int 149 Shuffle filter. 150 151 Returns 152 ------- 153 dest : bytes-like 154 Compressed data. 155 156 """ 157 158 cdef: 159 char *source_ptr 160 char *dest_ptr 161 Py_buffer source_buffer 162 size_t nbytes, cbytes, itemsize 163 200 506 2.5 0.2 array.array char_array_template = array.array('b', []) 164 array.array dest 165 166 # setup source buffer 167 200 458 2.3 0.2 PyObject_GetBuffer(source, &source_buffer, PyBUF_ANY_CONTIGUOUS) 168 200 119 0.6 0.0 source_ptr = source_buffer.buf 169 170 # setup destination 171 200 239 1.2 0.1 nbytes = source_buffer.len 172 200 103 0.5 0.0 itemsize = source_buffer.itemsize 173 200 2286 11.4 0.8 dest = array.clone(char_array_template, nbytes + BLOSC_MAX_OVERHEAD, 174 zero=False) 175 200 129 0.6 0.0 dest_ptr = dest.data.as_voidptr 176 177 # perform compression 178 200 1734 8.7 0.6 if _get_use_threads(): 179 # allow blosc to use threads internally 180 200 167 0.8 0.1 compressor_set = blosc_set_compressor(cname) 181 200 94 0.5 0.0 if compressor_set < 0: 182 raise ValueError('compressor not supported: %r' % cname) 183 200 288570 1442.8 97.1 with nogil: 184 cbytes = blosc_compress(clevel, shuffle, itemsize, nbytes, 185 source_ptr, dest_ptr, 186 nbytes + BLOSC_MAX_OVERHEAD) 187 188 else: 189 with nogil: 190 cbytes = blosc_compress_ctx(clevel, shuffle, itemsize, nbytes, 191 source_ptr, dest_ptr, 192 nbytes + BLOSC_MAX_OVERHEAD, cname, 193 0, 1) 194 195 # release source buffer 196 200 616 3.1 0.2 PyBuffer_Release(&source_buffer) 197 198 # check compression was successful 199 200 120 0.6 0.0 if cbytes <= 0: 200 raise RuntimeError('error during blosc compression: %d' % cbytes) 201 202 # resize after compression 203 200 1896 9.5 0.6 array.resize(dest, cbytes) 204 205 200 186 0.9 0.1 return dest ******************************************************************************* [0.24293352799941204, 0.2324290420001489, 0.24935673900017719, 0.25716222699975333, 0.24246313799994823, 0.23272456500035332, 0.2636815870000646, 0.2576046349995522, 0.2781278639995435, 0.23824110699933954] 0.2324290420001489 Timer unit: 1e-06 s Total time: 0.240178 s File: /home/aliman/code/github/alimanfoo/zarr/zarr/blosc.pyx Function: decompress at line 75 Line # Hits Time Per Hit % Time Line Contents ============================================================== 75 def decompress(source, dest): 76 """Decompress data. 77 78 Parameters 79 ---------- 80 source : bytes-like 81 Compressed data, including blosc header. 82 dest : array-like 83 Object to decompress into. 84 85 Notes 86 ----- 87 Assumes that the size of the destination buffer is correct for the size of 88 the uncompressed data. 89 90 """ 91 cdef: 92 int ret 93 char *source_ptr 94 char *dest_ptr 95 Py_buffer source_buffer 96 array.array source_array 97 Py_buffer dest_buffer 98 size_t nbytes 99 100 # setup source buffer 101 200 573 2.9 0.2 if PY2 and isinstance(source, array.array): 102 # workaround fact that array.array does not support new-style buffer 103 # interface in PY2 104 release_source_buffer = False 105 source_array = source 106 source_ptr = source_array.data.as_voidptr 107 else: 108 200 112 0.6 0.0 release_source_buffer = True 109 200 144 0.7 0.1 PyObject_GetBuffer(source, &source_buffer, PyBUF_ANY_CONTIGUOUS) 110 200 98 0.5 0.0 source_ptr = source_buffer.buf 111 112 # setup destination buffer 113 200 552 2.8 0.2 PyObject_GetBuffer(dest, &dest_buffer, 114 PyBUF_ANY_CONTIGUOUS | PyBUF_WRITEABLE) 115 200 100 0.5 0.0 dest_ptr = dest_buffer.buf 116 200 84 0.4 0.0 nbytes = dest_buffer.len 117 118 # perform decompression 119 200 1856 9.3 0.8 if _get_use_threads(): 120 # allow blosc to use threads internally 121 200 235286 1176.4 98.0 with nogil: 122 ret = blosc_decompress(source_ptr, dest_ptr, nbytes) 123 else: 124 with nogil: 125 ret = blosc_decompress_ctx(source_ptr, dest_ptr, nbytes, 1) 126 127 # release buffers 128 200 754 3.8 0.3 if release_source_buffer: 129 200 326 1.6 0.1 PyBuffer_Release(&source_buffer) 130 200 165 0.8 0.1 PyBuffer_Release(&dest_buffer) 131 132 # handle errors 133 200 128 0.6 0.1 if ret <= 0: 134 raise RuntimeError('error during blosc decompression: %d' % ret) zarr-python-3.2.1/changes/000077500000000000000000000000001517635743000154355ustar00rootroot00000000000000zarr-python-3.2.1/changes/.gitignore000066400000000000000000000000141517635743000174200ustar00rootroot00000000000000!.gitignore zarr-python-3.2.1/changes/README.md000066400000000000000000000005751517635743000167230ustar00rootroot00000000000000Writing a changelog entry ------------------------- Please put a new file in this directory named `xxxx..md`, where - `xxxx` is the pull request number associated with this entry - `` is one of: - feature - bugfix - doc - removal - misc Inside the file, please write a short description of what you have changed, and how it impacts users of `zarr-python`. zarr-python-3.2.1/ci/000077500000000000000000000000001517635743000144205ustar00rootroot00000000000000zarr-python-3.2.1/ci/check_changelog_entries.py000066400000000000000000000033111517635743000216050ustar00rootroot00000000000000""" Check changelog entries have the correct filename structure. """ import sys from pathlib import Path VALID_CHANGELOG_TYPES = ["feature", "bugfix", "doc", "removal", "misc"] CHANGELOG_DIRECTORY = (Path(__file__).parent.parent / "changes").resolve() def is_int(s: str) -> bool: try: int(s) except ValueError: return False else: return True if __name__ == "__main__": print(f"Looking for changelog entries in {CHANGELOG_DIRECTORY}") entries = CHANGELOG_DIRECTORY.glob("*") entries = [e for e in entries if e.name not in [".gitignore", "README.md"]] print(f"Found {len(entries)} entries") print() bad_suffix = [e for e in entries if e.suffix != ".md"] bad_issue_no = [e for e in entries if not is_int(e.name.split(".")[0])] bad_type = [e for e in entries if e.name.split(".")[1] not in VALID_CHANGELOG_TYPES] if len(bad_suffix) or len(bad_issue_no) or len(bad_type): if len(bad_suffix): print("Changelog entries without .md suffix") print("-------------------------------------") print("\n".join([p.name for p in bad_suffix])) print() if len(bad_issue_no): print("Changelog entries without integer issue number") print("----------------------------------------------") print("\n".join([p.name for p in bad_issue_no])) print() if len(bad_type): print("Changelog entries without valid type") print("------------------------------------") print("\n".join([p.name for p in bad_type])) print(f"Valid types are: {VALID_CHANGELOG_TYPES}") print() sys.exit(1) sys.exit(0) zarr-python-3.2.1/ci/check_unlinked_types.py000066400000000000000000000055171517635743000211740ustar00rootroot00000000000000"""Check for unlinked type annotations in built documentation. mkdocstrings renders resolved types as links and unresolved types as Name without an anchor. This script finds all such unlinked types in the built HTML and reports them. Usage: python ci/check_unlinked_types.py [site_dir] Raises ValueError if unlinked types are found. """ from __future__ import annotations import re import sys from pathlib import Path # Matches the griffe/mkdocstrings pattern for unlinked cross-references: # Name UNLINKED_PATTERN = re.compile( r'(?P[^<]+)' ) # Patterns to exclude from the report EXCLUDE_PATTERNS = [ # TypeVars and type parameters (single brackets like Foo[T]) re.compile(r"\[.+\]$"), # Dataclass field / namedtuple field references (contain parens) re.compile(r"\("), # Private names re.compile(r"\._"), # Dunder attributes re.compile(r"\.__\w+__$"), # Testing utilities re.compile(r"^zarr\.testing\."), # Third-party types (hypothesis, pytest, etc.) re.compile(r"^(hypothesis|pytest|typing_extensions|builtins|dataclasses)\."), ] def should_exclude(qualname: str) -> bool: return any(p.search(qualname) for p in EXCLUDE_PATTERNS) def find_unlinked_types(site_dir: Path) -> dict[str, set[str]]: """Find all unlinked types in built HTML files. Returns a dict mapping qualified type names to the set of pages where they appear. """ api_dir = site_dir / "api" if not api_dir.exists(): raise FileNotFoundError(f"{api_dir} does not exist. Run 'mkdocs build' first.") unlinked: dict[str, set[str]] = {} for html_file in api_dir.rglob("*.html"): content = html_file.read_text(errors="replace") rel_path = str(html_file.relative_to(site_dir)) for match in UNLINKED_PATTERN.finditer(content): qualname = match.group("qualname") if not should_exclude(qualname): unlinked.setdefault(qualname, set()).add(rel_path) return unlinked def main() -> None: site_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("site") unlinked = find_unlinked_types(site_dir) if not unlinked: print("No unlinked types found.") return lines = [f"Found {len(unlinked)} unlinked types:\n"] for qualname in sorted(unlinked): pages = sorted(unlinked[qualname]) lines.append(f" {qualname}") lines.extend(f" - {page}" for page in pages) all_pages = {p for ps in unlinked.values() for p in ps} lines.append(f"\nTotal: {len(unlinked)} unlinked types across {len(all_pages)} pages") report = "\n".join(lines) raise ValueError(report) if __name__ == "__main__": main() zarr-python-3.2.1/codecov.yml000066400000000000000000000012771517635743000162010ustar00rootroot00000000000000coverage: status: patch: default: target: auto informational: true project: default: target: auto threshold: 0.1 flags: - tests flags: tests: paths: - src/ carryforward: true gpu: paths: - src/ carryforward: true codecov: notify: # 6 = test.yml: 3 (optional+ubuntu) + 2 (upstream + min_deps), hypothesis: 1 after_n_builds: 6 wait_for_ci: yes comment: layout: "diff, files" behavior: default require_changes: true # if true: only post the comment if coverage changes branches: # branch names that can post comment - "main" github_checks: annotations: false zarr-python-3.2.1/design/000077500000000000000000000000001517635743000152765ustar00rootroot00000000000000zarr-python-3.2.1/design/chunk-grid.md000066400000000000000000001335061517635743000176630ustar00rootroot00000000000000# Unified Chunk Grid Version: 6 Design document for adding rectilinear (variable) chunk grid support to **zarr-python**, conforming to the [rectilinear chunk grid extension spec](https://github.com/zarr-developers/zarr-extensions/pull/25). **Related:** - [#3750](https://github.com/zarr-developers/zarr-python/issues/3750) (single ChunkGrid proposal) - [#3534](https://github.com/zarr-developers/zarr-python/pull/3534) (rectilinear implementation) - [#3735](https://github.com/zarr-developers/zarr-python/pull/3735) (chunk grid module/registry) - [ZEP0003](https://github.com/zarr-developers/zeps/blob/main/draft/ZEP0003.md) (variable chunking spec) - [zarr-specs#370](https://github.com/zarr-developers/zarr-specs/pull/370) (sharding v1.1: non-divisible subchunks) - [zarr-extensions#25](https://github.com/zarr-developers/zarr-extensions/pull/25) (rectilinear extension) - [zarr-extensions#34](https://github.com/zarr-developers/zarr-extensions/issues/34) (sharding + rectilinear) ## Problem Chunk grids form a hierarchy — the rectilinear grid is strictly more general than the regular grid. Any regular grid is expressible as a rectilinear grid. There is no known chunk grid that is both (a) more general than rectilinear and (b) retains the axis-aligned tessellation properties Zarr assumes. All known grids are special cases: | Grid type | Description | Example | |---|---|---| | Regular | Uniform chunk size, boundary chunks padded with fill_value | `[10, 10, 10, 10]` | | Regular-bounded (zarrs) | Uniform chunk size, boundary chunks trimmed to array extent | `[10, 10, 10, 5]` | | HPC boundary-padded | Regular interior, larger boundary chunks ([VirtualiZarr#217](https://github.com/zarr-developers/VirtualiZarr/issues/217)) | `[10, 8, 8, 8, 10]` | | Fully variable | Arbitrary per-chunk sizes | `[5, 12, 3, 20]` | Prior iterations on the chunk grid design were based on the Zarr V3 spec's definition of chunk grids as an extension point alongside codecs, dtypes, etc. Therefore, we started designing the chunk grid implementation following a similar registry-based approach. However, in practice chunk grids are fundamentally different than codecs. Codecs are independent; supporting `zstd` tells you nothing about `gzip`. Chunk grids are not: every regular grid is a valid rectilinear grid. A registry-based plugin system makes sense for codecs but adds complexity without clear benefit for chunk grids. Here we start from some basic goals and propose a more fitting design for supporting different chunk grids in zarr-python. ## Goals 1. **Follow the zarr extension proposal.** The implementation should conform to the [rectilinear chunk grid spec](https://github.com/zarr-developers/zarr-extensions/tree/main/chunk-grids/rectilinear), not innovate on the metadata format. 2. **Minimize changes to the public API.** Users creating regular arrays should see no difference. Rectilinear is additive. 3. **Maintain backwards compatibility.** Existing code using `.chunks`, `isinstance` checks, or importing `RegularChunkGrid`/`RectilinearChunkGrid` from `zarr.core.chunk_grids` should continue to work where practical (with deprecation warnings where appropriate). Internal code paths/imports may be broken with justification. 4. **Design for future iteration.** The internal architecture should allow refactoring (e.g., metadata/array separation, new dimension types) without breaking the public API. 5. **Minimize downstream changes.** xarray, VirtualiZarr, Icechunk, Cubed, etc. should need minimal updates. 6. **Minimize time to stable release.** Ship behind a feature flag, stabilize through real-world usage, promote to stable API. 7. **The new API should be useful.** `read_chunk_sizes`/`write_chunk_sizes`, `ChunkGrid.__getitem__`, `is_regular` — these should solve real problems, not just expose internals. 8. **Extensible for other serialization structures.** The per-dimension design should support future encodings (tile, temporal) without changes to indexing or codecs. ## Design ### Design choices 1. **A chunk grid is a concrete arrangement of chunks.** Not an abstract tiling pattern. This means that the chunk grid is bound to specific array dimensions, which enables the chunk grid to answer any question about any chunk (offset, size, count) without external parameters. 2. **One implementation, multiple serialization forms.** A single `ChunkGrid` class handles all chunking logic. The serialization format (`"regular"` vs `"rectilinear"`) is chosen by the metadata layer, not the grid. 3. **No chunk grid registry.** Simple name-based dispatch in the metadata layer's `parse_chunk_grid()`. 4. **Fixed vs Varying per dimension.** `FixedDimension(size, extent)` for uniform chunks; `VaryingDimension(edges, extent)` for per-chunk edge lengths with precomputed prefix sums. Avoids expanding regular dimensions into lists of identical values. 5. **Transparent transitions.** Operations like `resize()` can move an array from regular to rectilinear chunking. ### Internal representation ```python @dataclass(frozen=True) class FixedDimension: """Uniform chunk size. Boundary chunks contain less data but are encoded at full size by the codec pipeline.""" size: int # chunk edge length (>= 0) extent: int # array dimension length def __post_init__(self) -> None: # validates size >= 0 and extent >= 0 @property def nchunks(self) -> int: if self.size == 0: return 0 return ceildiv(self.extent, self.size) def index_to_chunk(self, idx: int) -> int: return idx // self.size # raises IndexError if OOB def chunk_offset(self, chunk_ix: int) -> int: return chunk_ix * self.size # raises IndexError if OOB def chunk_size(self, chunk_ix: int) -> int: return self.size # always uniform; raises IndexError if OOB def data_size(self, chunk_ix: int) -> int: return max(0, min(self.size, self.extent - chunk_ix * self.size)) # raises IndexError if OOB @property def unique_edge_lengths(self) -> Iterable[int]: return (self.size,) # O(1) def indices_to_chunks(self, indices: NDArray) -> NDArray: return indices // self.size def with_extent(self, new_extent: int) -> FixedDimension: return FixedDimension(size=self.size, extent=new_extent) def resize(self, new_extent: int) -> FixedDimension: return FixedDimension(size=self.size, extent=new_extent) @dataclass(frozen=True) class VaryingDimension: """Explicit per-chunk sizes. The last chunk may extend past the array extent (extent < sum(edges)), in which case data_size clips to the valid region while chunk_size returns the full edge length for codec processing. This underflow is allowed to match how regular grids handle boundary chunks, and to support shrinking an array without rewriting chunk edges (the spec allows trailing edges beyond the extent).""" edges: tuple[int, ...] # per-chunk edge lengths (all > 0) cumulative: tuple[int, ...] # prefix sums for O(log n) lookup extent: int # array dimension length (may be < sum(edges)) def __init__(self, edges: Sequence[int], extent: int) -> None: # validates edges non-empty, all > 0, extent >= 0, extent <= sum(edges) # computes cumulative via itertools.accumulate # uses object.__setattr__ for frozen dataclass @property def nchunks(self) -> int: # number of chunks that overlap [0, extent) if extent == 0: return 0 return bisect.bisect_left(self.cumulative, extent) + 1 @property def ngridcells(self) -> int: return len(self.edges) def index_to_chunk(self, idx: int) -> int: return bisect.bisect_right(self.cumulative, idx) # raises IndexError if OOB def chunk_offset(self, chunk_ix: int) -> int: return self.cumulative[chunk_ix - 1] if chunk_ix > 0 else 0 # raises IndexError if OOB def chunk_size(self, chunk_ix: int) -> int: return self.edges[chunk_ix] # raises IndexError if OOB def data_size(self, chunk_ix: int) -> int: offset = self.chunk_offset(chunk_ix) return max(0, min(self.edges[chunk_ix], self.extent - offset)) # raises IndexError if OOB @property def unique_edge_lengths(self) -> Iterable[int]: # lazy generator: yields unseen values, short-circuits deduplication def indices_to_chunks(self, indices: NDArray) -> NDArray: return np.searchsorted(self.cumulative, indices, side='right') def with_extent(self, new_extent: int) -> VaryingDimension: # validates cumulative[-1] >= new_extent (O(1)), re-binds extent return VaryingDimension(self.edges, extent=new_extent) def resize(self, new_extent: int) -> VaryingDimension: # grow past edge sum: append chunk of size (new_extent - sum(edges)) # shrink or grow within edge sum: preserve all edges, re-bind extent ``` Both types implement the `DimensionGrid` protocol: `nchunks`, `extent`, `index_to_chunk`, `chunk_offset`, `chunk_size`, `data_size`, `indices_to_chunks`, `unique_edge_lengths`, `with_extent`, `resize`. Memory usage scales with the number of *varying* dimensions, not total chunks. All per-chunk methods (`chunk_offset`, `chunk_size`, `data_size`) raise `IndexError` for out-of-bounds chunk indices, providing consistent fail-fast behavior across both dimension types. The two size methods serve different consumers: | Method | Returns | Consumer | |---|---|---| | `chunk_size` | Buffer size for codec processing | Codec pipeline (`ArraySpec.shape`) | | `data_size` | Valid data region within the buffer | Indexing pipeline (`chunk_selection` slicing) | For `FixedDimension`, these differ only at the boundary. For `VaryingDimension`, these differ only when the last chunk extends past the extent (i.e., `extent < sum(edges)`). This matches current zarr-python behavior: `get_chunk_spec` passes the full `chunk_shape` to the codec for all chunks, and the indexer generates a `chunk_selection` that clips the decoded buffer. ### DimensionGrid Protocol ```python @runtime_checkable class DimensionGrid(Protocol): """Structural interface shared by FixedDimension and VaryingDimension.""" @property def nchunks(self) -> int: ... @property def ngridcells(self) -> int: ... @property def extent(self) -> int: ... def index_to_chunk(self, idx: int) -> int: ... def chunk_offset(self, chunk_ix: int) -> int: ... # raises IndexError if OOB def chunk_size(self, chunk_ix: int) -> int: ... # raises IndexError if OOB def data_size(self, chunk_ix: int) -> int: ... # raises IndexError if OOB def indices_to_chunks(self, indices: NDArray[np.intp]) -> NDArray[np.intp]: ... @property def unique_edge_lengths(self) -> Iterable[int]: ... def with_extent(self, new_extent: int) -> DimensionGrid: ... def resize(self, new_extent: int) -> DimensionGrid: ... ``` The protocol is `@runtime_checkable`, enabling polymorphic handling of both dimension types without `isinstance` checks. `nchunks` and `ngridcells` differ when `extent < sum(edges)`: `nchunks` counts only chunks that overlap `[0, extent)`, while `ngridcells` counts total defined grid cells (i.e., `len(edges)`). For `FixedDimension`, both are equal. For `VaryingDimension`, they differ after a resize that shrinks the extent below the edge sum. ### ChunkSpec ```python @dataclass(frozen=True) class ChunkSpec: slices: tuple[slice, ...] # valid data region in array coordinates codec_shape: tuple[int, ...] # buffer shape for codec processing @property def shape(self) -> tuple[int, ...]: return tuple(s.stop - s.start for s in self.slices) @property def is_boundary(self) -> bool: return self.shape != self.codec_shape ``` For interior chunks, `shape == codec_shape`. For boundary chunks of a regular grid, `codec_shape` is the full declared chunk size while `shape` is clipped. For rectilinear grids, `shape == codec_shape` unless the last chunk extends past the extent. ### API ```python # Creating arrays arr = zarr.create_array(shape=(100, 200), chunks=(10, 20)) # regular arr = zarr.create_array(shape=(60, 100), chunks=[[10, 20, 30], [25, 25, 25, 25]]) # rectilinear # ChunkGrid as a collection grid = arr._chunk_grid # ChunkGrid (bound to array shape) grid.grid_shape # (10, 10) — number of chunks per dimension grid.ndim # 2 grid.is_regular # True if all dimensions are Fixed spec = grid[0, 1] # ChunkSpec for chunk at grid position (0, 1) spec.slices # (slice(0, 10), slice(20, 40)) spec.shape # (10, 20) — data shape spec.codec_shape # (10, 20) — same for interior chunks boundary = grid[9, 0] # boundary chunk (extent=100, size=10) boundary.shape # (10, 20) — data shape boundary.codec_shape # (10, 20) — codec sees full buffer grid[99, 99] # None — out of bounds for spec in grid: # iterate all chunks ... # .chunks property: retained for regular grids, raises NotImplementedError for rectilinear arr.chunks # (10, 20) # .read_chunk_sizes / .write_chunk_sizes: works for all grids (dask-style) arr.write_chunk_sizes # ((10, 10, ..., 10), (20, 20, ..., 20)) ``` `ChunkGrid.__getitem__` constructs `ChunkSpec` using `chunk_size` for `codec_shape` and `data_size` for `slices`: ```python def __getitem__(self, coords: int | tuple[int, ...]) -> ChunkSpec | None: if isinstance(coords, int): coords = (coords,) slices = [] codec_shape = [] for dim, ix in zip(self.dimensions, coords): if ix < 0 or ix >= dim.nchunks: return None offset = dim.chunk_offset(ix) slices.append(slice(offset, offset + dim.data_size(ix))) codec_shape.append(dim.chunk_size(ix)) return ChunkSpec(tuple(slices), tuple(codec_shape)) ``` #### Construction `from_sizes` requires `array_shape`, binding the extent per dimension at construction time. This is a core design choice: a chunk grid is a concrete arrangement for a specific array, not an abstract tiling pattern. ```python # Regular grid — all FixedDimension grid = ChunkGrid.from_sizes(array_shape=(100, 200), chunk_sizes=(10, 20)) # Rectilinear grid — extent = sum(edges) when shape matches grid = ChunkGrid.from_sizes(array_shape=(60, 100), chunk_sizes=[[10, 20, 30], [25, 25, 25, 25]]) # Rectilinear grid with boundary clipping — last chunk extends past array extent # e.g., shape=(55, 90) but edges sum to (60, 100): data_size clips at extent grid = ChunkGrid.from_sizes(array_shape=(55, 90), chunk_sizes=[[10, 20, 30], [25, 25, 25, 25]]) # Direct construction grid = ChunkGrid(dimensions=(FixedDimension(10, 100), VaryingDimension([10, 20, 30], 55))) ``` When `extent < sum(edges)`, the dimension is always stored as `VaryingDimension` (even if all edges are identical) to preserve the explicit edge count. The last chunk's `chunk_size` returns the full declared edge (codec buffer) while `data_size` clips to the extent. This mirrors how `FixedDimension` handles boundary chunks in regular grids. #### Serialization ```python # Regular grid: {"name": "regular", "configuration": {"chunk_shape": [10, 20]}} # Rectilinear grid (with RLE compression and "kind" field): {"name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[10, 20, 30], [[25, 4]]]}} ``` Both names deserialize to the same `ChunkGrid` class. The serialized form does not include the array extent — that comes from `shape` in array metadata and is combined with the chunk grid when constructing a `ChunkGrid` via `ChunkGrid.from_metadata()`. **The `ChunkGrid` does not serialize itself.** The format choice (`"regular"` vs `"rectilinear"`) belongs to `ArrayV3Metadata`. Serialization and deserialization are handled by the metadata-layer chunk grid classes (`RegularChunkGridMetadata` and `RectilinearChunkGridMetadata` in `metadata/v3.py`), which provide `to_dict()` and `from_dict()` methods. For `create_array`, the format is inferred from the `chunks` argument: a flat tuple produces `"regular"`, a nested list produces `"rectilinear"`. The `_is_rectilinear_chunks()` helper detects nested sequences like `[[10, 20], [5, 5]]`. ##### Rectilinear spec compliance The rectilinear format requires `"kind": "inline"` (validated by `validate_rectilinear_kind()`). Per the spec, each element of `chunk_shapes` can be: - A bare integer `m`: repeated until `sum >= array_extent` - A list of bare integers: explicit per-chunk sizes - A mixed array of bare integers and `[value, count]` RLE pairs RLE compression is used when serializing: runs of identical sizes become `[value, count]` pairs, singletons stay as bare integers. ```python # compress_rle([10, 10, 10, 5]) -> [[10, 3], 5] # expand_rle([[10, 3], 5]) -> [10, 10, 10, 5] ``` For a single-element `chunk_shapes` tuple like `(10,)`, `RectilinearChunkGridMetadata.to_dict()` serializes it as a bare integer `10`. Per the rectilinear spec, a bare integer is repeated until the sum >= extent, preserving the full codec buffer size for boundary chunks. **Zero-extent handling:** Regular grids serialize zero-extent dimensions without issue (the format encodes only `chunk_shape`, no edges). Rectilinear grids cannot represent zero-extent dimensions because the spec requires at least one positive-integer edge length per axis. #### read_chunk_sizes / write_chunk_sizes The `read_chunk_sizes` and `write_chunk_sizes` properties provide universal access to per-dimension chunk data sizes, matching the dask `Array.chunks` convention. They work for both regular and rectilinear grids: - `write_chunk_sizes`: always returns outer (storage) chunk sizes - `read_chunk_sizes`: returns inner chunk sizes when sharding is used, otherwise same as `write_chunk_sizes` ```python >>> arr = zarr.create_array(store, shape=(100, 80), chunks=(30, 40)) >>> arr.write_chunk_sizes ((30, 30, 30, 10), (40, 40)) >>> arr = zarr.create_array(store, shape=(60, 100), chunks=[[10, 20, 30], [50, 50]]) >>> arr.write_chunk_sizes ((10, 20, 30), (50, 50)) ``` The underlying `ChunkGrid.chunk_sizes` property (on the grid, not the array) returns the same as `write_chunk_sizes`. #### Resize ```python arr.resize((80, 100)) # re-binds extent; FixedDimension stays fixed arr.resize((200, 100)) # VaryingDimension grows by appending a new chunk arr.resize((30, 100)) # VaryingDimension shrinks: preserves all edges, re-binds extent ``` Resize uses `ChunkGrid.update_shape(new_shape)`, which delegates to each dimension's `.resize()` method: - `FixedDimension.resize()`: simply re-binds the extent (identical to `with_extent`) - `VaryingDimension.resize()`: grow past `sum(edges)` appends a chunk covering the gap; shrink or grow within `sum(edges)` preserves all edges and re-binds the extent (the spec allows trailing edges beyond the array extent) **Known limitation (deferred):** When growing a `VaryingDimension`, the current implementation always appends a single chunk covering the new region. For example, `[10, 10, 10]` resized from 30 to 45 produces `[10, 10, 10, 15]` instead of the more natural `[10, 10, 10, 10, 10]`. A future improvement should add an optional `chunks` parameter to `resize()` that controls how the new region is partitioned, with a sane default (e.g., repeating the last chunk size). This is safely deferrable because: - `FixedDimension` already handles resize correctly (regular grids stay regular) - The single-chunk default produces valid state, just suboptimal chunk layout - Rectilinear arrays are behind an experimental feature flag - Adding an optional parameter is backwards-compatible Open design questions for the `chunks` parameter: - Does it describe the new region only, or the entire post-resize array? - Must the overlapping portion agree with existing chunks (no rechunking)? - What is the type? Same as `chunks` in `create_array`? #### from_array The `from_array()` function handles both regular and rectilinear source arrays: ```python src = zarr.create_array(store, shape=(60, 100), chunks=[[10, 20, 30], [50, 50]]) new = zarr.from_array(data=src, store=new_store, chunks="keep") # Preserves rectilinear structure: new.write_chunk_sizes == ((10, 20, 30), (50, 50)) ``` When `chunks="keep"`, the logic checks `data._chunk_grid.is_regular`: - Regular: extracts `data.chunks` (flat tuple) and preserves shards - Rectilinear: extracts `data.write_chunk_sizes` (nested tuples) and forces shards to None ### Indexing The indexing pipeline is coupled to regular grid assumptions — every per-dimension indexer takes a scalar `dim_chunk_len: int` and uses `//` and `*`: ```python dim_chunk_ix = self.dim_sel // self.dim_chunk_len # IntDimIndexer dim_offset = dim_chunk_ix * self.dim_chunk_len # SliceDimIndexer ``` Replace `dim_chunk_len: int` with the dimension object (`FixedDimension | VaryingDimension`). The shared interface means the indexer code structure stays the same — `dim_sel // dim_chunk_len` becomes `dim_grid.index_to_chunk(dim_sel)`. O(1) for regular, binary search for varying. ### Codec pipeline Today, `get_chunk_spec()` returns the same `ArraySpec(shape=chunk_grid.chunk_shape)` for every chunk. For rectilinear grids, each chunk has a different codec shape: ```python def get_chunk_spec(self, chunk_coords, array_config, prototype) -> ArraySpec: spec = self._chunk_grid[chunk_coords] return ArraySpec(shape=spec.codec_shape, ...) ``` Note `spec.codec_shape`, not `spec.shape`. For regular grids, `codec_shape` is uniform (preserving current behavior). The boundary clipping flow is unchanged: ``` Write: user data → pad to codec_shape with fill_value → encode → store Read: store → decode to codec_shape → slice via chunk_selection → user data ``` ### Sharding The `ShardingCodec` constructs a `ChunkGrid` per shard using the shard shape as extent and the subchunk shape as `FixedDimension`. Each shard is self-contained — it doesn't need to know whether the outer grid is regular or rectilinear. Validation checks that every unique edge length per dimension is divisible by the inner chunk size, using `dim.unique_edge_lengths` for efficient polymorphic iteration (O(1) for fixed dimensions, lazy-deduplicated for varying). ``` Level 1 — Outer chunk grid (shard boundaries): regular or rectilinear Level 2 — Inner subchunk grid (within each shard): always regular Level 3 — Shard index: ceil(shard_dim / subchunk_dim) entries per dimension ``` [zarr-specs#370](https://github.com/zarr-developers/zarr-specs/pull/370) lifts the requirement that subchunk shapes evenly divide the shard shape. With the proposed `ChunkGrid`, this just means removing the `shard_shape % subchunk_shape == 0` validation — `FixedDimension` already handles boundary clipping via `data_size`. | Outer grid | Subchunk divisibility | Required change | |---|---|---| | Regular | Evenly divides (v1.0) | None | | Regular | Non-divisible (v1.1) | Remove divisibility validation | | Rectilinear | Evenly divides | Remove "sharding incompatible" guard | | Rectilinear | Non-divisible | Both changes | ### What this replaces | Current | Proposed | |---|---| | `ChunkGrid` ABC + `RegularChunkGrid` subclass | Single concrete `ChunkGrid` with `is_regular` | | `RectilinearChunkGrid` (#3534) | Same `ChunkGrid` class | | Chunk grid registry + entrypoints (#3735) | Direct name dispatch | | `arr.chunks` | Retained for regular; `arr.read_chunk_sizes`/`arr.write_chunk_sizes` for general use | | `get_chunk_shape(shape, coord)` | `grid[coord].codec_shape` or `grid[coord].shape` | ## Design decisions ### Why store the extent in ChunkGrid? The chunk grid is a concrete arrangement, not an abstract tiling pattern. A finite collection naturally has an extent. Storing it enables `__getitem__`, eliminates `dim_len` parameters from every method, and makes the grid self-describing. This does *not* mean `ArrayV3Metadata.shape` should delegate to the grid. The array shape remains an independent field in metadata. The extent is passed into the grid at construction time so it can answer boundary questions without external parameters. It is **not** serialized as part of the chunk grid JSON — it comes from the `shape` field in array metadata and is combined with the chunk grid configuration in `ChunkGrid.from_metadata()`. ### Why distinguish chunk_size from data_size? A chunk in a regular grid has two sizes. `chunk_size` is the buffer size the codec processes — always `size` for `FixedDimension`, even at the boundary (padded with `fill_value`). `data_size` is the valid data region — clipped to `extent % size` at the boundary. The indexing layer uses `data_size` to generate `chunk_selection` slices. This matches current zarr-python behavior and matters for: 1. **Backward compatibility.** Existing stores have boundary chunks encoded at full `chunk_shape`. 2. **Codec simplicity.** Codecs assume uniform input shapes for regular grids. 3. **Shard index correctness.** The index assumes `subchunk_dim`-sized entries. For `VaryingDimension`, `chunk_size == data_size` when `extent == sum(edges)`. When `extent < sum(edges)` (e.g., after a resize that keeps the last chunk oversized), `data_size` clips the last chunk. This is the fundamental difference: `FixedDimension` has a declared size plus an extent that clips data; `VaryingDimension` has explicit sizes that normally *are* the extent but can also extend past it. ### Why not a chunk grid registry? There is no known chunk grid outside the rectilinear family that retains the tessellation properties zarr-python assumes. A `match` on the grid name is sufficient. ### Why a single ChunkGrid class instead of RegularChunkGrid + RectilinearChunkGrid? [Discussed in #3534.](https://github.com/zarr-developers/zarr-python/pull/3534) @d-v-b argued that `RegularChunkGrid` is unnecessary since rectilinear is more general; @dcherian argued that downstream libraries need a fast way to detect regular grids without inspecting potentially millions of chunk edges (see [xarray#9808](https://github.com/pydata/xarray/pull/9808)). The resolution: a single `ChunkGrid` class with an `is_regular` property (O(1), cached at construction). This gives downstream code the fast-path detection @dcherian needed without the class hierarchy complexity @d-v-b wanted to avoid. The metadata document's `name` field (`"regular"` vs `"rectilinear"`) is also available for clients who inspect JSON directly. A backwards-compatibility shim in `chunk_grids.py` preserves the old `RegularChunkGrid` / `RectilinearChunkGrid` import paths with deprecation warnings — see [Backwards compatibility](#backwards-compatibility). ### Why is ChunkGrid a concrete class instead of a Protocol/ABC? The old design had `ChunkGrid` as an ABC with `RegularChunkGrid` as its only subclass. #3534 added `RectilinearChunkGrid` as a second subclass. This branch makes `ChunkGrid` a single concrete class instead, with separate metadata DTOs (`RegularChunkGridMetadata` and `RectilinearChunkGridMetadata` in `metadata/v3.py`) for serialization. All known grids are special cases of rectilinear, so there's no need for a class hierarchy at the grid level. A `ChunkGrid` Protocol/ABC would mean every caller programs against an abstract interface and adding a grid type requires implementing ~15 methods. A single class is simpler. Note: the *dimension* types (`FixedDimension`, `VaryingDimension`) do use a `DimensionGrid` Protocol — that's where the polymorphism lives. The grid-level class is concrete; the dimension-level types are polymorphic. If a genuinely novel grid type emerges that can't be expressed as a combination of per-dimension types, a grid-level Protocol can be extracted. ### Why `.chunks` raises for rectilinear grids [Debated in #3534.](https://github.com/zarr-developers/zarr-python/pull/3534) @d-v-b suggested making `.chunks` return `tuple[tuple[int, ...], ...]` (dask-style) for all grids. @dcherian strongly objected: every downstream consumer expects `tuple[int, ...]`, and silently returning a different type would be worse than raising. Materializing O(10M) chunk edges into a Python tuple is also a real performance risk ([xarray#8902](https://github.com/pydata/xarray/issues/8902#issuecomment-2546127373)). The resolution: - `.chunks` is retained for regular grids (returns `tuple[int, ...]` as before) - `.chunks` raises `NotImplementedError` for rectilinear grids with a message pointing to `.read_chunk_sizes`/`.write_chunk_sizes` - `.read_chunk_sizes` and `.write_chunk_sizes` return `tuple[tuple[int, ...], ...]` (dask convention) for all grids @maxrjones noted in review that deprecating `.chunks` for regular grids was not desirable. The current branch does not deprecate it. ### User control over grid serialization format @d-v-b raised in #3534 that users need a way to say "these chunks are regular, but serialize as rectilinear" (e.g., to allow future append/extend workflows without format changes). @jhamman initially made nested-list input always produce `RectilinearChunkGridMetadata`. The current branch resolves this via the metadata-layer chunk grid classes. When metadata is deserialized, the original name (from `{"name": "regular"}` or `{"name": "rectilinear"}`) determines which metadata class is instantiated (`RegularChunkGridMetadata` or `RectilinearChunkGridMetadata`), and that class handles serialization via `to_dict()`. Current inference behavior for `create_array`: - `chunks=(10, 20)` (flat tuple) → infers `"regular"` - `chunks=[[10, 20], [5, 5]]` (nested lists with varying sizes) → infers `"rectilinear"` - `chunks=[[10, 10], [20, 20]]` (nested lists with uniform sizes) → `from_sizes` collapses to `FixedDimension`, so `is_regular=True` and infers `"regular"` **Open question:** Should uniform nested lists preserve `"rectilinear"` to support future append workflows without a format change? This could be addressed by checking the input form before collapsing, or by allowing users to pass `chunk_grid_name` explicitly through the `create_array` API. ### Deferred: Tiled/periodic chunk patterns [#3750 discussion](https://github.com/zarr-developers/zarr-python/issues/3750) identified periodic chunk patterns as a use case not efficiently served by RLE alone. RLE compresses runs of identical values (`np.repeat`), but periodic patterns like days-per-month (`[31, 28, 31, 30, ...]` repeated 30 years) need a tile encoding (`np.tile`). Real-world examples include: - **Oceanographic models** (ROMS): HPC boundary-padded chunks like `[10, 8, 8, 8, 10]` — handled by RLE - **Temporal axes**: days-per-month, hours-per-day — need tile encoding for compact metadata - **Temporal-aware grids**: date/time-aware chunk grids that layer over other axes (raised by @LDeakin) A `TiledDimension` prototype was built ([commit 9c0f582](https://github.com/maxrjones/zarr-python/commit/9c0f582f)) demonstrating that the per-dimension design supports this without changes to indexing or the codec pipeline. However, it was intentionally excluded from this release because: 1. **Metadata format must come first.** Tile encoding requires a new `kind` value in the rectilinear spec (currently only `"inline"` is defined). This should go through [zarr-extensions#25](https://github.com/zarr-developers/zarr-extensions/pull/25), not zarr-python unilaterally. 2. **The per-dimension architecture doesn't preclude it.** A future `TiledDimension` can implement the `DimensionGrid` protocol alongside `FixedDimension` and `VaryingDimension` with no changes to indexing, codecs, or the `ChunkGrid` class. 3. **RLE covers the MVP.** Most real-world variable chunk patterns (HPC boundaries, irregular partitions) are efficiently encoded with RLE. Tile encoding is an optimization for a specific (temporal) subset. ### Metadata / Array separation (partially implemented) An earlier design doc proposed decoupling `ChunkGrid` (runtime) from `ArrayV3Metadata` (serialization), so that metadata would store only a plain dict and the array layer would construct the `ChunkGrid`. The current implementation partially realizes this separation: - **Metadata DTOs** (`RegularChunkGridMetadata`, `RectilinearChunkGridMetadata` in `metadata/v3.py`): Pure data, frozen dataclasses, no array shape. These live on `ArrayV3Metadata.chunk_grid` and represent only what goes into `zarr.json`. - **`ChunkGrid`** (`chunk_grids.py`): Shape-bound, supports indexing, iteration, and chunk specs. Lives on `AsyncArray._chunk_grid`, constructed from metadata + `shape` via `ChunkGrid.from_metadata()`. This means `ArrayV3Metadata.chunk_grid` is now a `ChunkGridMetadata` (the DTO union type), **not** the runtime `ChunkGrid`. Code that previously accessed runtime methods on `metadata.chunk_grid` (e.g., `all_chunk_coords()`, `__getitem__`) must now use the grid from the array layer instead. The name controls serialization format; each metadata DTO class provides its own `to_dict()` method for serialization. The `ChunkGrid` handles all runtime queries. ## Prior art **zarrs (Rust):** Three independent grid types behind a `ChunkGridTraits` trait. Key patterns adopted: Fixed vs Varying per dimension, prefix sums + binary search, `Option` for out-of-bounds, `NonZeroU64` for chunk dimensions, separate subchunk grid per shard, array shape at construction. **TensorStore (C++):** Stores only `chunk_shape` — boundary clipping via `valid_data_bounds` at query time. Both `RegularGridRef` and `IrregularGrid` internally. No registry. ## Migration ### Public API compatibility The user-facing API is fully backward-compatible. Existing code that creates, opens, reads, and writes zarr arrays continues to work without changes: - `zarr.create_array`, `zarr.open`, `zarr.open_array`, `zarr.open_group` -- unchanged signatures. The `chunks` parameter type is *widened* (now also accepts nested sequences for rectilinear grids), but all existing call patterns still work. - `arr.chunks` -- returns `tuple[int, ...]` for regular arrays, same as before. - `arr.shape`, `arr.dtype`, `arr.ndim`, `arr.shards` -- unchanged. - Top-level `zarr` exports -- unchanged. - Rectilinear chunks are gated behind `zarr.config.set({'array.rectilinear_chunks': True})`, so they cannot be created accidentally. New additions (purely additive): `arr.read_chunk_sizes`, `arr.write_chunk_sizes`, `zarr.experimental.ChunkGrid`, `zarr.experimental.ChunkSpec`. The breaking changes discussed below are confined to **internal modules** (`zarr.core.chunk_grids`, `zarr.core.metadata.v3`, `zarr.core.indexing`) that downstream libraries like cubed and VirtualiZarr access directly. ### Internal API compatibility trade-off analysis This section analyzes the internal breaking changes from the metadata/array separation and evaluates two strategies: (A) add backward-compatibility shims in zarr-python, vs. (B) require downstream packages to update. The baseline is **no shims at all**. #### What breaks without any shims Three API changes affect downstream code: 1. **`RegularChunkGrid` class removed from `zarr.core.chunk_grids`.** On `main`, `RegularChunkGrid` is defined in `chunk_grids.py` as a `Metadata` subclass. This branch replaces it with `RegularChunkGridMetadata` in `metadata/v3.py`. Without a shim, `from zarr.core.chunk_grids import RegularChunkGrid` raises `ImportError`. 2. **`RegularChunkGrid` no longer available from `zarr.core.metadata.v3`.** On `main`, `v3.py` imports `RegularChunkGrid` from `chunk_grids.py` for internal use. VirtualiZarr imports it from this location (`from zarr.core.metadata.v3 import RegularChunkGrid`). Without the internal import, this raises `ImportError`. 3. **`OrthogonalIndexer` constructor expects `ChunkGrid`, not `RegularChunkGrid`/`RegularChunkGridMetadata`.** Even if the import shims above resolve to `RegularChunkGridMetadata`, the indexer constructors access `chunk_grid._dimensions`, which only exists on the runtime `ChunkGrid` class. Cubed constructs `OrthogonalIndexer(selection, shape, RegularChunkGrid(chunk_shape=chunks))` directly. #### Downstream impact without shims **VirtualiZarr** (5 line changes across 2 files): ```python # manifests/array.py (line 6): import - from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGrid + from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGridMetadata # manifests/array.py (line 53): isinstance check - if not isinstance(_metadata.chunk_grid, RegularChunkGrid): + if not isinstance(_metadata.chunk_grid, RegularChunkGridMetadata): # parsers/zarr.py (line 16): import - from zarr.core.chunk_grids import RegularChunkGrid + from zarr.core.metadata.v3 import RegularChunkGridMetadata # parsers/zarr.py (line 270): isinstance check - if not isinstance(array_v3_metadata.chunk_grid, RegularChunkGrid): + if not isinstance(array_v3_metadata.chunk_grid, RegularChunkGridMetadata): # parsers/zarr.py (line 390): cast - cast(RegularChunkGrid, metadata.chunk_grid).chunk_shape + cast(RegularChunkGridMetadata, metadata.chunk_grid).chunk_shape ``` The `manifests/array.py` import is from `zarr.core.metadata.v3` (never a documented export; VirtualiZarr relied on a transitive import). The `parsers/zarr.py` import is from `zarr.core.chunk_grids` (the canonical location on `main`). Both are straightforward renames. The `.chunk_shape` attribute is unchanged on the new class. If VirtualiZarr needs to support both old and new zarr-python, a version-conditional import adds ~5 more lines. **Cubed** (3 line changes in 1 file): ```python # core/ops.py (lines 626-631) def _create_zarr_indexer(selection, shape, chunks): if zarr.__version__[0] == "3": - from zarr.core.chunk_grids import RegularChunkGrid + from zarr.core.chunk_grids import ChunkGrid from zarr.core.indexing import OrthogonalIndexer - return OrthogonalIndexer(selection, shape, RegularChunkGrid(chunk_shape=chunks)) + return OrthogonalIndexer(selection, shape, ChunkGrid.from_sizes(shape, chunks)) ``` Note that `ChunkGrid` is *not* a renamed class. `RegularChunkGrid(chunk_shape=chunks)` took only chunk sizes; `ChunkGrid.from_sizes(shape, chunks)` also requires the array shape. The `shape` parameter is already available at this call site. If cubed needs to support both old and new zarr-python: ```python def _create_zarr_indexer(selection, shape, chunks): if zarr.__version__[0] == "3": from zarr.core.indexing import OrthogonalIndexer try: from zarr.core.chunk_grids import ChunkGrid return OrthogonalIndexer(selection, shape, ChunkGrid.from_sizes(shape, chunks)) except ImportError: from zarr.core.chunk_grids import RegularChunkGrid return OrthogonalIndexer(selection, shape, RegularChunkGrid(chunk_shape=chunks)) else: from zarr.indexing import OrthogonalIndexer return OrthogonalIndexer(selection, ZarrArrayIndexingAdaptor(shape, chunks)) ``` #### What shims can cover **Shim 1: `__getattr__` in `chunk_grids.py`** (~15 lines) Maps `RegularChunkGrid` to `RegularChunkGridMetadata` with a deprecation warning. Covers: - The `from zarr.core.chunk_grids import RegularChunkGrid` import pattern (used by cubed and VirtualiZarr's `parsers/zarr.py`) - `isinstance(x, RegularChunkGrid)` checks (because the name resolves to the actual class) - `RegularChunkGrid(chunk_shape=(...))` construction (because `RegularChunkGridMetadata` accepts the same arguments) Does **not** cover: passing the result to `OrthogonalIndexer`, because `RegularChunkGridMetadata` lacks `._dimensions`. **Shim 2: `__getattr__` in `metadata/v3.py`** (~12 lines) Same pattern, covers VirtualiZarr's import from `zarr.core.metadata.v3`. Mirrors Shim 1 for a different import path. **Shim 3: Auto-coerce `ChunkGridMetadata` in indexer constructors** (~30 lines) A helper function + 1-line insertion in each of `BasicIndexer`, `OrthogonalIndexer`, `CoordinateIndexer`, and `MaskIndexer`: ```python def _resolve_chunk_grid(chunk_grid, shape): """Coerce ChunkGridMetadata to runtime ChunkGrid if needed.""" from zarr.core.chunk_grids import ChunkGrid as _ChunkGrid from zarr.core.metadata.v3 import ChunkGridMetadata if isinstance(chunk_grid, _ChunkGrid): return chunk_grid if isinstance(chunk_grid, ChunkGridMetadata): warnings.warn( "Passing ChunkGridMetadata to indexers is deprecated. " "Use ChunkGrid.from_sizes() instead.", DeprecationWarning, stacklevel=2, ) if hasattr(chunk_grid, "chunk_shape"): return _ChunkGrid.from_sizes(shape, tuple(chunk_grid.chunk_shape)) return _ChunkGrid.from_sizes(shape, chunk_grid.chunk_shapes) raise TypeError(f"Expected ChunkGrid or ChunkGridMetadata, got {type(chunk_grid)}") ``` This covers cubed's `OrthogonalIndexer(selection, shape, RegularChunkGrid(...))` pattern end-to-end (combined with Shim 1). #### Comparison | | No shims | Shims 1+2 only | Shims 1+2+3 | |---|---|---|---| | **zarr-python additions** | 0 lines | ~27 lines | ~57 lines | | **VirtualiZarr changes** | 5 lines | 0 lines | 0 lines | | **Cubed changes** | 3 lines | 3 lines | 0 lines | | **Maintenance burden** | None | Low (deprecation shims are well-understood) | Medium (indexer coercion blurs metadata/runtime boundary) | | **API clarity** | Clean (metadata DTOs and runtime types are distinct) | Good (old names redirect to new names) | Weaker (indexers implicitly accept two type families) | With Shims 1+2 only, VirtualiZarr's `manifests/array.py` import from `zarr.core.metadata.v3` is covered by Shim 2, and the `parsers/zarr.py` import from `zarr.core.chunk_grids` is covered by Shim 1. The `isinstance` checks work because both shims resolve to `RegularChunkGridMetadata`. The `cast` works because `.chunk_shape` is unchanged. So VirtualiZarr needs 0 changes with Shims 1+2. The 3 lines for cubed remain because Shim 1 resolves the import but `OrthogonalIndexer` still needs a runtime `ChunkGrid`. ### Downstream migration Migration from `main` (where only `RegularChunkGrid` and the abstract `ChunkGrid` ABC exist): | Old pattern (on `main`) | New pattern | |---|---| | `from zarr.core.chunk_grids import RegularChunkGrid` | `from zarr.core.metadata.v3 import RegularChunkGridMetadata` | | `from zarr.core.chunk_grids import ChunkGrid` (ABC) | `from zarr.core.chunk_grids import ChunkGrid` (concrete class, different API) | | `isinstance(cg, RegularChunkGrid)` | `isinstance(cg, RegularChunkGridMetadata)` or `grid.is_regular` on the runtime `ChunkGrid` | | `cg.chunk_shape` on `RegularChunkGrid` | `cg.chunk_shape` on `RegularChunkGridMetadata` (unchanged) | | `ChunkGrid.from_dict(data)` | `parse_chunk_grid(data)` from `zarr.core.metadata.v3` | | `chunk_grid.all_chunk_coords(array_shape)` | `chunk_grid.all_chunk_coords()` (shape now stored in grid) | | `chunk_grid.get_nchunks(array_shape)` | `chunk_grid.get_nchunks()` (shape now stored in grid) | During the earlier [#3534](https://github.com/zarr-developers/zarr-python/pull/3534) effort (which used separate `RegularChunkGrid`/`RectilinearChunkGrid` classes), downstream PRs and issues were opened to explore compatibility: - xarray ([#10880](https://github.com/pydata/xarray/pull/10880)), VirtualiZarr ([#877](https://github.com/zarr-developers/VirtualiZarr/pull/877)), Icechunk ([#1338](https://github.com/earth-mover/icechunk/issues/1338)), cubed ([#876](https://github.com/cubed-dev/cubed/issues/876)) These target #3534's API, not this branch's unified `ChunkGrid` design. New downstream POC branches for this design are linked in [Proofs of concepts](#proofs-of-concepts). ### Credits This implementation builds on prior work: - **[#3534](https://github.com/zarr-developers/zarr-python/pull/3534)** (@jhamman) — RLE helpers, validation logic, test cases, and the review discussion that shaped the architecture. - **[#3737](https://github.com/zarr-developers/zarr-python/pull/3737)** — extent-in-grid idea (adopted per-dimension). - **[#1483](https://github.com/zarr-developers/zarr-python/pull/1483)** — original variable chunking POC. - **[#3736](https://github.com/zarr-developers/zarr-python/pull/3736)** — resolved by storing extent per-dimension. ## Open questions 1. **Resize defaults (deferred):** When growing a rectilinear array, should `resize()` accept an optional `chunks` parameter? See the [Resize section](#resize) for details and open design questions. Regular arrays already stay regular on resize. 2. **`ChunkSpec` complexity:** `ChunkSpec` carries both `slices` and `codec_shape`. Should the grid expose separate methods for codec vs data queries instead? 3. **`__getitem__` with slices:** Should `grid[0, :]` or `grid[0:3, :]` return a sub-grid or an iterator of `ChunkSpec`s? 4. **Uniform nested lists:** Should `chunks=[[10, 10], [20, 20]]` serialize as `"rectilinear"` (preserving user intent for future append) or `"regular"` (current behavior, collapses uniform edges)? See [User control over grid serialization format](#user-control-over-grid-serialization-format). 5. **`zarr.open` with rectilinear:** @tomwhite noted in #3534 that `zarr.open(mode="w")` doesn't support rectilinear chunks directly. This could be addressed in a follow-up. ## Proofs of concepts - Zarr-Python: - branch - https://github.com/maxrjones/zarr-python/tree/poc/unified-chunk-grid - diff - https://github.com/zarr-developers/zarr-python/compare/main...maxrjones:zarr-python:poc/unified-chunk-grid?expand=1 - Xarray: - branch - https://github.com/maxrjones/xarray/tree/poc/unified-zarr-chunk-grid - diff - https://github.com/pydata/xarray/compare/main...maxrjones:xarray:poc/unified-zarr-chunk-grid?expand=1 - VirtualiZarr: - branch - https://github.com/maxrjones/VirtualiZarr/tree/poc/unified-chunk-grid - diff - https://github.com/zarr-developers/VirtualiZarr/compare/main...maxrjones:VirtualiZarr:poc/unified-chunk-grid?expand=1 - Virtual TIFF: - branch - https://github.com/virtual-zarr/virtual-tiff/tree/poc/unified-chunk-grid - diff - https://github.com/virtual-zarr/virtual-tiff/compare/main...poc/unified-chunk-grid?expand=1 - Cubed: - branch - https://github.com/maxrjones/cubed/tree/poc/unified-chunk-grid - Microbenchmarks: - https://github.com/maxrjones/zarr-chunk-grid-tests/tree/unified-chunk-grid zarr-python-3.2.1/docs/000077500000000000000000000000001517635743000147555ustar00rootroot00000000000000zarr-python-3.2.1/docs/_static/000077500000000000000000000000001517635743000164035ustar00rootroot00000000000000zarr-python-3.2.1/docs/_static/favicon-96x96.png000066400000000000000000000306521517635743000213470ustar00rootroot00000000000000PNG  IHDR_`{` pHYsodtEXtSoftwarewww.inkscape.org< IDATx}y|Uι7&lU* Ep\@[kTlUYVZpׯ-l, [s}~$dAk|>瓛s3g33̼!_L;[wG4kڽ3>B~I Nj4!oPD1۔Ec.:Џ[O觎/B}J1 8]Q?~jkPp_zkg-~޽)1 Ժ\Q6e`HJܗ6s\3_|77=el0"d~z"oeOU05uम8?:ՑLs\E!A @*G`EG3nyv,Ƽy``JZ{SNJOV:sϏbgav LI++?j]-HL2 @m4Pe?  W=SRG*>s~RL+Wzh##yx(f'Ҥf3Uu(w"T<ʍ1qsR*ں|QQI]g̘Ƭxibhӭ 5ulɭOqJ[;;Ң*& g@TYU2yj,P>x{ xɩcֵʚ`?}6x )鷉Ӳ+\ [Ϙ:D$bU)+[0{qwE7pg#;HNCP@.' Y譠XKʜ/P/B8Am (9ɩ=a 8m/SJP@䌽i臦 ,ŰaN~RMI @9Tp&Ӧ4pĽPP6JP@: 5a Q2X qYsABj:X<.+cFK5*U(bjpWc?!}t'SNNnZOBL،!\*PxkO (,VhUTJAIDêKTh=]OO0j܂{Rn+f q/' ܝ:Vw *DX`ЗWǧ6i,}nS{{0* / UU (B 0ķ'VZר@AțT^ Zma ~-RYok{"|ܓr;STp8M1= N]xԩ^DJ4OǍkC~)ό,E6p nV+WT,b*5By D:Ú aJw05q1QO ;~q僸d51}b,VG0J??Dl7.2 ot@⪺ZyKlJ!U-zXBc}(A@8|FͣJKߕ:2Q [<6()2N8` sZ,)eq)缚;kȖJs|Zeו7*n2o7.7.E-lϩW%aŷ64 Hxq5 z8Фp>tQ$ d47&DLݿx4?)DZV "y6Xg( ~e<@ߙRYR "H FZwqoѲ*t|2W|Ci>F?0{vwzB7~/0*9h+0C ͡##)OhNT >w`h#% '`( DD!Pט+cP( 'xT*a_Z5e x'Zע;y6D4- ϪЮL|rOmiЂ;ҴᢶOM-/6?ߞziB9P*04S Eu[EG'T 6"ʩɇ4RBѾ@c |d!,T]CEd)W>Z/17=S.ʽl^pGڈcǛwFA `KڠȦd]?9 e< ($!.wΚy`9nP MAαl>Xt6)4(!BԪqwP0ɢʐ29R/0yPp|YhV9֬TV4)hZS FE,VGZ^ aè7rN6@S@h~"꧍JE&oP4!olʱFbTI`w礼a"bEzͅbN~ b(dTJZuN`IQ%~Im1aӞmܜ'x0[Ӯ0/9Pg:B՚MH*:٬c O|>TjoMPgO~NMy$5PGřlmo+w4gE:gPz*QxǧaQ((/pTLe!7ՎP&ݒr[ȴUWCT,#FNu8~3?EřTD<6:oTUNLFk>7W5`ѨuY[9OzaFaNC_B y<  "T=ϟ"F>2U`(+Ð!bJEWK@ ^PbaO Fv!haM1~sj]vuK[?X?2'h kB-nB4Ӓ⛩R0ECi,Fbj a: Tx9 ŹUv@nr1}N`JDOl']Ee+P@ c^\(^v*|V֌V[ ot8Qv-(1#(ڏtQTWVy^H{5,n9 _hcn"332 iDR +AѼTO+EDs?Ӳ-9EUcmeybǍV1< ࣇ]-b!D(1fsS GԀ } \!hy!^֎@?cRD!!1<:-$({“YK/]/ynq A5\c%a͑Yn44հxZ_ +jpt q`&irOF:w/~*\ 2WXz9M-^*pݣQȱy_?@zcf^N0hr86\)x[‚ugXZ_(e~ kS{Ѱ#do OCo4/{Mפ-=={>+n-I >6v̈́Nt|P : z>('9"*!  KoJHd)GE\S9fŽ YT: 9b~9;}Æ9y^N`EG']s6Wj%7匵Y;44Vϻ6/+ŏ Jc"JƊ3&K:pgm˽&9/lkTEIR\.}UCRA _5aW( RϘm]?lﶻl;ImVtd ux÷uO ax$r!jY2Hv5`DK(o—o.E_'dP!lTQ2kjw%{$6cg%W"WGp:aPdCؓsXSH a5*Q<Ab1mJ|E_0. r,Q$?P`(R_m DQbuTk0jḡř g &Lrx0P_!1jʹ&&&Ð(xD,C·X|*FQo AH>[N1Q ^-[FLO`̵ KcJAVN:q*_F~qu) [~?BcwxhI]q.B,(ixsyE9Z=[Rb|@C9(HX[l_&)]Ȁ7aGoi,:U\|dgH3Q =GV7{tP5 7~E֢WCAi ֬R;8A#Ty>MZ1s. |ǮFq}qYv qlr`с|EnRs!Q:Xf+xEx~u\1FH8(bǯ-iJIdR'D++prx8GI):.{ K__xXرaK"6 X9K^w߻ӰмE(AhXDT mOcbRQ|@8MD$ @ @b\$bt8״Pk& fOtBە~ո6-}W$_0&@1VeίJKSCŪhZJ.IJM5r3 U6F"ʭ\h`!?PDLZ_("\͸[gToP}KHKT嶫fm-zn;#6v@Ҫ̑'>r/NYrq1RUw|_Օ(P\t= H;Ϫ)Zճ1lS+Ix'ňK#]}6u6e;!du_C pX**kK! ⥊ CT.\/ f Ljoʏ+δ5u+z*H@L\xәG[gVgV铲5Y=>^1B!K|?MH^仔)iܾq 5B .O|?=}Kڛ蚤)\PĔ x8!ѫG@(3h ^!r}y@LB0 @O(e @  &nF0a]:^0bcdJ@e8u'^A+rW6볖$&R>ԛhfv^ \UZJU/۩Ԭ|%u A_cؔ r)aIiD4% ;J\[<_Ghx| aY1G@IBX,p։׋h*^A;aY̱'>䁹|7`3qmf6벖" & #2J %oj$|9Ǹq8m<}k3tBPgkάWi&~[ˡ ~`\[mȮm? 4ZE%RQyD-y+ ˡӫ19B0"==/#ȦUt4]G*e/XDZuygUP-&l=9cLwOy\y f'3I0y&;7_ ~ygϲI\T­q"T+`Z8*ēˡ;ib ]4ŠsP#ė;;ܳ'~!Z8B\2VnkCNipeE‡Y*9gJ>=8+쉷>kbg1/I,*-D\@=v;{mZ;*Uv/?g9,ļ'aFV\C :ʤL6Q1miZ dhaJ<,GYb5;_X ⨴X*VvM^(EPq|3b( @a/vgc"_fTšt= H_%m^ `uNKA?kXk7V T3_d|ܮ/hQ:6Uv[2Ƽ.)'M@FtMF?jqgАٱ <F@\<6$Y/,J ,!Ey%,σEZW><[Q"-l"X4wŚQ 5C{Q+v:g25dlX)DXv>9ּNą3/rD?P*th/9cmpVmV,PLcIb@ E),h Og2@b1w">*+iCz2h7lYxfb ,$]Nu$ V&^tGtvfΧE;%/NT^M{[pUcM=UxNW(QM(ׯM+@5>I 8Aܤ1՛UpQJ'C` g7 J#!JhL[(1 p !&7cs˹CL*NH9@nSwN,}hy92XptxP͵`PkRxl(( p˜/`uE=%bVLObBkZbU>AOg2@M>6SyQ-,z(-[Z|V8S_hKKP U`)[O򛤼&5f썟\U62 6qbU>"%kN [ZΟZ&7rZ{ZOnTR*]7N-OIO=c]sZyШ+IDATC/ez0jk&Q1Xl'7œpuPϱchx$i<#=LLbn5`F\*MdkU1ݱfHӊp6f= yֈ@?fTW=T8Y|V!6C }%mfX/]*QlX@oqB6]~~!"cU 9J6튲'9 E^''|0?x4I1K|7"'69wMy6)IۃY𼈳F;|o29 1EN y@rH845r@9MdQǻ ]y쨈l-%1nͤRxai}R R N1*qn1eNN;u<2wǤ& 3<#|#Ltb '#IK`Mvib"'q@>oS43J׭QEW $x-0 !v&LwERQ"tbα4_y[B^ IROF8eNns |3m ^~~G L}5' ؽ>|Jq\[¬`u7I0bgl&`˕>%1)n>s%?d.|hS]Q\z9|(no|a-9f+ʚK1,q՞;ɗU%*_Ew t]PV' C Sꞕp6-$ \-*;j! &8CWp=4ri6aM>Pó%c*h jΘ.|Q۵=9-YYs-o$ulC+e"¸=FĵPTx>l@a I^<I#K;2/uIjn,=&(XW)F<|xڢHkt8\wJ7w(̚-:#"먦l06"ozL]CKk!kO㼢YA7$|8^ jMLr.gԵ b8AN}pEYaszulI/;\U@JҫZf#\Xs$:f?4|`-Q,B޼Nز%6h#y}UU#ו7"hֹFQW8%"$h4 OA]㳏Յ}K@ Եp|;~=:Ʀ@5\-ƴH+Q-:N?@H:75z":Ŷش3}ug$n "5rК֯gj&.TDU\C]5o -άʲ56 @vDo8r8if4[KH*B:8`-6"\E]JH)(hSTAYu^ Z\$EꉾBy񱣱җ:ޢWu$Mgͨ1]2>V ^ojr#&K7 4II &]=}w5Zlp|C;(Dt й£a@m롚s(X `@zqf:8FL6*3[F8:KB+_$1\'|z90\}t_QK.qW* N)] JjTf, V䁮p&$h6G^/" o|yƷ3RR0oGt"sLrM| "za/xOD2IċJ0ӎ՘JFu}]4.< K57L#o B):N͖ `w](́J\aoZl} # M;9~{!}| (G+ Њ4mE00/Qrt zi3\ߤqYUfSlZ4D`rb ÜM1F V VpХh[HuG6ŤjU݋BPū$k(-(kec =\}a da"N*.2˻ɊP " "(pw?xe%Fw<:iPk.6.S0ad=•K闱G2ĹVZwK}@;7Lic<l;Q{wX%A_@ML ŀB]xc'FkTOAVSo M49ENUO1KC9f}t; /kf>wUo~MiK,"po $*ec&jTzhJ,Ö˻MIfa}6 }#qB,{F߂_&xUŴ<0@@Ǵu|{CɁ5V'Tn~/.K꿬롩l$BdgC@9@HK)>(oSG]m09_//l`ZAUC{i[3lAl`vEE3=~GĔ,":jƝ@@VO2.HLD\rNԢ#~'$Q8@|v56/Üc:'E;)籓Y='Y-O&9}yF~ $ho$J߳MZe10*b{p2NG /7G{?occ7V#Qğ퐠(3*D8GoI* G{C Ю_>V̥a@sϟs:+"Aa"U;~1AnkL~0Muwc$W}Chsw0ۧ}raCB A.@yWS?AdژJoRT>pMO'#ek"Hȫ :PRO)J!K޷Afm $xQRϢ{ߣ>Tom _WXڻhNN:Ax*)zʚ?ڷW!ekYн42E"T>?U>F~l/+ LS1:F+}KNKh:P XtQ| ?X;^A~(V/Wf%SVg7و_U y9D>wpIIk~FޟX;/[ˊ욹?[+*vZ_ %S^8P= 0+/-ӷ^Jч >i}r$S_Oi*2ފ"z%~qWbE>PP|EStEGIENDB`zarr-python-3.2.1/docs/_static/logo1.png000066400000000000000000001416141517635743000201410ustar00rootroot00000000000000PNG  IHDR[~gAMA a cHRMz&u0`:pQ<bKGD pHYs+tIME ~IDATxwUչ߾wfl%[4jb {5Qaf`W5k F{G`Ez{^}ff; 0{v9oﯬ $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A $H A ga lL4y L{pfD(4dUw3AU8$ɃI7 ejkJ 9 ] *Ab@ՠwAڌ퀓Dž4/`o#Uc`ȌA׭!$HRӨTaf)Ik4j`1QL JAbO/m7 ŗe$2+k#~~  QXF _W\ qH?R}d䝜0b"ua Qv@cP+wSM$!=&^43HCPsr~kT?9 /]5":]o!1 k1寗 !p1p m)kztNc& d`$C!-ֲH$@zUw A Χ}xE@:,룭jB0tKa9Sc(FoLF!mT]6KW4%H\HN kՠAU";: GU7[qpr@y'Q!!Ev6G|5M9.Ћp"8x9l&nJꑜT6r)Gxc\v8cW*qڛxp|`bcpzl(hsB[{ow3x{lOtvO>9ÙqHRPu-#|tsh < !xv΢~u!fK0'3e)ISE%Lt&ѸNEb0L).Fx2QD*S^[ryc >g+B`u7ҳ S>PV5p)T/x /ƛLϰi`0r񉪋K 46"<uL˓Ǩ0s& 3HCԁ,ZkI}RԱ`"EΏ-Fp{&܊Xg^23 UyE8ZcGp{0<1 BTat\RzTߓ.UD.!P)&Py* {O!I"Eb,3& ȃr&CHK<^\Ro`#nfp#ASaP=ӧ(PQ\LAN;hPՋnC8  E!gR<8C^WO0a^쟠?cTOF_oN\q ݇ [:K$!2E,H"N"4E{710 T6S*TDjJ2C;4qsa>ǘqW4 c>.cؐ)4q".0<хL>=|E7&z I\68l !ޣ0h#` \[WN⽪su1q6*i=<r֔x8v0`H` KqH,&ÈbQ0tYླྀNy: 9նgԅ5xħ{} ģ51G#` %Pg\FO-btkS4Z  `ıPq}CB`HJ LyFqb40 BuQ8tGH:̮){|x_đ@:5!<Sg< 'qbZek(k݇.6] J)ظj?Lz87`2PK&cSR$Pѯ@rI8qp*KBj)%"$<~N8Jϱ _bN([v|8>W}"Ӂv5@b\(ln=8[H89Ip+0 Qd5OH#x ~";[cJyT^PN7&5 "1Qy~ L5!݀ÄNClEKrdx>n: )8R;g j,0e`l |\VA`ԠH&,N};=:j"0y^S8TRG`*N6ϘƬ.gnz.nl}q"_b"K,ac-<LQfU `Ya\~\4 xy{0,?Ӊ| ^|Cg0{YQzy8 xgp\tK>-4N3m1h6pn:O?!>n&%\sL;עa'`#;yx8`WD~qYQfc1oÆ >OLYj,1傁L:?I&ݏEݺg̥`Z׌#:#zȆ <<s1e92P$w*EfTFOtk2RSnd_}=":5 ,l<  a'1!ӵNukn< 0}'V[8o* \<23cyYnSʱWK߄? ϼz >`< tm(^Xڒ!d#.3 `BF/k=)u5Qqz[ } {40: q"mG Zmq&"Tįs}iFl ӌQvn@M׳ efv>гֵǓs;*Ͻpsw!99*|":ΜhA !W|R ˁ ;Tp:!UwF!cf{35 qMV3)aYs.Wшj@*n{Ay(up30{1@C|܉:.U7NTigaKjcVJ3r6/ 4=*g΢=nq<?w$ 畢("QYۤ7 ˊ}o`v- * RBqW?l`$0!40f~ omh.)*,B^F6n Y A嗁*-AmfH,Ifp'rs dψϣ✁,]x!@$aw  FkV"O#c)z= \/#1#:9kӀ_ L5Ǭ_c bE+(DG}G%?F^5ǿ6&&0sS謁T}K?i &#B\J{HQq@ cfnpڔxFhq;!}yO-/KQq@ov,΋t( Qv9ʯSyN3j rTE5wz=Jd;yKɒ1c$[c.A؁֞r;wqV>pXaa>KlaP$KWc":p{誔)䡪!EG(e0*ϿBs8"-G2|OST`T\&2+^U ܄9p6y)Sjy &49 Rv?q0{y%HO!1c 56  ȝv4ߒٞέQ20ALBC|G[I1$ƝA8||X^>!n{M78"?^ bcBb|q];sDL$-9v-4BXk |INb1#Gx[[gɗe̟^&i/cl87JS<}m)1͇My3)kh1xΕ B?:q wk@5b-YD鱋Kn4f܍NO'].0TF Ƀ B }#E0 oc%oSEZ߬C.ho!DKح]݆gvOb^fSd=k0~PySby'Lj8(pVzg|DR0Qxb4'[!taA7mlQpN*F։> ZRHSsn;P<pJ`ݭ$ky&X$cieOt޸8BXd&;_9E]<Ů˨8Ix܏3G:0kk#+!ģ.q["7mǽC)w85CXWf9ۈ[na kXz7A5`$́Iae"|:,]j ;;0WXDdG3 Tw_!vA`CۈZ9xÀsÂƱ4Nx7E/"[u Sh Z&ޫ:(2jǸgY`+H`2p<&7^b@=﫯ʕ4En_v;#^JL_"[y]h1T@}Us|ȝ1  Fb,$d2S1SKq*p$aeB mH; ` Cו/Â[ft@ l{Twoiګ #9ܜh%ژ#zX9.|d{?P^y{W̝{IH2HϦ' 춣 pڐkD/\kĽj~cb#+e">^sk{tC=Q 5SNci'֥.$ DbV&$,`z~!ӿmv7ieEST*3/!}q:-iGy;0FYCƅEOk9/x jjb҃}O5lX]{15i 8?ZΌb9/Z6oC%mӪ6rwiLQR:a>ai7xJ+e(Wx=44 p)Gݍ]4Eń{G}91urbp.:z`;5x:*>k)?jT ep 3 Q&8c~w5[fڔI_2zO\@Qy3-"p#YW[ނpes'&XHʄZxR''\Z&#Am1406D/ߍaFsO$3 K3KYy_SiLmxz*, lL16%Чu/0^-=}+,^‡G}KBF#dYI({-?k8Ռ˛n:W=?@QpRY 5`o/NڡLr: am-Kf(RL< ϛkNbzo$#1+M_1 dqy5^W##T6߻a:P.ߛW|Ja++#HБ@K8,w6U%u<yd:ց# H)K^lGb8Ȅz[(;QKAO<v0 c{Dt[_[B_.~?} p0bnF)j4W->2cJp>-$dI`#?+Y_Sxӌ)aޤWmޟi; }#lgB-c c-`GTVeXl+( GqHI4 3(c4{#K^D\^z}l&ke S٧evIj3vrl^ D76LZ0j"R)^G3prwpL1ǺZ3ՍSct%lZE ,H&PT_P +r{|M5) L'#bV!?s5&*h:g64f_1q AaH 7;f/6j*Z;~/^PϿA$QU8|*к.>2xVIC`쵌կa.!vcdz:wˊ *G1Aʘ= lۣVoDЇK[m4M?Wkt2uJ*zLmVhtCVq`:N/جڧN t6Ǔ=a~n^ Q`|Xp|eL}bo']-fDwcdȦx 4l'~>=R`Z`_"61B <Ⱂ0Wx,dpx셬T,t7QR[4G A'6м.]DgQFv̶T=:.C]\acYELY'N+h,z :̳OSuhĺҜNLǠӂL_sn6q.a"b˘UmqZ1Si/'.w2S /x'hP42pJ' gwBo6_p?8? 6#, 7M7S;/ά1iLC/גw|lmQ 2u?SFp vہ#iHl-TQU8L, ^gQ=T`mo!pE-">u1z.VF7w;Bn+-X{-paI[spa#iC b+Mn]/x[08<[ek)G3 \ 7 vOQ8:n%J(| `'Lv=%BvTBϑK=8 x9x~4xHFiBsR;,B$aem/|(O,`~QX>0׭׶!b o@.X o5BGal FawnE bJYEL8jZr)mC-Ć+b:eu?!F&*_h[~֫[wD!=G/vɲs/}oB@D`5dxo)y6b;?xL0x!y>M Cl>>jߙ9jHֿ {aj (rQ/Z\˻,(7w1$^VINYs+n1$\ԎyKlRd#j Ȕ66D(7@m.C[wDHE1)z>m  ¥ !XxC] En%p9<AYߋIg c` \ωSjn&EBxx8GUN|$YJAQ V 2]c˵@5ؖb٩U_%0`hPg*p5M劁1 bs#o's&bXJv?=(RL6oL FaT0 ZcaZtӔbRd3Yȅ.`;w&Kns96#oKm8),ʻ>{R*dLٯ -~g~d}^(32@xEe4b\fU}Klb6 x"c<0 A>MIrA!1+-0A<3N7t=A-pjR2|=b4ʹ*;&=0 U#L: #Hal@2}Q5\fUFrF !>7LdvÒ\h<,dcQ0`7 dz&絟K^X{<{CŤT#K]OǙB}^^w Rzʾ%(MAT\Gbl85mA$ae++Ȧ8˸c?aՖe{`NHQlhW|Tgg2ĝC>.m…x~pPw (gji**rc .~Y-L:#Q дX ,5 p)nN'd:7`<}&e ohL몚( O",m @P4\@DQ{ dmD#$6KCܷLa X,#SkPs]"G0F>-RTV sK3nE/H^%dl;%~eyBf/*fFlAtI^O>%rHP/VM|2b2vK(l6%QK]f/WAwnQ+#;MPo#@tpVojBB8Yvo g,⼁#{l"Ȣ0h^^j\&MEc^\[ H|FH; q$(sm(S{iK0aB`gMܽ?T Z74 @@\ jLsU-gZHnKpf" 65i=! 8)g$>LGGט!LsY^%XF$aeBo9&1>b )m &N=`KSxQa ez, ^SܡȾB E=6$O)?6hPM޵p3!3=9܎`TiQfHeRBoK%pB*quy= ٳ}nI'b\)E۬7ka]@עs:ԜJ| +"W1-CQ\Qg)`AVÂwQ8tq|D=㼃GL$a%Ba%dZfP-`٬Z(YoK| j=M:]|My"OTZ,tcR;]i6xq(b=";Дs4dR a[+$%|g籴.z:&6OZod-N `{=_Ln%p ^V|<aNRJA^˵g8L, ^xq~<0!ϒoV3d0#Fk]qлsRW]*"h:< ښvt0~tKQk >P$Vsn|q]EMa2y6zᖥvbpuoOʨ0_>2u~Fy{,$;IJX1'>p !`U|^jD!,:q8QMpF`L-#凐mGѻU XHq:b2D֠A;V-lRL=ipj>C~ڨ4 *Jr!*NVs Yyϵp+lֿqQ-2v~Ck[׎>xL q!6Z^HȘ%cyWHU0@틸kP9=̙]4BA\xx *$R DKƶ.hHj)R!." ' y/~ \?kd|j!J#9,QJ<mNϥgM.KQ 4>=ߍǓ4ER'fNȟ@ { 4!+זܹZ/#ВE!;6J;H8-(nI2O Y@Z3jп4j06aG^RǙ{ WJ[LT";!yc(@1)s V!2T#'w!}=s'&[V3lg>f?&&HmuB=؎%N>CY7vGZ^ź;73H2Ls!ě|v3ʥyT#U+lwcKS]" B1^Ԙ`"1+N%(ԉE5n+;Кgܺ wF0 Ӫ6ኢRm1 {|[܂ B}#dWA7L<x}{Cú_ukEX^ğ`#v|U F^)u-U0qӆ {oN3ɤ] y7ܢ(ۺMb}y#!o_BhN &gFn\;Wλy͌彘cb|n}! :a V2!@ӊ2 H?ʳ){"~>-v74V Y1Rsy筁qp8Tl]3G#n2J(P D -n4oD䑒Ǎ C%AR؝yy3X* {x./ ȧ^EN3MQ[V㰨oW` lc< Enyd#ogl64ʹ*/9A8EG'OWUvvgЎB-Sb}Eh{`HDp,54 LxJ#,gO ^I`_5x.3FB<8et~dKQB#Gwbt"(mZom/s}cvn%O9+ ?I[F6f`U"1+疆Paiʄv1V,Ƨ\{x\.WAmoz"-BA [e/KHuRn#anQ-Jy4=S_T zQi9uROiujV` .7T!v}*RQ1>0pO<aGR܇k1eYn+ oQh0 Ήi8X8аc/^RC)ߪB"cap'Sf)K-6,Fa ƝonB1~Gc5ew?L1 B/tc8B@5B˫?SYM RTM'R]~ֱ-JP({s~}Kq4pbsy*X!_#ἱw5jt&Bisc'y3ݮZ$GϏ/ΫCQ&fol؏%(A(΂'NPe lF. jɘ,7@ ;p0bM*;6O>[vy6 4y]0#_-W@D%`"1+j'Qo  vBJ ̨U^~&=N]_9⽏0N0xI!a BwM 3Q]ܼXjL廍c 6Ax(#4 wfVgUd1 xυ_DzMEvi&bTm\ qQ]^``R\v ʣbbQTyc]uU4XHD_2Siv~^||GX#a)-o#K<+yL!khBl 8x"-յ3\;qh>ǺB@ZbLi'z,bR)z&P9Ra@SR.pC,ݼޕٮFBb$;MCPux&R}yk?y23xPEB o?H`6{)ʷ(faֱw[|ZG~'o}޵:ogwE*s8;>6r3ȬτΥ0;˶JBbV&φTn{8t4ぇ0Sn+*@r9>E<*e}d:&V8LU` b[;}k[!WNwoah6""ef#$#6Öh"CpG܊8թul0p(P |"VZb'- {cP!6IsC^G`B8m]eb9O/Clb$a&9p2ٵ2/#>Հ~x C7u5B c|ND7/fNrSM_B; B ܊Wk39Xcp THNF>OC b1bdm{M2'Xypo3pax<#F AtBqvTؗaD@܍.m0Ύ9tb 4K[B<;#ن@W<7?/FGx#Ղe^ .s f0\+bg!v^4U,-  A)"aApOlܠm,0;@RWtkJ[{ڄת#\Pa120h/q19|:* s]&~~8vlqPqtN!^73U8\hSb3Z~0]pJ%qM["*W}oAx?ίNS>>gj vg`"1+ !pM _Q38/xkת˾ 'pKd:Zxr ioXR"uF OXȖw`z=6arӋ\8c/k-Ac^981x}`_<a@Lܠ?6bIXiG:7jq;x4tP~zofd]afI}Żx F1>x-GN5~1r(FID:?#"P*p aC`"1+CPB𛯳]^ܳc#{ov-ֵѺvdq|8O(|mXQ4On0[jM:py(lh6PE ƕ\(~AMp^ lgxW*"aؚxbӳA?|]_ <׊êDbV&BuWt˟?NX\vIhK7cZ 8:v!mɊ r"Ǹ8ucY2 adz~tG]ֻ8796ה)F> vL c(4<ce+1ֿt|*oiZ٭lS]ЎY1?-B+'[+,޽ʆ6=940 K7V8{:Hi1,`OBbđmoׇ2Fw*2 1+zZop܉w`[d:K_10jvZa첵c [ :Tϖƍ='>⛱e{R ßJc A®g@eQ;7.ư L-o,^sҾW\63׻ʋ/Xqy3Y_Fu6A1gb!!v\u-2q-Tl$P+̰ą8{+7/d}0-D?$[ie"S[,ăAx5܃R^6 5r,!Fw] -4A@9}8_q) D> hsx*u`Ţad!;w6M*gQq{ ̤{-uךe /Ϣhb(+* H0'b.b$OP]x5z-BC{nDv{'C| Wx"aU)8sX^ ބ !oӴ܈^:6aHsm$XEHD[z0P9};_a\'xx@_3ΤhPi{;.죑xV.85N/k&f4\ggSE?F=+gn2#3ʂa_TJF+B`B-=5ADSE>SKB{04A{\k5&Ex>cS\K'* J˛t,7L^1b BlDpeuڍ+j$aeC^xJ> wz?ٶT};!^08 1FZv!M+)yu4s6we"_3g;iB eI׾|`⹐PKq9TR(|e+[0; $YGil 556@͒YZB`{"N'ɒ 4#S,S82{G$H$/~I @~p cScif͵AԼ\涎C ?Rg jL>?kFOy 1+j=]e3T@!p3AH]m )|i0nE 4SQ (|XyOL8I/S_#~"8^5os$mq Pj8C,$JlEvi'㍅1zO B2]M g'`̍ Qn?>|F {g)R9py6xGZᘰ^)zM B9׽_j0w;.[!ŠCRyM\4姾x*Z@+hڐ̯9 2A/,ػ救<)3WQ`&SB*zȕMh_❻8. 5致gxrU51׈?sx|R_fLvaB3BbV&Z^ Fw5ΥFje K#, xn"{b3 BB c_R_%`{bHl`Ʌ~ڧ} >:0ĺK9zMi̝4q~d"q*r`&Ns_ Խ?rF<6xcm g w^Y?Z|L/ڛЭ?d}2EB"^la.GE5hהz]ȚK-tB1ٵ ,f'{qXh+%Pfثz"t%dNWx0vwAx-RvA=Q`gk"ޥ zՖ֋Dr hMnZz1So{u9 ЁgďD3`~'ɹaW![h&_)fUޞ"J,%; J47 {xqUk>u0fa<8?_=_8)P=5kV3'|G}U>%p8bH !1#( lLS(R@ -p,yLmƴ6- V8aYPBXlz-q̑ HpJp7R-kC$g"R+0k{B; AeTq#ufehMMnXA |D G+ p|1s_J:م)<lfɆ#D"{؞>8^K}7kͅ@K䙈 e*fw#G"nr n>ڧ>o(b1A9Úo/aRH)ˈVvֽ ' q@~sAW|ץ3ͼ gV g,n3󎓫.33PRh *~EERE+4`)HZCSDDAH $dC%ےl9sfɦl5MB3gf}nGb@wġNa3щ/@#6>u4>d+ۉԩ^~< B'>K8JCZf2|TT1b JP-f}›d<߼e'sLb#?o@D-lK&*&yXܢؿ}cɈ)HSb_ 24V81O ܭ:<8N) c{AVט7&74V"2q~c*KPє=s\.M1uW7f}px)(s>պ#0xbrb2K%ǮΡ%^GD`NS5@/ġٗ Yܯh\7Nadk y]Dl49JvHTMᇱ%zU"ج&'Nى8^ANWkʻ 1J[D5 ZEThѵװdȪh*A> z&p)orFz_mSyp)͗ 2XsՄ|ă^f2wM&!b#d]01<zUfvmmY@,{;Mn{Z"GYA8"뀙8]g..4Hy9; qʞW`q6Wh]Zw,ږ^"/QlT\n;Փկ L>y^3Ox8M] }@ zӋ˹Iљ; Td'&/晊iypǃ< ڞg'(jm=^,BwxdѲ']ӜE.[ZDbOk"{$:QlZl6ߤf;LͱH#f9>0k"e&sQQ]Y] s׹޹A>;4pYmj{?Jj*'SJ<޷ xTbMG8xa{ڛn{K}qA&Xu˯ʛ:qx`= ؓ/yFЃ š -Vt^BF> zt SEu9ijYɋjZ1LAY۱:)7U;Xu$]cV` wj @oJEh]E>e?jOslu92Pث5=Jov ;hEPNp{ |׀YB~&fc9 ^; jr@ϠudrҟZOŊfs|[E5A:>-qAomqA#; ਰFΜ}I?O՞؊6]pZ'pCE/$8q=UNɈ@BDػG;z`P~!h@a9#Q5jIv2 ʞu?oǦ,VƼr9 !D=/EQyMT|8 m"2([o`G{bRGGTiB@%1gd=vGL|0TTg(~w/ʧ'1(7tU@Hoi)kI$1~}NDaPj$ާtENUfh0-Uťٕ ?Igo|8GNWe(y]ky9Oݠv"h:U'A |INпa"q,eP: اMbQ6"UY'o"50lU-@ Ѓ=Ewb_*'ۓ?Oc֗_عE*uMygAOrk4E6T{uo%yhoR&I#A. ^圕y^ 2\RT%0a}yjMf`d>qC$ EQ<"[@uį Zh1 ~M;i U'yQkm"Ź,]۝JAH<4p(%V *Y( ]>0:~ qBTp@BgbA&${ = i% BJIDAT'5RO?76`mMr[:O'adnV` Nb; EJ![fo6j/P[J/-2hPЎUSQWp],^~;p8š-t.u'ڼr qMuT> 4µ}T%ay` ;ྜ^W]$ prݫ<=۫qf/~av￸d<rS}d/.-}zpT5>[޾EBG-*V5Yzпo(P8ɈdmžEQ-oPChQ Lj:F6r{oGR[ yvZIL*(܀.Z{S`V.R^iuaêx'cQU5]Nc-Q=-MrX՘3 ,ġ Ѓ_RPcFag0֜ζ27&湤PBZ{*_ B"f 7(ށh1vgZD5w#j0-OLVܿy IJgWy6r3g'02 oNvn-m9pez RI,MQjns8ܧ*"}ZK;_B%rf{#|ߍ^fqI|}B=fQFW ЧlW# pD'b~AI<*rw7%" ~NJ<ny'g\ݶ-#8+|/p5g5X6,Z٪:Ǎ(3ii̴oow)P4w >7>Eu{23vU'R禴˃[hn-ge.et0;X[(H5D~O\i'w^oo|Yα_qו K:XU7Z;vc7?0>W@٠%Pr8(oG^@tʯA!= c[oߦD^90GEAǩ7 w ܁7@p$x{n"%&/+. Kחˈ% ^U?y?Dh&(y7Í? Z CA֡/sO>CCe~0pA,[AtDpe^lk^/ M_)A V) 7{ME~"*0{=dN g}iS8m>"rZ[X F{ M`M*f~i8 [ڐk@Qd0wy{jAnt0NB3)D1x޵۶nR#fPoan~v^ձL2Ix+7 Xo͂^8pn/?c^uX˩5T;XYo=CZt8Z[z)6"Qa+X?) wH:ǾuWT0HD2!DUT *rWhFdp yu m!@8!B3S۱@CD8$|fUvߑo]80+ThA$M|Stb,b7y P 'c]B-2kS[QP`K1Um~~~,[?*S\g#9LDT|?>6A7n/"E7jێ߹7rp6;!R@zx氼ŵI]o& MDPx D&ϝiހQox^Eϯ@ȭrD 7",A8]ac:^>xPVAz5͝ V4/Y8g¸z {f~g,1~Ғ ݿ)LQj MXq/g6 æ 8 r6ҸV,[/фq W HԛHm c~ 8T3pܟZh|e50S>l%+X xi:O\ӛUE  gzך3h>C<U# `\])a_Q`oh[_Cε!QA?T6ϸ Ce"Ҫh#>RK\/S).e1*ܱǬ,UY"~udNߠ !  N`6X^<,dVqg~ۡuZܠ>Q~>EfyabŻ3,'=|8'%>yJ]l|h웖NGRA"9IU r=ss,'6z =f ~}Z io_-7!_F9$p+ȟ}[PegnKvHUi O\Dyor1..! E4bUd?/75]d$E' K.ն7-;0IZ\3MQo}X~T5& Wt\Jpӂ\1B]ntp3 po_я+.V1׸q7k7|*K9)ޖ,Ĭߡ%<0ُgNDȄ=Dġ)~aIכ5'¥_t ѯ%o5M]:!H{GnBSK8}`w,֣*Xhh9$(lHNG~h"~i4l,QZGDs-N i j;ҙjMrjN(S0DvcQxP! s#37IU1qix"}cbieIKUSirW \r=yUnCX@mïj^$Cϓ[zAw`Y1Zq[*09! X]WUA  xqGU2": 89ʪB'薌Zyr р Yp`M5k b;Vu>!Vʋcld'ĵk9Gb&u1E|" }mUxOe[36"RT$4+T*Q-$;i2V\Cv¼ *~1BY 1&'c9@/ġ';TAE6p:;~K4qoYa44D琺[z 8q $Prb9 ʻEfnP*2]|'爵_9ׂwaLE{X7*lF_z?UM&<2+Tڭ/i3<g{c^/3A\5̫<#Ng^n P@^CXiExǎL$]>&"SfA+ՓE0n>ő 'u݌wNE@ Xp㓘m*9XGCKe%x`lXbؿ]iiڔ`1F{AQ߆|iZpDۚr&) đgUC9l讫N؛J  m+.pz!/zi5,_< ѦpOhns+T9?qM8͝c& 'zertupWrPۀTFxsv)mS^$C҉f߁.U\c;V `:y~ivQ\jbUZ %)(1wv5a9Ǚj3DѤ6I2UcF_82fS͈l+y_Wʭ&'\DYlU(yItZ]8 cOD AuHG -2r՟׽}[F7*MCEդG/8u25QVAzDA%wp@m Gn%B"rСD4=: XR ,\tġ'~>pE| 0Zj\'isT;_Jz_;Mݳ(IM>DʿVq-,Z_JY`(Ǫp&p6:W"*o ,2Q(^."WjNBSc /ǩ̀('DġX́FDGʷ/:k.x_%Q$։ؐIk-Е3>?\V*t.9CP-moDq>û{sj p"(,w"W&JF+I"!WHŒ'ߏlz =V!#P W(CX7Zx6`$9GDiU|yxI,XuN;웭M=  П7N^(xU^QK4{GHHp4)2eմIq=f$yHPҽKG^N~~giu~`BV $*^7kvDh[$Ip3<$xl 9R%P?| 8Փs7(tx@g Ѓh!΀Qh})w`ڃy.@N P(TX4EU)$yy(_W7Z^sd{Q @2˱ă+-N}tc"H1+y|UO'|]Z a] X]Vnzg[gsڒ g(qq}>h:qY% Z榳e?̓L9T?|Ua%pWɻ,%8 墳+r/u {"6e @=_ma;C=Jdın>_Om%v BN`7,\w:6MJahO gNnɻB'f-#|zr*O0 m{ (1;gsQ| pe˥ЊeJ:A`pN<.q$!t9]s1p#]!|cM7YO'0BD&͘Ź}BM (@.q~.B937XUDbgO H3PS|[լu1g"Œe fѭڰS)NkfFv6 Qq<ഭl"Gb]@(*mUTw#PG+~BnGUDAAAz)(Wm#Ao\m)Pq#8F;ӵUz&;W;~( &D{`kNA`?-gN8J.w nNA%B?$C?\b1Qjep^k|d㇬ \ۧ5J򠎺8/qt\:HEAڅ;!([8SMéC2X'WY8ciN;Y| r W5b A?A9TMkGEՌUܯpK좗 ꋾFDIAս}>ʓ .DAۉb]Q0q <]yEld! D"TpXCB md8h* >hs!xP}40XŃ=ES1[bomҤ~'sPV(@6#mB,A( IE$I<;t;ǀ]Cv50Ww0|%5l;(xq؁x"w>m*!ΡXu)!HUh j$"R(w `_(Q D|* .,7$pKhqgsNBOA?\5G>#E* zQAvhhnRb;C}I"(&CWW:ji);|EQqy,7L ;98Ys "nwBA8G}xچ)ц;fiZqh(kqj\챥 Oy/׫3ؼ!Dָ#ޞۗHy<[My 4W;QȚ\{Qȯ~8$нql^l:XP0I<RGT'MjVmdDqf-M!If"hjJ7E NPMS?\WۗCx цL\Ql -rg3<Zb/$lBBU/&?D[y<[M>oI,l8c$a Ai~EkuDuSb*z"I`1p 9t"޾j\;Qvng#/K38 'rBB^2Q#(JL@yl}1#e8m&!IDqsR_WK.WUGωO n#Cx4Wx&y'%@p!քV9nY]LT)ĶSt } (z n\5"#-P4z? ܤ#q#_m()8OG#Cx${;\ *3d [P7zC'l(!Q8A=Σ q) 8[G֝C/}nvbˡ8du6iyQpПp;&AUj3sCt'Afܮ+!A 4ת.y mPtIMzN!o #{1v)Hmj$;(A;,d/Ft$@ @ @ @ @ @ @ @ @ @ @ @ @ @ fHo'ˆD!@mpؽ޾@ E޾@>x A{B@ =HzzΝ/&t ]C (@e 00p}yОD!@`!q@8@A@ Ё @ @@ t C :!q@8t/v@peb8B_5%>nm { vQT ]8Q&c\hp A@:c(|.ϟT`LUj@ Y(+'[}K|׀o=_z (!!=@"  :¤aAD+ @&z@+O ׷{(DJDc&(躾.dYCmꋟ1} A5'^yFn6pF7n2VFw]Y6qlEV@)c!p`K co_kZXhdhJOMnPTzlAl;ou/ˎGȪ#"<~`W`l(#ܹeV[Go!&7NTH2UDtDzFC2l#AK8[>%K]0x[]Z5BF!FypW`,3eCA8dXɭNJZIEA;鍝MN0B@bׁex'R,!p0ša=?۫k"^U/vGń}'UQa q ,$?gعħy "akJZe'zTD``7b+ sWS ,izcOG 5(B5^V׌p:m`{ʁ#`60q_(E98 8([g[ܤJPؓ`|c/M<X3 7] \#aQjKgϏ(?8؇ -g#`&sZ-TM`tKv߅=o/+h1$#J(ӰSמ>otaIn>﬊U:jؾahMM](Sj`n_]i_P Yr1p%ӡC8/J~H&F +فe'"*Dr+p })qg,3S .Eց|K47!.Faq Ϙ=#GcOJ|d`%(!i(VΈ󎓁G71%*d׵ "1gBA,Θ}iG%C rv-jsjƛ$K~%:~N>_p ~o_. CBzRvd8SK$j))W:ԕ.J]$q5d݁F^?g_2J_8 ,E8C VP0)5ǧ&^YJ⭁@c=!&rfjr(ڲ_/fa?'qgc!P:VW1vc%o׊b{t}F8;EWgY(p xJs޾@~IH%*zJW:d,& ,#VFUuUnmݍ9/cU`YUi0/1R (8+\)YTRrWO Q 0fuo೪ kEVCzfl˙MX ',-ð*[ \ۥXGws*c҇$M#GUA!Q"NۍV&592,Bc1=ŒXOiy(nq[?Uzml_} }WxD<:( pB2ny-Km蚏P{fTec[q BPPU^PHl'Ҳ j }e٩$@X/@ħY Ld##VލF(,FUSQ WƞU@ S(˭<=גUP|N>J+bKͮXtME^Zt+[{OSPV=< ŒQ맼i2 T ;{ϋ^i$3-o21Yl 5؜yL 2\*,ދ>I>ʲx@PePj%pL7W uZ * f5c*%>fvoEx?` WZ+ʟ-ki0qR;E9#خi  ռD  QyKX_p"4-a#2LD[g Y-FZ`KiU Լt=O^yahϘWrYmEBQ"^TY6j&Tw`8MzZvh82Nb% 6;J|:\TFa +6bw b#1*,<l/-s.ײ=,uٳ.*U8˲Q֙[?@#2[M$A}bPr:Ox/V0&PYbf]o_v ;m5P$C,;Cb R8`2ʲ2Cۼ,;Q+ B'`Пgo^/Ç˼}j 0T ^:c1163rBB<+N9S@AKXi達1mb_Y 09lAzeU_KCJ| Ĝ Fw Ksh1o-o|@Xw %?B =ҜD|_R,GMaXkKk0f8kUIi>zTMAQu_~9jOz3i{p΃CdcWirס@,CAC#xTC)}bu%0Yх+I_ݘeWsg>ﲭĵ_~UCJHMͺa7,]&"7=ʆǪ^= DO{r[hJ~v\}>4d9z H!r:Bn8ͯA0P B?\B C3Vp?>/+('o$V忀4*¨ھUj}sÆ@ K@ħp~P2o9ӹ KU4 5ى2֚h*XqMĹ~:3؈ Ad8H/A~ۀ^:65Q'%8k݀<ഀzQ rl4i?blsՉ_f>K8:xe?=All5"ۨ@'JtMD_OTSXص?i#M\͋Uꆗu} neU/a!w~F5Q7H"EE2 Oq'{8z CKp0HAV냈wyd97us 8B>򵑗)83u&9:&v܉u.5{z_ 'vF#]}=a-j1wB@"y5H_ au,'4u9,@0=+h Gmu Ч%Vg+$rM1y+VM!C]we`=Auoǀ4JnװXC]T'CGi#PAxǤszF,ua)ĈĭIi C^ =ġkt^{8iDq Slx)`SJM݋Á#D$tK}(t鯳xJ1 Q5 k%6 ׋&hg\{?ܓmIҵR )yMD8lf:Tնlqٖ]w%˪&~Unek{#*+T֦rʇ41z5@2 {b` tnM?xΕAKKy=Q+= 6a)EwF{Mn40vmS?@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ .[cȫL%tEXtdate:create2022-05-20T13:22:18+00:00Kc%tEXtdate:modify2022-05-20T13:22:18+00:00:>tEXtSoftwarewww.inkscape.org<IENDB`zarr-python-3.2.1/docs/_static/logo_bw.png000066400000000000000000001302301517635743000205400ustar00rootroot00000000000000PNG  IHDR;' pHYsodtEXtSoftwarewww.inkscape.org< IDATxweſ'*z/JE$XAPDP,"EcB'$T@swݛ[vw&a3;3LfO;Mm| i`S} p x #:~O\I@G>"lO}0&!3@҂fk-mhA$_9|\VOg;$D/V}8bʬےfOFO!\F?pgRFDaڡfm-mLhA=%\ '`U%m/nJNnCkafFDd~XAZgI Mmt IHpq&َdugr1{Jr "aljTN :5†>`;*02j;Hon* e{퇳6Npm' a n "h|mVCl-=cI>&96@4@((؜ WCIH&@5l?j{o"Ev Q5N~~ĹDk!=^3*ʮ6ʇ6 %]O [mOg֖t%Q}Av2;W$%%V"%Q{y=*Kfj-mm"#ͪ$|emDEp'HZ]߁ZnU ؾ gˀ%)ٽL'b oo!ۻ"@[ʹҖ6ʅ?֐4wh{vw%$b½;U*ؾvD@e"PbV:f2 83Q0hq@{D>eHn+(Ǡz? m_ߑ_J?Hپ6DK*%sA;rj>&V3O2ydžO؟HZS?Aw( 1nD CI%͞a?Dr$/i1= 4z=牂zPjm"zpJ9QEڶi; `" lntl?b{wo?ptw"42+ߵճr۷؜eETM3$ S/ؾ$G(AO$*r? կ/}H׏/IkBfD?{%}Նھd_9&m"A#]2T]?ARC ukcny J "69)#m"!ipJ o0gٞ i>I?&V'ja*y,~Ib:v[;+$ݚ ]}uG`:%]@^˒Z"`yg`I)'j$yo=IPҠt̵D#]@Ds"p /k$),mm" izY/CϘWTϰ$ 'V+'hc~MJTz?.bI;p*bg &TؖxOP^M KT<t0տhNopgyG%#iDWph.o V++$N;~&#|rVvtLt2MaDLpe6;F؏StES϶oYVi/b;p$mJ[=DPRHZJ9 @~JO耤Y$} s% |pyu o 2:Hŝ$4I ?Ib[mD^B- Fm@JT\+b`:rֿKo }ۯf;$C+)Ⱦ}SmN̾ W1"sm+>̹S#B[moDc}"%]/iVƴ&@b$*fWZgyߵRcV%ϲ}4%N_y8C$R;X81<.\I-%+QDTUA+mi>DP0rյ2W}nyIRbFU"N?+N+UO=Is~=O'dAV3Ոt笪*IҖ6Mv΢ !f f߰ tمh\7hi Y]7t 04/iY\Dp`%}w{,Dq&I:"rޱ}*q?!fAVgcۗ+ɃehA!iy>&WOf;$KQy=B&nm?퐴BfOw8-N9H!NeH*[ 矑SCp:jپ4.:E4 PY|&Բֵ}IHP-Ⱦ*nAu3q5?afrE~Y"|BY*ꨡ~IIJ*oG*})ؖO Dd(hv*Pwkjl͒6bfYf6{%%_TTz !OA%imD<.o"S(< 54ۯ8<*n &qDѽFJZ@[>hA 3N Oy\g{wۏdJP("fQ[logCIJ xhΘ,NE' }๞aOT֕ڣHGnKR"XI^X|xc$]*ilTM Fr  2 CII1nS6}[%ܓ&Ni$A[?7ڎО Bc%Yۇ߉zBB)7D 췉>V%JYhD (:l~ SkMmoo{tB=_̸Z@G Wтm*%%i۴ k 7\PC |pYΫm#^E:jHBkc&~B) άJ@eFD)^%T" M !.!(t"eKJ5諔 5tY!oßTu/Wo&{]^:"[.HL"X{I[iˌ6Gi?Jv9we*$,/ 'S>TN%~PJ@'!I>w:A#Jӻ7!ZRMg+K=sKWeu7qDr6;=̃B`8i:`=W#`SSvrhAp.!(#ђsmܷ%P5`:۟%rnǁnJDo%-J[g`P&1%1+#:e9~$&*yW};gY)G#. f$"iṱ7'Խ>tߊ|!;hSNJ#D $-H(OYҢt/-%rć>Q{q"B(C])TJ^7"gsٴHC]fqؾM[ vj*}o-DPI`s'$zìl?(WzxH\9,nN)ˣOaT%z.e90%eFHZ" "Chs._}:9꩒pE~S Y{nM~Wbu71* _}4B8f{Jfhfx"HѾ$\e†i|OFPzMbt<#neZuM&+TvqH좟P?]U1^F-!HE[Typ K"ݭ,%|@K ZLGei{gCxa.xT?R8%."bGS$+i4>3qJRDgÈ(eJex 8P҅` 3?N$mlPQɹ;AP:28G_ee|uv7&k]s~o)7DJw9s&ί׼8?~؏-DBND3E\ߧ{%Dc[1r+܀H F L,IS`Ԓu쾩A\~k4|D aʄAD /%GwbaG;P"ȝw!6uoRA-k5r]X!  M:"yJ6L%s(V x A5g3޹sOlݍ&1+wMO={I:G/4r_9KYryTk4r? {O:>"3/SYU/֋7r[d[Mt#PZd-ˀmSI;/gaBd-~,!iI)δ^?ƶwtgq%U( ?ۇI $!,m<{r:WRpG1Ht>BDږm_'ދ&+OL\h#>!ҫ>l$ \IyVm_6` ؾ3PjFk;X< ih"L~%:"s%[5Y~&GDҟȉKHc}oJ]_''EދU 7F%(HFҹEQBkI[H h[f\Omqu$]In`;ۛپ%PJ.%{ucI:D[\m\8{?LRJiU؅8#W9l]VW&4A%j5?$7D"<{K*~FHRҭ(`âJ`{}(icI!^M gWt9ک0z/CED ɾ}fu pOCtZ, jwf+R},WsM OHru4yG,%X,Nؤ;DNe Dζ>BvJqY~%IQnmqDKZFA(qf!tn8{};fBZ!T\߈Ⱦa´+˄2fDCgNn#'%f%𴤣LkD!"ZÍ&}O$]AUu=h`9z iDcLR fu(Akؾ̎tIK@p3>K<!\JX{.Ljb1$$͞Kt(n/iaIlߢY 8ʹ}+s7)`AW+Ժzί*cVLdD{&E%]Md{l_-2 >#-JA,nx=O$RJL ?hZ,nNIJ*itL^Kθ"~vI)ؾǡE|lWK!ym'lo)j=Jk $B^JURv,†:C;usI-" Db\L8Z/?S .$ t*TcnF[nVIrt1/?$zť&Ky#b<[%m^uDZZ] }ǒ]㊲eIJ2ɝ~'eUA@#'왻FZLda"PV;#[LdW{Zr_N IYG[#iBd:h"xh-Хg `{h85pAlе㬈:c5uL w ծHj=1pHزi8T=磓}˥:fC(Lvԋk4r2&\BhkHʒ.#L9S*/q%I3)>[cb0Ii qm_DpE |䟳klGZ%mYdI7B{~/͟*2UB.gyNI!{|^]={ A)wWqL(x8h=. KWZ 8/$-JjWqI!<9ϓl:"go]yk%_ #ϑ=i$ T4K Ot=N1ˊwslCʶ~:ܟ>W%~ؙP޳K*]v#N퓈GkRtL̾7&ۑeq{ *y({+{$"b#Enۛ1C}]!jW4&JS"f%ճY%}hA0.+'V)t=?FgCl`{ tD+m !(#IG*T;O"N DrJDs~$iUΑ6ȗ/r!TV9%2V9;꟱}-NI#"4HBuf3K2[IXYk}G@$DR@*)>'>O6t RN lL\OΚuaDjLz8I~? Srg5өr~Ba:۟#3)t4zv-(:SOu?Um i&I_"rEClf0Nˈ[0 %m-p|aM17PQ:D,_}<1PJJG[#cG ߓ4WZpH[!2&K:9V9W}} DȆڙ00m7DxbWvShy'$>Db++>" H :f5[oZhT6= z晴hڞh7}',>? +&1XpLmI~|'Jsg5 <漶fq0"+y6IwJƮDOHE`ݹyieOo_~'}@WD]'JodKysߚ56Zg!8Թ˛`3zB&C3N`ihV"?&ɇP"ȝwM\תڿ Y`>^D;`"po Rq{NuN5103dh/"ȝgh6TM8iM W\N'\RELxֶuV&*Eonp31{9q6!Te}Cۏ;J+8L~3=:Thzn3$m,?RXOT6 b87cx*]ew+AdRyλH9y%*LyDϨ$( ˕yqys\Ūp:4J-+܊cV:a74Օ$*1CCv%^Jc SWzHPBQ Yn];"M4.YP^ }9R{pZ"2ȶ=I+4 Rs&n]Lص59X0& qZxX`7,DpE'`00pTt]Ev'n&B܊ n~9T q*D68b%Qpc( Rg̻(6L+ad-DH-s/KhI;lHrnݒ=LqʥaP @{^L/Yhd!wEm>I?&ϣ e"|PZG?i(bvZ/QU#(:v71`ygvG5bBL8lEbQķ}Ʊ"=+Dͫ%DйfHHMUԂl?nv3i= Nq y@7=,L;&v-!r_&dų+ON :jM=r.^",?1X ݥhx>O_爕ڜ=vB't|&1AP,Nm9{4p^/128QO"s,O#]> gMwVMlε7]L'^%bKk7s bD!{ N3`f[ N3E`:캿h6Nvď0krߝ*HN&~'RKQh԰gX7Ib;[zY~DPu?q]]++,Wڅ{mH:^}^È`<1뙩EFg[3f|105A}Mgs YDP1nԵFsGtAdX>%zve.@ -vcܱ?=ǵFUe:fӯQ)MB'8f#edb%17L!zg\ubQG[#ۆ\4h~k$NU͆\DDo4Ca p"ȝkA |؟!QǞ@ gc{Ufn TV9 'ܹf%T͒/t̿DPtnLO lߋܗ` Um_`#I+zМԇs׃ eGPM 6l!n)s*kW_Q_},`."qay2^ j>ù4VAl%V;9̒ftBcۗ_%R鉖y(s = ¾+"Gv_xr:ۛ^dA瞠7DQ9WRthZVO>q% 'f?iel!yQ  > C:^s" /0ۥ?kFS`I$_z$a,TI :d@dJ+JXRf&'[IK%B;^IT(/uTմ!tI&=9".olWwt,A'K߲-b2I߂qC /)H^xך=Ol/ ʊw኶ t.}$>]B\GdVђ" `NIG+B DI?V`{Qkq _km7UgIZQ@x)Qu.|PHIe{'Idժi(FLntvwz>W~*l_:kBY~Mmb\fI߭oBY~M Sg[DTM>T;T(3|L ٶoT(3 a Sz/Qʍp '#M69aBCTbf<jԴA:_]a{q4J>;[8+w7Mss2NdqQzu]Q.KdAS?U)5@?u9Y"d QP]Ki-M@ɛteљޢB1^G#yrˆ wשLa2B7A9k<%sQDl IOV$Ot!\{x"#D;ih"EBT҃kvJkfp?,P]}D.yt&7FhOqy#e%[BƎZ J@c]1 {FJ}tB wluMt& WFjYQ!oP ʔ^ FD1yp][{z"f^9rT/QJ"CZHxM" "}gc#jU1JB`UޔsTִ 2?Ϫ +?nk%Gq{:z&jCOg[Dpף~nzw[XUOde:_`bN`/Oo z"'>/rͷy1Ι۟/T=矦fz?ـê_|%[_VAe5ZI\@=Wo$ eMKwUm bUѝy@ Ltܶ͏ "ȝcUBѩp4D;Dzw̞?o{3T=_7]>ӥEfoV5:& zkᏤmEQĤHVV 8b `abV]-x<F w]Od0y9=~WY"ȝkyCGD5~oQ]Jo"ȝkq"e!uκ5jk|BH>,"YNg\5qC7:$T1.:} `Qn`~^'O"ȝs}B*SJ7w.1~ȕkιדI~pyA>\aD;T@~d&uM4nΏxE "ةqzm5'J5[?Z-,I 0:BBk8Ν|jy>\D;jD|u~gM!{xA܇Cz mdê*2@snc{ wϫNmE969T ө}IKX-[ݗ́ԖHJxgJߦ5jsw4 7d!J'>p5BO5G~,7,imI"~!=.=PNy[v>ˁ+3Ǹ!١,$i sّ _eNIKH:Xz,[e%w_.6t qw_"N$eR[tm&P"᭪G( =D5&,#Fc$!IqoTWY<$ T m2TfϷe*4\Bvܳ燉fP8Uz =O$3VWW9tf" 2ysE3 /$af0 "$":[!&bf}-0YDP tlQ ;pG>*i?Ie2L< ih"'m:(PIݖq91fDǿy qZxN(z6N !A\!y!8 IDAT `DEVt1ޗ?loݠ0"0 . o"c1+cms{آ쭅1ߙ(+rKnj=Qk<熢D+Ξt 1{>(R)+'f$?{87 $ϿI9:-_4S."fx \H}C4^@}@t %DBI1#[UnVO娵ؚHIoM ~l!cdBY_1F/[XU0 B{BY_s|{I,JFmD3S2E8 Y*_o5Gޑ#eF bJS sܫJ^5sݺ e}ݺO7zof֙a6B# aof;(?O/h6LJ )]+k؝DDO=#VKec#OKI*]FÄi좣UTTmA~BZ7TB`53b?w+y?o@5.Oa6rhdT?3=ښA)isfk|@F#_k{pIQi\Wk{H\({y?y"x&}}z FʈX]^ :DE8Sh$UY9YzRtQo"x`ߺM(^~Z[)iK9맵M~MNj&,K6üt/iր؃Ӧt6X(x_] /&Hy\hT%]gJ>&ӆM,N*Mf?gI! {zaзs2xw#xxʢ);#ʻW#x2gd=}A4l27f@ۓ>&:~nPY7ǁTCJHRM|d2m -5p+in>I4R$~pt@%$i~G|D|йh8һ lIK8RkB FKR_[>)R~Zov{@'y'"TxLܾ<j˰&XH/X i*Yq^:E;yߟ2(v :k*U6:STabB,%F;'ǷI)\"xG;qsz-zswc4`*) z"e" j_0@TKgD0 eDK Bx2#!bpz a~&0"4%I}rNFgEm`"s/o,t!>مXd(+ؚh"ڳӾi Dp `U"]`7 9,A)t#UILt'r$\,SeR"ڲϚ??p21~p,J | NԎRks>AaSB7(C Y8Dp7}{XwsLEsL7<;.L2s?/*v=,&N=Jϋӿ@$7L/꼫Sk*g #˃ҾB|$W9ྡFu $D+KD:5sm_j" آ=FD{+n_ϋ:+OGׄ,0g6^%ēzyaU.'ܗYm`bJ‰ Ust],JGɈs0ߤVpK$ZIԸu込'[+KӞ=Okk%lFR{g$Q4DW Ù{q/˧OyA#.IzLuƔ/T4D;ZTzFZ,IrGS!.>AsSSAyl"K]ql"حt[F5aIyjBwZ/o36"@op(U}ʶٞDYqԮ 5Ν2lWHnݏ{7ΕuYc6o"ȝ+Br{AşרYDpU[+}=ښE}˳,pgϽݚI 2&Y5m I+{q/u>F?ݪ:6zAg"]cGt. t~(Џ5r׼Z@%*}w^z #:y< $L`:7?{5R{]^h"k칯[3` UAt;uW4dySIcmѽETdךsJW?_7dY*j:% ?Շk5rޜ jW.M^"ȝ{з^CCYeJ+h-HW2aL]H'u*I Ve mI`(Z`U@'Ś/ΡT$q>l= $]At_ݝRwMzYl^%/9&lY6s/wƃv)-TzL 2ۤ?$&ƏJb{סia18c gSi$=PzlKYg>_y=sDG@Ҽk2yJ)a0{R^D*qAlV HXҵ|(1F?q?.C""Ë;HF|#%]5LstmK̾ Hڱ$"i3=Ah <"iIKaDB\s}RB~|;۟LQ#h&nmON򜓉xh"C"Ck 9%};+"h]q;j" M-$HvGٞ؏kOW]߯;Ɂ$Mjo9E!p;w ,pK"OůIڭUN _-n8Q"]VdXC칖Z=pTLevwT%ixLjk/#k߱A&n WFzف HY# 9Ջńnm$NloDI[fmmlAL>C~Q "")kt_0ř8MdxuWqc{, B^X1-Cd$qCO}uqtvt7QXXk@{ppr#N%G4Y+/<!/#Dђ)it@ڊhY1K6"x=dyÞHIY͞>"xL9ED̯:î^A `8 ,\1ll`$Jخ$jIt^ ؙhi]4lHmeb2&D;u56x>:x;RK5+B6ğԓ_FDd܍I@1[=*I!VOa*fu]L`FM[ݷc+󋘚f/R2._:70ϡk.l5=Ap" N U){6 P sx@58 o)0 ~l=#Y}d*dFR'*'J\>Ͽ cVagc4E<4 :3`0D5?c &`6< 1 < )y .;PyK œ,`s@0ػK ˍ;Z ؤ뀍n;œxX`t5ɘ | $a>󆑰t X$1 F=s,my[cj/ONtṖ޽Ԓ 6sA"`YnJ/"7vJ/92>[XKD~h94~;ƹAֶzY0UʁԯQNu}#nKkf}Մ,]Ǒp,_31Kle^=j:smD`הv}#:N}M@45ihVe/2?^=EkWyj] O`klJQv`8aJ-hb;gǩ:5!'y)&(C ߺotsA=,TOP< /y. :i @$fǥzkT4OW`-&u%vR ?vExڤ>y'mf}t;m\xAAY|3{pNF193asDuw Sɳne"ۙ<&%o;0۸hV:ҙs5 - g]C+gx:y}@(J>w6LM{ֹWT\G61zbS*9oM^]=gL/8B3:  O1&/#4#? ZNYscҙ|ys]֍F;0Vݻ)ST6?7-4`j0aXI?갎4 Ob!m3ԬS/^#=G]7:( sݎZr}o^1X6 trfeq.|=:@pSDѨHhU`zQ0ؤ>o ;+>^ 1 @:]Jjd1;Q"j,-Ldqձt~ ̍#R{QI7Kq&@ib!&mOk`+fe;Sa%*I[Ž3hBE[?DFaoKuQO6>JR#mܗǗ 8gu9a69 I1cA9T{G W`v>F`D=A٪>`˔{hܾ@uNب~/1}7fg-O~\zc]V!,k㫐LcowJ!f7|)y? _i;$R΢nE=gi@0:à"9{U0wKv^~T*QGs F`;bQ\`#2T5&_FD.G9Xؚ1hζM{Rnrvt[\¾Cqrgk`:PW2ԹfeOUy{*jrƃ-UD-"sBrэk+NTuOq"rl?a^{Ն>{3|‹Yѭe[D#KAۯs&Qh-=ϰҾXBxDd]}Z(|9܋l*_ZU/P眘A8/S+38@SS|3hHYrܩDn*%"wzMXUw8KU.U<.66?!"r)zn**ZU$P~rm"%1-RED6aδ$lt݉7cYdLwE1)\Tum/"W#`"rl6U@w[q܄ƞ#!ߦ(.XTVU=;X*`5Uc?ueEp5~sY;rgv" ni1/ߡ\ȳ?i=owwb~uebϻ`ܤFN%VDn t$nW̑n&8^Un?_D\DDUoP 1­˼xN]YUx\DB2S~rFg)쎥- xՐ|ADnż?6M9R/bdI-`goϩOs`7"2n>_cQ;aEDVMSUט ,0Yȝ/̠5E&^D6(uv9`}DD>)"sPa}|wM%(*G!zݬ, "ce70eU4M\e@#܅%ZrۘzNcc.L $MKw*g`n^&'߉ )]Tu *PDd-8p,{f(c?J0cm\)HWs AD1[}6]FUG"2 S̕ee#pu,՘671ak~EGUC}ܔI_]T u~  evl?^DVO%"lmP< "r|"RDlVYA2yLD~#"PkUj"" @S_d[Dʧ> =L׶{3l"f8K\63JOv"2DD+6=װ~|XW1<9/~o\K)qvEhWU0я1{S"r,EO-`!"GF睱\[IQ~`m0PZ >z~3'beHeT("+O4e)X׃JYEd{؄˘}foL[lE-sgU}3b襃v&Y3!pt3TVeb_YeǕU|9H\-V|cq$b*Tɍdmjwϐ}^#矩\C>vI*bgEd]U)e"" w.qҹ)\*qy}XҒMV%ލx P;NúvMly4WI1gȍT`nآy'$aoߐˠy4r0xύ \qQlBI@-9FݱI:Wi1sN uR?Qad{L}r!v^VN^QUwW`XIQE= dłnR]"6KAv"2W`':tH n6뤝@NTջ6U[OSTS2Tf|TTuoˆW]DdV]}jrv^E)j#10+[z`p3EUvrz}ʹy`ێ}|$n;:X,=D9_Ի2 m ^.Ŏ آ2 p"o\i00[]9}p.R똼UC̰HNa+4ͨR'̻ظ`/lXjձNVP,yMj\s;E}iדL#yAFHXEΙSx9sxV`;˝u+C۫{؂Pl15$o܀3O`zsh#,0I|!IE^W@p-ٔ{X [=y@mn༅&܁}\+ar}Nd)kD̹ KFuٯp;k 4Բt- MTu];C+>,y&$oPU{> 9Ey. BF.%o XA?у+MF 0Y=7*uX Mx0ZتzNKDm|Té7OQO9vC:uWot#MQUb;ϰ8rswCU͘3N͂ǿ~8a݌:"Ra~2 6<xπ hV`? , r?c`m(Ẹ(Z(6^t5XTu[U}b~G_&h>wEdu+F?WakO~JTm(p<:FUl wajDdj9L|CDDU_R=1OIb-ȡEdg .vD(⤥'N 8Kڬ펠9E^I'N5d'oU^YUN1P``gJ-x+Q^:ק^MtN?vioo!ro4 MO;Q׻ uѹESzr1A ɓ1!hsGp=nEd-ڽ2N{iz~8UTې'w93|SUM7vqW/Wv [zW:7qK\P\0j(eiK64;9{K}}|@UǪ6,3"qM1 Q# q$)Fv&?[h/Fn T0z^O."b.rSnvgU3m "Ww0X-f;ޫSռs`;c/lFap(ˋHsyj0[곆'@UP/Ӏ47XGx6sǟR`1/'Ed)Xr%zX=9lHDf @p'nEd%6nC 9iyd|U V8]jUU&ȩy#TXt7{<^y ˅dn軬s jIrԵ1p$"u[0oTjx4-"2^FI}('@USo`;'.X)4 s~skw|&7`jرW 0o Tz_y>y `ҟJVkD珹X \=)~%UDvEDl3>&/aY䗘q<9DdI5 [ٕFx.ƀOl+sF ە螳{5<z9T:ф~Kla1IU6[?hX:)Zg|NyROƂ[^{ =Lu1Ot0p{6:^5F\07УgI:}~ |,8l6{ kȍ1GD +߂5vQl]L0Kñzt&S khAws܉cvu/K5fo+h {Sa4KNPLB6?\?l\ Surl)ob.g<+#'[@1[INԄi՗$FQ=/nK-~1u[QףwFZf>]ds),eXm^(II{ Q3˯BoM {~FybapM\6h'@?.t] |6z0u#>-> e,z 3FzTsyUGU_la6>-LR0{8\Px6Deչ$"si-i11]Tml?D$YHGQqXD|VEI*FN,kGLm7;9}+)G"UT]*"ct?V 0X%q6A{K>--duy|LVS3T#j5':?P|؊Y_DZX9]D84DX_lr/a RG2 SȢjRrIDV.P x8#U-."u.T}71/ae^EUwVXqrYr>I9CUn.+"{0U}GUD"" n^r5f=#DdUg`p8.LY׮=]Gz!s>*"8HߤH nd˫mEԧ4. {Kl% ],( 9:K/~#LQlS^#rN;܆IԅᣱLyDs fG#Ryck ωߝTuo#Hujap4?ʢ1x[US!9/v9`ivOȟEl@`YU=Z={ß\Vnc\DV.m֪QD61~M\ӛ&"},/d?Ezy9oZnݱ03"X7Tl2= MTLPvϊNhvAgbk8=slLLe~[UDd# PPڕU˪zQD>-/qY6U1CmY,$7q=bǼ\t9 S IVՋQՇׯ9^\馷st˥4J!~$aڙ<n}fC8zQWTul¿|VDK1j@oL?"r'N7>dk6:MD>+`zמCYV~0ض9O(716w6똧G9$uIT>`x&"ϩ.)Ɋ:U}BU=H0RDfqc;;T^xex+"?tиmr.3MdѠ51pR^JPD=svR¤sQ>aYLwbmq SsCuPNC_3SUbovtX;׀2WgfJjɧ'_׾Lm U%c !; mӷnem)cWx+܄ֈ!"TY[S;U}-眡RUB0]jn""C05f^4INS6j-tMIU?ݕcDj,] 'I_d8Xs, 9cމr v9ޙC,-7鐜er(0F51yWj4̟<<2feU㨝ȸΉ|SYpۤ 3֟V{=<1,dHm/? 4`]w`A;P}:u~$uӁ"  pNH9gL߶i㯍r0qNJir. ލǗvS4ߩ :ԟI5Mu[zo,lg|Pl UBH15)=g^UO)* nƶƹ V\UO:B?TZ5_ñ08*lz[t/sB+~4йn7")!/}AYdXqwMBé^ $cgqr/ȭ"RLwIZ_Hܷz r1Ꝫ%$d :YD@/GWxڀ^D vo^,:'uCOE%\l4>>4էM?|!A Ys<: e_U}.ώ-hבX,w:8F;@ΡC )p\."hy*ޯK&`1"7u">[rH,.'0yZON+,A͌&ǰTuFgYʭuj)瓱ޔ5ZrE`lpɄ1X`ap^9uZ?EUPʩzx( F(ux#1qsbtʐ\mOlGX~TzUY.`y[U g鿈ccٶDp,DYr5Xn!͈8fHBΠxHc SSLI^X<r>XY{,S0='ՠ«U=cmU'ۖ*P19oay޾'4i7ƿ77XkJLy`q 7zj,vu$q&yu~c 0IJu8Z/EA; kV A\=̉yDq) >|OEm}MlK\>SfsbiI?ckn6np$`; +^Ԓzr 'Q|R0Db'l0 s_%:$^H>W)gs5)@,F1kt"X NWUr Qm( x3Xvv SiFQ }04l낼>1sh:SǶz0{a/:gg6s"S!vf.@?ph4&n~w ͎Ss C7Lt/.54i=c%o Ixɩ_)6ű'0% IDAT#ð߷9KDFtoo<*m2N\QC0/'iJ}bf2"Iw}n@1lE=x8 :>'5wΙSo\ 7BHZϩՕ z~~} &aa 1ف`{ÛCvcx$n zFEmu.Y'OֹSN^vaʶv4ZM+1>w $fd{X`nI/1a_ѹ6z\@!]"_ ႣR8j2Ap:pF/-dHiܤGyyfǢ:̡~7mȦF<rG@$rǜXv\ 5 5 c-ژ` oC4,J)|sMQ/CY;@Jd=$U P{Y^_%\S:'!;z3ECbF= C15 9fR@1\sc7HMQ$@V֘,<@0ծ З75c5k>Π%I|L.FotYww5s}c1Liiw^?B-m"+(`a [?A,n|65L~ҹ/ >2Z)>$횢]8t3Z DɬVIiCRELHzy3x 7gӤ_+`oܗrcc 8cI$,]qZ`ie1q .'1:LWY {ݢmD|b>SkحY$b`6IrxqB .)=4X5vPQ>8rфB_\tB^aWz&$j+qG$Ĥ6{~puh|)F#Ra .V`I,tfkyXXӀM3ֹ%mQY$]{ S;7*Apl]ȭÝ2UJP j;"b~0H. ^baEmL&۸@5Z$DL8vyqژ sZ:>b65y Koy\Ӄ?P  B?@=9rXG$;X@\YƦcIj % ?6Hi|-L=MIYaܾo@d,%m"e;Sj<U5 9$zOw:9H7|AV}M6uj`Tu)ouHlG! ,/px@jn>iT u4Lw1f^wm8:>L.-,~8l܃vVvJN:=hɓm=cN'"{ xG=dԍ- RWMQ?ek{'M`.|a5Lډ]ʩS--!y@P }KMJJw\A$;Sˆ19HP5-Ap|V,CxX%^w-8m>8υ:otð#(v`y,l[خlF`WUB:b❽@ϳo ys`>-{*(?!a+lhuX1/i)5$wn׻ygLp/&M}raicrmh6ha,[Е^gWy[`m:סqAC,Emw_ G_m9^:ݍ:w  rP IR/,V (ՂI7Y8Y1?nM^M̨=_/? 5ޝ]ڣvA:5KD ߐѣ@6~l!ݮVGiIxpDM-9} z}"8>.T|0lgZ,+@zp7},Km &c9Buq\Fo`hK s Em#NX[ F) `.yjMٝ׷)yR}G;O`ޯr/ ׇc|,&^# nG6}- <|Z z3{%9v?IIkaPj>>R@пL@3f OM;? 1 <oos<\D~Zs!!q`nH1^,ݜl( hVƬ~BzSו/zN県̓y:3:h&Lr}g_H/6vBR[+C<isb>!ߙdd#g2j126y AҞ_f Y_ǼVe-BJs XYg~dm@e!5?;QU!$S>`*Ȯ>U%@azFP4x> n$6>4NmJC>rަ_Z=J8bǀp-s@01c19O]JNF] Jfyh a!Do>{H_:s!jc4S9(,Z ز< 4I=(|!gGa`~rg&0u,#V6`TZ/6&$/zޙUO$[a_Ƒn̍vRAb>B do8KӔ>G6>SMcr"`zL*azLͲ,ALAbAEa;es u,.MkŔTf\ A!HVvCj3!!OD,@pNJ/ 7a{|nCo2^V=a.|jAp4̮/OsChH2w yv1,khHNS1oq Kz4+azC?``M,㢟 _P}<^TY fd ?̉ޚ'R,Z5|<灎KSf !# q=H>΋r]Ӏ;jwoApFM27va`);%xka^eJ֠aA|\zţk8|Fie6*Qv>T[ Ga\tt}p;nL-92@;6DEXyTxhStY99jxtX.Z_CByBi˧LQbI`B"p0{%mPql}F?AÈ}% --,|6vgs`ѫ{NMoppZ@i ~>t ^< qtިu09xNT^d2ps6Ki'F@qz/3yM'ȟC;sQ-vu2o >8i"ӱE:E3=z..qYf@ Gb}xՑFN?kM>ϲkbrLnp*nhev`>1=qoQ;gE\Q7b0}z:D<`ǐFFB & eJ;S$ ZtNHN ]f%' xK6A#e 4gt}O_B}@`.҄:F?97~dVxT*RH8c'28=1w@lW+fk(ރ@")cev`]}e*KVb =PSDlQ;!Xj a@N, ac@1 WƖ@9gǂT[:~2 hLbhL=jҨ27;< \rkdBw2,12Ņg:0ƂI&%ȨQyѹ濍@@&!& PeX,Ѧ/{1Cw6jΞ#c">lr ziF*[r֜|g*6~,UtkjIb-^vϔl2Sk._ |AmO4_tlԇp|lkq >hp?fy5`v0%-t6i!@O (cpR=@:25c+Iks7_@sWB8JI2c0OxB!Ā:-݇Es  N>WjaG[߁ 8/$X880nA }pw_3Z<C~@ijjwyo$jv>N@1vEu~tWƂEOƈ3E`W/{9@?NM#rOGG%@pf<UQ09m)s\:q^-y1;X8]@p׬mv؏PSK6[78>;>wtaMLv,6rtk$ح*3۷g?뻡0+M>-aғw'ΦN [q*G,ƍgC;+'=Rv`tpͳ5@>NmꁏβԦ3V*2NR,Vt?g@ [ITs <BV Y3} iP(yD7l׌v(LG5yK,nX3p^2]F:'{hU-Bmd76= /|)&$ ?vAOꝌsd]"Va}7!}.{OTgKm +;^K4! IM 6vyAP$Aot\@#7V@KF3c}"ى@~#N,U>R)91a 3 \}ׇ(l}1JP{d1,˽-ޙ//:P( `/I7f 6 ˛Xg?U;^ ޻+5C؝Z`h6z/j =yn ~_ s+ *wk[!d Pl 3v6aX^wϩATOa:a0AKqS,yxUji@$p6qF?$벯0Vh&`.|Ϫt ,mz$Ir=GCXm0/,A=Ob*@+x޷q1_Un H*`T! 4eHLI!Vtd˫!z|ј`L;NU3NVꙪ111K;eTOٺY~ EjleyUN{+)LTUK2/𜈜'"KDOQ1ϜK#hY/\fT%JI|szVRQg),a/39QU uIh<!;}rKOva"܉YVSߧ]SI$T|U; C8]D5S^@0DtY "܅0|X^?TPtjS2\gCy3/gw#!Q pS},)hG!ZDҗ@/>!he(RzVq|+ y Vt; *i[T5U=R3b@p;`po@Iq*(?/ (zJ!)8$`vB@)Kt bK2Cy-þwA+ ,ͷ8@X<: NN=$cTPvvxȻ6^+)KΨ6B]@ N3T0@0l"2|::۵*/f1) 4 niPy_fm{.ʮUIi$ J @/ fxm&"bju#҃:K#YEd$XP([WI+a솒NC@_GD2&IDAT~R X5X[U.L%v"ҿ3ƅ6RտVc2>{b w; 1*X#C| u^x*K hPv"2qY\mCKf5+h/; u~pB.!zT obpv?Fp^U},B@DȞl =<:.`KU]WUt%YT9Uй8 /"NjȂSU"`a ~UX"p vc1PJ*UFc*aO3mG]2vn1PU{+lb6oC8@X;?=8A-F "sXʇ0Pn6QT2I`gsc0NDYP0k܉`B%=#p/f9F Tu3U_̅9Dl~R,╪Q͊PUoWխ1k01&+@t2 @Dv0Gr;u*-NuTuku<UZxsœJz. Dd9GAznU?*S 7Cpۋo2 l ,r=nw~% 9;/mMu+DdVU鸾<DdX{XD(U}+sL`?3ډ+[,%w"ӱ-Ѫd+d0 RJ ,?cT3^LDvf%S|FKDdj=ŗ\7uLmM4TvJ fߋaY!x@\)$7KDdML=}'h0ފKUu|vK?:qlT QI%^RK<`F; !"ñ~wL>p!pڳ+EDd>,oQ,ӁX;EERHWP3|ʇW(U}]#.}nc * _DrQYs6aLtǩޡJ:@m/LwSU})UR`pIT޾,Ӂ׈7V6 HW.J*) ORXU}ȾUҾ 1o1ATVI%e~h,2NRY%K534UPp*PIl>lhR* "" cʷJ*pR@,/".MTէ[%TRI#i1SnKSJJ(ITYZ2 ̃%G;M_%$J ^x g<C%TRI^2ˀ/m,Z-#_%TRI4#"]/ epl/sTRI%386 hY/\$:ZTR ^xiuPJ*?""CP c6 X2(K/ TT*rKGaia40܎!._TRIdf/3x |K>ZUo(cTRI2 /H۱ WֱJ*T Xe/ 8X\B% rVfj UX|'p){%T26`/qJ燀+@)4 R$c'PB% i` 4[aQT2U^T*R@gRA""|.\3w:URI%TНT@Ёpq*@¤`mH]l?~zET7iUhRAFqpN_EJz%K* (Xpׁ_;EJ )LDd5 ,Yv}nrK%Ys;.pTPɀfG* (*۩0(U}ȾURI#iW@I"",CNQ7[%x``JLDdla"pZd*y- P0}B̆Jq=dfppbG* ""KPT"V T@0HDDvf>.VЧ p`,w'?aǿA̴y!MN lH1/;knc bah ;*14mj166hC?{S:~哪[om7s {OK+`58$k}u 5pLw.ҤFJ>A @3( WM>D'.}smR] @(-af7w l".}smR @;)-cf7 Zw,$Bƀ3Xh r-A.E`o˹633b$3ES0\OI> vSZF!lz}FAx8Cؒsm; LG!I AX,w7d Lc@@J~5 Qw'd @fdfo<=nkr ̅B Rr4Vl IJx7 RdNl/*`8:ǁ!w+Z`I+2+ ̋ \ Ǥqk6 B R*4 Cx5׺N^R'R.'K .l k 3XG7Rd^)38C8>'ӴqF B }QpBN쩬 AIv]&H_]o6l5@E!(| ա.m(2h T!ci'^?wgG=(g dQV>Zv!ϸ&HV%w.4@HU(R ӽ6 UHLsxɶRt\ F_AxNAH9i  HR9+  uHm FQwߙk]S)R7 N (pI}ls?3KZRRwl3 L]'ĥ+@jA!3 =;AxUi 3;xbـ3z@F!1D.crtK\F 4B ef+G`6ϙ O( 4F Owuߛ Hk٩2J`wg!`RT6Ruqx0=`p~mD6 @L!2SN;gxH(zfv @?v[()[ IENDB`zarr-python-3.2.1/docs/_static/logo_horizontal.svg000066400000000000000000000351061517635743000223420ustar00rootroot00000000000000 zarr-python-3.2.1/docs/api/000077500000000000000000000000001517635743000155265ustar00rootroot00000000000000zarr-python-3.2.1/docs/api/zarr/000077500000000000000000000000001517635743000165045ustar00rootroot00000000000000zarr-python-3.2.1/docs/api/zarr/abc/000077500000000000000000000000001517635743000172315ustar00rootroot00000000000000zarr-python-3.2.1/docs/api/zarr/abc/buffer.md000066400000000000000000000002321517635743000210210ustar00rootroot00000000000000--- title: buffer --- ::: zarr.abc options: show_root_heading: true show_root_toc_entry: true members: false ::: zarr.abc.buffer zarr-python-3.2.1/docs/api/zarr/abc/codec.md000066400000000000000000000000511517635743000206240ustar00rootroot00000000000000--- title: codec --- ::: zarr.abc.codec zarr-python-3.2.1/docs/api/zarr/abc/index.md000066400000000000000000000011131517635743000206560ustar00rootroot00000000000000## Abstract base classes - **[buffer](./buffer.md)** - Providing access to underlying memory via [buffers](https://docs.python.org/3/c-api/buffer.html) - **[codec](./codec.md)** - Expressing [zarr codecs](https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-encoding) - **[metadata](./metadata.md)** - Creating metadata classes compatible with the Zarr API - **[numcodec](./numcodec.md)** - Protocols and classes for modeling codec interface used by numcodecs - **[store](./store.md)** - ABC for implementing Zarr stores and managing getting and setting bytes in a storezarr-python-3.2.1/docs/api/zarr/abc/metadata.md000066400000000000000000000000571517635743000213350ustar00rootroot00000000000000--- title: metadata --- ::: zarr.abc.metadata zarr-python-3.2.1/docs/api/zarr/abc/numcodec.md000066400000000000000000000000571517635743000213520ustar00rootroot00000000000000--- title: numcodec --- ::: zarr.abc.numcodec zarr-python-3.2.1/docs/api/zarr/abc/store.md000066400000000000000000000000511517635743000207030ustar00rootroot00000000000000--- title: store --- ::: zarr.abc.store zarr-python-3.2.1/docs/api/zarr/api/000077500000000000000000000000001517635743000172555ustar00rootroot00000000000000zarr-python-3.2.1/docs/api/zarr/api/asynchronous.md000066400000000000000000000000661517635743000223340ustar00rootroot00000000000000--- title: asynchronous --- ::: zarr.api.asynchronouszarr-python-3.2.1/docs/api/zarr/api/index.md000066400000000000000000000002131517635743000207020ustar00rootroot00000000000000--- title: API --- Zarr provides both an [async](./asynchronous.md) and a [sync](./synchronous.md) API. See those pages for more details. zarr-python-3.2.1/docs/api/zarr/api/synchronous.md000066400000000000000000000002421517635743000221670ustar00rootroot00000000000000--- title: synchronous --- ::: zarr.api options: show_root_heading: true show_root_toc_entry: true members: false ::: zarr.api.synchronouszarr-python-3.2.1/docs/api/zarr/array.md000066400000000000000000000000431517635743000201410ustar00rootroot00000000000000::: zarr.Array ::: zarr.AsyncArray zarr-python-3.2.1/docs/api/zarr/buffer/000077500000000000000000000000001517635743000177555ustar00rootroot00000000000000zarr-python-3.2.1/docs/api/zarr/buffer/cpu.md000066400000000000000000000000241517635743000210620ustar00rootroot00000000000000::: zarr.buffer.cpu zarr-python-3.2.1/docs/api/zarr/buffer/gpu.md000066400000000000000000000000241517635743000210660ustar00rootroot00000000000000::: zarr.buffer.gpu zarr-python-3.2.1/docs/api/zarr/buffer/index.md000066400000000000000000000002251517635743000214050ustar00rootroot00000000000000Zarr provides buffer classes for both the [cpu](./cpu.md) and [gpu](./gpu.md). Generic buffer functionality is also detailed below. ::: zarr.buffer zarr-python-3.2.1/docs/api/zarr/codecs.md000066400000000000000000000000471517635743000202670ustar00rootroot00000000000000--- title: codecs --- ::: zarr.codecs zarr-python-3.2.1/docs/api/zarr/codecs/000077500000000000000000000000001517635743000177445ustar00rootroot00000000000000zarr-python-3.2.1/docs/api/zarr/codecs/numcodecs.md000066400000000000000000000000641517635743000222460ustar00rootroot00000000000000--- title: numcodecs --- ::: zarr.codecs.numcodecs zarr-python-3.2.1/docs/api/zarr/config.md000066400000000000000000000000471517635743000202740ustar00rootroot00000000000000--- title: config --- ::: zarr.config zarr-python-3.2.1/docs/api/zarr/create.md000066400000000000000000000004471517635743000202760ustar00rootroot00000000000000--- title: create --- ::: zarr.array ::: zarr.create ::: zarr.create_array ::: zarr.create_group ::: zarr.create_hierarchy ::: zarr.empty ::: zarr.empty_like ::: zarr.full ::: zarr.full_like ::: zarr.from_array ::: zarr.group ::: zarr.ones ::: zarr.ones_like ::: zarr.zeros ::: zarr.zeros_like zarr-python-3.2.1/docs/api/zarr/dtype.md000066400000000000000000000000451517635743000201520ustar00rootroot00000000000000--- title: dtype --- ::: zarr.dtype zarr-python-3.2.1/docs/api/zarr/errors.md000066400000000000000000000000461517635743000203420ustar00rootroot00000000000000--- title: errors --- ::: zarr.errorszarr-python-3.2.1/docs/api/zarr/experimental.md000066400000000000000000000002401517635743000215170ustar00rootroot00000000000000--- title: experimental --- Experimental functionality is not stable and may change or be removed at any point. ## Classes ::: zarr.experimental.cache_store zarr-python-3.2.1/docs/api/zarr/group.md000066400000000000000000000000431517635743000201570ustar00rootroot00000000000000::: zarr.Group ::: zarr.AsyncGroup zarr-python-3.2.1/docs/api/zarr/index.md000066400000000000000000000040641517635743000201410ustar00rootroot00000000000000# API Reference Complete reference documentation for the Zarr-Python API. ::: zarr options: show_root_heading: true show_root_toc_entry: true members: false ## Core API ### Essential Classes and Functions - **[Array](array.md)** - The main Zarr array class for N-dimensional data - **[Group](group.md)** - Hierarchical organization of arrays and subgroups - **[Create](create.md)** - Functions for creating new arrays and groups - **[Open](open.md)** - Opening existing Zarr stores and arrays ### Data Operations - **[Load](load.md)** - Loading data from Zarr stores - **[Save](save.md)** - Saving data to Zarr format ### Data Types and Configuration - **[Data Types](dtype.md)** - Supported NumPy data types and type handling - **[Configuration](config.md)** - Runtime configuration and settings ## Storage and Compression - **[Codecs](codecs.md)** - Compression and filtering codecs - **[Storage](storage.md)** - Storage backend implementations and interfaces - **[Registry](registry.md)** - Codec and storage backend registry ## API Variants Zarr-Python provides both synchronous and asynchronous APIs: - **[Async API](./api/asynchronous.md)** - Asynchronous operations for concurrent access - **[Sync API](./api/synchronous.md)** - Synchronous operations for simple usage ## Abstract Base Classes The ABC module defines interfaces for extending Zarr: - **[Codec ABC](abc/codec.md)** - Interface for custom compression codecs - **[Metadata ABC](abc/metadata.md)** - Interface for metadata handling - **[Store ABC](abc/store.md)** - Interface for custom storage backends ## Utilities - **[Errors](errors.md)** - Exception classes and error handling - **[Testing](testing/index.md)** - Utilities for testing Zarr-based code ## Getting Help - Check the [User Guide](../../user-guide/index.md) for tutorials and examples - Browse function signatures and docstrings in the API reference - Report issues on [GitHub](https://github.com/zarr-developers/zarr-python) - Join discussions on the [Zarr community forum](https://github.com/zarr-developers/community) zarr-python-3.2.1/docs/api/zarr/load.md000066400000000000000000000000431517635743000177420ustar00rootroot00000000000000--- title: load --- ::: zarr.load zarr-python-3.2.1/docs/api/zarr/metadata.md000066400000000000000000000001101517635743000205760ustar00rootroot00000000000000--- title: metadata --- ::: zarr.metadata ::: zarr.metadata.migrate_v3 zarr-python-3.2.1/docs/api/zarr/open.md000066400000000000000000000001711517635743000177660ustar00rootroot00000000000000--- title: open --- ::: zarr.open ::: zarr.open_array ::: zarr.open_consolidated ::: zarr.open_group ::: zarr.open_like zarr-python-3.2.1/docs/api/zarr/registry.md000066400000000000000000000000521517635743000206730ustar00rootroot00000000000000--- title: registry --- ::: zarr.registryzarr-python-3.2.1/docs/api/zarr/save.md000066400000000000000000000001131517635743000177570ustar00rootroot00000000000000--- title: save --- ::: zarr.save ::: zarr.save_array ::: zarr.save_group zarr-python-3.2.1/docs/api/zarr/storage.md000066400000000000000000000001401517635743000204650ustar00rootroot00000000000000--- title: storage --- ## Attributes ::: zarr.storage.StoreLike ## Classes ::: zarr.storage zarr-python-3.2.1/docs/api/zarr/testing/000077500000000000000000000000001517635743000201615ustar00rootroot00000000000000zarr-python-3.2.1/docs/api/zarr/testing/buffer.md000066400000000000000000000000431517635743000217510ustar00rootroot00000000000000## Buffer ::: zarr.testing.buffer zarr-python-3.2.1/docs/api/zarr/testing/conftest.md000066400000000000000000000000471517635743000223310ustar00rootroot00000000000000## Conftest ::: zarr.testing.conftest zarr-python-3.2.1/docs/api/zarr/testing/index.md000066400000000000000000000003241517635743000216110ustar00rootroot00000000000000--- title: testing --- See the following sub-modules: - [buffer](./buffer.md) - [conftest](./conftest.md) - [stateful](./stateful.md) - [store](./store.md) - [strategies](./strategies.md) - [utils](./utils.md) zarr-python-3.2.1/docs/api/zarr/testing/stateful.md000066400000000000000000000000471517635743000223330ustar00rootroot00000000000000## Stateful ::: zarr.testing.stateful zarr-python-3.2.1/docs/api/zarr/testing/store.md000066400000000000000000000000421517635743000216330ustar00rootroot00000000000000 ## Store ::: zarr.testing.store zarr-python-3.2.1/docs/api/zarr/testing/strategies.md000066400000000000000000000000541517635743000226540ustar00rootroot00000000000000 ## Strategies ::: zarr.testing.strategies zarr-python-3.2.1/docs/api/zarr/testing/utils.md000066400000000000000000000000411517635743000216360ustar00rootroot00000000000000## Utils ::: zarr.testing.utils zarr-python-3.2.1/docs/contributing.md000066400000000000000000000522601517635743000200130ustar00rootroot00000000000000# Contributing Zarr is a community maintained project. We welcome contributions in the form of bug reports, bug fixes, documentation, enhancement proposals and more. This page provides information on how best to contribute. ## Asking for help If you have a question about how to use Zarr, please post your question on StackOverflow using the ["zarr" tag](https://stackoverflow.com/questions/tagged/zarr). If you don't get a response within a day or two, feel free to raise a [GitHub issue](https://github.com/zarr-developers/zarr-python/issues/new) including a link to your StackOverflow question. We will try to respond to questions as quickly as possible, but please bear in mind that there may be periods where we have limited time to answer questions due to other commitments. ## Bug reports If you find a bug, please raise a [GitHub issue](https://github.com/zarr-developers/zarr-python/issues/new). Please include the following items in a bug report: 1. A minimal, self-contained snippet of Python code reproducing the problem. You can format the code nicely using markdown, e.g.: ```python import zarr g = zarr.group() # etc. ``` 2. An explanation of why the current behaviour is wrong/not desired, and what you expect instead. 3. Information about the version of Zarr, along with versions of dependencies and the Python interpreter, and installation information. The version of Zarr can be obtained from the `zarr.__version__` property. Please also state how Zarr was installed, e.g., "installed via pip into a virtual environment", or "installed using conda". Information about other packages installed can be obtained by executing `pip freeze` (if using pip to install packages) or `conda env export` (if using conda to install packages) from the operating system command prompt. The version of the Python interpreter can be obtained by running a Python interactive session, e.g.: ```console python ``` ```ansi Python 3.12.7 | packaged by conda-forge | (main, Oct 4 2024, 15:57:01) [Clang 17.0.6 ] on darwin ``` ## Enhancement proposals If you have an idea about a new feature or some other improvement to Zarr, please raise a [GitHub issue](https://github.com/zarr-developers/zarr-python/issues/new) first to discuss. We very much welcome ideas and suggestions for how to improve Zarr, but please bear in mind that we are likely to be conservative in accepting proposals for new features. The reasons for this are that we would like to keep the Zarr code base lean and focused on a core set of functionalities, and available time for development, review and maintenance of new features is limited. But if you have a great idea, please don't let that stop you from posting it on GitHub, just please don't be offended if we respond cautiously. ## AI-assisted contributions AI coding tools are increasingly common in open source development. These tools are welcome in Zarr-Python, but the same standards apply to all contributions regardless of how they were produced — whether written by hand, with AI assistance, or generated entirely by an AI tool. ### You are responsible for your changes If you submit a pull request, you are responsible for understanding and having fully reviewed the changes. You must be able to explain why each change is correct and how it fits into the project. ### Communication must be your own PR descriptions, issue comments, and review responses must be in your own words. The substance and reasoning must come from you. Using AI to polish grammar or phrasing is fine, but do not paste AI-generated text as comments or review responses. ### Review every line You must have personally reviewed and understood all changes before submitting. If you used AI to generate code, you are expected to have read it critically and tested it. The PR description should explain the approach and reasoning — do not leave it to reviewers to figure out what the code does and why. ### Keep PRs reviewable Generating code with AI is fast; reviewing it is not. A large diff shifts the burden from the contributor to the reviewer. PRs that cannot be reviewed in reasonable time with reasonable effort may be closed, regardless of their potential usefulness or correctness. Use AI tools not only to write code but to prepare better, more reviewable PRs — well-structured commits, clear descriptions, and minimal scope. If you are planning a large AI-assisted contribution (e.g., a significant refactor or a new subsystem), **open an issue first** to discuss the scope and approach with maintainers. Maintainers may also request that large changes be broken into smaller, reviewable pieces. ### Documentation The same principles apply to documentation. Zarr has domain-specific semantics (chunked storage, codec pipelines, Zarr v2/v3 format details) that AI tools frequently get wrong. Do not submit documentation that you haven't carefully read and verified. ## Contributing code and/or documentation ### Forking the repository The Zarr source code is hosted on GitHub at the following location: * [https://github.com/zarr-developers/zarr-python](https://github.com/zarr-developers/zarr-python) You will need your own fork to work on the code. Go to the link above and hit the ["Fork"](https://github.com/zarr-developers/zarr-python/fork) button. Then clone your fork to your local machine: ```bash git clone git@github.com:your-user-name/zarr-python.git cd zarr-python git remote add upstream git@github.com:zarr-developers/zarr-python.git ``` ### Creating a development environment To work with the Zarr source code, it is recommended to use [hatch](https://hatch.pypa.io/latest/index.html) to create and manage development environments. Hatch will automatically install all Zarr dependencies using the same versions as are used by the core developers and continuous integration services. Assuming you have a Python 3 interpreter already installed, and you have cloned the Zarr source code and your current working directory is the root of the repository, you can do something like the following: ```bash pip install hatch hatch env show # list all available environments ``` To verify that your development environment is working, you can run the unit tests for one of the test environments, e.g.: ```bash hatch env run --env test.py3.12-optional run ``` ### 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. It's best to synchronize your fork with the upstream repository, then create a new, separate branch for each piece of work you want to do. E.g.: ```bash git checkout main git fetch upstream git checkout -b shiny-new-feature upstream/main git push -u origin shiny-new-feature ``` This changes your working directory to the 'shiny-new-feature' branch. Keep any changes in this branch specific to one bug or feature so it is clear what the branch brings to Zarr. To update this branch with latest code from Zarr, you can retrieve the changes from the main branch and perform a rebase: ```bash git fetch upstream git rebase upstream/main ``` This will replay your commits on top of the latest Zarr git main. If this leads to merge conflicts, these need to be resolved before submitting a pull request. Alternatively, you can merge the changes in from upstream/main instead of rebasing, which can be simpler: ```bash git pull upstream main ``` Again, any conflicts need to be resolved before submitting a pull request. ### Running the test suite Zarr includes a suite of unit tests. The simplest way to run the unit tests is to activate your development environment (see [creating a development environment](#creating-a-development-environment) above) and invoke: ```bash hatch env run --env test.py3.12-optional run ``` All tests are automatically run via GitHub Actions for every pull request and must pass before code can be accepted. Test coverage is also collected automatically via the Codecov service. > **Note:** Previous versions of Zarr-Python made extensive use of doctests. These tests were not maintained during the 3.0 refactor but may be brought back in the future. See issue #2614 for more details. ### Code standards - using prek All code must conform to the PEP8 standard. Regarding line length, lines up to 100 characters are allowed, although please try to keep under 90 wherever possible. `Zarr` uses a set of git hooks managed by [`prek`](https://github.com/j178/prek), a fast, Rust-based pre-commit hook manager that is fully compatible with `.pre-commit-config.yaml` files. `prek` can be installed locally by running: ```bash uv tool install prek ``` or: ```bash pip install prek ``` The hooks can be installed locally by running: ```bash prek install ``` This will run the checks every time a commit is created locally. The checks will by default only run on the files modified by a commit, but the checks can be triggered for all the files by running: ```bash prek run --all-files ``` You can also run hooks only for files in a specific directory: ```bash prek run --directory src/zarr ``` Or run hooks for files changed in the last commit: ```bash prek run --last-commit ``` To list all available hooks: ```bash prek list ``` If you would like to skip the failing checks and push the code for further discussion, use the `--no-verify` option with `git commit`. ### Test coverage > **Note:** Test coverage for Zarr-Python 3 is currently not at 100%. This is a known issue and help is welcome to bring test coverage back to 100%. See issue #2613 for more details. Zarr strives to maintain 100% test coverage under the latest Python stable release. Both unit tests and docstring doctests are included when computing coverage. Running: ```bash hatch env run --env test.py3.12-optional run-coverage ``` will automatically run the test suite with coverage and produce an XML coverage report. This should be 100% before code can be accepted into the main code base. You can also generate an HTML coverage report by running: ```bash hatch env run --env test.py3.12-optional run-coverage-html ``` When submitting a pull request, coverage will also be collected across all supported Python versions via the Codecov service, and will be reported back within the pull request. Codecov coverage must also be 100% before code can be accepted. ### Documentation Docstrings for user-facing classes and functions should follow the [numpydoc](https://numpydoc.readthedocs.io/en/stable/format.html#docstring-standard) standard, including sections for Parameters and Examples. All examples should run and pass as doctests under Python 3.12. Zarr uses mkdocs for documentation, hosted on readthedocs.org. Documentation is written in the Markdown markup language (.md files) in the `docs` folder. The documentation consists both of prose and API documentation. All user-facing classes and functions are included in the API documentation, under the `docs/api` folder using the [mkdocstrings](https://mkdocstrings.github.io/) extension. Add any new public functions or classes to the relevant markdown file in `docs/api/*.md`. Any new features or important usage information should be included in the user-guide (`docs/user-guide`). Any changes should also be included as a new file in the `changes` directory. The documentation can be built locally by running: ```bash hatch --env docs run build ``` The resulting built documentation will be available in the `docs/_build/html` folder. Hatch can also be used to serve continuously updating version of the documentation during development at [http://0.0.0.0:8000/](http://0.0.0.0:8000/). This can be done by running: ```bash hatch --env docs run serve ``` #### Adding executable code blocks in the documentation Zarr uses [Markdown Exec](https://pawamoy.github.io/markdown-exec/usage/) to execute code blocks in Markdown files. Add `exec="on"` to a code block header for it to be executed when the docs are built. For example: ````md ```python exec="on" print("Hello world") ``` ```` Below are other useful options that can be added to the code block. See [Markdown Exec's documentation](https://pawamoy.github.io/markdown-exec/usage/#options-summary) for a full list: - `source="above"` makes sure the code within the code block is also rendered in the documentation (rather than just the output). - `session=""` executes code blocks in a named session reusing previously defined variables. - `result="ansi"` or `result="html"` to render the output. If the code does not produce output, you should leave off the `result` option to prevent an empty cell from rendering in the docs. For example: ````md ```python exec="true" session="contributing" source="above" result="ansi" print("Hello world") ``` ```` renders as: ```python exec="true" session="contributing" source="above" result="ansi" print("Hello world") ``` #### Building documentation without executing code blocks Sometimes, you may want the documentation to build quicker. You can disable code block execution by commenting out the [markdown-exec](https://github.com/zarr-developers/zarr-python/blob/884a8c91afcc3efe28b3da952be3b85125c453cb/mkdocs.yml#L132 plugin in the mkdocs configuration file). This will make code blocks and cross references render incorrectly (i.e., expect build warnings), but also reduces build time by ~3x. Be sure to undo the commenting out before opening your pull request. ### Changelog zarr-python uses [towncrier](https://towncrier.readthedocs.io/en/stable/tutorial.html) to manage release notes. Most pull requests should include at least one news fragment describing the changes. To add a release note, you'll need the GitHub issue or pull request number and the type of your change (`feature`, `bugfix`, `doc`, `removal`, `misc`). With that, run `towncrier create` with your development environment, which will prompt you for the issue number, change type, and the news text: ```bash towncrier create ``` Alternatively, you can manually create the files in the `changes` directory using the naming convention `{issue-number}.{change-type}.md`. See the [towncrier](https://towncrier.readthedocs.io/en/stable/tutorial.html) docs for more. ## Merging pull requests Pull requests submitted by an external contributor should be reviewed and approved by at least one core developer before being merged. Ideally, pull requests submitted by a core developer should be reviewed and approved by at least one other core developer before being merged. Pull requests should not be merged until all CI checks have passed (GitHub Actions, Codecov) against code that has had the latest main merged in. Before merging, the milestone must be set to decide whether a PR will be in the next patch, minor, or major release. The next section explains which types of changes go in each release. ## Compatibility and versioning policies ### Versioning Versions of this library are identified by a triplet of integers with the form `..`, for example `3.0.4`. A release of `zarr-python` is associated with a new version identifier. That new identifier is generated by incrementing exactly one of the components of the previous version identifier by 1. When incrementing the `major` component of the version identifier, the `minor` and `patch` components are reset to 0. When incrementing the minor component, the patch component is reset to 0. Releases are classified by the library changes contained in that release. This classification determines which component of the version identifier is incremented on release. * **major** releases (for example, `2.18.0` -> `3.0.0`) are for changes that will require extensive adaptation efforts from many users and downstream projects. For example, breaking changes to widely-used user-facing APIs should only be applied in a major release. Users and downstream projects should carefully consider the impact of a major release before adopting it. In advance of a major release, developers should communicate the scope of the upcoming changes, and help users prepare for them. * **minor** releases (for example, `3.0.0` -> `3.1.0`) are for changes that do not require significant effort from most users or downstream projects to respond to. API changes are possible in minor releases if the burden on users imposed by those changes is sufficiently small. For example, a recently released API may need fixes or refinements that are breaking, but low impact due to the recency of the feature. Such API changes are permitted in a minor release. Minor releases are safe for most users and downstream projects to adopt. * **patch** releases (for example, `3.1.0` -> `3.1.1`) are for changes that contain no breaking or behaviour changes for downstream projects or users. Examples of changes suitable for a patch release are bugfixes and documentation improvements. Users should always feel safe upgrading to the latest patch release. Note that this versioning scheme is not consistent with [Semantic Versioning](https://semver.org/). Contrary to SemVer, the Zarr library may release breaking changes in `minor` releases, or even `patch` releases under exceptional circumstances. But we should strive to avoid doing so. A better model for our versioning scheme is [Intended Effort Versioning](https://jacobtomlinson.dev/effver/), or "EffVer". The guiding principle of EffVer is to categorize releases based on the *expected effort required to upgrade to that release*. Zarr developers should make changes as smooth as possible for users. This means making backwards-compatible changes wherever possible. When a backwards-incompatible change is necessary, users should be notified well in advance, e.g. via informative deprecation warnings. ### Data format compatibility The Zarr library is an implementation of a file format standard defined externally -- see the [Zarr specifications website](https://zarr-specs.readthedocs.io) for the list of Zarr file format specifications. If an existing Zarr format version changes, or a new version of the Zarr format is released, then the Zarr library will generally require changes. It is very likely that a new Zarr format will require extensive breaking changes to the Zarr library, and so support for a new Zarr format in the Zarr library will almost certainly come in new `major` release. When the Zarr library adds support for a new Zarr format, there may be a period of accelerated changes as developers refine newly added APIs and deprecate old APIs. In such a transitional phase breaking changes may be more frequent than usual. ## Experimental API policy The `zarr.experimental` namespace contains features that are under active development and may change without notice. When contributing to or depending on experimental features, please keep the following in mind: ### For contributors When adding a new feature to `zarr.experimental`: 1. Place the feature under `src/zarr/experimental/` and export it from `src/zarr/experimental/__init__.py`. 2. Document the feature in `docs/user-guide/experimental.md` and note clearly that it is experimental. 3. Add a changelog entry categorized as `feature`. We aim to either **promote** or **remove** experimental features within **6 months** of their addition. To promote a feature to stable: 1. Move it from `zarr.experimental` to the appropriate stable module. 2. Keep a deprecated re-export in `zarr.experimental` for one minor release. 3. Update the documentation to reflect the stable location. ### For users 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. ## Benchmarks Zarr uses [pytest-benchmark](https://pytest-benchmark.readthedocs.io/en/latest/) for running performance benchmarks as part of our test suite. The benchmarks are found in `tests/benchmarks`. By default pytest is configured to run these benchmarks as plain tests (i.e., no benchmarking). To run a benchmark with timing measurements, use the `--benchmark-enable` when invoking `pytest`. The benchmarks are run as part of the continuous integration suite through [codspeed](https://codspeed.io/zarr-developers/zarr-python). zarr-python-3.2.1/docs/index.md000066400000000000000000000055011517635743000164070ustar00rootroot00000000000000# Zarr-Python **Useful links**: [Source Repository](https://github.com/zarr-developers/zarr-python) | [Issue Tracker](https://github.com/zarr-developers/zarr-python/issues) | [Developer Chat](https://ossci.zulipchat.com/) | [Zarr specifications](https://zarr-specs.readthedocs.io) Zarr is a powerful library for storage of n-dimensional arrays, supporting chunking, compression, and various backends, making it a versatile choice for scientific and large-scale data. Zarr-Python is a Python library for reading and writing Zarr groups and arrays. Highlights include: * Specification support for both Zarr format 2 and 3. * Create and read from N-dimensional arrays using NumPy-like semantics. * Flexible storage enables reading and writing from local, cloud and in-memory stores. * High performance: Enables fast I/O with support for asynchronous I/O and multi-threading. * Extensible: Customizable with user-defined codecs and stores. ## Installation Zarr requires Python 3.12 or higher. You can install it via `pip`: ```bash pip install zarr ``` or `conda`: ```bash conda install --channel conda-forge zarr ``` ## Navigating the documentation
- [:material-clock-fast:{ .lg .middle } __Quick start__](quick-start.md) --- New to Zarr? Check out the quick start guide. It contains a brief introduction to Zarr's main concepts and links to additional tutorials. - [:material-book-open:{ .lg .middle } __User guide__](user-guide/installation.md) --- A detailed guide for how to use Zarr-Python. - [:material-api:{ .lg .middle } __API Reference__](api/zarr/open.md) --- The reference guide contains a detailed description of the functions, modules, and objects included in Zarr. The reference describes how the methods work and which parameters can be used. It assumes that you have an understanding of the key concepts. - [:material-account-group:{ .lg .middle } __Contributor's Guide__](contributing.md) --- Want to contribute to Zarr? We welcome contributions in the form of bug reports, bug fixes, documentation, enhancement proposals and more. The contributing guidelines will guide you through the process of improving Zarr.
## Project Status More information about the Zarr format can be found on the [main website](https://zarr.dev). If you are using Zarr-Python, we would [love to hear about it](https://github.com/zarr-developers/community/issues/19). ### Funding and Support The project is fiscally sponsored by [NumFOCUS](https://numfocus.org/), a US 501(c)(3) public charity, and development has been supported by the [MRC Centre for Genomics and Global Health](https://github.com/cggh/) and the [Chan Zuckerberg Initiative](https://chanzuckerberg.com/). [Donate to Zarr](https://numfocus.org/donate-to-zarr) to support the project! zarr-python-3.2.1/docs/overrides/000077500000000000000000000000001517635743000167575ustar00rootroot00000000000000zarr-python-3.2.1/docs/overrides/main.html000066400000000000000000000003051517635743000205670ustar00rootroot00000000000000 {% extends "base.html" %} {% block outdated %} You're not viewing the latest version.
Click here to go to latest. {% endblock %} zarr-python-3.2.1/docs/overrides/stylesheets/000077500000000000000000000000001517635743000213335ustar00rootroot00000000000000zarr-python-3.2.1/docs/overrides/stylesheets/extra.css000066400000000000000000000111711517635743000231710ustar00rootroot00000000000000:root { --gradient-start: #e58077; --gradient-mid-1: #e57a77; --gradient-mid-2: #e46876; --gradient-mid-3: #e34b75; --gradient-mid-4: #e12374; --gradient-mid-5: #e01073; --gradient-end: #bb1085; /* Primary theme colors --md-primary-fg-color: #e34b75; --md-primary-fg-color--light: #e57a77; --md-primary-fg-color--dark: #bb1085; /* Accent colors */ --md-accent-fg-color: #e01073; --md-accent-fg-color--transparent: rgba(224, 16, 115, 0.1); /* Text colors that work well with the palette */ --md-text-color: #333333; --md-text-color--light: #666666; } /* Dark mode color adjustments */ [data-md-color-scheme="slate"] { --md-primary-fg-color: #e57a77; --md-primary-fg-color--light: #e58077; --md-primary-fg-color--dark: #bb1085; --md-accent-fg-color: #e46876; --md-accent-fg-color--transparent: rgba(228, 104, 118, 0.1); } /* Header styling with gradient background */ .md-header { background: linear-gradient( 135deg, var(--gradient-start) 0%, var(--gradient-mid-1) 16.66%, var(--gradient-mid-2) 33.33%, var(--gradient-mid-3) 50%, var(--gradient-mid-4) 66.66%, var(--gradient-mid-5) 83.33%, var(--gradient-end) 100% ); box-shadow: 0 2px 8px rgba(187, 16, 133, 0.15); } /* Ensure header text is readable over gradient */ .md-header__title, .md-header__button, .md-header .md-icon { color: white; } /* Search box styling in the header */ .md-header .md-search__input { background-color: rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.2); } /* Navigation tabs */ .md-tabs { background: linear-gradient( 90deg, var(--gradient-mid-3) 0%, var(--gradient-mid-4) 50%, var(--gradient-mid-5) 100% ); } .md-tabs__link { color: rgba(255, 255, 255, 0.9); } .md-tabs__link--active, .md-tabs__link:hover { color: white; opacity: 1; } /* Sidebar navigation */ .md-nav__link--active { color: var(--md-primary-fg-color); font-weight: 500; } .md-nav__link:hover { color: var(--md-accent-fg-color); } /* Code blocks */ .highlight { border-left: 4px solid var(--md-accent-fg-color); background-color: rgba(228, 104, 118, 0.05); } /* Admonitions */ .md-typeset .admonition.note { border-color: var(--md-primary-fg-color); } .md-typeset .admonition.note > .admonition-title { background-color: rgba(227, 75, 117, 0.1); border-color: var(--md-primary-fg-color); } .md-typeset .admonition.tip { border-color: var(--gradient-mid-1); } .md-typeset .admonition.tip > .admonition-title { background-color: rgba(229, 122, 119, 0.1); border-color: var(--gradient-mid-1); } .md-typeset .admonition.warning { border-color: var(--gradient-end); } .md-typeset .admonition.warning > .admonition-title { background-color: rgba(187, 16, 133, 0.1); border-color: var(--gradient-end); } /* Links */ .md-content a { color: var(--md-accent-fg-color); } .md-content a:hover { color: var(--gradient-end); } /* Table of contents */ .md-nav--secondary .md-nav__link--active { color: var(--md-accent-fg-color); border-left: 2px solid var(--md-accent-fg-color); padding-left: calc(1rem - 2px); } /* Footer */ .md-footer { background-color: var(--gradient-end); } /* Buttons and interactive elements */ .md-button { background: linear-gradient(135deg, var(--md-primary-fg-color), var(--md-accent-fg-color)); border: none; color: white; transition: all 0.3s ease; } .md-button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(187, 16, 133, 0.3); } /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: rgba(187, 16, 133, 0.1); } ::-webkit-scrollbar-thumb { background: linear-gradient( 180deg, var(--md-primary-fg-color), var(--md-accent-fg-color) ); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: linear-gradient( 180deg, var(--md-accent-fg-color), var(--gradient-end) ); } /* Search results highlighting */ .md-search-result__title { color: var(--md-primary-fg-color); } .md-search-result__teaser mark { background-color: rgba(224, 16, 115, 0.2); color: var(--gradient-end); } .md-header__button.md-logo img, .md-header__button.md-logo svg { height: 42px !important; /* Increase from default ~24px */ width: auto !important; max-height: none !important; padding: 0 0 0 16px !important; /* Keep left padding, remove others */ margin: 0 !important; /* Remove any margin */ } /* Also remove padding from the logo button container except left */ .md-header__button.md-logo { padding: 0 0 0 8px !important; /* Keep some left padding on container */ margin: 0 !important; min-width: auto !important; } zarr-python-3.2.1/docs/quick-start.md000066400000000000000000000114161517635743000175510ustar00rootroot00000000000000This section will help you get up and running with the Zarr library in Python to efficiently manage and analyze multi-dimensional arrays. ### Creating an Array To get started, you can create a simple Zarr array: ```python exec="true" session="quickstart" import shutil shutil.rmtree('data', ignore_errors=True) import numpy as np from pprint import pprint import io import warnings warnings.filterwarnings( "ignore", message="Numcodecs codecs are not in the Zarr version 3 specification*", category=UserWarning ) np.random.seed(0) ``` ```python exec="true" session="quickstart" source="above" result="ansi" import zarr import numpy as np # Create a 2D Zarr array z = zarr.create_array( store="data/example-1.zarr", shape=(100, 100), chunks=(10, 10), dtype="f4" ) # Assign data to the array z[:, :] = np.random.random((100, 100)) print(z.info) ``` Here, we created a 2D array of shape `(100, 100)`, chunked into blocks of `(10, 10)`, and filled it with random floating-point data. This array was written to a `LocalStore` in the `data/example-1.zarr` directory. #### Compression and Filters Zarr supports data compression and filters. For example, to use Blosc compression: ```python exec="true" session="quickstart" source="above" result="code" # Create a 2D Zarr array with Blosc compression z = zarr.create_array( store="data/example-2.zarr", shape=(100, 100), chunks=(10, 10), dtype="f4", compressors=zarr.codecs.BloscCodec( cname="zstd", clevel=3, shuffle=zarr.codecs.BloscShuffle.shuffle ) ) # Assign data to the array z[:, :] = np.random.random((100, 100)) print(z.info) ``` This compresses the data using the Blosc codec with shuffle enabled for better compression. ### Hierarchical Groups Zarr allows you to create hierarchical groups, similar to directories: ```python exec="true" session="quickstart" source="above" result="ansi" # Create nested groups and add arrays root = zarr.group("data/example-3.zarr") foo = root.create_group(name="foo") bar = root.create_array( name="bar", shape=(100, 10), chunks=(10, 10), dtype="f4" ) spam = foo.create_array(name="spam", shape=(10,), dtype="i4") # Assign values bar[:, :] = np.random.random((100, 10)) spam[:] = np.arange(10) # print the hierarchy print(root.tree()) ``` This creates a group hierarchy with a group (`foo`) and two arrays (`bar` and `spam`). #### Batch Hierarchy Creation Zarr provides tools for creating a collection of arrays and groups with a single function call. Suppose we want to copy existing groups and arrays into a new storage backend: ```python exec="true" session="quickstart" source="above" result="html" # Create nested groups and add arrays root = zarr.group("data/example-4.zarr", attributes={'name': 'root'}) foo = root.create_group(name="foo") bar = root.create_array( name="bar", shape=(100, 10), chunks=(10, 10), dtype="f4" ) nodes = {'': root.metadata} | {k: v.metadata for k,v in root.members()} # Report nodes output = io.StringIO() pprint(nodes, stream=output, width=60, depth=3) result = output.getvalue() print(result) # Create new hierarchy from nodes new_nodes = dict(zarr.create_hierarchy(store=zarr.storage.MemoryStore(), nodes=nodes)) new_root = new_nodes[''] assert new_root.attrs == root.attrs ``` Note that [`zarr.create_hierarchy`][] will only initialize arrays and groups -- copying array data must be done in a separate step. ### Persistent Storage Zarr supports persistent storage to disk or cloud-compatible backends. While examples above utilized a [`zarr.storage.LocalStore`][], a number of other storage options are available. Zarr integrates seamlessly with cloud object storage such as Amazon S3 and Google Cloud Storage using external libraries like [s3fs](https://s3fs.readthedocs.io) or [gcsfs](https://gcsfs.readthedocs.io): ```python import s3fs z = zarr.create_array("s3://example-bucket/foo", mode="w", shape=(100, 100), chunks=(10, 10), dtype="f4") z[:, :] = np.random.random((100, 100)) ``` A single-file store can also be created using the [`zarr.storage.ZipStore`][]: ```python exec="true" session="quickstart" source="above" # Store the array in a ZIP file store = zarr.storage.ZipStore("data/example-5.zip", mode="w") z = zarr.create_array( store=store, shape=(100, 100), chunks=(10, 10), dtype="f4" ) # write to the array z[:, :] = np.random.random((100, 100)) # the ZipStore must be explicitly closed store.close() ``` To open an existing array from a ZIP file: ```python exec="true" session="quickstart" source="above" result="code" # Open the ZipStore in read-only mode store = zarr.storage.ZipStore("data/example-5.zip", read_only=True) z = zarr.open_array(store, mode='r') # read the data as a NumPy Array print(z[:]) ``` Read more about Zarr's storage options in the [User Guide](user-guide/index.md). zarr-python-3.2.1/docs/release-notes.md000066400000000000000000001430401517635743000200470ustar00rootroot00000000000000# Release notes ## 3.2.1 (2026-05-05) ### Bugfixes - Fixed a `CastValue` validation bug where the "can we use an out-of-range mode" check inspected the source dtype instead of the target dtype. This meant arrays with a float source dtype and an integer target dtype incorrectly raised a `ValueError` when configured with a `wrap` out-of-range mode. ([#3938](https://github.com/zarr-developers/zarr-python/issues/3938)) - Fixed a bug where the codec pipeline evolved each codec against the original array spec instead of the spec produced by upstream array-to-array codecs. This caused failures whenever an upstream codec changed the dtype between codec boundaries — e.g. arrays using `CastValue` to convert a single-byte source dtype (`int8`) to a multi-byte target dtype (`int16`) raised a `ValueError` from `BytesCodec` about a missing `endian` configuration. ([#3941](https://github.com/zarr-developers/zarr-python/issues/3941)) - Fixed breakage in existing fsspec-dependent workflows caused by associating the "memory" URL scheme with instances of `ManagedMemoryStore` instead of fsspec's memory-backed store. After this change, store URLs with a "memory" scheme are handled differently when `fsspec` is installed: with `fsspec`, a `FsspecStore` backed by a `MemoryFileSystem` is used. Without `fsspec`, a `ManagedMemoryStore` is used. ([#3944](https://github.com/zarr-developers/zarr-python/issues/3944)) ## 3.2.0 (2026-04-30) ### Features - Adds a new in-memory storage backend called `ManagedMemoryStore`. Instances of `ManagedMemoryStore` function similarly to `MemoryStore`, but instances of `ManagedMemoryStore` can be constructed from a URL like `memory://store`. ([#3679](https://github.com/zarr-developers/zarr-python/issues/3679)) - Added `array.read_missing_chunks` configuration option. When set to `False`, reading missing chunks raises a `ChunkNotFoundError` instead of filling them with the array's fill value. ([#3748](https://github.com/zarr-developers/zarr-python/issues/3748)) - Added `Struct` class (subclass of `Structured`) implementing the zarr-extensions `struct` dtype spec. Uses object-style field format and dict fill values. Legacy `Structured` remains available for backward compatibility. ([#3781](https://github.com/zarr-developers/zarr-python/issues/3781)) - Add support for rectilinear (variable-sized) chunk grids. This feature is experimental and must be explicitly enabled via `zarr.config.set({'array.rectilinear_chunks': True})`. Rectilinear chunks can be used through: - **Creating arrays**: Pass nested sequences (e.g., `[[10, 20, 30], [50, 50]]`) to `chunks` in `zarr.create_array`, `zarr.from_array`, `zarr.zeros`, `zarr.ones`, `zarr.full`, `zarr.open`, and related functions, or to `chunk_shape` in `zarr.create`. - **Opening existing arrays**: Arrays stored with the `rectilinear` chunk grid are read transparently via `zarr.open` and `zarr.open_array`. - **Rectilinear sharding**: Shard boundaries can be rectilinear while inner chunks remain regular. **Breaking change**: The `validate` method on `BaseCodec` and `CodecPipeline` now receives a `ChunkGridMetadata` instance instead of a `ChunkGrid` instance for the `chunk_grid` parameter. Third-party codecs that override `validate` and inspect the chunk grid will need to update their type annotations. No known downstream packages were using this parameter. ([#3802](https://github.com/zarr-developers/zarr-python/issues/3802)) - Add `cast_value` and `scale_offset` codecs. ([#3874](https://github.com/zarr-developers/zarr-python/issues/3874)) ### Bugfixes - Fix `SyncError` raised when assigning a `zarr.Array` as the value in a `__setitem__` call (e.g. `dst[:] = src` where `src` is a zarr array). The source array is now converted to a NumPy array before entering the async codec pipeline. ([#3611](https://github.com/zarr-developers/zarr-python/issues/3611)) - Fix an issue that prevents the correct parsing of special NumPy `uint32` dtypes resulting e.g. from bit wise operations on `uint32` arrays on Windows. ([#3797](https://github.com/zarr-developers/zarr-python/issues/3797)) - Fix `ZipStore.list()`, `list_dir()`, and `exists()` to auto-open the zip file when called before `open()`, consistent with the existing behavior of `get()` and `set()`. ([#3846](https://github.com/zarr-developers/zarr-python/issues/3846)) - Fix handling of `NaT` default fill values for `datetime64` and `timedelta64` data types. Equality checks now use `numpy.isnat` so that the default fill value compares correctly against `NaT`. ([#3863](https://github.com/zarr-developers/zarr-python/issues/3863)) - Use the unit associated with the `Datetime64` data type when creating the default `Nat` scalar value. ([#3920](https://github.com/zarr-developers/zarr-python/issues/3920)) ### Improved Documentation - Document removal of `zarr.storage.init_group` in v3 migration guide, with replacement using `zarr.open_group`/`zarr.create_group`. ([#2720](https://github.com/zarr-developers/zarr-python/issues/2720)) - Document the `threading.max_workers` configuration option in the performance guide. ([#3492](https://github.com/zarr-developers/zarr-python/issues/3492)) - Corrects the type annotation reported for the `batch_info` parameter in the `CodecPipeline.write` method docstring. ([#3836](https://github.com/zarr-developers/zarr-python/issues/3836)) - Remove result="ansi" from code blocks in the user guide that were causing empty output cells in the rendered documentation. ([#3845](https://github.com/zarr-developers/zarr-python/issues/3845)) ### Deprecations and Removals - Remove deprecated `zarr.convenience` and `zarr.creation` modules. ([#3900](https://github.com/zarr-developers/zarr-python/issues/3900)) - Remove the deprecated `zarr_version` parameter from several functions and methods. That parameter is replaced with `zarr_format`. ([#3901](https://github.com/zarr-developers/zarr-python/issues/3901)) - Remove deprecated `Group` methods `array`, `require_dataset`, and `create_dataset`. ([#3902](https://github.com/zarr-developers/zarr-python/issues/3902)) - Remove deprecated `AsyncArray.create` and `Array.create` methods. ([#3903](https://github.com/zarr-developers/zarr-python/issues/3903)) ### Misc - [#3546](https://github.com/zarr-developers/zarr-python/issues/3546), [#3793](https://github.com/zarr-developers/zarr-python/issues/3793), [#3800](https://github.com/zarr-developers/zarr-python/issues/3800), [#3828](https://github.com/zarr-developers/zarr-python/issues/3828), [#3830](https://github.com/zarr-developers/zarr-python/issues/3830), [#3833](https://github.com/zarr-developers/zarr-python/issues/3833), [#3837](https://github.com/zarr-developers/zarr-python/issues/3837), [#3897](https://github.com/zarr-developers/zarr-python/issues/3897) ## 3.1.6 (2026-03-19) ### Features - Exposes the array runtime configuration as an attribute called `config` on the `Array` and `AsyncArray` classes. The previous `AsyncArray._config` attribute is now a deprecated alias for `AsyncArray.config`. ([#3668](https://github.com/zarr-developers/zarr-python/issues/3668)) - Adds a method for creating a new `Array` / `AsyncArray` instance with a new runtime configuration, and fixes inaccurate documentation about the `write_empty_chunks` configuration parameter. ([#3668](https://github.com/zarr-developers/zarr-python/issues/3668)) - Adds synchronous methods to stores that do not benefit from an async event loop. The shape of these methods is defined by protocol classes to support structural subtyping. ([#3725](https://github.com/zarr-developers/zarr-python/pull/3725)) - Fix near-miss penalty in `_morton_order` with hybrid ceiling+argsort strategy. ([#3718](https://github.com/zarr-developers/zarr-python/pull/3718)) ### Bugfixes - Correct the target bytes number for auto-chunking when auto-sharding. ([#3603](https://github.com/zarr-developers/zarr-python/issues/3603)) - Fixed a bug in the sharding codec that prevented nested shard reads in certain cases. ([#3655](https://github.com/zarr-developers/zarr-python/issues/3655)) - Fix obstore `_transform_list_dir` implementation to correctly relativize paths (removing `lstrip` usage). ([#3657](https://github.com/zarr-developers/zarr-python/issues/3657)) - Raise error when trying to encode `numpy.dtypes.StringDType` with `na_object` set. ([#3695](https://github.com/zarr-developers/zarr-python/issues/3695)) - `CacheStore`, `LoggingStore` and `LatencyStore` now support with_read_only. ([#3700](https://github.com/zarr-developers/zarr-python/issues/3700)) - Skip chunk coordinate enumeration in resize when the array is only growing, avoiding unbounded memory usage for large arrays. ([#3702](https://github.com/zarr-developers/zarr-python/issues/3702)) - Fix a performance bug in morton curve generation. ([#3705](https://github.com/zarr-developers/zarr-python/issues/3705)) - Add a dedicated in-memory cache for byte-range requests to the experimental `CacheStore`. ([#3710](https://github.com/zarr-developers/zarr-python/issues/3710)) - `BaseFloat._check_scalar` rejects invalid string values. ([#3586](https://github.com/zarr-developers/zarr-python/issues/3586)) - Apply drop_axes squeeze in partial decode path for sharding. ([#3763](https://github.com/zarr-developers/zarr-python/issues/3763)) - Set `copy=False` in reshape operation. ([#3649](https://github.com/zarr-developers/zarr-python/issues/3649)) - Validate that dask-style chunks have regular shapes. ([#3779](https://github.com/zarr-developers/zarr-python/issues/3779)) ### Improved Documentation - Add documentation example for creating uncompressed arrays in the Compression section of the user guide. ([#3464](https://github.com/zarr-developers/zarr-python/issues/3464)) - Add AI-assisted code policy to the contributing guide. ([#3769](https://github.com/zarr-developers/zarr-python/issues/3769)) - Added a glossary. ([#3767](https://github.com/zarr-developers/zarr-python/issues/3767)) ### Misc - [#3562](https://github.com/zarr-developers/zarr-python/issues/3562), [#3605](https://github.com/zarr-developers/zarr-python/issues/3605), [#3619](https://github.com/zarr-developers/zarr-python/issues/3619), [#3623](https://github.com/zarr-developers/zarr-python/issues/3623), [#3636](https://github.com/zarr-developers/zarr-python/issues/3636), [#3648](https://github.com/zarr-developers/zarr-python/issues/3648), [#3656](https://github.com/zarr-developers/zarr-python/issues/3656), [#3658](https://github.com/zarr-developers/zarr-python/issues/3658), [#3673](https://github.com/zarr-developers/zarr-python/issues/3673), [#3704](https://github.com/zarr-developers/zarr-python/issues/3704), [#3706](https://github.com/zarr-developers/zarr-python/issues/3706), [#3708](https://github.com/zarr-developers/zarr-python/issues/3708), [#3712](https://github.com/zarr-developers/zarr-python/issues/3712), [#3713](https://github.com/zarr-developers/zarr-python/issues/3713), [#3717](https://github.com/zarr-developers/zarr-python/issues/3717), [#3721](https://github.com/zarr-developers/zarr-python/issues/3721), [#3728](https://github.com/zarr-developers/zarr-python/issues/3728), [#3778](https://github.com/zarr-developers/zarr-python/issues/3778) ## zarr 3.1.5 (2025-11-21) ## Bugfixes - Fix formatting errors in the release notes section of the docs. ([#3594](https://github.com/zarr-developers/zarr-python/issues/3594)) ## 3.1.4 (2025-11-20) ### Features - The `Array` class can now also be parametrized in the same manner as the `AsyncArray` class, allowing Zarr format v2 and v3 `Array`s to be distinguished. New types have been added to `zarr.types` to help with this. ([#3304](https://github.com/zarr-developers/zarr-python/issues/3304)) - Adds `zarr.experimental.cache_store.CacheStore`, a `Store` that implements caching by combining two other `Store` instances. See the [docs page](https://zarr.readthedocs.io/en/latest/user-guide/experimental#cachestore) for more information about this feature. ([#3366](https://github.com/zarr-developers/zarr-python/issues/3366)) - Adds a `zarr.experimental` module for unstable user-facing features. ([#3490](https://github.com/zarr-developers/zarr-python/issues/3490)) - Add a `array.target_shard_size_bytes` to [`zarr.config`][] to allow users to set a maximum number of bytes per-shard when `shards="auto"` in, for example, [`zarr.create_array`][]. ([#3547](https://github.com/zarr-developers/zarr-python/issues/3547)) - Make `async_array` on the [`zarr.Array`][] class public (`_async_array` will remain untouched, but its stability is not guaranteed). ([#3556](https://github.com/zarr-developers/zarr-python/issues/3556)) ### Bugfixes - Fix a bug that prevented `PCodec` from being properly resolved when loading arrays using that compressor. ([#3483](https://github.com/zarr-developers/zarr-python/issues/3483)) - Fixed a bug that prevented Zarr Python from opening Zarr V3 array metadata documents that contained extra keys with permissible values (dicts with a `"must_understand"` key set to `"false"`). ([#3530](https://github.com/zarr-developers/zarr-python/issues/3530)) - Fixed a bug where the `"consolidated_metadata"` key was written to metadata documents even when consolidated metadata was not used, resulting in invalid metadata documents. ([#3535](https://github.com/zarr-developers/zarr-python/issues/3535)) - Improve write performance to large shards by up to 10x. ([#3560](https://github.com/zarr-developers/zarr-python/issues/3560)) ### Improved Documentation - Use mkdocs-material for Zarr-Python documentation ([#3118](https://github.com/zarr-developers/zarr-python/issues/3118)) - Document different values of StoreLike with examples in the user guide. ([#3303](https://github.com/zarr-developers/zarr-python/issues/3303)) - Reorganize the top-level `examples` directory to give each example its own sub-directory. Adds content to the docs for each example. ([#3502](https://github.com/zarr-developers/zarr-python/issues/3502)) - Updated 3.0 Migration Guide to include function signature change to zarr.Array.resize function. ([#3536](https://github.com/zarr-developers/zarr-python/issues/3536)) ### Misc - [#3515](https://github.com/zarr-developers/zarr-python/issues/3515), [#3532](https://github.com/zarr-developers/zarr-python/issues/3532), [#3533](https://github.com/zarr-developers/zarr-python/issues/3533), [#3553](https://github.com/zarr-developers/zarr-python/issues/3553) ## zarr 3.1.3 (2025-09-18) ### Features - Add a command-line interface to migrate v2 Zarr metadata to v3. Corresponding functions are also provided under zarr.metadata. ([#1798](https://github.com/zarr-developers/zarr-python/issues/1798)) - Add obstore implementation of delete_dir. ([#3310](https://github.com/zarr-developers/zarr-python/issues/3310)) - Adds a registry for chunk key encodings for extensibility. This allows users to implement a custom `ChunkKeyEncoding`, which can be registered via `register_chunk_key_encoding` or as an entry point under `zarr.chunk_key_encoding`. ([#3436](https://github.com/zarr-developers/zarr-python/issues/3436)) - Trying to open a group at a path where an array already exists now raises a helpful error. ([#3444](https://github.com/zarr-developers/zarr-python/issues/3444)) ### Bugfixes - Prevents creation of groups (.create_group) or arrays (.create_array) as children of an existing array. ([#2582](https://github.com/zarr-developers/zarr-python/issues/2582)) - Fix a bug preventing `ones_like`, `full_like`, `empty_like`, `zeros_like` and `open_like` functions from accepting an explicit specification of array attributes like shape, dtype, chunks etc. The functions `full_like`, `empty_like`, and `open_like` now also more consistently infer a `fill_value` parameter from the provided array. ([#2992](https://github.com/zarr-developers/zarr-python/issues/2992)) - LocalStore now uses atomic writes, which should prevent some cases of corrupted data. ([#3411](https://github.com/zarr-developers/zarr-python/issues/3411)) - Fix a potential race condition when using `zarr.create_array` with the `data` parameter set to a NumPy array. Previously Zarr was iterating over the newly created array with a granularity that was too low. Now Zarr chooses a granularity that matches the size of the stored objects for that array. ([#3422](https://github.com/zarr-developers/zarr-python/issues/3422)) - Fix ChunkGrid definition (broken in 3.1.2) ([#3425](https://github.com/zarr-developers/zarr-python/issues/3425)) - Ensure syntax like `root['/subgroup']` works equivalently to `root['subgroup']` when using consolidated metadata. ([#3428](https://github.com/zarr-developers/zarr-python/issues/3428)) - Creating a new group with `zarr.group` no longer errors. This fixes a regression introduced in version 3.1.2. ([#3431](https://github.com/zarr-developers/zarr-python/issues/3431)) - Setting `fill_value` to a float like `0.0` when the data type of the array is an integer is a common mistake. This change lets Zarr Python read arrays with this erroneous metadata, although Zarr Python will not create such arrays. ([#3448](https://github.com/zarr-developers/zarr-python/issues/3448)) ### Deprecations and Removals - The `Store.set_partial_writes` method, which was not used by Zarr-Python, has been removed. `store.supports_partial_writes` is now always `False`. ([#2859](https://github.com/zarr-developers/zarr-python/issues/2859)) ### Misc - [#3376](https://github.com/zarr-developers/zarr-python/issues/3376), [#3390](https://github.com/zarr-developers/zarr-python/issues/3390), [#3403](https://github.com/zarr-developers/zarr-python/issues/3403), [#3449](https://github.com/zarr-developers/zarr-python/issues/3449) ## 3.1.2 (2025-08-25) ### Features - Added support for async vectorized and orthogonal indexing. ([#3083](https://github.com/zarr-developers/zarr-python/issues/3083)) - Make config param optional in init_array ([#3391](https://github.com/zarr-developers/zarr-python/issues/3391)) ### Bugfixes - Ensure that -0.0 is not considered equal to 0.0 when checking if all the values in a chunk are equal to an array's fill value. ([#3144](https://github.com/zarr-developers/zarr-python/issues/3144)) - Fix a bug in `create_array` caused by iterating over chunk-aligned regions instead of shard-aligned regions when writing data. Additionally, the behavior of `nchunks_initialized` has been adjusted. This function consistently reports the number of chunks present in stored objects, even when the array uses the sharding codec. ([#3299](https://github.com/zarr-developers/zarr-python/issues/3299)) - Opening an array or group with `mode="r+"` will no longer create new arrays or groups. ([#3307](https://github.com/zarr-developers/zarr-python/issues/3307)) - Added `zarr.errors.ArrayNotFoundError`, which is raised when attempting to open a zarr array that does not exist, and `zarr.errors.NodeNotFoundError`, which is raised when failing to open an array or a group in a context where either an array or a group was expected. ([#3367](https://github.com/zarr-developers/zarr-python/issues/3367)) - Ensure passing `config` is handled properly when `open`ing an existing array. ([#3378](https://github.com/zarr-developers/zarr-python/issues/3378)) - Raise a Zarr-specific error class when a codec can't be found by name when deserializing the given codecs. This avoids hiding this error behind a "not part of a zarr hierarchy" warning. ([#3395](https://github.com/zarr-developers/zarr-python/issues/3395)) ### Misc - [#3098](https://github.com/zarr-developers/zarr-python/issues/3098), [#3288](https://github.com/zarr-developers/zarr-python/issues/3288), [#3318](https://github.com/zarr-developers/zarr-python/issues/3318), [#3368](https://github.com/zarr-developers/zarr-python/issues/3368), [#3371](https://github.com/zarr-developers/zarr-python/issues/3371), [#3372](https://github.com/zarr-developers/zarr-python/issues/3372), [#3374](https://github.com/zarr-developers/zarr-python/issues/3374) ## 3.1.1 (2025-07-28) ### Features - Add lightweight implementations of `.getsize()` and `.getsize_prefix()` for ObjectStore. ([#3227](https://github.com/zarr-developers/zarr-python/issues/3227)) ### Bugfixes - Creating a Zarr format 2 array with the `order` keyword argument no longer raises a warning. ([#3112](https://github.com/zarr-developers/zarr-python/issues/3112)) - Fixed the error message when passing both `config` and `write_empty_chunks` arguments to reflect the current behaviour (`write_empty_chunks` takes precedence). ([#3112](https://github.com/zarr-developers/zarr-python/issues/3112)) - Creating a Zarr format 3 array with the `order` argument now consistently ignores this argument and raises a warning. ([#3112](https://github.com/zarr-developers/zarr-python/issues/3112)) - When using [`from_array`][zarr.api.asynchronous.from_array] to copy a Zarr format 2 array to a Zarr format 3 array, if the memory order of the input array is `"F"` a warning is raised and the order ignored. This is because Zarr format 3 arrays are always stored in "C" order. ([#3112](https://github.com/zarr-developers/zarr-python/issues/3112)) - The `config` argument to [`zarr.create`][zarr.create] (and functions that create arrays) is now used - previously it had no effect. ([#3112](https://github.com/zarr-developers/zarr-python/issues/3112)) - Ensure that all abstract methods of [`ZDType`][zarr.core.dtype.ZDType] raise a `NotImplementedError` when invoked. ([#3251](https://github.com/zarr-developers/zarr-python/issues/3251)) - Register 'gpu' marker with pytest for downstream StoreTests. ([#3258](https://github.com/zarr-developers/zarr-python/issues/3258)) - Expand the range of types accepted by `parse_data_type` to include strings and Sequences. - Move the functionality of `zarr.core.dtype.parse_data_type` to a new function called `zarr.dtype.parse_dtype`. This change ensures that nomenclature is consistent across the codebase. `zarr.core.dtype.parse_data_type` remains, so this change is not breaking. ([#3264](https://github.com/zarr-developers/zarr-python/issues/3264)) - Fix a regression introduced in 3.1.0 that prevented `inf`, `-inf`, and `nan` values from being stored in `attributes`. ([#3280](https://github.com/zarr-developers/zarr-python/issues/3280)) - Fixes [`Group.nmembers()`][zarr.Group.nmembers] ignoring depth when using consolidated metadata. ([#3287](https://github.com/zarr-developers/zarr-python/issues/3287)) ### Improved Documentation - Expand the data type docs to include a demonstration of the `parse_data_type` function. Expand the docstring for the `parse_data_type` function. ([#3249](https://github.com/zarr-developers/zarr-python/issues/3249)) - Add a section on codecs to the migration guide. ([#3273](https://github.com/zarr-developers/zarr-python/issues/3273)) ### Misc - Remove warnings about vlen-utf8 and vlen-bytes codecs ([#3268](https://github.com/zarr-developers/zarr-python/issues/3268)) ## 3.1.0 (2025-07-14) ### Features - Ensure that invocations of `create_array` use consistent keyword arguments, with consistent defaults. [`zarr.api.synchronous.create_array`][] now takes a `write_data` keyword argument The `Group.create_array` method takes `data` and `write_data` keyword arguments. The functions [`zarr.api.asynchronous.create`][], [`zarr.api.asynchronous.create_array`] and the methods `Group.create_array`, `Group.array`, had the default `fill_value` changed from `0` to the `DEFAULT_FILL_VALUE` value, which instructs Zarr to use the default scalar value associated with the array's data type as the fill value. These are all functions or methods for array creation that mirror, wrap or are wrapped by, another function that already has a default `fill_value` set to `DEFAULT_FILL_VALUE`. This change is necessary to make these functions consistent across the entire codebase, but as this changes default values, new data might have a different fill value than expected after this change. For data types where 0 is meaningful, like integers or floats, the default scalar is 0, so this change should not be noticeable. For data types where 0 is ambiguous, like fixed-length unicode strings, the default fill value might be different after this change. Users who were relying on how Zarr interpreted `0` as a non-numeric scalar value should set their desired fill value explicitly after this change. - Added public API for Buffer ABCs and implementations. Use `zarr.buffer` to access buffer implementations, and `zarr.abc.buffer` for the interface to implement new buffer types. Users previously importing buffer from `zarr.core.buffer` should update their imports to use `zarr.buffer`. As a reminder, all of `zarr.core` is considered a private API that's not covered by zarr-python's versioning policy. ([#2871](https://github.com/zarr-developers/zarr-python/issues/2871)) - Adds zarr-specific data type classes. This change adds a `ZDType` base class for Zarr V2 and Zarr V3 data types. Child classes are defined for each NumPy data type. Each child class defines routines for `JSON` serialization. New data types can be created and registered dynamically. Prior to this change, Zarr Python had two streams for handling data types. For Zarr V2 arrays, we used NumPy data type identifiers. For Zarr V3 arrays, we used a fixed set of string enums. Both of these systems proved hard to extend. This change is largely internal, but it does change the type of the `dtype` and `data_type` fields on the `ArrayV2Metadata` and `ArrayV3Metadata` classes. Previously, `ArrayV2Metadata.dtype` was a NumPy `dtype` object, and `ArrayV3Metadata.data_type` was an internally-defined `enum`. After this change, both `ArrayV2Metadata.dtype` and `ArrayV3Metadata.data_type` are instances of `ZDType`. A NumPy data type can be generated from a `ZDType` via the `ZDType.to_native_dtype()` method. The internally-defined Zarr V3 `enum` class is gone entirely, but the `ZDType.to_json(zarr_format=3)` method can be used to generate either a string, or dictionary that has a string `name` field, that represents the string value previously associated with that `enum`. For more on this new feature, see the [documentation](user-guide/data_types.md) ([#2874](https://github.com/zarr-developers/zarr-python/issues/2874)) - Added `NDBuffer.empty` method for faster ndbuffer initialization. ([#3191](https://github.com/zarr-developers/zarr-python/issues/3191)) - The minimum version of NumPy has increased to 1.26. ([#3226](https://github.com/zarr-developers/zarr-python/issues/3226)) - Add an alternate `from_array_metadata_and_store` constructor to `CodecPipeline`. ([#3233](https://github.com/zarr-developers/zarr-python/issues/3233)) ### Bugfixes - Fixes a variety of issues related to string data types. - Brings the `VariableLengthUTF8` data type Zarr V3 identifier in alignment with Zarr Python 3.0.8 - Disallows creation of 0-length fixed-length data types - Adds a regression test for the `VariableLengthUTF8` data type that checks against version 3.0.8 - Allows users to request the `VariableLengthUTF8` data type with `str`, `"str"`, or `"string"`. ([#3170](https://github.com/zarr-developers/zarr-python/issues/3170)) - Add human readable size for No. bytes stored to `info_complete` ([#3190](https://github.com/zarr-developers/zarr-python/issues/3190)) - Restores the ability to create a Zarr V2 array with a `null` fill value by introducing a new class `DefaultFillValue`, and setting the default value of the `fill_value` parameter in array creation routines to an instance of `DefaultFillValue`. For Zarr V3 arrays, `None` will act as an alias for a `DefaultFillValue` instance, thus preserving compatibility with existing code. ([#3198](https://github.com/zarr-developers/zarr-python/issues/3198)) - Fix the type of `ArrayV2Metadata.codec` to constrain it to `numcodecs.abc.Codec | None`. Previously the type was more permissive, allowing objects that can be parsed into Codecs (e.g., the codec name). The constructor of `ArrayV2Metadata` still allows the permissive input when creating new objects. ([#3232](https://github.com/zarr-developers/zarr-python/issues/3232)) ### Improved Documentation - Add a self-contained example of data type extension to the `examples` directory, and expanded the documentation for data types. ([#3157](https://github.com/zarr-developers/zarr-python/issues/3157)) - Add a description on how to create a RemoteStore of a specific filesystem to the `Remote Store` section in `docs/user-guide/storage.md`. State in the docstring of `FsspecStore.from_url` that the filesystem type is inferred from the URL scheme. It should help a user handling the case when the type of FsspecStore doesn't match the URL scheme. ([#3212](https://github.com/zarr-developers/zarr-python/issues/3212)) ### Deprecations and Removals - Removes default chunk encoding settings (filters, serializer, compressors) from the global configuration object. This removal is justified on the basis that storing chunk encoding settings in the config required a brittle, confusing, and inaccurate categorization of array data types, which was particularly unsuitable after the recent addition of new data types that didn't fit naturally into the pre-existing categories. The default chunk encoding is the same (Zstandard compression, and the required object codecs for variable length data types), but the chunk encoding is now generated by functions that cannot be reconfigured at runtime. Users who relied on setting the default chunk encoding via the global configuration object should instead specify the desired chunk encoding explicitly when creating an array. This change also adds an extra validation step to the creation of Zarr V2 arrays, which ensures that arrays with a `VariableLengthUTF8` or `VariableLengthBytes` data type cannot be created without the correct "object codec". ([#3228](https://github.com/zarr-developers/zarr-python/issues/3228)) - Removes support for passing keyword-only arguments positionally to the following functions and methods: `save_array`, `open`, `group`, `open_group`, `create`, `get_basic_selection`, `set_basic_selection`, `get_orthogonal_selection`, `set_orthogonal_selection`, `get_mask_selection`, `set_mask_selection`, `get_coordinate_selection`, `set_coordinate_selection`, `get_block_selection`, `set_block_selection`, `Group.create_array`, `Group.empty`, `Group.zeroes`, `Group.ones`, `Group.empty_like`, `Group.full`, `Group.zeros_like`, `Group.ones_like`, `Group.full_like`, `Group.array`. Prior to this change, passing a keyword-only argument positionally to one of these functions or methods would raise a deprecation warning. That warning is now gone. Passing keyword-only arguments to these functions and methods positionally is now an error. ## 3.0.10 (2025-07-03) ### Bugfixes - Removed an unnecessary check from `_fsspec._make_async` that would raise an exception when creating a read-only store backed by a local file system with `auto_mkdir` set to `False`. ([#3193](https://github.com/zarr-developers/zarr-python/issues/3193)) - Add missing import for AsyncFileSystemWrapper for _make_async in _fsspec.py ([#3195](https://github.com/zarr-developers/zarr-python/issues/3195)) ## 3.0.9 (2025-06-30) ### Features - Add `zarr.storage.FsspecStore.from_mapper()` so that `zarr.open()` supports stores of type `fsspec.mapping.FSMap`. ([#2774](https://github.com/zarr-developers/zarr-python/issues/2774)) - Implemented `move` for `LocalStore` and `ZipStore`. This allows users to move the store to a different root path. ([#3021](https://github.com/zarr-developers/zarr-python/issues/3021)) - Added `zarr.errors.GroupNotFoundError`, which is raised when attempting to open a group that does not exist. ([#3066](https://github.com/zarr-developers/zarr-python/issues/3066)) - Adds `fill_value` to the list of attributes displayed in the output of the `AsyncArray.info()` method. ([#3081](https://github.com/zarr-developers/zarr-python/issues/3081)) - Use `numpy.zeros` instead of `np.full` for a performance speedup when creating a `zarr.core.buffer.NDBuffer` with `fill_value=0`. ([#3082](https://github.com/zarr-developers/zarr-python/issues/3082)) - Port more stateful testing actions from [Icechunk](https://icechunk.io). ([#3130](https://github.com/zarr-developers/zarr-python/issues/3130)) - Adds a `with_read_only` convenience method to the `Store` abstract base class (raises `NotImplementedError`) and implementations to the `MemoryStore`, `ObjectStore`, `LocalStore`, and `FsspecStore` classes. ([#3138](https://github.com/zarr-developers/zarr-python/issues/3138)) ### Bugfixes - Ignore stale child metadata when reconsolidating metadata. ([#2921](https://github.com/zarr-developers/zarr-python/issues/2921)) - For Zarr format 2, allow fixed-length string arrays to be created without automatically inserting a `Vlen-UT8` codec in the array of filters. Fixed-length string arrays do not need this codec. This change fixes a regression where fixed-length string arrays created with Zarr Python 3 could not be read with Zarr Python 2.18. ([#3100](https://github.com/zarr-developers/zarr-python/issues/3100)) - When creating arrays without explicitly specifying a chunk size using `zarr.create` and other array creation routines, the chunk size will now set automatically instead of defaulting to the data shape. For large arrays this will result in smaller default chunk sizes. To retain previous behaviour, explicitly set the chunk shape to the data shape. This fix matches the existing chunking behaviour of `zarr.save_array` and `zarr.api.asynchronous.AsyncArray.create`. ([#3103](https://github.com/zarr-developers/zarr-python/issues/3103)) - When `zarr.save` has an argument `path=some/path/` and multiple arrays in `args`, the path resulted in `some/path/some/path` due to using the `path` argument twice while building the array path. This is now fixed. ([#3127](https://github.com/zarr-developers/zarr-python/issues/3127)) - Fix `zarr.open` default for argument `mode` when `store` is `read_only` ([#3128](https://github.com/zarr-developers/zarr-python/issues/3128)) - Suppress `FileNotFoundError` when deleting non-existent keys in the `obstore` adapter. When writing empty chunks (i.e. chunks where all values are equal to the array's fill value) to a zarr array, zarr will delete those chunks from the underlying store. For zarr arrays backed by the `obstore` adapter, this will potentially raise a `FileNotFoundError` if the chunk doesn't already exist. Since whether or not a delete of a non-existing object raises an error depends on the behavior of the underlying store, suppressing the error in all cases results in consistent behavior across stores, and is also what `zarr` seems to expect from the store. ([#3140](https://github.com/zarr-developers/zarr-python/issues/3140)) - Trying to open a StorePath/Array with `mode='r'` when the store is not read-only creates a read-only copy of the store. ([#3156](https://github.com/zarr-developers/zarr-python/issues/3156)) ## 3.0.8 (2025-05-19) !!! warning In versions 3.0.0 to 3.0.7 opening arrays or groups with `mode='a'` (the default for many builtin functions) would cause any existing paths in the store to be deleted. This is fixed in 3.0.8, and we recommend all users upgrade to avoid this bug that could cause unintentional data loss. ### Features - Added a `print_debug_info` function for bug reports. ([#2913](https://github.com/zarr-developers/zarr-python/issues/2913)) ### Bugfixes - Fix a bug that prevented the number of initialized chunks being counted properly. ([#2862](https://github.com/zarr-developers/zarr-python/issues/2862)) - Fixed sharding with GPU buffers. ([#2978](https://github.com/zarr-developers/zarr-python/issues/2978)) - Fix structured `dtype` fill value serialization for consolidated metadata ([#2998](https://github.com/zarr-developers/zarr-python/issues/2998)) - It is now possible to specify no compressor when creating a zarr format 2 array. This can be done by passing `compressor=None` to the various array creation routines. The default behaviour of automatically choosing a suitable default compressor remains if the compressor argument is not given. To reproduce the behaviour in previous zarr-python versions when `compressor=None` was passed, pass `compressor='auto'` instead. ([#3039](https://github.com/zarr-developers/zarr-python/issues/3039)) - Fixed the typing of `dimension_names` arguments throughout so that it now accepts iterables that contain `None` alongside `str`. ([#3045](https://github.com/zarr-developers/zarr-python/issues/3045)) - Using various functions to open data with `mode='a'` no longer deletes existing data in the store. ([#3062](https://github.com/zarr-developers/zarr-python/issues/3062)) - Internally use `typesize` constructor parameter for `numcodecs.blosc.Blosc` to improve compression ratios back to the v2-package levels. ([#2962](https://github.com/zarr-developers/zarr-python/issues/2962)) - Specifying the memory order of Zarr format 2 arrays using the `order` keyword argument has been fixed. ([#2950](https://github.com/zarr-developers/zarr-python/issues/2950)) ### Misc - [#2972](https://github.com/zarr-developers/zarr-python/issues/2972), [#3027](https://github.com/zarr-developers/zarr-python/issues/3027), [#3049](https://github.com/zarr-developers/zarr-python/issues/3049) ## 3.0.7 (2025-04-22) ### Features - Add experimental ObjectStore storage class based on obstore. ([#1661](https://github.com/zarr-developers/zarr-python/issues/1661)) - Add `zarr.from_array` using concurrent streaming of source data ([#2622](https://github.com/zarr-developers/zarr-python/issues/2622)) ### Bugfixes - 0-dimensional arrays are now returning a scalar. Therefore, the return type of `__getitem__` changed to NDArrayLikeOrScalar. This change is to make the behavior of 0-dimensional arrays consistent with `numpy` scalars. ([#2718](https://github.com/zarr-developers/zarr-python/issues/2718)) - Fix `fill_value` serialization for `NaN` in `ArrayV2Metadata` and add property-based testing of round-trip serialization ([#2802](https://github.com/zarr-developers/zarr-python/issues/2802)) - Fixes `ConsolidatedMetadata` serialization of `nan`, `inf`, and `-inf` to be consistent with the behavior of `ArrayMetadata`. ([#2996](https://github.com/zarr-developers/zarr-python/issues/2996)) ### Improved Documentation - Updated the 3.0 migration guide to include the removal of "." syntax for getting group members. ([#2991](https://github.com/zarr-developers/zarr-python/issues/2991), [#2997](https://github.com/zarr-developers/zarr-python/issues/2997)) ### Misc - Define a new versioning policy based on Effective Effort Versioning. This replaces the old Semantic Versioning-based policy. ([#2924](https://github.com/zarr-developers/zarr-python/issues/2924), [#2910](https://github.com/zarr-developers/zarr-python/issues/2910)) - Make warning filters in the tests more specific, so warnings emitted by tests added in the future are more likely to be caught instead of ignored. ([#2714](https://github.com/zarr-developers/zarr-python/issues/2714)) - Avoid an unnecessary memory copy when writing Zarr to a local file ([#2944](https://github.com/zarr-developers/zarr-python/issues/2944)) ## 3.0.6 (2025-03-20) ### Bugfixes - Restore functionality of `del z.attrs['key']` to actually delete the key. ([#2908](https://github.com/zarr-developers/zarr-python/issues/2908)) ## 3.0.5 (2025-03-07) ### Bugfixes - Fixed a bug where `StorePath` creation would not apply standard path normalization to the `path` parameter, which led to the creation of arrays and groups with invalid keys. ([#2850](https://github.com/zarr-developers/zarr-python/issues/2850)) - Prevent update_attributes calls from deleting old attributes ([#2870](https://github.com/zarr-developers/zarr-python/issues/2870)) ### Misc - [#2796](https://github.com/zarr-developers/zarr-python/issues/2796) ## 3.0.4 (2025-02-23) ### Features - Adds functions for concurrently creating multiple arrays and groups. ([#2665](https://github.com/zarr-developers/zarr-python/issues/2665)) ### Bugfixes - Fixed a bug where `ArrayV2Metadata` could save `filters` as an empty array. ([#2847](https://github.com/zarr-developers/zarr-python/issues/2847)) - Fix a bug when setting values of a smaller last chunk. ([#2851](https://github.com/zarr-developers/zarr-python/issues/2851)) ### Misc - [#2828](https://github.com/zarr-developers/zarr-python/issues/2828) ## 3.0.3 (2025-02-14) ### Features - Improves performance of FsspecStore.delete_dir for remote filesystems supporting concurrent/batched deletes, e.g., s3fs. ([#2661](https://github.com/zarr-developers/zarr-python/issues/2661)) - Added `zarr.config.enable_gpu` to update Zarr's configuration to use GPUs. ([#2751](https://github.com/zarr-developers/zarr-python/issues/2751)) - Avoid reading chunks during writes where possible. [#757](https://github.com/zarr-developers/zarr-python/issues/757) ([#2784](https://github.com/zarr-developers/zarr-python/issues/2784)) - `LocalStore` learned to `delete_dir`. This makes array and group deletes more efficient. ([#2804](https://github.com/zarr-developers/zarr-python/issues/2804)) - Add `zarr.testing.strategies.array_metadata` to generate ArrayV2Metadata and ArrayV3Metadata instances. ([#2813](https://github.com/zarr-developers/zarr-python/issues/2813)) - Add arbitrary `shards` to Hypothesis strategy for generating arrays. ([#2822](https://github.com/zarr-developers/zarr-python/issues/2822)) ### Bugfixes - Fixed bug with Zarr using device memory, instead of host memory, for storing metadata when using GPUs. ([#2751](https://github.com/zarr-developers/zarr-python/issues/2751)) - The array returned by `zarr.empty` and an empty `zarr.core.buffer.cpu.NDBuffer` will now be filled with the specified fill value, or with zeros if no fill value is provided. This fixes a bug where Zarr format 2 data with no fill value was written with un-predictable chunk sizes. ([#2755](https://github.com/zarr-developers/zarr-python/issues/2755)) - Fix zip-store path checking for stores with directories listed as files. ([#2758](https://github.com/zarr-developers/zarr-python/issues/2758)) - Use removeprefix rather than replace when removing filename prefixes in `FsspecStore.list` ([#2778](https://github.com/zarr-developers/zarr-python/issues/2778)) - Enable automatic removal of `needs release notes` with labeler action ([#2781](https://github.com/zarr-developers/zarr-python/issues/2781)) - Use the proper label config ([#2785](https://github.com/zarr-developers/zarr-python/issues/2785)) - Alters the behavior of `create_array` to ensure that any groups implied by the array's name are created if they do not already exist. Also simplifies the type signature for any function that takes an ArrayConfig-like object. ([#2795](https://github.com/zarr-developers/zarr-python/issues/2795)) - Enitialise empty chunks to the default fill value during writing and add default fill values for datetime, timedelta, structured, and other (void* fixed size) data types ([#2799](https://github.com/zarr-developers/zarr-python/issues/2799)) - Ensure utf8 compliant strings are used to construct numpy arrays in property-based tests ([#2801](https://github.com/zarr-developers/zarr-python/issues/2801)) - Fix pickling for ZipStore ([#2807](https://github.com/zarr-developers/zarr-python/issues/2807)) - Update numcodecs to not overwrite codec configuration ever. Closes [#2800](https://github.com/zarr-developers/zarr-python/issues/2800). ([#2811](https://github.com/zarr-developers/zarr-python/issues/2811)) - Fix fancy indexing (e.g. arr[5, [0, 1]]) with the sharding codec ([#2817](https://github.com/zarr-developers/zarr-python/issues/2817)) ### Improved Documentation - Added new user guide on GPU. ([#2751](https://github.com/zarr-developers/zarr-python/issues/2751)) ## 3.0.2 (2025-01-31) ### Features - Test `getsize()` and `getsize_prefix()` in `StoreTests`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Test that a `ValueError` is raised for invalid byte range syntax in `StoreTests`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Separate instantiating and opening a store in `StoreTests`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Add a test for using Stores as a context managers in `StoreTests`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Implemented `LoggingStore.open()`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - `LoggingStore` is now a generic class. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Change StoreTest's `test_store_repr`, `test_store_supports_writes`, `test_store_supports_partial_writes`, and `test_store_supports_listing` to to be implemented using `@abstractmethod`, rather raising `NotImplementedError`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Test the error raised for invalid buffer arguments in `StoreTests`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Test that data can be written to a store that's not yet open using the store.set method in `StoreTests`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Adds a new function `init_array` for initializing an array in storage, and refactors `create_array` to use `init_array`. `create_array` takes two new parameters: `data`, an optional array-like object, and `write_data`, a bool which defaults to `True`. If `data` is given to `create_array`, then the `dtype` and `shape` attributes of `data` are used to define the corresponding attributes of the resulting Zarr array. Additionally, if `data` given and `write_data` is `True`, then the values in `data` will be written to the newly created array. ([#2761](https://github.com/zarr-developers/zarr-python/issues/2761)) ### Bugfixes - Wrap sync fsspec filesystems with `AsyncFileSystemWrapper`. ([#2533](https://github.com/zarr-developers/zarr-python/issues/2533)) - Added backwards compatibility for Zarr format 2 structured arrays. ([#2681](https://github.com/zarr-developers/zarr-python/issues/2681)) - Update equality for `LoggingStore` and `WrapperStore` such that 'other' must also be a `LoggingStore` or `WrapperStore` respectively, rather than only checking the types of the stores they wrap. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Ensure that `ZipStore` is open before getting or setting any values. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Use stdout rather than stderr as the default stream for `LoggingStore`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Match the errors raised by read only stores in `StoreTests`. ([#2693](https://github.com/zarr-developers/zarr-python/issues/2693)) - Fixed `ZipStore` to make sure the correct attributes are saved when instances are pickled. This fixes a previous bug that prevent using `ZipStore` with a `ProcessPoolExecutor`. ([#2762](https://github.com/zarr-developers/zarr-python/issues/2762)) - Updated the optional test dependencies to include `botocore` and `fsspec`. ([#2768](https://github.com/zarr-developers/zarr-python/issues/2768)) - Fixed the fsspec tests to skip if `botocore` is not installed. Previously they would have failed with an import error. ([#2768](https://github.com/zarr-developers/zarr-python/issues/2768)) - Optimize full chunk writes. ([#2782](https://github.com/zarr-developers/zarr-python/issues/2782)) ### Improved Documentation - Changed the machinery for creating changelog entries. Now individual entries should be added as files to the `changes` directory in the `zarr-python` repository, instead of directly to the changelog file. ([#2736](https://github.com/zarr-developers/zarr-python/issues/2736)) ### Other - Created a type alias `ChunkKeyEncodingLike` to model the union of `ChunkKeyEncoding` instances and the dict form of the parameters of those instances. `ChunkKeyEncodingLike` should be used by high-level functions to provide a convenient way for creating `ChunkKeyEncoding` objects. ([#2763](https://github.com/zarr-developers/zarr-python/issues/2763)) ## 3.0.1 (Jan. 17, 2025) * Implement `zarr.from_array` using concurrent streaming ([#2622](https://github.com/zarr-developers/zarr-python/issues/2622)). ### Bug fixes * Fixes `order` argument for Zarr format 2 arrays ([#2679](https://github.com/zarr-developers/zarr-python/issues/2679)). * Fixes a bug that prevented reading Zarr format 2 data with consolidated metadata written using `zarr-python` version 2 ([#2694](https://github.com/zarr-developers/zarr-python/issues/2694)). * Ensure that compressor=None results in no compression when writing Zarr format 2 data ([#2708](https://github.com/zarr-developers/zarr-python/issues/2708)). * Fix for empty consolidated metadata dataset: backwards compatibility with Zarr-Python 2 ([#2695](https://github.com/zarr-developers/zarr-python/issues/2695)). ### Documentation * Add v3.0.0 release announcement banner ([#2677](https://github.com/zarr-developers/zarr-python/issues/2677)). * Quickstart guide alignment with V3 API ([#2697](https://github.com/zarr-developers/zarr-python/issues/2697)). * Fix doctest failures related to numcodecs 0.15 ([#2727](https://github.com/zarr-developers/zarr-python/issues/2727)). ### Other * Removed some unnecessary files from the source distribution to reduce its size. ([#2686](https://github.com/zarr-developers/zarr-python/issues/2686)). * Enable codecov in GitHub actions ([#2682](https://github.com/zarr-developers/zarr-python/issues/2682)). * Speed up hypothesis tests ([#2650](https://github.com/zarr-developers/zarr-python/issues/2650)). * Remove multiple imports for an import name ([#2723](https://github.com/zarr-developers/zarr-python/issues/2723)). ## 3.0.0 (Jan. 9, 2025) 3.0.0 is a new major release of Zarr-Python, with many breaking changes. See the [v3 migration guide](user-guide/v3_migration.md) for a listing of what's changed. Normal release note service will resume with further releases in the 3.0.0 series. Release notes for the zarr-python 2.x and 1.x releases can be found here: https://zarr.readthedocs.io/en/support-v2/release.html zarr-python-3.2.1/docs/user-guide/000077500000000000000000000000001517635743000170265ustar00rootroot00000000000000zarr-python-3.2.1/docs/user-guide/arrays.md000066400000000000000000000700611517635743000206550ustar00rootroot00000000000000# Working with arrays ## Creating an array Zarr has several functions for creating arrays. For example: ```python exec="true" session="arrays" import shutil shutil.rmtree('data', ignore_errors=True) import numpy as np np.random.seed(0) ``` ```python exec="true" session="arrays" source="above" result="ansi" import zarr z = zarr.create_array(store="memory://arrays-demo", shape=(10000, 10000), chunks=(1000, 1000), dtype='int32') print(z) ``` The code above creates a 2-dimensional array of 32-bit integers with 10000 rows and 10000 columns, divided into chunks where each chunk has 1000 rows and 1000 columns (and so there will be 100 chunks in total). The data is written to an in-memory store (see [`zarr.storage.MemoryStore`][] for more details). See [Persistent arrays](#persistent-arrays) for details on storing arrays in other stores, and see [Data types](data_types.md) for an in-depth look at the data types supported by Zarr. See the [creation API documentation](../api/zarr/create.md) for more detailed information about creating arrays. ## Reading and writing data Zarr arrays support a similar interface to [NumPy](https://numpy.org/doc/stable/) arrays for reading and writing data. For example, the entire array can be filled with a scalar value: ```python exec="true" session="arrays" source="above" z[:] = 42 ``` Regions of the array can also be written to, e.g.: ```python exec="true" session="arrays" source="above" import numpy as np z[0, :] = np.arange(10000) z[:, 0] = np.arange(10000) ``` The contents of the array can be retrieved by slicing, which will load the requested region into memory as a NumPy array, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" print(z[0, 0]) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z[-1, -1]) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z[0, :]) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z[:, 0]) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z[:]) ``` More information about NumPy-style indexing can be found in the [NumPy documentation](https://numpy.org/doc/stable/user/basics.indexing.html). ## Persistent arrays In the examples above, compressed data for each chunk of the array was stored in main memory. Zarr arrays can also be stored on a file system, enabling persistence of data between sessions. To do this, we can change the store argument to point to a filesystem path: ```python exec="true" session="arrays" source="above" z1 = zarr.create_array(store='data/example-1.zarr', shape=(10000, 10000), chunks=(1000, 1000), dtype='int32') ``` The array above will store its configuration metadata and all compressed chunk data in a directory called `'data/example-1.zarr'` relative to the current working directory. The [`zarr.create_array`][] function provides a convenient way to create a new persistent array or continue working with an existing array. Note, there is no need to close an array: data are automatically flushed to disk, and files are automatically closed whenever an array is modified. Persistent arrays support the same interface for reading and writing data, e.g.: ```python exec="true" session="arrays" source="above" z1[:] = 42 z1[0, :] = np.arange(10000) z1[:, 0] = np.arange(10000) ``` Check that the data have been written and can be read again: ```python exec="true" session="arrays" source="above" result="ansi" z2 = zarr.open_array('data/example-1.zarr', mode='r') print(np.all(z1[:] == z2[:])) ``` If you are just looking for a fast and convenient way to save NumPy arrays to disk then load back into memory later, the functions [`zarr.save`][] and [`zarr.load`][] may be useful. E.g.: ```python exec="true" session="arrays" source="above" result="ansi" a = np.arange(10) zarr.save('data/example-2.zarr', a) print(zarr.load('data/example-2.zarr')) ``` Please note that there are a number of other options for persistent array storage, see the [Storage Guide](storage.md) for more details. ## Resizing and appending A Zarr array can be resized, which means that any of its dimensions can be increased or decreased in length. For example: ```python exec="true" session="arrays" source="above" result="ansi" z = zarr.create_array(store='data/example-3.zarr', shape=(10000, 10000), dtype='int32',chunks=(1000, 1000)) z[:] = 42 print(f"Original shape: {z.shape}") z.resize((20000, 10000)) print(f"New shape: {z.shape}") ``` Note that when an array is resized, the underlying data are not rearranged in any way. If one or more dimensions are shrunk, any chunks falling outside the new array shape will be deleted from the underlying store. [`zarr.Array.append`][] is provided as a convenience function, which can be used to append data to any axis. E.g.: ```python exec="true" session="arrays" source="above" result="ansi" a = np.arange(10000000, dtype='int32').reshape(10000, 1000) z = zarr.create_array(store='data/example-4.zarr', shape=a.shape, dtype=a.dtype, chunks=(1000, 100)) z[:] = a print(f"Original shape: {z.shape}") z.append(a) print(f"Shape after first append: {z.shape}") z.append(np.vstack([a, a]), axis=1) print(f"Shape after second append: {z.shape}") ``` ## Runtime configuration Zarr arrays are parametrized with a configuration that determines certain aspects of array behavior. We currently support three configuration options for arrays: `write_empty_chunks`, `read_missing_chunks`, and `order`. | field | type | default | description | | - | - | - | - | | `write_empty_chunks` | `bool` | `False` | Controls whether empty chunks are written to storage. See [Empty chunks](performance.md#empty-chunks). | `read_missing_chunks` | `bool` | `True` | Controls whether missing chunks are filled with the array's fill value on read. If `False`, reading missing chunks raises a [`ChunkNotFoundError`][zarr.errors.ChunkNotFoundError]. | `order` | `Literal["C", "F"]` | `"C"` | The memory layout of arrays returned when reading data from the store. !!! info The Zarr V3 spec states that readers should interpret an uninitialized chunk as containing the array's `fill_value`. By default, Zarr-Python follows this behavior: a missing chunk is treated as uninitialized and filled with the array's `fill_value`. However, if you know that all chunks have been written (i.e., are initialized), you may want to treat a missing chunk as an error. Set `read_missing_chunks=False` to raise a [`ChunkNotFoundError`][zarr.errors.ChunkNotFoundError] instead. !!! note `write_empty_chunks=False` skips writing chunks that are entirely the array's fill value. If `read_missing_chunks=False`, attempting to read these missing chunks will raise a [`ChunkNotFoundError`][zarr.errors.ChunkNotFoundError]. You can specify the configuration when you create an array with the `config` keyword argument. `config` can be passed as either a `dict` or an `ArrayConfig` object. ```python exec="true" session="arrays" source="above" result="ansi" arr = zarr.create_array({}, shape=(10,), dtype='int8', config={"write_empty_chunks": True}) print(arr.config) ``` To get an array view with a different config, use the `with_config` method. ```python exec="true" session="arrays" source="above" result="ansi" arr_f = arr.with_config({"order": "F"}) print(arr_f.config) ``` ## Compressors A number of different compressors can be used with Zarr. Zarr includes Blosc, Zstandard and Gzip compressors. Additional compressors are available through a separate package called [NumCodecs](https://numcodecs.readthedocs.io/) which provides various compressor libraries including LZ4, Zlib, BZ2 and LZMA. Different compressors can be provided via the `compressors` keyword argument accepted by all array creation functions. For example: ```python exec="true" session="arrays" source="above" result="ansi" compressors = zarr.codecs.BloscCodec(cname='zstd', clevel=3, shuffle=zarr.codecs.BloscShuffle.bitshuffle) data = np.arange(100000000, dtype='int32').reshape(10000, 10000) z = zarr.create_array(store='data/example-5.zarr', shape=data.shape, dtype=data.dtype, chunks=(1000, 1000), compressors=compressors) z[:] = data print(z.compressors) ``` This array above will use Blosc as the primary compressor, using the Zstandard algorithm (compression level 3) internally within Blosc, and with the bit-shuffle filter applied. When using a compressor, it can be useful to get some diagnostics on the compression ratio. Zarr arrays provide the [`zarr.Array.info`][] property which can be used to print useful diagnostics, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" print(z.info) ``` The [`zarr.Array.info_complete`][] method inspects the underlying store and prints additional diagnostics, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" print(z.info_complete()) ``` !!! note [`zarr.Array.info_complete`][] will inspect the underlying store and may be slow for large arrays. Use [`zarr.Array.info`][] if detailed storage statistics are not needed. If you don't specify a compressor, by default Zarr uses the Zstandard compressor. To create an array without any compression, set `compressors=None`: ```python exec="true" session="arrays" source="above" result="ansi" z_no_compress = zarr.create_array(store='data/example-uncompressed.zarr', shape=(10000, 10000), chunks=(1000, 1000), dtype='int32', compressors=None) print(f"Compressors: {z_no_compress.compressors}") ``` In addition to Blosc and Zstandard, other compression libraries can also be used. For example, here is an array using Gzip compression, level 1: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(100000000, dtype='int32').reshape(10000, 10000) z = zarr.create_array(store='data/example-6.zarr', shape=data.shape, dtype=data.dtype, chunks=(1000, 1000), compressors=zarr.codecs.GzipCodec(level=1)) z[:] = data print(f"Compressors: {z.compressors}") ``` Here is an example using LZMA from [NumCodecs](https://numcodecs.readthedocs.io/) with a custom filter pipeline including LZMA's built-in delta filter: ```python exec="true" session="arrays" source="above" result="ansi" import lzma from zarr.codecs.numcodecs import LZMA lzma_filters = [dict(id=lzma.FILTER_DELTA, dist=4), dict(id=lzma.FILTER_LZMA2, preset=1)] compressors = LZMA(filters=lzma_filters) data = np.arange(100000000, dtype='int32').reshape(10000, 10000) z = zarr.create_array(store='data/example-7.zarr', shape=data.shape, dtype=data.dtype, chunks=(1000, 1000), compressors=compressors) print(f"Compressors: {z.compressors}") ``` To disable compression, set `compressors=None` when creating an array, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" z = zarr.create_array( store='data/example-8.zarr', shape=(100000000,), chunks=(1000000,), dtype='int32', compressors=None ) print(f"Compressors: {z.compressors}") ``` ## Filters In some cases, compression can be improved by transforming the data in some way. For example, if nearby values tend to be correlated, then shuffling the bytes within each numerical value or storing the difference between adjacent values may increase compression ratio. Some compressors provide built-in filters that apply transformations to the data prior to compression. For example, the Blosc compressor has built-in implementations of byte- and bit-shuffle filters, and the LZMA compressor has a built-in implementation of a delta filter. However, to provide additional flexibility for implementing and using filters in combination with different compressors, Zarr also provides a mechanism for configuring filters outside of the primary compressor. Here is an example using a delta filter with the Blosc compressor: ```python exec="true" session="arrays" source="above" result="ansi" from zarr.codecs.numcodecs import Delta filters = [Delta(dtype='int32')] compressors = zarr.codecs.BloscCodec(cname='zstd', clevel=1, shuffle=zarr.codecs.BloscShuffle.shuffle) data = np.arange(100000000, dtype='int32').reshape(10000, 10000) z = zarr.create_array(store='data/example-9.zarr', shape=data.shape, dtype=data.dtype, chunks=(1000, 1000), filters=filters, compressors=compressors) print(z.info_complete()) ``` For more information about available filter codecs, see the [Numcodecs](https://numcodecs.readthedocs.io/) documentation. ## Advanced indexing Zarr arrays support several methods for advanced or "fancy" indexing, which enable a subset of data items to be extracted or updated in an array without loading the entire array into memory. Note that although this functionality is similar to some of the advanced indexing capabilities available on NumPy arrays and on h5py datasets, **the Zarr API for advanced indexing is different from both NumPy and h5py**, so please read this section carefully. For a complete description of the indexing API, see the documentation for the [`zarr.Array`][] class. ### Indexing with coordinate arrays Items from a Zarr array can be extracted by providing an integer array of coordinates. E.g.: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(10) ** 2 z = zarr.create_array(store='data/example-10.zarr', shape=data.shape, dtype=data.dtype) z[:] = data print(z[:]) print(z.get_coordinate_selection([2, 5])) ``` Coordinate arrays can also be used to update data, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" z.set_coordinate_selection([2, 5], [-1, -2]) print(z[:]) ``` For multidimensional arrays, coordinates must be provided for each dimension, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(15).reshape(3, 5) z = zarr.create_array(store='data/example-11.zarr', shape=data.shape, dtype=data.dtype) z[:] = data print(z[:]) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z.get_coordinate_selection(([0, 2], [1, 3]))) ``` ```python exec="true" session="arrays" source="above" result="ansi" z.set_coordinate_selection(([0, 2], [1, 3]), [-1, -2]) print(z[:]) ``` For convenience, coordinate indexing is also available via the `vindex` property, as well as the square bracket operator, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" print(z.vindex[[0, 2], [1, 3]]) z.vindex[[0, 2], [1, 3]] = [-3, -4] ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z[:]) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z[[0, 2], [1, 3]]) ``` When the indexing arrays have different shapes, they are broadcast together. That is, the following two calls are equivalent: ```python exec="true" session="arrays" source="above" result="ansi" print(z[1, [1, 3]]) print(z[[1, 1], [1, 3]]) ``` ### Indexing with a mask array Items can also be extracted by providing a Boolean mask. E.g.: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(10) ** 2 z = zarr.create_array(store='data/example-12.zarr', shape=data.shape, dtype=data.dtype) z[:] = data print(z[:]) ``` ```python exec="true" session="arrays" source="above" result="ansi" sel = np.zeros_like(z, dtype=bool) sel[2] = True sel[5] = True print(z.get_mask_selection(sel)) ``` ```python exec="true" session="arrays" source="above" result="ansi" z.set_mask_selection(sel, [-1, -2]) print(z[:]) ``` Here's a multidimensional example: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(15).reshape(3, 5) z = zarr.create_array(store='data/example-13.zarr', shape=data.shape, dtype=data.dtype) z[:] = data print(z[:]) ``` ```python exec="true" session="arrays" source="above" result="ansi" sel = np.zeros_like(z, dtype=bool) sel[0, 1] = True sel[2, 3] = True print(z.get_mask_selection(sel)) ``` ```python exec="true" session="arrays" source="above" result="ansi" z.set_mask_selection(sel, [-1, -2]) print(z[:]) ``` For convenience, mask indexing is also available via the `vindex` property, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" print(z.vindex[sel]) ``` ```python exec="true" session="arrays" source="above" result="ansi" z.vindex[sel] = [-3, -4] print(z[:]) ``` Mask indexing is conceptually the same as coordinate indexing, and is implemented internally via the same machinery. Both styles of indexing allow selecting arbitrary items from an array, also known as point selection. ### Orthogonal indexing Zarr arrays also support methods for orthogonal indexing, which allows selections to be made along each dimension of an array independently. For example, this allows selecting a subset of rows and/or columns from a 2-dimensional array. E.g.: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(15).reshape(3, 5) z = zarr.create_array(store='data/example-14.zarr', shape=data.shape, dtype=data.dtype) z[:] = data print(z[:]) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z.get_orthogonal_selection(([0, 2], slice(None)))) # select first and third rows ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z.get_orthogonal_selection((slice(None), [1, 3]))) # select second and fourth columns) ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z.get_orthogonal_selection(([0, 2], [1, 3]))) # select rows [0, 2] and columns [1, 4] ``` Data can also be modified, e.g.: ```python exec="true" session="arrays" source="above" z.set_orthogonal_selection(([0, 2], [1, 3]), [[-1, -2], [-3, -4]]) ``` For convenience, the orthogonal indexing functionality is also available via the `oindex` property, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(15).reshape(3, 5) z = zarr.create_array(store='data/example-15.zarr', shape=data.shape, dtype=data.dtype) z[:] = data print(z.oindex[[0, 2], :]) # select first and third rows ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z.oindex[:, [1, 3]]) # select second and fourth columns ``` ```python exec="true" session="arrays" source="above" result="ansi" print(z.oindex[[0, 2], [1, 3]]) # select rows [0, 2] and columns [1, 4] ``` ```python exec="true" session="arrays" source="above" result="ansi" z.oindex[[0, 2], [1, 3]] = [[-1, -2], [-3, -4]] print(z[:]) ``` Any combination of integer, slice, 1D integer array and/or 1D Boolean array can be used for orthogonal indexing. If the index contains at most one iterable, and otherwise contains only slices and integers, orthogonal indexing is also available directly on the array: ```python exec="true" session="arrays" source="above" result="ansi" data = np.arange(15).reshape(3, 5) z = zarr.create_array(store='data/example-16.zarr', shape=data.shape, dtype=data.dtype) z[:] = data print(np.all(z.oindex[[0, 2], :] == z[[0, 2], :])) ``` ### Block Indexing Zarr also support block indexing, which allows selections of whole chunks based on their logical indices along each dimension of an array. For example, this allows selecting a subset of chunk aligned rows and/or columns from a 2-dimensional array. E.g.: ```python exec="true" session="arrays" source="above" data = np.arange(100).reshape(10, 10) z = zarr.create_array(store='data/example-17.zarr', shape=data.shape, dtype=data.dtype, chunks=(3, 3)) z[:] = data ``` Retrieve items by specifying their block coordinates: ```python exec="true" session="arrays" source="above" result="ansi" print(z.get_block_selection(1)) ``` Equivalent slicing: ```python exec="true" session="arrays" source="above" result="ansi" print(z[3:6]) ``` For convenience, the block selection functionality is also available via the `blocks` property, e.g.: ```python exec="true" session="arrays" source="above" result="ansi" print(z.blocks[1]) ``` Block index arrays may be multidimensional to index multidimensional arrays. For example: ```python exec="true" session="arrays" source="above" result="ansi" print(z.blocks[0, 1:3]) ``` Data can also be modified. Let's start by a simple 2D array: ```python exec="true" session="arrays" source="above" z = zarr.create_array(store='data/example-18.zarr', shape=(6, 6), dtype=int, chunks=(2, 2)) ``` Set data for a selection of items: ```python exec="true" session="arrays" source="above" result="ansi" z.set_block_selection((1, 0), 1) print(z[...]) ``` For convenience, this functionality is also available via the `blocks` property. E.g.: ```python exec="true" session="arrays" source="above" result="ansi" z.blocks[:, 2] = 7 print(z[...]) ``` Any combination of integer and slice can be used for block indexing: ```python exec="true" session="arrays" source="above" result="ansi" print(z.blocks[2, 1:3]) ``` ```python exec="true" session="arrays" source="above" result="ansi" root = zarr.create_group('data/example-19.zarr') foo = root.create_array(name='foo', shape=(1000, 100), chunks=(10, 10), dtype='float32') bar = root.create_array(name='bar', shape=(100,), dtype='int32') foo[:, :] = np.random.random((1000, 100)) bar[:] = np.arange(100) print(root.tree()) ``` ## Sharding Using small chunk shapes in very large arrays can lead to a very large number of chunks. This can become a performance issue for file systems and object storage. With Zarr format 3, a new sharding feature has been added to address this issue. With sharding, multiple chunks can be stored in a single storage object (e.g. a file). Within a shard, chunks are compressed and serialized separately. This allows individual chunks to be read independently. However, when writing data, a full shard must be written in one go for optimal performance and to avoid concurrency issues. That means that shards are the units of writing and chunks are the units of reading. Users need to configure the chunk and shard shapes accordingly. Sharded arrays can be created by providing the `shards` parameter to [`zarr.create_array`][]. ```python exec="true" session="arrays" source="above" result="ansi" a = zarr.create_array('data/example-20.zarr', shape=(10000, 10000), shards=(1000, 1000), chunks=(100, 100), dtype='uint8') a[:] = (np.arange(10000 * 10000) % 256).astype('uint8').reshape(10000, 10000) print(a.info_complete()) ``` In this example a shard shape of (1000, 1000) and a chunk shape of (100, 100) is used. This means that `10*10` chunks are stored in each shard, and there are `10*10` shards in total. Without the `shards` argument, there would be 10,000 chunks stored as individual files. ## Rectilinear (variable) chunk grids !!! warning "Experimental" Rectilinear chunk grids are an experimental feature and may change in future releases. This feature is expected to stabilize in Zarr version 3.3. Because the feature is still stabilizing, it is disabled by default and must be explicitly enabled: ```python import zarr zarr.config.set({"array.rectilinear_chunks": True}) ``` Or via the environment variable `ZARR_ARRAY__RECTILINEAR_CHUNKS=True`. The examples below assume this config has been set. By default, Zarr arrays use a regular chunk grid where every chunk along a given dimension has the same size (except possibly the final boundary chunk). Rectilinear chunk grids allow each chunk along a dimension to have a different size. This is useful when the natural partitioning of the data is not uniform — for example, satellite swaths of varying width, time series with irregular intervals, or spatial tiles of different extents. ### Creating arrays with rectilinear chunks To create an array with rectilinear chunks, pass a nested list to the `chunks` parameter where each inner list gives the chunk sizes along one dimension: ```python exec="true" session="arrays" source="above" result="ansi" zarr.config.set({"array.rectilinear_chunks": True}) z = zarr.create_array( store=zarr.storage.MemoryStore(), shape=(60, 100), chunks=[[10, 20, 30], [50, 50]], dtype='int32', ) print(z.info) ``` In this example the first dimension is split into three chunks of sizes 10, 20, and 30, while the second dimension is split into two equal chunks of size 50. ### Reading and writing data Rectilinear arrays support the same indexing interface as regular arrays. Reads and writes that cross chunk boundaries of different sizes are handled automatically: ```python exec="true" session="arrays" source="above" result="ansi" import numpy as np data = np.arange(60 * 100, dtype='int32').reshape(60, 100) z[:] = data # Read a slice that spans the first two chunks (sizes 10 and 20) along axis 0 print(z[5:25, 0:5]) ``` ### Inspecting chunk sizes The `.write_chunk_sizes` property returns the actual data size of each storage chunk along every dimension. It works for both regular and rectilinear arrays and returns a tuple of tuples (matching the dask `Array.chunks` convention). When sharding is used, `.read_chunk_sizes` returns the inner chunk sizes instead: ```python exec="true" session="arrays" source="above" result="ansi" print(z.write_chunk_sizes) ``` For regular arrays, this includes the boundary chunk: ```python exec="true" session="arrays" source="above" result="ansi" z_regular = zarr.create_array( store=zarr.storage.MemoryStore(), shape=(100, 80), chunks=(30, 40), dtype='int32', ) print(z_regular.write_chunk_sizes) ``` Note that the `.chunks` property is only available for regular chunk grids. For rectilinear arrays, use `.write_chunk_sizes` (or `.read_chunk_sizes`) instead. ### Resizing and appending Rectilinear arrays can be resized. When growing past the current edge sum, a new chunk is appended covering the additional extent. When shrinking, the chunk edges are preserved and the extent is re-bound (chunks beyond the new extent simply become inactive): ```python exec="true" session="arrays" source="above" result="ansi" z = zarr.create_array( store=zarr.storage.MemoryStore(), shape=(30,), chunks=[[10, 20]], dtype='float64', ) z[:] = np.arange(30, dtype='float64') print(f"Before resize: chunk_sizes={z.write_chunk_sizes}") z.resize((50,)) print(f"After resize: chunk_sizes={z.write_chunk_sizes}") ``` The `append` method also works with rectilinear arrays: ```python exec="true" session="arrays" source="above" result="ansi" z.append(np.arange(10, dtype='float64')) print(f"After append: shape={z.shape}, chunk_sizes={z.write_chunk_sizes}") ``` ### Compressors and filters Rectilinear arrays work with all codecs — compressors, filters, and checksums. Since each chunk may have a different size, the codec pipeline processes each chunk independently: ```python exec="true" session="arrays" source="above" result="ansi" z = zarr.create_array( store=zarr.storage.MemoryStore(), shape=(60, 100), chunks=[[10, 20, 30], [50, 50]], dtype='float64', filters=[zarr.codecs.TransposeCodec(order=(1, 0))], compressors=[zarr.codecs.BloscCodec(cname='zstd', clevel=3)], ) z[:] = np.arange(60 * 100, dtype='float64').reshape(60, 100) np.testing.assert_array_equal(z[:], np.arange(60 * 100, dtype='float64').reshape(60, 100)) print("Roundtrip OK") ``` ### Rectilinear shard boundaries Rectilinear chunk grids can also be used for shard boundaries when combined with sharding. In this case, the outer grid (shards) is rectilinear while the inner chunks remain regular. Each shard dimension must be divisible by the corresponding inner chunk size: ```python exec="true" session="arrays" source="above" result="ansi" z = zarr.create_array( store=zarr.storage.MemoryStore(), shape=(120, 100), chunks=(10, 10), shards=[[60, 40, 20], [50, 50]], dtype='int32', ) z[:] = np.arange(120 * 100, dtype='int32').reshape(120, 100) print(z[50:70, 40:60]) ``` Note that rectilinear inner chunks with sharding are not supported — only the shard boundaries can be rectilinear. ### Metadata format Rectilinear chunk grid metadata uses run-length encoding (RLE) for compact serialization. When reading metadata, both bare integers and `[value, count]` pairs are accepted: - `[10, 20, 30]` — three chunks with explicit sizes - `[[10, 3]]` — three chunks of size 10 (RLE shorthand) - `[[10, 3], 5]` — three chunks of size 10, then one chunk of size 5 When writing, Zarr automatically compresses repeated values into RLE format. ## Missing features in 3.0 The following features have not been ported to 3.0 yet. ### Copying and migrating data See the Zarr-Python 2 documentation on [Copying and migrating data](https://zarr.readthedocs.io/en/support-v2/tutorial.html#copying-migrating-data) for more details. zarr-python-3.2.1/docs/user-guide/attributes.md000066400000000000000000000021011517635743000215300ustar00rootroot00000000000000# Working with attributes Zarr arrays and groups support custom key/value attributes, which can be useful for storing application-specific metadata. For example: ```python exec="true" session="attributes" source="above" result="ansi" import zarr root = zarr.create_group(store="memory://attributes-demo") root.attrs['foo'] = 'bar' z = root.create_array(name='zzz', shape=(10000, 10000), dtype='int32') z.attrs['baz'] = 42 z.attrs['qux'] = [1, 4, 7, 12] print(sorted(root.attrs)) ``` ```python exec="true" session="attributes" source="above" result="ansi" print('foo' in root.attrs) ``` ```python exec="true" session="attributes" source="above" result="ansi" print(root.attrs['foo']) ``` ```python exec="true" session="attributes" source="above" result="ansi" print(sorted(z.attrs)) ``` ```python exec="true" session="attributes" source="above" result="ansi" print(z.attrs['baz']) ``` ```python exec="true" session="attributes" source="above" result="ansi" print(z.attrs['qux']) ``` Internally Zarr uses JSON to store array attributes, so attribute values must be JSON serializable. zarr-python-3.2.1/docs/user-guide/cli.md000066400000000000000000000050671517635743000201270ustar00rootroot00000000000000# Command-line interface Zarr-Python provides a command-line interface that enables: - migration of Zarr v2 metadata to v3 - removal of v2 or v3 metadata To see available commands run the following in a terminal: ```bash zarr --help ``` or to get help on individual commands: ```bash zarr migrate --help zarr remove-metadata --help ``` ## Migrate metadata from v2 to v3 ### Migrate to a separate location To migrate a Zarr array/group's metadata from v2 to v3 run: ```bash zarr migrate v3 path/to/input.zarr path/to/output.zarr ``` This will write new `zarr.json` files to `output.zarr`, leaving `input.zarr` un-touched. Note - this will migrate the entire Zarr hierarchy, so if `input.zarr` contains multiple groups/arrays, new `zarr.json` will be made for all of them. ### Migrate in-place If you'd prefer to migrate the metadata in-place run: ```bash zarr migrate v3 path/to/input.zarr ``` This will write new `zarr.json` files to `input.zarr`, leaving the existing v2 metadata un-touched. To open the array/group using the new metadata use: ```python import zarr zarr_with_v3_metadata = zarr.open('path/to/input.zarr', zarr_format=3) ``` Once you are happy with the conversion, you can run the following to remove the old v2 metadata: ```bash zarr remove-metadata v2 path/to/input.zarr ``` Note there is also a shortcut to migrate and remove v2 metadata in one step: ```bash zarr migrate v3 path/to/input.zarr --remove-v2-metadata ``` ## Remove metadata Remove v2 metadata using: ```bash zarr remove-metadata v2 path/to/input.zarr ``` or v3 with: ```bash zarr remove-metadata v3 path/to/input.zarr ``` By default, this will only allow removal of metadata if a valid alternative exists. For example, you can't remove v2 metadata unless v3 metadata exists at that location. To override this behaviour use `--force`: ```bash zarr remove-metadata v3 path/to/input.zarr --force ``` ## Dry run All commands provide a `--dry-run` option that will log changes that would be made on a real run, without creating or modifying any files. ```bash zarr migrate v3 path/to/input.zarr --dry-run Dry run enabled - no new files will be created or changed. Log of files that would be created on a real run: Saving metadata to path/to/input.zarr/zarr.json ``` ## Verbose You can also add `--verbose` **before** any command, to see a full log of its actions: ```bash zarr --verbose migrate v3 path/to/input.zarr zarr --verbose remove-metadata v2 path/to/input.zarr ``` ## Equivalent functions All features of the command-line interface are also available via functions under `zarr.metadata`.zarr-python-3.2.1/docs/user-guide/config.md000066400000000000000000000040721517635743000206200ustar00rootroot00000000000000# Runtime configuration [`zarr.config`][] is responsible for managing the configuration of zarr and is based on the [donfig](https://github.com/pytroll/donfig) Python library. Configuration values can be set using code like the following: ```python exec="true" session="config" source="above" result="ansi" import zarr print(zarr.config.get('array.order')) ``` ```python exec="true" session="config" source="above" result="ansi" zarr.config.set({'array.order': 'F'}) print(zarr.config.get('array.order')) ``` Alternatively, configuration values can be set using environment variables, e.g. `ZARR_ARRAY__ORDER=F`. The configuration can also be read from a YAML file in standard locations. For more information, see the [donfig documentation](https://donfig.readthedocs.io/en/latest/). Configuration options include the following: - Default Zarr format `default_zarr_format` - Default array order in memory `array.order` - Whether empty chunks are written to storage `array.write_empty_chunks` - Enable experimental rectilinear chunks `array.rectilinear_chunks` - Whether missing chunks are filled with the array's fill value on read `array.read_missing_chunks` (default `True`). Set to `False` to raise a [`ChunkNotFoundError`][zarr.errors.ChunkNotFoundError] instead. - Async and threading options, e.g. `async.concurrency` and `threading.max_workers` - Selections of implementations of codecs, codec pipelines and buffers - Enabling GPU support with `zarr.config.enable_gpu()`. See GPU support for more. For selecting custom implementations of codecs, pipelines, buffers and ndbuffers, first register the implementations in the registry and then select them in the config. For example, an implementation of the bytes codec in a class `'custompackage.NewBytesCodec'`, requires the value of `codecs.bytes.name` to be `'custompackage.NewBytesCodec'`. This is the current default configuration: ```python exec="true" session="config" source="above" result="ansi" from pprint import pprint import io output = io.StringIO() zarr.config.pprint(stream=output, width=60) print(output.getvalue()) ``` zarr-python-3.2.1/docs/user-guide/consolidated_metadata.md000066400000000000000000000122021517635743000236550ustar00rootroot00000000000000# Consolidated metadata !!! warning The Consolidated Metadata feature in Zarr-Python is considered experimental for v3 stores. [zarr-specs#309](https://github.com/zarr-developers/zarr-specs/pull/309) has proposed a formal extension to the v3 specification to support consolidated metadata. Zarr-Python implements the [Consolidated Metadata](https://github.com/zarr-developers/zarr-specs/pull/309) for v2 and v3 stores. Consolidated metadata can reduce the time needed to load the metadata for an entire hierarchy, especially when the metadata is being served over a network. Consolidated metadata essentially stores all the metadata for a hierarchy in the metadata of the root Group. ## Usage If consolidated metadata is present in a Zarr Group's metadata then it is used by default. The initial read to open the group will need to communicate with the store (reading from a file for a [`zarr.storage.LocalStore`][], making a network request for a [`zarr.storage.FsspecStore`][]). After that, any subsequent metadata reads get child Group or Array nodes will *not* require reads from the store. In Python, the consolidated metadata is available on the `.consolidated_metadata` attribute of the `GroupMetadata` object. ```python exec="true" session="consolidated_metadata" source="above" result="ansi" import zarr import warnings warnings.filterwarnings("ignore", category=UserWarning) group = zarr.create_group(store="memory://consolidated-metadata-demo") print(group) array = group.create_array(shape=(1,), name='a', dtype='float64') print(array) ``` ```python exec="true" session="consolidated_metadata" source="above" result="ansi" array = group.create_array(shape=(2, 2), name='b', dtype='float64') print(array) ``` ```python exec="true" session="consolidated_metadata" source="above" result="ansi" array = group.create_array(shape=(3, 3, 3), name='c', dtype='float64') print(array) ``` ```python exec="true" session="consolidated_metadata" source="above" result="ansi" result = zarr.consolidate_metadata("memory://consolidated-metadata-demo") print(result) ``` If we open that group, the Group's metadata has a `zarr.core.group.ConsolidatedMetadata` that can be used.: ```python exec="true" session="consolidated_metadata" source="above" result="ansi" from pprint import pprint import io consolidated = zarr.open_group(store="memory://consolidated-metadata-demo") consolidated_metadata = consolidated.metadata.consolidated_metadata.metadata # Note: pprint can be users without capturing the output regularly output = io.StringIO() pprint(dict(sorted(consolidated_metadata.items())), stream=output, width=60) print(output.getvalue()) ``` Operations on the group to get children automatically use the consolidated metadata.: ```python exec="true" session="consolidated_metadata" source="above" result="ansi" print(consolidated['a']) # no read / HTTP request to the Store is required ``` With nested groups, the consolidated metadata is available on the children, recursively.: ```python exec="true" session="consolidated_metadata" source="above" result="ansi" child = group.create_group('child', attributes={'kind': 'child'}) grandchild = child.create_group('child', attributes={'kind': 'grandchild'}) consolidated = zarr.consolidate_metadata("memory://consolidated-metadata-demo") output = io.StringIO() pprint(consolidated['child'].metadata.consolidated_metadata, stream=output, width=60) print(output.getvalue()) ``` !!! info "Added in version 3.1.1" The keys in the consolidated metadata are sorted prior to writing. Keys are sorted in ascending order by path depth, where a path is defined as a sequence of strings joined by `"/"`. For keys with the same path length, lexicographic order is used to break the tie. This behaviour ensures deterministic metadata output for a given group. ## Synchronization and Concurrency Consolidated metadata is intended for read-heavy use cases on slowly changing hierarchies. For hierarchies where new nodes are constantly being added, removed, or modified, consolidated metadata may not be desirable. 1. It will add some overhead to each update operation, since the metadata would need to be re-consolidated to keep it in sync with the store. 2. Readers using consolidated metadata will regularly see a "past" version of the metadata, at the time they read the root node with its consolidated metadata. ## Stores Without Support for Consolidated Metadata Some stores may want to opt out of the consolidated metadata mechanism. This may be for several reasons like: * They want to maintain read-write consistency, which is challenging with consolidated metadata. * They have their own consolidated metadata mechanism. * They offer good enough performance without need for consolidation. This type of store can declare it doesn't want consolidation by implementing `Store.supports_consolidated_metadata` and returning `False`. For stores that don't support consolidation, Zarr will: * Raise an error on `consolidate_metadata` calls, maintaining the store in its unconsolidated state. * Raise an error in `AsyncGroup.open(..., use_consolidated=True)` * Not use consolidated metadata in `AsyncGroup.open(..., use_consolidated=None)` zarr-python-3.2.1/docs/user-guide/data_types.md000066400000000000000000000463511517635743000215160ustar00rootroot00000000000000# Array data types ## Zarr's Data Type Model Zarr is designed for interoperability with NumPy, so if you are familiar with NumPy or any other N-dimensional array library, Zarr's model for array data types should seem familiar. However, Zarr data types have some unique features that are described in this document. Zarr arrays operate under an essential design constraint: unlike NumPy arrays, Zarr arrays are designed to be stored and accessed by other Zarr implementations. This means that, among other things, Zarr data types must be serializable to metadata documents in accordance with the Zarr specifications, which adds some unique aspects to the Zarr data type model. The following sections explain Zarr's data type model in greater detail and demonstrate the Zarr Python APIs for working with Zarr data types. ### Array Data Types Every Zarr array has a data type, which defines the meaning of the array's elements. An array's data type is encoded in the JSON metadata for the array. This means that the data type of an array must be JSON-serializable. In Zarr V2, the data type of an array is stored in the `dtype` field in array metadata. Zarr V3 changed the name of this field to `data_type` and also defined new rules for the values that can be assigned to the `data_type` field. For example, in Zarr V2, the boolean array data type was represented in array metadata as the string `"|b1"`. In Zarr V3, the same type is represented as the string `"bool"`. ### Scalars Zarr also specifies how array elements, i.e., scalars, are encoded in array metadata. This is necessary because Zarr uses a field in array metadata to define a default value for chunks that are not stored. This field, called `fill_value` in both Zarr V2 and Zarr V3 metadata documents, contains a JSON value that can be decoded to a scalar value compatible with the array's data type. For the boolean data type, the scalar encoding is simple—booleans are natively supported by JSON, so Zarr saves booleans as JSON booleans. Other scalars, like floats or raw bytes, have more elaborate encoding schemes, and in some cases, this scheme depends on the Zarr format version. ## Data Types in Zarr Version 2 Version 2 of the Zarr format defined its data types relative to [NumPy's data types](https://numpy.org/doc/2.1/reference/arrays.dtypes.html#data-type-objects-dtype), and added a few non-NumPy data types as well. With one exception ([structured data types](#structured-data-type)), the Zarr V2 JSON identifier for a data type is just the NumPy `str` attribute of that data type: ```python exec="true" session="data_types" source="above" result="ansi" import zarr import numpy as np import json store = {} np_dtype = np.dtype('int64') print(np_dtype.str) ``` ```python exec="true" session="data_types" source="above" result="ansi" z = zarr.create_array(store=store, shape=(1,), dtype=np_dtype, zarr_format=2) dtype_meta = json.loads(store['.zarray'].to_bytes())["dtype"] print(dtype_meta) ``` !!! note The `<` character in the data type metadata encodes the [endianness](https://numpy.org/doc/2.2/reference/generated/numpy.dtype.byteorder.html), or "byte order," of the data type. As per the NumPy model, in Zarr version 2 each data type has an endianness where applicable. However, Zarr version 3 data types do not store endianness information. There are two special cases to consider: ["structured" data types](#structured-data-type), and ["object"](#object-data-type) data types. ### Structured Data Type NumPy allows the construction of a so-called "structured" data types comprised of ordered collections of named fields, where each field is itself a distinct NumPy data type. See the NumPy documentation [here](https://numpy.org/doc/stable/user/basics.rec.html). Crucially, NumPy does not use a special data type for structured data types—instead, NumPy implements structured data types as an optional feature of the so-called "Void" data type, which models arbitrary fixed-size byte strings. The `str` attribute of a regular NumPy void data type is the same as the `str` of a NumPy structured data type. This means that the `str` attribute does not convey information about the fields contained in a structured data type. For these reasons, Zarr V2 uses a special data type encoding for structured data types. They are stored in JSON as lists of pairs, where the first element is a string, and the second element is a Zarr V2 data type specification. This representation supports recursion. For example: ```python exec="true" session="data_types" source="above" result="ansi" store = {} np_dtype = np.dtype([('field_a', '>i2'), ('field_b', [('subfield_c', '>f4'), ('subfield_d', 'i2')])]) print(np_dtype.str) ``` ```python exec="true" session="data_types" source="above" result="ansi" z = zarr.create_array(store=store, shape=(1,), dtype=np_dtype, zarr_format=2) dtype_meta = json.loads(store['.zarray'].to_bytes())["dtype"] print(dtype_meta) ``` ### Object Data Type The NumPy "object" type is essentially an array of references to arbitrary Python objects. It can model arrays of variable-length UTF-8 strings, arrays of variable-length byte strings, or even arrays of variable-length arrays, each with a distinct data type. This makes the "object" data type expressive, but also complicated to store. Zarr Python cannot persistently store references to arbitrary Python objects. But if each of those Python objects has a consistent type, then we can use a special encoding procedure to store the array. This is how Zarr Python stores variable-length UTF-8 strings, or variable-length byte strings. Although these are separate data types in this library, they are both "object" arrays in NumPy, which means they have the *same* Zarr V2 string representation: `"|O"`. So for Zarr V2 we have to disambiguate different "object" data type arrays on the basis of their encoding procedure, i.e., the codecs declared in the `filters` and `compressor` attributes of array metadata. If an array with data type "object" used the `"vlen-utf8"` codec, then it was interpreted as an array of variable-length strings. If an array with data type "object" used the `"vlen-bytes"` codec, then it was interpreted as an array of variable-length byte strings. This all means that the `dtype` field alone does not fully specify a data type in Zarr V2. The name of the object codec used, if one was used, is also required. Although this fact can be ignored for many simple numeric data types, any comprehensive approach to Zarr V2 data types must either reject the "object" data types or include the "object codec" identifier in the JSON form of the basic data type model. ## Data Types in Zarr Version 3 The NumPy-based Zarr V2 data type representation was effective for simple data types but struggled with more complex data types, like "object" and "structured" data types. To address these limitations, Zarr V3 introduced several key changes to how data types are represented: - Instead of copying NumPy character codecs, Zarr V3 defines an identifier for each data type. The basic data types are identified by strings like `"int8"`, `"int16"`, etc., and data types that require a configuration can be identified by a JSON object. For example, this JSON object declares a datetime data type: ```json { "name": "numpy.datetime64", "configuration": { "unit": "s", "scale_factor": 10 } } ``` - Zarr V3 data types do not have endianness. This is a departure from Zarr V2, where multi-byte data types are defined with endianness information. Instead, Zarr V3 requires that the endianness of encoded array chunks is specified in the `codecs` attribute of array metadata. The Zarr V3 specification leaves the in-memory endianness of decoded array chunks as an implementation detail. For more about data types in Zarr V3, see the [V3 specification](https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html). ## Data Types in Zarr Python The two Zarr formats that Zarr Python supports specify data types in different ways: data types in Zarr version 2 are encoded as NumPy-compatible strings (or lists, in the case of structured data types), while data types in Zarr V3 are encoded as either strings or JSON objects. Zarr V3 data types do not have any associated endianness information, unlike Zarr V2 data types. Zarr Python needs to support both Zarr V2 and V3, which means we need to abstract over these differences. We do this with an abstract Zarr data type class: [ZDType][zarr.dtype.ZDType] which provides Zarr V2 and Zarr V3 compatibility routines for "native" data types. In this context, a "native" data type is a Python class, typically defined in another library, that models an array's data type. For example, [`numpy.dtypes.UInt8DType`][] is a native data type defined in NumPy. Zarr Python wraps the NumPy `uint8` with a [ZDType][zarr.dtype.ZDType] instance called [UInt8][zarr.dtype.UInt8]. As of this writing, the only native data types Zarr Python supports are NumPy data types. We could avoid the "native data type" jargon and just say "NumPy data type," but we do not want to rule out the possibility of using non-NumPy array backends in the future. Each data type supported by Zarr Python is modeled by a [ZDType][zarr.dtype.ZDType] subclass, which provides an API for the following operations: - Encoding and decoding a native data type - Encoding and decoding a data type to and from Zarr V2 and Zarr V3 array metadata - Encoding and decoding a scalar value to and from Zarr V2 and Zarr V3 array metadata - Casting a Python object to a scalar value consistent with the data type ### List of data types The following section lists the data types built in to Zarr Python. With a few exceptions, Zarr Python supports nearly all of the data types in NumPy. If you need a data type that is not listed here, it's possible to create it yourself: see [Adding New Data Types](#adding-new-data-types). #### Boolean - [Boolean][zarr.dtype.Bool] #### Integral - [Signed 8-bit integer][zarr.dtype.Int8] - [Signed 16-bit integer][zarr.dtype.Int16] - [Signed 32-bit integer][zarr.dtype.Int32] - [Signed 64-bit integer][zarr.dtype.Int64] - [Unsigned 8-bit integer][zarr.dtype.UInt8] - [Unsigned 16-bit integer][zarr.dtype.UInt16] - [Unsigned 32-bit integer][zarr.dtype.UInt32] - [Unsigned 64-bit integer][zarr.dtype.UInt64] #### Floating-point - [16-bit floating-point][zarr.dtype.Float16] - [32-bit floating-point][zarr.dtype.Float32] - [64-bit floating-point][zarr.dtype.Float64] - [64-bit complex floating-point][zarr.dtype.Complex64] - [128-bit complex floating-point][zarr.dtype.Complex128] #### String - [Fixed-length UTF-32 string][zarr.dtype.FixedLengthUTF32] - [Variable-length UTF-8 string][zarr.dtype.VariableLengthUTF8] #### Bytes - [Fixed-length null-terminated bytes][zarr.dtype.NullTerminatedBytes] - [Fixed-length raw bytes][zarr.dtype.RawBytes] - [Variable-length bytes][zarr.dtype.VariableLengthBytes] #### Temporal - [DateTime64][zarr.dtype.DateTime64] - [TimeDelta64][zarr.dtype.TimeDelta64] #### Struct-like - [Structured][zarr.dtype.Structured] !!! note "Zarr V3 Structured Data Types" In Zarr V3, structured data types are specified using the `struct` extension defined in the [zarr-extensions repository](https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/struct). The JSON representation uses an object format for fields: ```json { "name": "struct", "configuration": { "fields": [ {"name": "x", "data_type": "float32"}, {"name": "y", "data_type": "int64"} ] } } ``` For backward compatibility, Zarr Python also accepts the legacy `structured` name with tuple-format fields when reading existing data. Fill values for structured types are represented as JSON objects mapping field names to values: ```json {"x": 1.5, "y": 42} ``` When using structured types with multi-byte fields, the `bytes` codec must specify an explicit `endian` parameter. If omitted, Zarr Python assumes little-endian for legacy compatibility but emits a warning. ### Example Usage This section will demonstrates the basic usage of Zarr data types. Create a `ZDType` from a native data type: ```python exec="true" session="data_types" source="above" from zarr.core.dtype import Int8 import numpy as np int8 = Int8.from_native_dtype(np.dtype('int8')) ``` Convert back to a native data type: ```python exec="true" session="data_types" source="above" native_dtype = int8.to_native_dtype() assert native_dtype == np.dtype('int8') ``` Get the default scalar value for the data type: ```python exec="true" session="data_types" source="above" default_value = int8.default_scalar() assert default_value == np.int8(0) ``` Serialize to JSON for Zarr V2: ```python exec="true" session="data_types" source="above" result="ansi" json_v2 = int8.to_json(zarr_format=2) print(json_v2) {'name': '|i1', 'object_codec_id': None} ``` !!! note The representation returned by `to_json(zarr_format=2)` is more abstract than the literal contents of Zarr V2 array metadata, because the JSON representation used by the `ZDType` classes must be distinct across different data types. As noted [earlier](#object-data-type), Zarr V2 identifies multiple distinct data types with the "object" data type identifier `"|O"`. Extra information is needed to disambiguate these data types from one another. That's the reason for the `object_codec_id` field you see here. And for V3: ```python exec="true" session="data_types" source="above" result="ansi" json_v3 = int8.to_json(zarr_format=3) print(json_v3) ``` Serialize a scalar value to JSON: ```python exec="true" session="data_types" source="above" result="ansi" json_value = int8.to_json_scalar(42, zarr_format=3) print(json_value) ``` Deserialize a scalar value from JSON: ```python exec="true" session="data_types" source="above" scalar_value = int8.from_json_scalar(42, zarr_format=3) assert scalar_value == np.int8(42) ``` ### Adding New Data Types Each Zarr data type is a separate Python class that inherits from [ZDType][zarr.dtype.ZDType]. You can define a custom data type by writing your own subclass of [ZDType][zarr.dtype.ZDType] and adding your data type to the data type registry. To see an executable demonstration of this process, see the [`custom_dtype` example](../user-guide/examples/custom_dtype.md). ### Data Type Resolution Although Zarr Python uses a different data type model from NumPy, you can still define a Zarr array with a NumPy data type object: ```python exec="true" session="data_types" source="above" result="ansi" from zarr import create_array import numpy as np a = create_array({}, shape=(10,), dtype=np.dtype('int')) print(a) ``` Or a string representation of a NumPy data type: ```python exec="true" session="data_types" source="above" result="ansi" a = create_array({}, shape=(10,), dtype=' ``` This example illustrates a general problem Zarr Python has to solve: how can we allow users to specify a data type as a string or a NumPy `dtype` object, and produce the right Zarr data type from that input? We call this process "data type resolution." Zarr Python also performs data type resolution when reading stored arrays, although in this case the input is a JSON value instead of a NumPy data type. For simple data types like `int`, the solution could be extremely simple: just maintain a lookup table that maps a NumPy data type to the Zarr data type equivalent. But not all data types are so simple. Consider this case: ```python exec="true" session="data_types" source="above" from zarr import create_array import warnings import numpy as np warnings.simplefilter("ignore", category=FutureWarning) a = create_array({}, shape=(10,), dtype=[('a', 'f8'), ('b', 'i8')]) print(a.dtype) # this is the NumPy data type ``` ```python exec="true" session="data_types" source="above" print(a.metadata.data_type) # this is the Zarr data type ``` In this example, we created a [NumPy structured data type](https://numpy.org/doc/stable/user/basics.rec.html#structured-datatypes). This data type is a container that can hold any NumPy data type, which makes it recursive. It is not possible to make a lookup table that relates all NumPy structured data types to their Zarr equivalents, as there is a nearly unbounded number of different structured data types. So instead of a static lookup table, Zarr Python relies on a dynamic approach to data type resolution. Zarr Python defines a collection of Zarr data types. This collection, called a "data type registry," is essentially a dictionary where the keys are strings (a canonical name for each data type), and the values are the data type classes themselves. Dynamic data type resolution entails iterating over these data type classes, invoking that class' [from_native_dtype][zarr.dtype.ZDType.from_native_dtype] method, and returning a concrete data type instance if and only if exactly one of those constructor invocations is successful. In plain language, we take some user input, like a NumPy data type, offer it to all the known data type classes, and return an instance of the one data type class that can accept that user input. We want to avoid a situation where the same native data type matches multiple Zarr data types; that is, a NumPy data type should *uniquely* specify a single Zarr data type. But data type resolution is dynamic, so it's not possible to statically guarantee this uniqueness constraint. Therefore, we attempt data type resolution against *every* data type class, and if, for some reason, a native data type matches multiple Zarr data types, we treat this as an error and raise an exception. If you have a NumPy data type and you want to get the corresponding `ZDType` instance, you can use the `parse_dtype` function, which will use the dynamic resolution described above. `parse_dtype` handles a range of input types: - NumPy data types: ```python exec="true" session="data_types" source="above" result="ansi" import numpy as np from zarr.dtype import parse_dtype my_dtype = np.dtype('>M8[10s]') print(parse_dtype(my_dtype, zarr_format=2)) ``` - NumPy data type-compatible strings: ```python exec="true" session="data_types" source="above" result="ansi" dtype_str = '>M8[10s]' print(parse_dtype(dtype_str, zarr_format=2)) ``` - `ZDType` instances: ```python exec="true" session="data_types" source="above" result="ansi" from zarr.dtype import DateTime64 zdt = DateTime64(endianness='big', scale_factor=10, unit='s') print(parse_dtype(zdt, zarr_format=2)) # Use a ZDType (this is a no-op) ``` - Python dictionaries (requires `zarr_format=3`). These dictionaries must be consistent with the `JSON` form of the data type: ```python exec="true" session="data_types" source="above" result="ansi" dt_dict = {"name": "numpy.datetime64", "configuration": {"unit": "s", "scale_factor": 10}} print(parse_dtype(dt_dict, zarr_format=3)) ``` ```python exec="true" session="data_types" source="above" result="ansi" print(parse_dtype(dt_dict, zarr_format=3).to_json(zarr_format=3)) ``` zarr-python-3.2.1/docs/user-guide/examples/000077500000000000000000000000001517635743000206445ustar00rootroot00000000000000zarr-python-3.2.1/docs/user-guide/examples/custom_dtype.md000066400000000000000000000001671517635743000237110ustar00rootroot00000000000000--8<-- "examples/custom_dtype/README.md" ## Source Code ```python --8<-- "examples/custom_dtype/custom_dtype.py" ``` zarr-python-3.2.1/docs/user-guide/examples/rectilinear_chunks.ipynb000066400000000000000000000346141517635743000255730ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "da9139cc", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:20.792275Z", "iopub.status.busy": "2026-03-30T13:18:20.792050Z", "iopub.status.idle": "2026-03-30T13:18:20.801655Z", "shell.execute_reply": "2026-03-30T13:18:20.797952Z", "shell.execute_reply.started": "2026-03-30T13:18:20.792253Z" } }, "outputs": [], "source": [ "# /// script\n", "# requires-python = \">=3.12\"\n", "# dependencies = [\n", "# \"dask\",\n", "# \"healpix-geo\",\n", "# \"matplotlib\",\n", "# \"numpy\",\n", "# \"obstore\",\n", "# \"xarray\",\n", "# \"zarr\",\n", "# ]\n", "#\n", "# [tool.uv.sources]\n", "# zarr = { git = \"https://github.com/zarr-developers/zarr-python\", branch = \"main\" }\n", "# xarray = { git = \"https://github.com/maxrjones/xarray\", branch = \"poc/unified-zarr-chunk-grid\" }\n", "# ///" ] }, { "cell_type": "markdown", "id": "71gnhfq4pfe", "metadata": {}, "source": [ "# Rectilinear Chunk Grids\n", "\n", "This notebook demonstrates the unified chunk grid implementation from [#3802](https://github.com/zarr-developers/zarr-python/pull/3802), which adds support for rectilinear (variable) chunk grids.\n", "\n", "Note that it requires installing from a fork of Xarray; this will ideally be incorporated in the codebase and included in a future Xarray release.\n", "\n", "Rectilinear grids allow different chunk sizes along each dimension, which is useful for data that doesn't partition evenly. For example, sparse HEALPix cells grouped by parent tile, boundary-padded HPC arrays, or ingesting existing variable-chunked datasets via VirtualiZarr." ] }, { "cell_type": "code", "execution_count": 2, "id": "9e9nyjdx06f", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:20.802629Z", "iopub.status.busy": "2026-03-30T13:18:20.802471Z", "iopub.status.idle": "2026-03-30T13:18:21.183147Z", "shell.execute_reply": "2026-03-30T13:18:21.182751Z", "shell.execute_reply.started": "2026-03-30T13:18:20.802615Z" } }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import tempfile\n", "from pathlib import Path\n", "import json\n", "\n", "import numpy as np\n", "import xarray as xr\n", "from healpix_geo import nested\n", "from obstore.store import HTTPStore\n", "\n", "import zarr\n", "from zarr.storage import ObjectStore\n", "\n", "zarr.config.set({'async.concurrency': 128}) # Increase concurrency for better performance with obstore\n", "zarr.config.set({\"array.rectilinear_chunks\": True}) # Opt-in to rectilinear chunks\n" ] }, { "cell_type": "markdown", "id": "kj1o9xik9l", "metadata": {}, "source": [ "## 1. Inspect HEALPix dataset\n", "\n", "Load the remote Zarr store to understand the data structure before chunking it." ] }, { "cell_type": "code", "execution_count": 3, "id": "v6cot74r1gq", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:21.183653Z", "iopub.status.busy": "2026-03-30T13:18:21.183505Z", "iopub.status.idle": "2026-03-30T13:18:22.028419Z", "shell.execute_reply": "2026-03-30T13:18:22.027356Z", "shell.execute_reply.started": "2026-03-30T13:18:21.183644Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Members: [('cell_ids', ), ('da', )]\n", "Attrs: {}\n", "Write chunk sizes: ((55611, 55611, 55611, 55609),)\n" ] } ], "source": [ "ob_store = HTTPStore.from_url(\"https://data-taos.ifremer.fr/GRID4EARTH/no_chunk_healpix.zarr\")\n", "store = ObjectStore(ob_store)\n", "g = zarr.open_group(store, mode=\"r\", zarr_format=2, use_consolidated=True)\n", "arr = g['da']\n", "\n", "print(\"Members:\", list(g.members()))\n", "print(\"Attrs:\", dict(g.attrs))\n", "print(\"Write chunk sizes:\", arr.write_chunk_sizes)" ] }, { "cell_type": "markdown", "id": "wmuqi66d46", "metadata": {}, "source": [ "## 2. HEALPix-style variable chunking\n", "\n", "Inspired by [this use case](https://github.com/zarr-developers/zarr-python/pull/3534#issuecomment-3848669859): HEALPix grids where cells are grouped by parent tile at a coarser resolution level, producing variable-sized chunks along the cell dimension when accounting for sparsity." ] }, { "cell_type": "code", "execution_count": 4, "id": "90bc91b9", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:22.029842Z", "iopub.status.busy": "2026-03-30T13:18:22.029258Z", "iopub.status.idle": "2026-03-30T13:18:23.629597Z", "shell.execute_reply": "2026-03-30T13:18:23.628896Z", "shell.execute_reply.started": "2026-03-30T13:18:22.029824Z" } }, "outputs": [], "source": [ "da = xr.open_zarr(\n", " store,\n", " zarr_format=2,\n", " consolidated=True,\n", ")" ] }, { "cell_type": "code", "execution_count": 5, "id": "0d7785b0-d72f-4ef8-8a57-91d61f07be96", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:23.630244Z", "iopub.status.busy": "2026-03-30T13:18:23.629978Z", "iopub.status.idle": "2026-03-30T13:18:23.633850Z", "shell.execute_reply": "2026-03-30T13:18:23.632930Z", "shell.execute_reply.started": "2026-03-30T13:18:23.630232Z" } }, "outputs": [ { "data": { "text/plain": [ "10" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "depth = da.cell_ids.attrs['level']\n", "depth" ] }, { "cell_type": "code", "execution_count": 6, "id": "72c80224-dcac-4724-8caf-5717b29a25d5", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:23.634211Z", "iopub.status.busy": "2026-03-30T13:18:23.634119Z", "iopub.status.idle": "2026-03-30T13:18:23.642291Z", "shell.execute_reply": "2026-03-30T13:18:23.641668Z", "shell.execute_reply.started": "2026-03-30T13:18:23.634203Z" } }, "outputs": [ { "data": { "text/plain": [ "array([ 25, 645, 1510, 2363, 3203, 74, 769, 3963, 4096, 233, 1603,\n", " 2450, 4096, 4096, 3327, 4047, 4096, 4096, 1278, 2113, 4096, 3879,\n", " 4096, 3842, 2173, 983, 4046, 2187, 4095, 1369, 4096, 4096, 4096,\n", " 4096, 3515, 1395, 4096, 3622, 4096, 4096, 3875, 4096, 4096, 4096,\n", " 4096, 4096, 2034, 4096, 358, 3991, 4096, 4096, 4096, 4096, 2714,\n", " 1210, 4096, 4096, 4096, 4096, 92, 3826, 4096, 2629, 4096, 1438,\n", " 4096, 353, 4078, 3410, 2407, 226, 132, 2738, 1223, 23])" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "new_depth = depth-6\n", "parents = nested.zoom_to(da.cell_ids, depth=depth, new_depth=new_depth)\n", "_, chunk_sizes =np.unique(parents, return_counts=True)\n", "chunk_sizes" ] }, { "cell_type": "code", "execution_count": 7, "id": "a79a281b-ca74-49c3-a467-60490a4ad63e", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:23.642721Z", "iopub.status.busy": "2026-03-30T13:18:23.642622Z", "iopub.status.idle": "2026-03-30T13:18:23.649165Z", "shell.execute_reply": "2026-03-30T13:18:23.648723Z", "shell.execute_reply.started": "2026-03-30T13:18:23.642712Z" } }, "outputs": [ { "data": { "text/plain": [ "Frozen({'cell_ids': (25, 645, 1510, 2363, 3203, 74, 769, 3963, 4096, 233, 1603, 2450, 4096, 4096, 3327, 4047, 4096, 4096, 1278, 2113, 4096, 3879, 4096, 3842, 2173, 983, 4046, 2187, 4095, 1369, 4096, 4096, 4096, 4096, 3515, 1395, 4096, 3622, 4096, 4096, 3875, 4096, 4096, 4096, 4096, 4096, 2034, 4096, 358, 3991, 4096, 4096, 4096, 4096, 2714, 1210, 4096, 4096, 4096, 4096, 92, 3826, 4096, 2629, 4096, 1438, 4096, 353, 4078, 3410, 2407, 226, 132, 2738, 1223, 23)})" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "da = da.chunk({\"cell_ids\": tuple(chunk_sizes.tolist())})\n", "da.chunks" ] }, { "cell_type": "markdown", "id": "bsp6y7otkzb", "metadata": {}, "source": [ "## 3. Write as rectilinear Zarr V3\n", "\n", "Write the variable-chunked dataset to a local Zarr V3 store with rectilinear chunk grids enabled." ] }, { "cell_type": "code", "execution_count": 8, "id": "ribguojdr0s", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:23.649823Z", "iopub.status.busy": "2026-03-30T13:18:23.649737Z", "iopub.status.idle": "2026-03-30T13:18:24.089390Z", "shell.execute_reply": "2026-03-30T13:18:24.088640Z", "shell.execute_reply.started": "2026-03-30T13:18:23.649815Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Written to: /var/folders/70/hc_nynms54d8lp67z4rsfctc0000gp/T/tmp6dibcrho/healpix_rectilinear.zarr\n" ] } ], "source": [ "output_path = Path(tempfile.mkdtemp()) / \"healpix_rectilinear.zarr\"\n", "\n", "encoding = {\n", " \"da\": {\"chunks\": [chunk_sizes.tolist()]},\n", " \"cell_ids\": {\"chunks\": [chunk_sizes.tolist()]},\n", "}\n", "\n", "da.to_zarr(output_path, zarr_format=3, mode=\"w\", encoding=encoding, consolidated=False)\n", "\n", "print(f\"Written to: {output_path}\")" ] }, { "cell_type": "markdown", "id": "rbfm1hn63g9", "metadata": {}, "source": [ "## 4. Verify rectilinear metadata\n", "\n", "Inspect the output store to confirm the chunk grid is serialized as `\"rectilinear\"` in `zarr.json`,\n", "following the [rectilinear chunk grid extension spec](https://github.com/zarr-developers/zarr-extensions/tree/main/chunk-grids/rectilinear).\n", "\n", "Key things to look for in `chunk_grid`:\n", "- **`name`**: `\"rectilinear\"` (the extension identifier)\n", "- **`configuration.kind`**: `\"inline\"` (edge lengths stored directly in metadata)\n", "- **`configuration.chunk_shapes`**: one entry per dimension — here a single list for the 1D `cell_ids` axis. Each element is either:\n", " - a **bare integer** for a unique edge length (e.g., `25`, `645`)\n", " - a **`[value, count]` array** using [run-length encoding](https://github.com/zarr-developers/zarr-extensions/tree/main/chunk-grids/rectilinear#run-length-encoding) for consecutive repeated sizes (e.g., `[4096, 4]` means four consecutive chunks of size 4096)" ] }, { "cell_type": "code", "execution_count": 9, "id": "mpdn5hxp7lp", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:24.090312Z", "iopub.status.busy": "2026-03-30T13:18:24.090192Z", "iopub.status.idle": "2026-03-30T13:18:24.093595Z", "shell.execute_reply": "2026-03-30T13:18:24.092908Z", "shell.execute_reply.started": "2026-03-30T13:18:24.090303Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'name': 'rectilinear', 'configuration': {'kind': 'inline', 'chunk_shapes': [[25, 645, 1510, 2363, 3203, 74, 769, 3963, 4096, 233, 1603, 2450, [4096, 2], 3327, 4047, [4096, 2], 1278, 2113, 4096, 3879, 4096, 3842, 2173, 983, 4046, 2187, 4095, 1369, [4096, 4], 3515, 1395, 4096, 3622, [4096, 2], 3875, [4096, 5], 2034, 4096, 358, 3991, [4096, 4], 2714, 1210, [4096, 4], 92, 3826, 4096, 2629, 4096, 1438, 4096, 353, 4078, 3410, 2407, 226, 132, 2738, 1223, 23]]}}\n" ] } ], "source": [ "\n", "# Read the zarr.json for the 'da' array\n", "da_meta_path = output_path / \"da\" / \"zarr.json\"\n", "meta = json.loads(da_meta_path.read_text())\n", "print(meta['chunk_grid'])" ] }, { "cell_type": "markdown", "id": "inz7s8ugu2c", "metadata": {}, "source": [ "## 5. Round-trip verification\n", "\n", "Read the rectilinear store back and confirm the chunk sizes are preserved." ] }, { "cell_type": "code", "execution_count": 10, "id": "308gxly6r3j", "metadata": { "execution": { "iopub.execute_input": "2026-03-30T13:18:24.094252Z", "iopub.status.busy": "2026-03-30T13:18:24.094013Z", "iopub.status.idle": "2026-03-30T13:18:24.117313Z", "shell.execute_reply": "2026-03-30T13:18:24.116670Z", "shell.execute_reply.started": "2026-03-30T13:18:24.094242Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Round-trip chunk sizes: Frozen({'cell_ids': (25, 645, 1510, 2363, 3203, 74, 769, 3963, 4096, 233, 1603, 2450, 4096, 4096, 3327, 4047, 4096, 4096, 1278, 2113, 4096, 3879, 4096, 3842, 2173, 983, 4046, 2187, 4095, 1369, 4096, 4096, 4096, 4096, 3515, 1395, 4096, 3622, 4096, 4096, 3875, 4096, 4096, 4096, 4096, 4096, 2034, 4096, 358, 3991, 4096, 4096, 4096, 4096, 2714, 1210, 4096, 4096, 4096, 4096, 92, 3826, 4096, 2629, 4096, 1438, 4096, 353, 4078, 3410, 2407, 226, 132, 2738, 1223, 23)})\n" ] } ], "source": [ "roundtrip = xr.open_zarr(output_path, zarr_format=3, consolidated=False)\n", "\n", "print(\"Round-trip chunk sizes:\", roundtrip.chunks)" ] }, { "cell_type": "code", "execution_count": null, "id": "e8d42341-c242-44f5-ad6a-491370e3ffab", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" } }, "nbformat": 4, "nbformat_minor": 5 } zarr-python-3.2.1/docs/user-guide/experimental.md000066400000000000000000000217601517635743000220530ustar00rootroot00000000000000# Experimental features This section contains documentation for experimental Zarr Python features. The features described here are exciting and potentially useful, but also volatile -- we might change them at any time. Take this into account if you consider depending on these features. ## `CacheStore` Zarr Python 3.1.4 adds [`zarr.experimental.cache_store.CacheStore`][] provides a dual-store caching implementation that can be wrapped around any Zarr store to improve performance for repeated data access. This is particularly useful when working with remote stores (e.g., S3, HTTP) where network latency can significantly impact data access speed. The CacheStore implements a cache that uses a separate Store instance as the cache backend, providing persistent caching capabilities with time-based expiration, size-based eviction, and flexible cache storage options. It automatically evicts the least recently used items when the cache reaches its maximum size. Because the `CacheStore` uses an ordinary Zarr `Store` object as the caching layer, you can reuse the data stored in the cache later. > **Note:** The CacheStore is a wrapper store that maintains compatibility with the full > `zarr.abc.store.Store` API while adding transparent caching functionality. ## Basic Usage Creating a CacheStore requires both a source store and a cache store. The cache store can be any Store implementation, providing flexibility in cache persistence: ```python exec="true" session="experimental" source="above" import zarr from zarr.storage import LocalStore import numpy as np from tempfile import mkdtemp from zarr.experimental.cache_store import CacheStore # Create a local store and a separate cache store local_store_path = mkdtemp(suffix='.zarr') source_store = LocalStore(local_store_path) cache_store = zarr.storage.MemoryStore() # In-memory cache cached_store = CacheStore( store=source_store, cache_store=cache_store, max_size=256*1024*1024 # 256MB cache ) # Create an array using the cached store zarr_array = zarr.zeros((100, 100), chunks=(10, 10), dtype='f8', store=cached_store, mode='w') # Write some data to force chunk creation zarr_array[:] = np.random.random((100, 100)) ``` The dual-store architecture allows you to use different store types for source and cache, such as a remote store for source data and a local store for persistent caching. ## Performance Benefits The CacheStore provides significant performance improvements for repeated data access: ```python exec="true" session="experimental" source="above" import time # Benchmark reading with cache start = time.time() for _ in range(100): _ = zarr_array[:] elapsed_cache = time.time() - start # Compare with direct store access (without cache) zarr_array_nocache = zarr.open(local_store_path, mode='r') start = time.time() for _ in range(100): _ = zarr_array_nocache[:] elapsed_nocache = time.time() - start # Cache provides speedup for repeated access speedup = elapsed_nocache / elapsed_cache print(f"Speedup is {speedup}") ``` Cache effectiveness is particularly pronounced with repeated access to the same data chunks. ## Cache Configuration The CacheStore can be configured with several parameters: **max_size**: Controls the maximum size of cached data in bytes ```python exec="true" session="experimental" source="above" # 256MB cache with size limit cache = CacheStore( store=source_store, cache_store=cache_store, max_size=256*1024*1024 ) # Unlimited cache size (use with caution) cache = CacheStore( store=source_store, cache_store=cache_store, max_size=None ) ``` **max_age_seconds**: Controls time-based cache expiration ```python exec="true" session="experimental" source="above" # Cache expires after 1 hour cache = CacheStore( store=source_store, cache_store=cache_store, max_age_seconds=3600 ) # Cache never expires cache = CacheStore( store=source_store, cache_store=cache_store, max_age_seconds="infinity" ) ``` **cache_set_data**: Controls whether written data is cached ```python exec="true" session="experimental" source="above" # Cache data when writing (default) cache = CacheStore( store=source_store, cache_store=cache_store, cache_set_data=True ) # Don't cache written data (read-only cache) cache = CacheStore( store=source_store, cache_store=cache_store, cache_set_data=False ) ``` ## Cache Statistics The CacheStore provides statistics to monitor cache performance and state: ```python exec="true" session="experimental" source="above" # Access some data to generate cache activity data = zarr_array[0:50, 0:50] # First access - cache miss data = zarr_array[0:50, 0:50] # Second access - cache hit # Get comprehensive cache information info = cached_store.cache_info() print(info['cache_store_type']) # e.g., 'MemoryStore' print(info['max_age_seconds']) print(info['max_size']) print(info['current_size']) print(info['tracked_keys']) print(info['cached_keys']) print(info['cache_set_data']) ``` The `cache_info()` method returns a dictionary with detailed information about the cache state. ## Cache Management The CacheStore provides methods for manual cache management: ```python exec="true" session="experimental" source="above" # Clear all cached data and tracking information import asyncio asyncio.run(cached_store.clear_cache()) # Check cache info after clearing info = cached_store.cache_info() assert info['tracked_keys'] == 0 assert info['current_size'] == 0 ``` The `clear_cache()` method is an async method that clears both the cache store (if it supports the `clear` method) and all internal tracking data. ## Best Practices 1. **Choose appropriate cache store**: Use MemoryStore for fast temporary caching or LocalStore for persistent caching 2. **Size the cache appropriately**: Set `max_size` based on available storage and expected data access patterns 3. **Use with remote stores**: The cache provides the most benefit when wrapping slow remote stores 4. **Monitor cache statistics**: Use `cache_info()` to tune cache size and access patterns 5. **Consider data locality**: Group related data accesses together to improve cache efficiency 6. **Set appropriate expiration**: Use `max_age_seconds` for time-sensitive data or "infinity" for static data ## Working with Different Store Types The CacheStore can wrap any store that implements the `zarr.abc.store.Store` interface and use any store type for the cache backend: ### Local Store with Memory Cache ```python exec="true" session="experimental-memory-cache" source="above" from zarr.storage import LocalStore, MemoryStore from zarr.experimental.cache_store import CacheStore from tempfile import mkdtemp local_store_path = mkdtemp(suffix='.zarr') source_store = LocalStore(local_store_path) cache_store = MemoryStore() cached_store = CacheStore( store=source_store, cache_store=cache_store, max_size=128*1024*1024 ) ``` ### Memory Store with Persistent Cache ```python exec="true" session="experimental-local-cache" source="above" from tempfile import mkdtemp from zarr.storage import MemoryStore, LocalStore from zarr.experimental.cache_store import CacheStore memory_store = MemoryStore() local_store_path = mkdtemp(suffix='.zarr') persistent_cache = LocalStore(local_store_path) cached_store = CacheStore( store=memory_store, cache_store=persistent_cache, max_size=256*1024*1024 ) ``` The dual-store architecture provides flexibility in choosing the best combination of source and cache stores for your specific use case. ## Examples from Real Usage Here's a complete example demonstrating cache effectiveness: ```python exec="true" session="experimental-final" source="above" import numpy as np import time from tempfile import mkdtemp import zarr import zarr.storage from zarr.experimental.cache_store import CacheStore # Create test data with dual-store cache local_store_path = mkdtemp(suffix='.zarr') source_store = zarr.storage.LocalStore(local_store_path) cache_store = zarr.storage.MemoryStore() cached_store = CacheStore( store=source_store, cache_store=cache_store, max_size=256*1024*1024 ) zarr_array = zarr.zeros((100, 100), chunks=(10, 10), dtype='f8', store=cached_store, mode='w') zarr_array[:] = np.random.random((100, 100)) # Demonstrate cache effectiveness with repeated access start = time.time() data = zarr_array[20:30, 20:30] # First access (cache miss) first_access = time.time() - start print(f"First access took {first_access}") start = time.time() data = zarr_array[20:30, 20:30] # Second access (cache hit) second_access = time.time() - start print(f"Second access took {second_access}") # Check cache statistics info = cached_store.cache_info() assert info['cached_keys'] > 0 # Should have cached keys assert info['current_size'] > 0 # Should have cached data print(f"Cache contains {info['cached_keys']} keys with {info['current_size']} bytes") ``` This example shows how the CacheStore can significantly reduce access times for repeated data reads, particularly important when working with remote data sources. The dual-store architecture allows for flexible cache persistence and management. zarr-python-3.2.1/docs/user-guide/extending.md000066400000000000000000000103621517635743000213370ustar00rootroot00000000000000# Extending Zarr Zarr-Python 3 was designed to be extensible. This means that you can extend the library by writing custom classes and plugins. Currently, Zarr can be extended in the following ways: ## Custom codecs !!! note This section explains how custom codecs can be created for Zarr format 3 arrays. For Zarr format 2, codecs should subclass the [numcodecs.abc.Codec](https://numcodecs.readthedocs.io/en/stable/abc.html#numcodecs.abc.Codec) base class and register through [numcodecs.registry.register_codec](https://numcodecs.readthedocs.io/en/stable/registry.html#numcodecs.registry.register_codec). There are three types of codecs in Zarr: - array-to-array - array-to-bytes - bytes-to-bytes Array-to-array codecs are used to transform the array data before serializing to bytes. Examples include delta encoding or scaling codecs. Array-to-bytes codecs are used for serializing the array data to bytes. In Zarr, the main codec to use for numeric arrays is the [`zarr.codecs.BytesCodec`][]. Bytes-to-bytes codecs transform the serialized bytestreams of the array data. Examples include compression codecs, such as [`zarr.codecs.GzipCodec`][], [`zarr.codecs.BloscCodec`][] or [`zarr.codecs.ZstdCodec`][], and codecs that add a checksum to the bytestream, such as [`zarr.codecs.Crc32cCodec`][]. Custom codecs for Zarr are implemented by subclassing the relevant base class, see [`zarr.abc.codec.ArrayArrayCodec`][], [`zarr.abc.codec.ArrayBytesCodec`][] and [`zarr.abc.codec.BytesBytesCodec`][]. Most custom codecs should implement the `_encode_single` and `_decode_single` methods. These methods operate on single chunks of the array data. Alternatively, custom codecs can implement the `encode` and `decode` methods, which operate on batches of chunks, in case the codec is intended to implement its own batch processing. Custom codecs should also implement the following methods: - `compute_encoded_size`, which returns the byte size of the encoded data given the byte size of the original data. It should raise `NotImplementedError` for codecs with variable-sized outputs, such as compression codecs. - `validate` (optional), which can be used to check that the codec metadata is compatible with the array metadata. It should raise errors if not. - `resolve_metadata` (optional), which is important for codecs that change the shape, dtype or fill value of a chunk. - `evolve_from_array_spec` (optional), which can be useful for automatically filling in codec configuration metadata from the array metadata. To use custom codecs in Zarr, they need to be registered using the [entrypoint mechanism](https://packaging.python.org/en/latest/specifications/entry-points/). Commonly, entrypoints are declared in the `pyproject.toml` of your package under the `[project.entry-points."zarr.codecs"]` section. Zarr will automatically discover and load all codecs registered with the entrypoint mechanism from imported modules. ```toml [project.entry-points."zarr.codecs"] "custompackage.fancy_codec" = "custompackage:FancyCodec" ``` New codecs need to have their own unique identifier. To avoid naming collisions, it is strongly recommended to prefix the codec identifier with a unique name. For example, the codecs from `numcodecs` are prefixed with `numcodecs.`, e.g. `numcodecs.delta`. !!! note Note that the extension mechanism for the Zarr format 3 is still under development. Requirements for custom codecs including the choice of codec identifiers might change in the future. It is also possible to register codecs as replacements for existing codecs. This might be useful for providing specialized implementations, such as GPU-based codecs. In case of multiple codecs, the [`zarr.config`][] mechanism can be used to select the preferred implementation. ## Custom stores Coming soon. ## Custom array buffers Zarr-python provides control over where and how arrays stored in memory through [`zarr.abc.buffer.Buffer`][]. Currently both CPU (the default) and GPU implementations are provided (see [Using GPUs with Zarr](gpu.md) for more information). You can implement your own buffer classes by implementing the interface defined in [`zarr.abc.buffer.BufferPrototype`][]. ## Other extensions In the future, Zarr will support writing custom custom data types and chunk grids. zarr-python-3.2.1/docs/user-guide/glossary.md000066400000000000000000000123621517635743000212170ustar00rootroot00000000000000# Glossary This page defines key terms used throughout the zarr-python documentation and API. ## Array Structure ### Array An N-dimensional typed array stored in a Zarr [store](#store). An array's [metadata](#metadata) defines its shape, data type, chunk layout, and codecs. ### Chunk The fundamental unit of data in a Zarr array. An array is divided into chunks along each dimension according to the [chunk grid](#chunk-grid), which is currently part of Zarr's private API. Each chunk is independently compressed and encoded through the array's [codec](#codec) pipeline. When [sharding](#shard) is used, "chunk" refers to the inner chunks within each shard, because those are the compressible units. The chunks are the smallest units that can be read independently. !!! warning "Convention specific to zarr-python" The use of "chunk" to mean the inner sub-chunk within a shard is a convention adopted by zarr-python's `Array` API. In the Zarr V3 specification and in other Zarr implementations, "chunk" may refer to the top-level grid cells (which zarr-python calls "shards" when the sharding codec is used). Be aware of this distinction when working across libraries. **API**: [`Array.chunks`][zarr.Array.chunks] returns the chunk shape. When sharding is used, this is the inner chunk shape. ### Chunk Grid The partitioning of an array's elements into [chunks](#chunk). In Zarr V3, the chunk grid is defined in the array [metadata](#metadata) and determines the boundaries of each storage object. Zarr V3 supports two chunk grid types: - **Regular**: All chunks have the same shape (the last chunk along each dimension may be smaller than the declared size). - **Rectilinear** *(experimental)*: Each dimension can have different chunk sizes, specified as a list of edge lengths per dimension. Enable with `zarr.config.set({'array.rectilinear_chunks': True})`. When sharding is used, the chunk grid defines the [shard](#shard) boundaries, not the inner chunk boundaries. The inner chunk shape is defined within the [sharding codec](#shard). **API**: The `chunk_grid` field in array metadata contains the storage-level grid. [`Array.chunks`][zarr.Array.chunks] returns the chunk shape for regular grids. For all grid types, `Array.read_chunk_sizes` and `Array.write_chunk_sizes` return the per-dimension chunk sizes in dask-style `tuple[tuple[int, ...], ...]` format. ### Shard A storage object that contains one or more [chunks](#chunk). Sharding reduces the number of objects in a [store](#store) by grouping chunks together, which improves performance on file systems and object storage. Within each shard, chunks are compressed independently and can be read individually. However, writing requires updating the full shard for consistency, making shards the unit of writing and chunks the unit of reading. Sharding is implemented as a [codec](#codec) (the sharding indexed codec). When sharding is used: - The [chunk grid](#chunk-grid) in metadata defines the shard boundaries - The sharding codec's `chunk_shape` defines the inner chunk size - Each shard contains `shard_shape / chunk_shape` chunks per dimension **API**: [`Array.shards`][zarr.Array.shards] returns the shard shape, or `None` if sharding is not used. [`Array.chunks`][zarr.Array.chunks] returns the inner chunk shape. ## Storage ### Store A key-value storage backend that holds Zarr data and metadata. Stores implement the [`zarr.abc.store.Store`][] interface. Examples include local file systems, cloud object storage (S3, GCS, Azure), zip files, and in-memory dictionaries. Each [chunk](#chunk) or [shard](#shard) is stored as a single value (object or file) in the store, addressed by a key derived from its grid coordinates. ### Metadata The JSON document (`zarr.json`) that describes an [array](#array) or group. For arrays, metadata includes the shape, data type, [chunk grid](#chunk-grid), fill value, and [codec](#codec) pipeline. Metadata is stored alongside the data in the [store](#store). Zarr-Python does not yet expose its internal metadata representation as part of its public API. ## Codecs ### Codec A transformation applied to array data during reading and writing. Codecs are chained into a pipeline and come in three types: - **Array-to-array**: Transforms like transpose that rearrange array elements - **Array-to-bytes**: Serialization that converts an array to a byte sequence (exactly one required) - **Bytes-to-bytes**: Compression or checksums applied to the serialized bytes The [sharding indexed codec](#shard) is a special array-to-bytes codec that groups multiple [chunks](#chunk) into a single storage object. ## API Properties The following properties are available on [`zarr.Array`][]: | Property | Description | |----------|-------------| | `.chunks` | Chunk shape — the inner chunk shape when sharding is used. Raises for rectilinear grids | | `.shards` | Shard shape, or `None` if no sharding | | `.read_chunk_sizes` | Per-dimension chunk data sizes (`tuple[tuple[int, ...], ...]`). Works for all grid types | | `.write_chunk_sizes` | Per-dimension storage chunk sizes (`tuple[tuple[int, ...], ...]`). Works for all grid types | | `.nchunks` | Total number of independently compressible units across the array | | `.cdata_shape` | Number of independently compressible units per dimension | zarr-python-3.2.1/docs/user-guide/gpu.md000066400000000000000000000017161517635743000201500ustar00rootroot00000000000000# Using GPUs with Zarr Zarr can use GPUs to accelerate your workload by running `zarr.Config.enable_gpu`. !!! note `zarr-python` currently supports reading the ndarray data into device (GPU) memory as the final stage of the codec pipeline. Data will still be read into or copied to host (CPU) memory for encoding and decoding. In the future, codecs will be available compressing and decompressing data on the GPU, avoiding the need to move data between the host and device for compression and decompression. ## Reading data into device memory [`zarr.config`][] configures Zarr to use GPU memory for the data buffers used internally by Zarr via `enable_gpu()`. ```python import zarr import cupy as cp zarr.config.enable_gpu() z = zarr.create_array( store="memory://gpu-demo", shape=(100, 100), chunks=(10, 10), dtype="float32", ) type(z[:10, :10]) # cupy.ndarray ``` Note that the output type is a `cupy.ndarray` rather than a NumPy array. zarr-python-3.2.1/docs/user-guide/groups.md000066400000000000000000000107371517635743000206770ustar00rootroot00000000000000# Working with groups Zarr supports hierarchical organization of arrays via groups. As with arrays, groups can be stored in memory, on disk, or via other storage systems that support a similar interface. To create a group, use the [`zarr.group`][] function: ```python exec="true" session="groups" source="above" result="ansi" import zarr root = zarr.create_group(store="memory://groups-demo") print(root) ``` Groups have a similar API to the Group class from [h5py](https://www.h5py.org/). For example, groups can contain other groups: ```python exec="true" session="groups" source="above" foo = root.create_group('foo') bar = foo.create_group('bar') ``` Groups can also contain arrays, e.g.: ```python exec="true" session="groups" source="above" result="ansi" z1 = bar.create_array(name='baz', shape=(10000, 10000), chunks=(1000, 1000), dtype='int32') print(z1) ``` Members of a group can be accessed via the suffix notation, e.g.: ```python exec="true" session="groups" source="above" result="ansi" print(root['foo']) ``` The '/' character can be used to access multiple levels of the hierarchy in one call, e.g.: ```python exec="true" session="groups" source="above" result="ansi" print(root['foo/bar']) ``` ```python exec="true" session="groups" source="above" result="ansi" print(root['foo/bar/baz']) ``` The [`zarr.Group.tree`][] method can be used to print a tree representation of the hierarchy, e.g.: ```python exec="true" session="groups" source="above" result="ansi" print(root.tree()) ``` The [`zarr.open_group`][] function provides a convenient way to create or re-open a group stored in a directory on the file-system, with sub-groups stored in sub-directories, e.g.: ```python exec="true" session="groups" source="above" result="ansi" root = zarr.open_group('data/group.zarr', mode='w') print(root) ``` ```python exec="true" session="groups" source="above" result="ansi" z = root.create_array(name='foo/bar/baz', shape=(10000, 10000), chunks=(1000, 1000), dtype='int32') print(z) ``` For more information on groups see the [`zarr.Group` API docs](../api/zarr/group.md). ## Batch Group Creation You can also create multiple groups concurrently with a single function call. [`zarr.create_hierarchy`][] takes a [`zarr Storage instance`](../api/zarr/storage.md) instance and a dict of `key : metadata` pairs, parses that dict, and writes metadata documents to storage: ```python exec="true" session="groups" source="above" result="ansi" from zarr import create_hierarchy from zarr.core.group import GroupMetadata from zarr.storage import LocalStore from pprint import pprint import io node_spec = {'a/b/c': GroupMetadata()} nodes_created = dict(create_hierarchy(store=LocalStore(root='data'), nodes=node_spec)) # Report nodes (pprint is used for cleaner rendering in the docs) output = io.StringIO() pprint(nodes_created, stream=output, width=60) print(output.getvalue()) ``` Note that we only specified a single group named `a/b/c`, but 4 groups were created. These additional groups were created to ensure that the desired node `a/b/c` is connected to the root group `''` by a sequence of intermediate groups. [`zarr.create_hierarchy`][] normalizes the `nodes` keyword argument to ensure that the resulting hierarchy is complete, i.e. all groups or arrays are connected to the root of the hierarchy via intermediate groups. Because [`zarr.create_hierarchy`][] concurrently creates metadata documents, it's more efficient than repeated calls to [`create_group`][zarr.create_group] or [`create_array`][zarr.create_array], provided you can statically define the metadata for the groups and arrays you want to create. ## Array and group diagnostics Diagnostic information about arrays and groups is available via the `info` property. E.g.: ```python exec="true" session="groups" source="above" result="ansi" root = zarr.group(store="memory://diagnostics-demo") foo = root.create_group('foo') bar = foo.create_array(name='bar', shape=1000000, chunks=100000, dtype='int64') bar[:] = 42 baz = foo.create_array(name='baz', shape=(1000, 1000), chunks=(100, 100), dtype='float32') baz[:] = 4.2 print(root.info) ``` ```python exec="true" session="groups" source="above" result="ansi" print(foo.info) ``` ```python exec="true" session="groups" source="above" result="ansi" print(bar.info_complete()) ``` ```python exec="true" session="groups" source="above" result="ansi" print(baz.info) ``` Groups also have the [`zarr.Group.tree`][] method, e.g.: ```python exec="true" session="groups" source="above" result="ansi" print(root.tree()) ``` zarr-python-3.2.1/docs/user-guide/index.md000066400000000000000000000030621517635743000204600ustar00rootroot00000000000000# User Guide Welcome to the user guide, where you can learn more about using Zarr-Python! ## Getting Started New to Zarr-Python? Start here: - **[Installation](installation.md)** - Install Zarr-Python - **[Quick-start](../quick-start.md)** - Quick overview of core functionality ## Core Concepts Learn the essential building blocks: - **[Arrays](arrays.md)** - Learn the fundamentals of working with arrays - **[Groups](groups.md)** - Organize your data with groups - **[Attributes](attributes.md)** - Configure metadata to your data structures - **[Storage](storage.md)** - Learn how data is stored and accessed ## Configuration & Setup Customize your experience: - **[Runtime Configuration](config.md)** - Configure Zarr-Python for your needs - **[V3 Migration](v3_migration.md)** - Upgrading from version 2 to version 3 ## Advanced Topics Take your skills to the next level: - **[Data Types](data_types.md)** - Learn about supported and extensible data types - **[Performance](performance.md)** - Optimize for speed and efficiency - **[GPU](gpu.md)** - Leverage GPU acceleration - **[Extending](extending.md)** - Extend functionality with custom code - **[Consolidated Metadata](consolidated_metadata.md)** - Advanced metadata management ## Reference - **[Glossary](glossary.md)** - Definitions of key terms (chunks, shards, codecs, etc.) ## Need Help? - Browse the [API Reference](../api/zarr/index.md) for detailed function documentation - Report issues on [GitHub](https://github.com/zarr-developers/zarr-python/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen) zarr-python-3.2.1/docs/user-guide/installation.md000066400000000000000000000040631517635743000220540ustar00rootroot00000000000000# Installation ## Required dependencies Required dependencies include: - [Python](https://docs.python.org/3/) (3.12 or later) - [packaging](https://packaging.pypa.io) (22.0 or later) - [numpy](https://numpy.org) (2.0 or later) - [numcodecs](https://numcodecs.readthedocs.io) (0.14 or later) - [google-crc32c](https://github.com/googleapis/python-crc32c) (1.5 or later) - [typing_extensions](https://typing-extensions.readthedocs.io) (4.9 or later) - [donfig](https://donfig.readthedocs.io) (0.8 or later) ## pip Zarr is available on [PyPI](https://pypi.org/project/zarr/). Install it using `pip`: ```console pip install zarr ``` There are a number of optional dependency groups you can install for extra functionality. These can be installed using `pip install "zarr[]"`, e.g. `pip install "zarr[gpu]"` - `gpu`: support for GPUs - `remote`: support for reading/writing to remote data stores Additional optional dependencies include `universal_pathlib`. These must be installed separately. ## conda Zarr is also published to [conda-forge](https://conda-forge.org). Install it using `conda`: ```console conda install -c conda-forge zarr ``` Conda does not support optional dependencies, so you will have to manually install any packages needed to enable extra functionality. # Nightly wheels Development wheels are built nightly and published to the [scientific-python-nightly-wheels](https://anaconda.org/scientific-python-nightly-wheels) index. To install the latest nightly build: ```console pip install --pre --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple zarr ``` Note that nightly wheels may be unstable and are intended for testing purposes. ## Dependency support Zarr has endorsed [Scientific-Python SPEC 0](https://scientific-python.org/specs/spec-0000/) and now follows the version support window as outlined below: - Python: 36 months after initial release - Core package dependencies (e.g. NumPy): 24 months after initial release ## Development To install the latest development version of Zarr, see the contributing guide. zarr-python-3.2.1/docs/user-guide/performance.md000066400000000000000000000331661517635743000216620ustar00rootroot00000000000000# Optimizing performance ## Chunk optimizations ### Chunk size and shape In general, chunks of at least 1 megabyte (1M) uncompressed size seem to provide better performance, at least when using the Blosc compression library. The optimal chunk shape will depend on how you want to access the data. E.g., for a 2-dimensional array, if you only ever take slices along the first dimension, then chunk across the second dimension. If you know you want to chunk across an entire dimension you can use the full size of that dimension within the `chunks` argument, e.g.: ```python exec="true" session="performance" source="above" result="ansi" import zarr z1 = zarr.create_array(store={}, shape=(10000, 10000), chunks=(100, 10000), dtype='int32') print(z1.chunks) ``` Alternatively, if you only ever take slices along the second dimension, then chunk across the first dimension, e.g.: ```python exec="true" session="performance" source="above" result="ansi" z2 = zarr.create_array(store={}, shape=(10000, 10000), chunks=(10000, 100), dtype='int32') print(z2.chunks) ``` If you require reasonable performance for both access patterns then you need to find a compromise, e.g.: ```python exec="true" session="performance" source="above" result="ansi" z3 = zarr.create_array(store={}, shape=(10000, 10000), chunks=(1000, 1000), dtype='int32') print(z3.chunks) ``` If you are feeling lazy, you can let Zarr guess a chunk shape for your data by providing `chunks='auto'`, although please note that the algorithm for guessing a chunk shape is based on simple heuristics and may be far from optimal. E.g.: ```python exec="true" session="performance" source="above" result="ansi" z4 = zarr.create_array(store={}, shape=(10000, 10000), chunks='auto', dtype='int32') print(z4.chunks) ``` If you know you are always going to be loading the entire array into memory, you can turn off chunks by providing `chunks` equal to `shape`, in which case there will be one single chunk for the array: ```python exec="true" session="performance" source="above" result="ansi" z5 = zarr.create_array(store={}, shape=(10000, 10000), chunks=(10000, 10000), dtype='int32') print(z5.chunks) ``` ### Sharding If you have large arrays but need small chunks to efficiently access the data, you can use sharding. Sharding provides a mechanism to store multiple chunks in a single storage object or file. This can be useful because traditional file systems and object storage systems may have performance issues storing and accessing many files. Additionally, small files can be inefficient to store if they are smaller than the block size of the file system. Picking a good combination of chunk shape and shard shape is important for performance. The chunk shape determines what unit of your data can be read independently, while the shard shape determines what unit of your data can be written efficiently. For an example, consider you have a 100 GB array and need to read small chunks of 1 MB. Without sharding, each chunk would be one file resulting in 100,000 files. That can already cause performance issues on some file systems. With sharding, you could use a shard size of 1 GB. This would result in 1000 chunks per file and 100 files in total, which seems manageable for most storage systems. You would still be able to read each 1 MB chunk independently, but you would need to write your data in 1 GB increments. To use sharding, you need to specify the `shards` parameter when creating the array. ```python exec="true" session="performance" source="above" result="ansi" z6 = zarr.create_array(store={}, shape=(10000, 10000, 1000), shards=(1000, 1000, 1000), chunks=(100, 100, 100), dtype='uint8') print(z6.info) ``` `shards` can be `"auto"` as well, in which case the `array.target_shard_size_bytes` setting can be used to control the size of shards (i.e., the size of the chunks cumulatively and uncompressed within the shard will be as close to, without being bigger than, `array.target_shard_size_bytes`); otherwise, a default is used. ### Chunk memory layout The order of bytes **within each chunk** of an array can be changed via the `order` config option, to use either C or Fortran layout. For multi-dimensional arrays, these two layouts may provide different compression ratios, depending on the correlation structure within the data. E.g.: ```python exec="true" session="performance" source="above" result="ansi" import numpy as np a = np.arange(100000000, dtype='int32').reshape(10000, 10000).T c = zarr.create_array(store={}, shape=a.shape, chunks=(1000, 1000), dtype=a.dtype, config={'order': 'C'}) c[:] = a print(c.info_complete()) ``` ```python exec="true" session="performance" source="above" result="ansi" with zarr.config.set({'array.order': 'F'}): f = zarr.create_array(store={}, shape=a.shape, chunks=(1000, 1000), dtype=a.dtype) f[:] = a print(f.info_complete()) ``` In the above example, Fortran order gives a better compression ratio. This is an artificial example but illustrates the general point that changing the order of bytes within chunks of an array may improve the compression ratio, depending on the structure of the data, the compression algorithm used, and which compression filters (e.g., byte-shuffle) have been applied. ### Empty chunks It is possible to configure how Zarr handles the storage of chunks that are "empty" (i.e., every element in the chunk is equal to the array's fill value). When creating an array with `write_empty_chunks=False`, Zarr will check whether a chunk is empty before compression and storage. If a chunk is empty, then Zarr does not store it, and instead deletes the chunk from storage if the chunk had been previously stored. This optimization prevents storing redundant objects and can speed up reads, but the cost is added computation during array writes, since the contents of each chunk must be compared to the fill value, and these advantages are contingent on the content of the array. If you know that your data will form chunks that are almost always non-empty, then there is no advantage to the optimization described above. In this case, creating an array with `write_empty_chunks=True` will instruct Zarr to write every chunk without checking for emptiness. The default value of `write_empty_chunks` is `False`: ```python exec="true" session="performance" source="above" result="ansi" arr = zarr.create_array(store={}, shape=(1,), dtype='uint8') assert arr.config.write_empty_chunks == False ``` The following example illustrates the effect of the `write_empty_chunks` flag on the time required to write an array with different values.: ```python exec="true" session="performance" source="above" result="ansi" import zarr import numpy as np import time def timed_write(write_empty_chunks): """ Measure the time required and number of objects created when writing to a Zarr array with random ints or fill value. """ chunks = (8192,) shape = (chunks[0] * 1024,) data = np.random.randint(0, 255, shape) dtype = 'uint8' arr = zarr.create_array( f'data/example-{write_empty_chunks}.zarr', shape=shape, chunks=chunks, dtype=dtype, fill_value=0, config={'write_empty_chunks': write_empty_chunks} ) # initialize all chunks arr[:] = 100 result = [] for value in (data, arr.fill_value): start = time.time() arr[:] = value elapsed = time.time() - start result.append((elapsed, arr.nchunks_initialized)) return result # log results for write_empty_chunks in (True, False): full, empty = timed_write(write_empty_chunks) print(f'\nwrite_empty_chunks={write_empty_chunks}:\n\tRandom Data: {full[0]:.4f}s, {full[1]} objects stored\n\t Empty Data: {empty[0]:.4f}s, {empty[1]} objects stored\n') ``` In this example, writing random data is slightly slower with `write_empty_chunks=True`, but writing empty data is substantially faster and generates far fewer objects in storage. ### Changing chunk shapes (rechunking) Coming soon. ## Parallel computing and synchronization Zarr is designed to support parallel computing and enables concurrent reads and writes to arrays. This section covers how to optimize Zarr's concurrency settings for different parallel computing scenarios. ### Concurrent I/O operations Zarr uses asynchronous I/O internally to enable concurrent reads and writes across multiple chunks. The level of concurrency is controlled by the `async.concurrency` configuration setting, which determines the maximum number of concurrent I/O operations. The default value is 10, which is a conservative value. You may get improved performance by tuning the concurrency limit. You can adjust this value based on your specific needs: ```python import zarr # Set concurrency for the current session zarr.config.set({'async.concurrency': 128}) # Or use environment variable # export ZARR_ASYNC_CONCURRENCY=128 ``` Higher concurrency values can improve throughput when: - Working with remote storage (e.g., S3, GCS) where network latency is high - Reading/writing many small chunks in parallel - The storage backend can handle many concurrent requests Lower concurrency values may be beneficial when: - Working with local storage with limited I/O bandwidth - Memory is constrained (each concurrent operation requires buffer space) - Using Zarr within a parallel computing framework (see below) ### Thread pool size (`threading.max_workers`) When synchronous Zarr code calls async operations internally, Zarr uses a `ThreadPoolExecutor` to run those coroutines. The `threading.max_workers` configuration option controls the maximum number of worker threads in that pool. By default it is `None`, which lets Python choose the pool size (typically `min(32, os.cpu_count() + 4)`). You can set it explicitly when you want more predictable resource usage: ```python import zarr zarr.config.set({'threading.max_workers': 8}) ``` Reducing this value can help avoid overloading the event loop when Zarr is used inside a parallel computing framework such as Dask that already manages its own thread pool (see the Dask section below). Increasing it may improve throughput in CPU-bound workloads where many synchronous-to-async dispatches happen concurrently. ### Using Zarr with Dask [Dask](https://www.dask.org/) is a popular parallel computing library that works well with Zarr for processing large arrays. When using Zarr with Dask, it's important to consider the interaction between Dask's thread pool and Zarr's concurrency settings. **Important**: When using many Dask threads, you may need to reduce both Zarr's `async.concurrency` and `threading.max_workers` settings to avoid creating too many concurrent operations. The total number of concurrent I/O operations can be roughly estimated as: ``` total_concurrency ≈ dask_threads × zarr_async_concurrency ``` For example, if you're running Dask with 10 threads and Zarr's default concurrency of 64, you could potentially have up to 640 concurrent operations, which may overwhelm your storage system or cause memory issues. **Recommendation**: When using Dask with many threads, configure Zarr's concurrency settings: ```python import zarr import dask.array as da # If using Dask with many threads (e.g., 8-16), reduce Zarr's concurrency settings zarr.config.set({ 'async.concurrency': 4, # Limit concurrent async operations 'threading.max_workers': 4, # Limit Zarr's internal thread pool }) # Open Zarr array z = zarr.open_array('data/large_array.zarr', mode='r') # Create Dask array from Zarr array arr = da.from_array(z, chunks=z.chunks) # Process with Dask result = arr.mean(axis=0).compute() ``` **Configuration guidelines for Dask workloads**: - `async.concurrency`: Controls the maximum number of concurrent async I/O operations. Start with a lower value (e.g., 4-8) when using many Dask threads. - `threading.max_workers`: Controls Zarr's internal thread pool size for blocking operations (defaults to CPU count). Reduce this to avoid thread contention with Dask's scheduler. You may need to experiment with different values to find the optimal balance for your workload. Monitor your system's resource usage and adjust these settings based on whether your storage system or CPU is the bottleneck. ### Thread safety and process safety Zarr arrays are designed to be thread-safe for concurrent reads and writes from multiple threads within the same process. However, proper synchronization is required when writing to overlapping regions from multiple threads. For multi-process parallelism, Zarr provides safe concurrent writes as long as: - Different processes write to different chunks - The storage backend supports atomic writes (most do) When writing to the same chunks from multiple processes, you should use external synchronization mechanisms or ensure that writes are coordinated to avoid race conditions. ## Pickle support Zarr arrays and groups can be pickled, as long as the underlying store object can be pickled. With the exception of the `zarr.storage.MemoryStore`, any of the storage classes provided in the `zarr.storage` module can be pickled. If an array or group is backed by a persistent store such as the a `zarr.storage.LocalStore`, `zarr.storage.ZipStore` or `zarr.storage.FsspecStore` then the store data **are not** pickled. The only thing that is pickled is the necessary parameters to allow the store to re-open any underlying files or databases upon being unpickled. E.g., pickle/unpickle a local store array: ```python exec="true" session="performance" source="above" result="ansi" import pickle data = np.arange(100000) z1 = zarr.create_array(store='data/perf-example-2.zarr', shape=data.shape, chunks=data.shape, dtype=data.dtype) z1[:] = data s = pickle.dumps(z1) z2 = pickle.loads(s) assert z1 == z2 print(np.all(z1[:] == z2[:])) ``` ## Configuring Blosc Coming soon. zarr-python-3.2.1/docs/user-guide/storage.md000066400000000000000000000167461517635743000210320ustar00rootroot00000000000000# Storage guide Zarr-Python supports multiple storage backends, including: local file systems, Zip files, remote stores via [fsspec](https://filesystem-spec.readthedocs.io) (S3, HTTP, etc.), and in-memory stores. In Zarr-Python 3, stores must implement the abstract store API from [`zarr.abc.store.Store`][]. !!! note Unlike Zarr-Python 2 where the store interface was built around a generic `MutableMapping` API, Zarr-Python 3 utilizes a custom store API that utilizes Python's AsyncIO library. ## Implicit Store Creation In most cases, it is not required to create a `Store` object explicitly. Passing a string (or other [StoreLike value](#storelike)) to Zarr's top level API will result in the store being created automatically: ```python exec="true" session="storage" source="above" result="ansi" import zarr # Implicitly create a writable LocalStore group = zarr.create_group(store='data/foo/bar') print(group) ``` ```python exec="true" session="storage" source="above" result="ansi" # Implicitly create a read-only FsspecStore # Note: requires s3fs to be installed group = zarr.open_group( store='s3://noaa-nwm-retro-v2-zarr-pds', mode='r', storage_options={'anon': True} ) print(group) ``` ```python exec="true" session="storage" source="above" result="ansi" # Implicitly creates a MemoryStore data = {} group = zarr.create_group(store=data) print(group) ``` [](){#user-guide-store-like} ### StoreLike `StoreLike` values can be: - a `Path` or string indicating a location on the local file system. This will create a [local store](#local-store): ```python exec="true" session="storage" source="above" result="ansi" group = zarr.open_group(store='data/foo/bar') print(group) ``` ```python exec="true" session="storage" source="above" result="ansi" from pathlib import Path group = zarr.open_group(store=Path('data/foo/bar')) print(group) ``` - an FSSpec URI string, indicating a [remote store](#remote-store) location: ```python exec="true" session="storage" source="above" result="ansi" # Note: requires s3fs to be installed group = zarr.open_group( store='s3://noaa-nwm-retro-v2-zarr-pds', mode='r', storage_options={'anon': True} ) print(group) ``` - an empty dictionary or None, which will create a new [memory store](#memory-store): ```python exec="true" session="storage" source="above" result="ansi" group = zarr.create_group(store={}) print(group) ``` ```python exec="true" session="storage" source="above" result="ansi" group = zarr.create_group(store=None) print(group) ``` - a dictionary of string to [`Buffer`][zarr.abc.buffer.Buffer] mappings. This will create a [memory store](#memory-store), using this dictionary as the [`store_dict` argument][zarr.storage.MemoryStore]. - an FSSpec [FSMap object](https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.FSMap), which will create an [FsspecStore](#remote-store). - a [`Store`][zarr.abc.store.Store] or [`StorePath`][zarr.storage.StorePath] - see explicit store creation below. ## Explicit Store Creation In some cases, it may be helpful to create a store instance directly. Zarr-Python offers built-in stores: [`zarr.storage.LocalStore`][], [`zarr.storage.FsspecStore`][], [`zarr.storage.ZipStore`][], [`zarr.storage.MemoryStore`][], and [`zarr.storage.ObjectStore`][]. ### Local Store The [`zarr.storage.LocalStore`][] stores data in a nested set of directories on a local filesystem: ```python exec="true" session="storage" source="above" result="ansi" store = zarr.storage.LocalStore('data/foo/bar', read_only=True) group = zarr.open_group(store=store, mode='r') print(group) ``` ### Zip Store The [`zarr.storage.ZipStore`][] stores the contents of a Zarr hierarchy in a single Zip file. The [Zip Store specification](https://github.com/zarr-developers/zarr-specs/pull/311) is currently in draft form: ```python exec="true" session="storage" source="above" result="ansi" store = zarr.storage.ZipStore('data.zip', mode='w') array = zarr.create_array(store=store, shape=(2,), dtype='float64') print(array) ``` ### Remote Store The [`zarr.storage.FsspecStore`][] stores the contents of a Zarr hierarchy in following the same logical layout as the [`LocalStore`][zarr.storage.LocalStore], except the store is assumed to be on a remote storage system such as cloud object storage (e.g. AWS S3, Google Cloud Storage, Azure Blob Store). The [`zarr.storage.FsspecStore`][] is backed by [fsspec](https://filesystem-spec.readthedocs.io) and can support any backend that implements the [AbstractFileSystem](https://filesystem-spec.readthedocs.io/en/stable/api.html#fsspec.spec.AbstractFileSystem) API. `storage_options` can be used to configure the fsspec backend: ```python exec="true" session="storage" source="above" result="ansi" # Note: requires s3fs to be installed store = zarr.storage.FsspecStore.from_url( 's3://noaa-nwm-retro-v2-zarr-pds', read_only=True, storage_options={'anon': True} ) group = zarr.open_group(store=store, mode='r') print(group) ``` The type of filesystem (e.g. S3, https, etc..) is inferred from the scheme of the url (e.g. s3 for "**s3**://noaa-nwm-retro-v2-zarr-pds"). In case a specific filesystem is needed, one can explicitly create it. For example to create an S3 filesystem: ```python exec="true" session="storage" source="above" result="ansi" # Note: requires s3fs to be installed import fsspec fs = fsspec.filesystem( 's3', anon=True, asynchronous=True, client_kwargs={'endpoint_url': "https://noaa-nwm-retro-v2-zarr-pds.s3.amazonaws.com"} ) store = zarr.storage.FsspecStore(fs) print(store) ``` ### Memory Store The [`zarr.storage.MemoryStore`][] an in-memory store that allows for serialization of Zarr data (metadata and chunks) to a dictionary: ```python exec="true" session="storage" source="above" result="ansi" data = {} store = zarr.storage.MemoryStore(data) array = zarr.create_array(store=store, shape=(2,), dtype='float64') print(array) ``` ### Object Store [`zarr.storage.ObjectStore`][] stores the contents of the Zarr hierarchy using any ObjectStore [storage implementation](https://developmentseed.org/obstore/latest/api/store/), including AWS S3 ([`obstore.store.S3Store`][]), Google Cloud Storage ([`obstore.store.GCSStore`][]), and Azure Blob Storage ([`obstore.store.AzureStore`][]). This store is backed by [obstore](https://developmentseed.org/obstore/latest/), which builds on the production quality Rust library [object_store](https://docs.rs/object_store/latest/object_store/). ```python exec="true" session="storage" source="above" result="ansi" from zarr.storage import ObjectStore from obstore.store import MemoryStore store = ObjectStore(MemoryStore()) array = zarr.create_array(store=store, shape=(2,), dtype='float64') print(array) ``` Here's an example of using ObjectStore for accessing remote data: ```python exec="true" session="storage" source="above" result="ansi" from zarr.storage import ObjectStore from obstore.store import S3Store s3_store = S3Store('noaa-nwm-retro-v2-zarr-pds', skip_signature=True, region="us-west-2") store = zarr.storage.ObjectStore(store=s3_store, read_only=True) group = zarr.open_group(store=store, mode='r') print(group.info) ``` !!! warning The [`zarr.storage.ObjectStore`][] class is experimental. ## Developing custom stores Zarr-Python [`zarr.abc.store.Store`][] API is meant to be extended. The Store Abstract Base Class includes all of the methods needed to be a fully operational store in Zarr Python. Zarr also provides a test harness for custom stores: [`zarr.testing.store.StoreTests`][]. zarr-python-3.2.1/docs/user-guide/v3_migration.md000066400000000000000000000251261517635743000217570ustar00rootroot00000000000000# 3.0 Migration Guide Zarr-Python 3 represents a major refactor of the Zarr-Python codebase. Some of the goals motivating this refactor included: * adding support for the Zarr format 3 specification (along with the Zarr format 2 specification) * cleaning up internal and user facing APIs * improving performance (particularly in high latency storage environments like cloud object stores) To accommodate this, Zarr-Python 3 introduces a number of changes to the API, including a number of significant breaking changes and deprecations. This page provides a guide explaining breaking changes and deprecations to help you migrate your code from version 2 to version 3. If we have missed anything, please open a [GitHub issue](https://github.com/zarr-developers/zarr-python/issues/new) so we can improve this guide. ## Compatibility target The goals described above necessitated some breaking changes to the API (hence the major version update), but where possible we have maintained backwards compatibility in the most widely used parts of the API. This includes the [`zarr.Array`][] and [`zarr.Group`][] classes and the "top-level API" (e.g. [`zarr.open_array`][] and [`zarr.open_group`][]). ## Getting ready for 3.0 Before migrating to Zarr-Python 3, we suggest projects that depend on Zarr-Python take the following actions in order: 1. Pin the supported Zarr-Python version to `zarr>=2,<3`. This is a best practice and will protect your users from any incompatibilities that may arise during the release of Zarr-Python 3. This pin can be removed after migrating to Zarr-Python 3. 2. Limit your imports from the Zarr-Python package. Most of the primary API `zarr.*` will be compatible in Zarr-Python 3. However, the following breaking API changes are planned: - `numcodecs.*` will no longer be available in `zarr.*`. To migrate, import codecs directly from `numcodecs`: ```python from numcodecs import Blosc # instead of: # from zarr import Blosc ``` - The `zarr.v3_api_available` feature flag is being removed. In Zarr-Python 3 the v3 API is always available, so you shouldn't need to use this flag. - The following internal modules are being removed or significantly changed. If your application relies on imports from any of the below modules, you will need to either a) modify your application to no longer rely on these imports or b) vendor the parts of the specific modules that you need. * `zarr.attrs` has gone, with no replacement * `zarr.codecs` has changed, see "Codecs" section below for more information * `zarr.context` has gone, with no replacement * `zarr.core` remains but should be considered private API * `zarr.hierarchy` has gone, with no replacement (use `zarr.Group` inplace of `zarr.hierarchy.Group`) * `zarr.indexing` has gone, with no replacement * `zarr.meta` has gone, with no replacement * `zarr.meta_v1` has gone, with no replacement * `zarr.sync` has gone, with no replacement * `zarr.types` has gone, with no replacement * `zarr.util` has gone, with no replacement * `zarr.n5` has gone, see below for an alternative N5 options 3. Test that your package works with version 3. 4. Update the pin to include `zarr>=3,<4`. ## Zarr-Python 2 support window Zarr-Python 2.x is still available, though we recommend migrating to Zarr-Python 3 for its performance improvements and new features. Security and bug fixes will be made to the 2.x series for at least six months following the first Zarr-Python 3 release. If you need to use the latest Zarr-Python 2 release, you can install it with: ```console $ pip install "zarr==2.*" ``` !!! note Development and maintenance of the 2.x release series has moved to the [support/v2](https://github.com/zarr-developers/zarr-python/tree/support/v2) branch. Issues and pull requests related to this branch are tagged with the [V2](https://github.com/zarr-developers/zarr-python/labels/V2) label. ## Migrating to Zarr-Python 3 The following sections provide details on breaking changes in Zarr-Python 3. ### The Array class 1. Disallow direct construction - the signature for initializing the `Array` class has changed significantly. Please use [`zarr.create_array`][] or [`zarr.open_array`][] instead of directly constructing the [`zarr.Array`][] class. 2. Defaulting to `zarr_format=3` - newly created arrays will use the version 3 of the Zarr specification. To continue using version 2, set `zarr_format=2` when creating arrays or set `default_zarr_format=2` in Zarr's runtime configuration. 3. Function signature change to [`zarr.Array.resize`][] - the `resize` function now takes a `zarr.core.common.ShapeLike` input rather than separate arguments for each dimension. Use `resize((10,10))` in place of `resize(10,10)`. ### The Group class 1. Disallow direct construction - use [`zarr.open_group`][] or [`zarr.create_group`][] instead of directly constructing the `zarr.Group` class. 2. The h5py compatibility methods `create_dataset` and `require_dataset` have been removed. Use the following replacements: - [`zarr.Group.create_array`][] in place of `Group.create_dataset` - [`zarr.Group.require_array`][] in place of `Group.require_dataset` 3. Disallow "." syntax for getting group members. To get a member of a group named `foo`, use `group["foo"]` in place of `group.foo`. 4. The `zarr.storage.init_group` low-level helper function has been removed. Use [`zarr.open_group`][] or [`zarr.create_group`][] instead: ```diff - from zarr.storage import init_group - init_group(store, overwrite=True, path="my/path") + import zarr + zarr.open_group(store, mode="w", path="my/path") ``` ### The Store class The Store API has changed significant in Zarr-Python 3. #### The base store class The `MutableMapping` base class has been replaced in favor of a custom abstract base class ([`zarr.abc.store.Store`][]). An asynchronous interface is used for all store methods that use I/O. This change ensures that these store methods are non-blocking and are as performant as possible. #### Store implementations Store implementations have moved from the top-level module to `zarr.storage`: ```diff title="Store import changes from v2 to v3" # Before (v2) - from zarr import MemoryStore + from zarr.storage import MemoryStore ``` The following stores have been renamed or changed: | v2 | v3 | |------------------------|------------------------------------| | `DirectoryStore` | [`zarr.storage.LocalStore`][] | | `FSStore` | [`zarr.storage.FsspecStore`][] | | `TempStore` | Use [`tempfile.TemporaryDirectory`][] with [`LocalStore`][zarr.storage.LocalStore] | | `zarr. A number of deprecated stores were also removed. See issue #1274 for more details on the removal of these stores. - `N5Store` - see https://github.com/zarr-developers/n5py for an alternative interface to N5 formatted data. - `ABSStore` - use the [`zarr.storage.FsspecStore`][] instead along with fsspec's [adlfs backend](https://github.com/fsspec/adlfs). - `DBMStore` - `LMDBStore` - `SQLiteStore` - `MongoDBStore` - `RedisStore` The latter five stores in this list do not have an equivalent in Zarr-Python 3. If you are interested in developing a custom store that targets these backends, see [developing custom stores](storage.md/#developing-custom-stores) or open an [issue](https://github.com/zarr-developers/zarr-python/issues) to discuss your use case. ### Codecs Codecs defined in ``numcodecs`` (and also imported into the ``zarr.codecs`` namespace in Zarr-Python 2) should still be used when creating Zarr format 2 arrays. Codecs for creating Zarr format 3 arrays are available in two locations: - `zarr.codecs` contains Zarr format 3 codecs that are defined in the [codecs section of the Zarr format 3 specification](https://zarr-specs.readthedocs.io/en/latest/v3/codecs/index.html). - `numcodecs.zarr3` contains codecs from `numcodecs` that can be used to create Zarr format 3 arrays, but are not necessarily part of the Zarr format 3 specification. ### Dependencies When installing using `pip`: - The new `remote` dependency group can be used to install a supported version of `fsspec`, required for remote data access. - The new `gpu` dependency group can be used to install a supported version of `cuda`, required for GPU functionality. - The `jupyter` optional dependency group has been removed, since v3 contains no jupyter specific functionality. ### Miscellaneous - The keyword argument `zarr_version` in most creation functions in `zarr` (e.g. [`zarr.create`][], [`zarr.open`][], [`zarr.group`][], [`zarr.array`][]) has been removed. Use `zarr_format` instead. ## 🚧 Work in Progress 🚧 Zarr-Python 3 is still under active development, and is not yet fully complete. The following list summarizes areas of the codebase that we expect to build out after the 3.0.0 release. If features listed below are important to your use case of Zarr-Python, please open (or comment on) a [GitHub issue](https://github.com/zarr-developers/zarr-python/issues/new). The following functions / methods have not been ported to Zarr-Python 3 yet: - `zarr.copy` ([issue #2407](https://github.com/zarr-developers/zarr-python/issues/2407)) - `zarr.copy_all` ([issue #2407](https://github.com/zarr-developers/zarr-python/issues/2407)) - `zarr.copy_store` ([issue #2407](https://github.com/zarr-developers/zarr-python/issues/2407)) - `zarr.Group.move` ([issue #2108](https://github.com/zarr-developers/zarr-python/issues/2108)) The following features (corresponding to function arguments to functions in `zarr`) have not been ported to Zarr-Python 3 yet. Using these features will raise a warning or a `NotImplementedError`: - `cache_attrs` - `cache_metadata` - `chunk_store` ([issue #2495](https://github.com/zarr-developers/zarr-python/issues/2495)) - `meta_array` - `object_codec` ([issue #2617](https://github.com/zarr-developers/zarr-python/issues/2617)) - `synchronizer` ([issue #1596](https://github.com/zarr-developers/zarr-python/issues/1596)) - `dimension_separator` The following features that were supported by Zarr-Python 2 have not been ported to Zarr-Python 3 yet: - Object dtypes ([issue #2616](https://github.com/zarr-developers/zarr-python/issues/2616)) - Ragged arrays ([issue #2618](https://github.com/zarr-developers/zarr-python/issues/2618)) - Groups and Arrays do not implement `__enter__` and `__exit__` protocols ([issue #2619](https://github.com/zarr-developers/zarr-python/issues/2619)) - Default filters for object dtypes for Zarr format 2 arrays ([issue #2627](https://github.com/zarr-developers/zarr-python/issues/2627)) zarr-python-3.2.1/examples/000077500000000000000000000000001517635743000156435ustar00rootroot00000000000000zarr-python-3.2.1/examples/README.md000066400000000000000000000023411517635743000171220ustar00rootroot00000000000000# Zarr Python Examples This directory contains complete, runnable examples demonstrating various features and use cases of Zarr Python. ## Directory Structure Each example is organized in its own subdirectory with the following structure: ``` examples/ ├── example_name/ │ ├── README.md # Documentation for the example │ └── example_name.py # Python source code └── ... ``` ## Adding New Examples To add a new example: 1. Create a new subdirectory: `examples/my_example/` 2. Add your Python code: `examples/my_example/my_example.py` 3. Create documentation: `examples/my_example/README.md` 4. Create a documentation page at `docs/user-guide/examples/my_example.md`. The documentation page should simply link to the `README.md` and the source code, e.g.: ```` # docs/user-guide/examples/my_example.md --8<-- "examples/my_example/README.md" ## Source Code ```python --8<-- "examples/my_example/my_example.py" ``` ```` 5. Update `mkdocs.yml` to include the new example in the navigation. ### Example README.md Format Your README.md should include: - A title (`# Example Name`) - Description of what the example demonstrates - Instructions for running the example zarr-python-3.2.1/examples/custom_dtype/000077500000000000000000000000001517635743000203625ustar00rootroot00000000000000zarr-python-3.2.1/examples/custom_dtype/README.md000066400000000000000000000011451517635743000216420ustar00rootroot00000000000000# Custom Data Type Example This example demonstrates how to extend Zarr Python by defining a new data type. The example shows how to: - Define a custom `ZDType` class for the `int2` data type from [`ml_dtypes`](https://pypi.org/project/ml-dtypes/) - Implement all required methods for serialization and deserialization - Register the custom data type with Zarr's registry - Create and use arrays with the custom data type in both Zarr v2 and v3 formats ## Running the Example ```bash python examples/custom_dtype/custom_dtype.py ``` Or run with uv: ```bash uv run examples/custom_dtype/custom_dtype.py ``` zarr-python-3.2.1/examples/custom_dtype/custom_dtype.py000066400000000000000000000214551517635743000234620ustar00rootroot00000000000000# /// script # requires-python = ">=3.12" # dependencies = [ # "zarr @ git+https://github.com/zarr-developers/zarr-python.git@main", # "ml_dtypes==0.5.4", # "pytest==8.4.1" # ] # /// # """ Demonstrate how to extend Zarr Python by defining a new data type """ import json import sys from pathlib import Path from typing import ClassVar, Literal, Self, TypeGuard, overload import ml_dtypes # necessary to add extra dtypes to NumPy import numpy as np import pytest import zarr from zarr.core.common import JSON, ZarrFormat from zarr.core.dtype import ZDType, data_type_registry from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, check_dtype_spec_v2, ) # This is the int2 array data type int2_dtype_cls = type(np.dtype("int2")) # This is the int2 scalar type int2_scalar_cls = ml_dtypes.int2 class Int2(ZDType[int2_dtype_cls, int2_scalar_cls]): """ This class provides a Zarr compatibility layer around the int2 data type (the ``dtype`` of a NumPy array of type int2) and the int2 scalar type (the ``dtype`` of the scalar value inside an int2 array). """ # This field is as the key for the data type in the internal data type registry, and also # as the identifier for the data type when serializaing the data type to disk for zarr v3 _zarr_v3_name: ClassVar[Literal["int2"]] = "int2" # this field will be used internally _zarr_v2_name: ClassVar[Literal["int2"]] = "int2" # we bind a class variable to the native data type class so we can create instances of it dtype_cls = int2_dtype_cls @classmethod def from_native_dtype(cls, dtype: np.dtype) -> Self: """Create an instance of this ZDType from a native dtype.""" if cls._check_native_dtype(dtype): return cls() raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self: Self) -> int2_dtype_cls: """Create an int2 dtype instance from this ZDType""" return self.dtype_cls() @classmethod def _check_json_v2(cls, data: DTypeJSON) -> TypeGuard[DTypeConfig_V2[Literal["|b1"], None]]: """ Type check for Zarr v2-flavored JSON. This will check that the input is a dict like this: .. code-block:: json { "name": "int2", "object_codec_id": None } Note that this representation differs from the ``dtype`` field looks like in zarr v2 metadata. Specifically, whatever goes into the ``dtype`` field in metadata is assigned to the ``name`` field here. See the Zarr docs for more information about the JSON encoding for data types. """ return ( check_dtype_spec_v2(data) and data["name"] == "int2" and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[Literal["int2"]]: """ Type check for Zarr V3-flavored JSON. Checks that the input is the string "int2". """ return data == cls._zarr_v3_name @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this ZDType from Zarr V3-flavored JSON. """ if cls._check_json_v2(data): return cls() # This first does a type check on the input, and if that passes we create an instance of the ZDType. msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v2_name!r}" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls: type[Self], data: DTypeJSON) -> Self: """ Create an instance of this ZDType from Zarr V3-flavored JSON. This first does a type check on the input, and if that passes we create an instance of the ZDType. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload # type: ignore[override] def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal["int2"], None]: ... @overload def to_json(self, zarr_format: Literal[3]) -> Literal["int2"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal["int2"], None] | Literal["int2"]: """ Serialize this ZDType to v2- or v3-flavored JSON If the zarr_format is 2, then return a dict like this: .. code-block:: json { "name": "int2", "object_codec_id": None } If the zarr_format is 3, then return the string "int2" """ if zarr_format == 2: return {"name": "int2", "object_codec_id": None} elif zarr_format == 3: return self._zarr_v3_name raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[int | ml_dtypes.int2]: """ Check if a python object is a valid int2-compatible scalar The strictness of this type check is an implementation degree of freedom. You could be strict here, and only accept int2 values, or be open and accept any integer or any object and rely on exceptions from the int2 constructor that will be called in cast_scalar. """ return isinstance(data, (int, int2_scalar_cls)) def cast_scalar(self, data: object) -> ml_dtypes.int2: """ Attempt to cast a python object to an int2. We first perform a type check to ensure that the input type is appropriate, and if that passes we call the int2 scalar constructor. """ if self._check_scalar(data): return ml_dtypes.int2(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> ml_dtypes.int2: """ Get the default scalar value. This will be used when automatically selecting a fill value. """ return ml_dtypes.int2(0) def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int: """ Convert a python object to a JSON representation of an int2 scalar. This is necessary for taking user input for the ``fill_value`` attribute in array metadata. In this implementation, we optimistically convert the input to an int, and then check that it lies in the acceptable range for this data type. """ # We could add a type check here, but we don't need to for this example val: int = int(data) # type: ignore[call-overload] if val not in (-2, -1, 0, 1): raise ValueError("Invalid value. Expected -2, -1, 0, or 1.") return val def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> ml_dtypes.int2: """ Read a JSON-serializable value as an int2 scalar. We first perform a type check to ensure that the JSON value is well-formed, then call the int2 scalar constructor. The base definition of this method requires that it take a zarr_format parameter because other data types serialize scalars differently in zarr v2 and v3, but we don't use this here. """ if self._check_scalar(data): return ml_dtypes.int2(data) raise TypeError(f"Invalid type: {data}. Expected an int.") # after defining dtype class, it must be registered with the data type registry so zarr can use it data_type_registry.register(Int2._zarr_v3_name, Int2) # this parametrized function will create arrays in zarr v2 and v3 using our new data type @pytest.mark.parametrize("zarr_format", [2, 3]) def test_custom_dtype(tmp_path: Path, zarr_format: ZarrFormat) -> None: # create array and write values z_w = zarr.create_array( store=tmp_path, shape=(4,), dtype="int2", zarr_format=zarr_format, compressors=None ) z_w[:] = [-1, -2, 0, 1] # open the array z_r = zarr.open_array(tmp_path, mode="r") print(z_r.info_complete()) # look at the array metadata if zarr_format == 2: meta_file = tmp_path / ".zarray" else: meta_file = tmp_path / "zarr.json" print(json.dumps(json.loads(meta_file.read_text()), indent=2)) if __name__ == "__main__": # Run the example with printed output, and a dummy pytest configuration file specified. # Without the dummy configuration file, at test time pytest will attempt to use the # configuration file in the project root, which will error because Zarr is using some # plugins that are not installed in this example. sys.exit(pytest.main(["-s", __file__, f"-c {__file__}"])) zarr-python-3.2.1/mkdocs.yml000066400000000000000000000213011517635743000160250ustar00rootroot00000000000000# Based on https://github.com/developmentseed/obspec/blob/main/mkdocs.yml site_name: zarr-python repo_name: zarr-developers/zarr-python repo_url: https://github.com/zarr-developers/zarr-python site_description: An implementation of chunked, compressed, N-dimensional arrays for Python. site_author: Alistair Miles site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://zarr.readthedocs.io/'] docs_dir: docs use_directory_urls: true nav: - "index.md" - "quick-start.md" - User Guide: - user-guide/index.md - user-guide/installation.md - user-guide/arrays.md - user-guide/groups.md - user-guide/attributes.md - user-guide/storage.md - user-guide/config.md - user-guide/cli.md - user-guide/v3_migration.md - user-guide/data_types.md - user-guide/performance.md - user-guide/extending.md - user-guide/gpu.md - user-guide/consolidated_metadata.md - user-guide/experimental.md - user-guide/glossary.md - Examples: - user-guide/examples/custom_dtype.md - user-guide/examples/rectilinear_chunks.ipynb - API Reference: - api/zarr/index.md - api/zarr/array.md - api/zarr/group.md - api/zarr/create.md - api/zarr/dtype.md - api/zarr/load.md - api/zarr/open.md - api/zarr/save.md - api/zarr/codecs.md - api/zarr/codecs/numcodecs.md - api/zarr/config.md - api/zarr/errors.md - api/zarr/metadata.md - api/zarr/registry.md - api/zarr/storage.md - api/zarr/experimental.md - ABC: - api/zarr/abc/index.md - api/zarr/abc/buffer.md - api/zarr/abc/codec.md - api/zarr/abc/numcodec.md - api/zarr/abc/metadata.md - api/zarr/abc/store.md - API: - api/zarr/api/index.md - api/zarr/api/asynchronous.md - api/zarr/api/synchronous.md - Buffer: - api/zarr/buffer/index.md - api/zarr/buffer/cpu.md - api/zarr/buffer/gpu.md - Testing: - api/zarr/testing/index.md - api/zarr/testing/buffer.md - api/zarr/testing/conftest.md - api/zarr/testing/stateful.md - api/zarr/testing/store.md - api/zarr/testing/strategies.md - api/zarr/testing/utils.md - release-notes.md - contributing.md watch: - src/zarr - docs theme: language: en name: material custom_dir: docs/overrides logo: _static/logo_bw.png favicon: _static/favicon-96x96.png palette: # Light mode - media: "(prefers-color-scheme: light)" scheme: default primary: custom accent: custom toggle: icon: material/brightness-7 name: Switch to dark mode # Dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: custom accent: custom toggle: icon: material/brightness-4 name: Switch to light mode font: text: Roboto code: Roboto Mono features: - content.code.annotate - content.code.copy - navigation.indexes - navigation.instant - navigation.tracking - search.suggest - search.share extra: social: - icon: fontawesome/brands/mastodon link: https://fosstodon.org/@zarr - icon: fontawesome/brands/bluesky link: https://bsky.app/profile/zarr.dev extra_css: - overrides/stylesheets/extra.css plugins: - autorefs - search - mkdocs-jupyter: include: ["docs/user-guide/examples/*.ipynb"] execute: false ignore_h1_titles: true show_input: true - markdown-exec - mkdocstrings: enable_inventory: true handlers: python: paths: [src/zarr] options: allow_inspection: true docstring_section_style: list docstring_style: numpy inherited_members: true line_length: 60 separate_signature: true show_root_heading: true show_signature_annotations: true show_source: true show_symbol_type_toc: true signature_crossrefs: true show_if_no_docstring: true extensions: - griffe_inherited_docstrings inventories: - https://docs.python.org/3/objects.inv - https://docs.xarray.dev/en/stable/objects.inv - https://numpy.org/doc/stable/objects.inv - https://numcodecs.readthedocs.io/en/stable/objects.inv - https://developmentseed.org/obstore/latest/objects.inv - https://filesystem-spec.readthedocs.io/en/latest/objects.inv - https://requests.readthedocs.io/en/latest/objects.inv - https://docs.aiohttp.org/en/stable/objects.inv - https://s3fs.readthedocs.io/en/latest/objects.inv - https://docs.h5py.org/en/stable/objects.inv - https://icechunk.io/en/stable/objects.inv - https://lithops-cloud.github.io/docs/objects.inv - https://docs.dask.org/en/stable/objects.inv - redirects: redirect_maps: 'spec/index.md': 'https://zarr-specs.readthedocs.io' 'spec/v1.md': 'https://zarr-specs.readthedocs.io/en/latest/v1/v1.0.html' 'spec/v2.md': 'https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html' 'spec/v3.md': 'https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html' 'license.md': 'https://github.com/zarr-developers/zarr-python/blob/main/LICENSE.txt' 'genindex.html.md': 'index.md' 'py-modindex.html.md': 'index.md' 'search.html.md': 'index.md' 'tutorial.md': 'user-guide/installation.md' 'getting-started.md': 'quick-start.md' 'roadmap.md': 'https://zarr.readthedocs.io/en/v3.0.8/developers/roadmap.html' 'installation.md': 'user-guide/installation.md' 'release.md': 'release-notes.md' 'about.html.md': 'index.md' 'arrays.html.md': 'user-guide/arrays.md' 'attributes.html.md': 'user-guide/attributes.md' 'cli.html.md': 'user-guide/cli.md' 'config.html.md': 'user-guide/config.md' 'consolidated_metadata.html.md': 'user-guide/consolidated_metadata.md' 'data_types.html.md': 'user-guide/data_types.md' 'extending.html.md': 'user-guide/extending.md' 'gpu.html.md': 'user-guide/gpu.md' 'groups.html.md': 'user-guide/groups.md' 'installation.html.md': 'user-guide/installation.md' 'performance.html.md': 'user-guide/performance.md' 'quickstart.html.md': 'quick-start.md' 'release-notes.html.md': 'release-notes.md' 'storage.html.md': 'user-guide/storage.md' 'v3_migration.html.md': 'user-guide/v3_migration.md' 'user-guide/arrays.html.md': 'user-guide/arrays.md' 'user-guide/attributes.html.md': 'user-guide/attributes.md' 'user-guide/cli.html.md': 'user-guide/cli.md' 'user-guide/config.html.md': 'user-guide/config.md' 'user-guide/consolidated_metadata.html.md': 'user-guide/consolidated_metadata.md' 'user-guide/data_types.html.md': 'user-guide/data_types.md' 'user-guide/extending.html.md': 'user-guide/extending.md' 'user-guide/gpu.html.md': 'user-guide/gpu.md' 'user-guide/groups.html.md': 'user-guide/groups.md' 'user-guide/installation.html.md': 'user-guide/installation.md' 'user-guide/performance.html.md': 'user-guide/performance.md' 'user-guide/storage.html.md': 'user-guide/storage.md' 'user-guide/v3_migration.html.md': 'user-guide/v3_migration.md' 'developers/contributing.html.md': 'contributing.md' 'developers/index.html.md': 'contributing.md' 'developers/roadmap.html.md': 'https://zarr.readthedocs.io/en/v3.0.8/developers/roadmap.html' 'api.md': 'api/zarr/index.md' 'api/zarr/metadata/migrate_v3.md': 'api/zarr/metadata.md' # Based on https://github.com/developmentseed/titiler/blob/50934c929cca2fa8d3c408d239015f8da429c6a8/docs/mkdocs.yml#L115-L140 markdown_extensions: - admonition - attr_list - codehilite: guess_lang: false - def_list - footnotes - md_in_html - pymdownx.arithmatex - pymdownx.betterem - pymdownx.caret: insert: false - pymdownx.details - pymdownx.escapeall: hardbreak: true nbsp: true - pymdownx.magiclink: hide_protocol: true repo_url_shortener: true - pymdownx.smartsymbols - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - toc: permalink: true - pymdownx.highlight: anchor_linenums: true line_spans: __span pygments_lang_class: true - pymdownx.inlinehilite - pymdownx.snippets zarr-python-3.2.1/packages/000077500000000000000000000000001517635743000156035ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/000077500000000000000000000000001517635743000203375ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/LICENSE.txt000066400000000000000000000021441517635743000221630ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015-2025 Zarr Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. zarr-python-3.2.1/packages/zarr-metadata/README.md000066400000000000000000000035151517635743000216220ustar00rootroot00000000000000# zarr-metadata Python type definitions for Zarr v2 and v3 metadata. ## What this is A typed-data package: `TypedDict` definitions and `Literal` aliases for the JSON shapes specified by the [Zarr v2](https://zarr-specs.readthedocs.io/en/latest/v2/v2.0.html) and [Zarr v3](https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html) specifications, plus types for [`zarr-extensions`](https://github.com/zarr-developers/zarr-extensions/) and a few widely-used-but-unspecified entities (e.g. consolidated metadata). ## What this is for These types describe the JSON shape of Zarr metadata. They are intended for libraries that **read, write, validate, or transform** Zarr metadata. Pair them with a runtime validator like [pydantic](https://docs.pydantic.dev/) to check JSON loaded from disk: ```python import json from pydantic import TypeAdapter from zarr_metadata.v3.array import ArrayMetadataV3 with open("zarr.json", "rb") as f: raw = json.load(f) metadata = TypeAdapter(ArrayMetadataV3).validate_python(raw) ``` ## What this is *not* - Not a parser or builder. There are no `make_array_metadata(...)` factories — that surface belongs to consumer libraries. - Not a runtime validator on its own. Pair with `pydantic`, `msgspec`, or similar to enforce shapes at decode time. Even with a runtime validator, these types only describe **structural** shape — they will not flag *semantically* invalid metadata, like a 3D v3 array whose `dimension_names` has 4 entries instead of 3. That's a job for downstream validator routines. ## Scope At minimum, this library supports what Zarr-Python needs: the complete Zarr v2 and v3 specs, consolidated metadata, and a subset of the metadata defined in `zarr-extensions`. We are generally open to contributions that add types for Zarr metadata with a published spec. ## License [MIT](./LICENSE.txt) zarr-python-3.2.1/packages/zarr-metadata/pyproject.toml000066400000000000000000000032441517635743000232560ustar00rootroot00000000000000[build-system] requires = ["hatchling>=1.29.0"] build-backend = "hatchling.build" [project] name = "zarr-metadata" version = "0.1.0" description = "Spec-defined metadata types for Zarr v2 and v3." readme = "README.md" requires-python = ">=3.11" license = "MIT" license-files = ["LICENSE.txt"] authors = [ { name = "Davis Bennett", email = "davis.v.bennett@gmail.com" }, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Typing :: Typed", ] dependencies = [ "typing_extensions>=4.13", ] [dependency-groups] test = ["pytest", "pydantic>=2"] [tool.hatch.build.targets.wheel] packages = ["src/zarr_metadata"] [tool.ruff] extend = "../../pyproject.toml" target-version = "py311" [tool.pytest.ini_options] minversion = "7" testpaths = ["tests"] xfail_strict = true addopts = ["-ra", "--strict-config", "--strict-markers"] filterwarnings = [ "error", # pydantic warns about ReadOnly TypedDict items not being enforced at runtime. # That's expected here — we rely on type-checker enforcement, not pydantic mutation guards. "ignore::UserWarning:pydantic._internal._generate_schema", ] [tool.numpydoc_validation] checks = [ "GL10", "SS04", "PR02", "PR03", "PR05", "PR06", ] [tool.pyright] include = ["src"] enableExperimentalFeatures = true typeCheckingMode = "strict" pythonVersion = "3.11" zarr-python-3.2.1/packages/zarr-metadata/src/000077500000000000000000000000001517635743000211265ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/000077500000000000000000000000001517635743000237445ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/__init__.py000066400000000000000000000021101517635743000260470ustar00rootroot00000000000000from zarr_metadata._common import NamedConfig from zarr_metadata.v2.array import ( ArrayDimensionSeparatorV2, ArrayMetadataV2, ArrayOrderV2, DataTypeMetadataV2, ) from zarr_metadata.v2.codec import CodecMetadataV2 from zarr_metadata.v2.consolidated import ConsolidatedMetadataV2 from zarr_metadata.v2.group import GroupMetadataV2 from zarr_metadata.v3._common import MetadataFieldV3 from zarr_metadata.v3.array import ArrayMetadataV3, ExtensionFieldV3 from zarr_metadata.v3.consolidated import ConsolidatedMetadataV3 from zarr_metadata.v3.group import GroupMetadataV3 __version__ = "0.1.0" """Hardcoded package version. Must match the `version` field in `pyproject.toml`; the sync is enforced by `tests/test_version.py`.""" __all__ = [ "ArrayDimensionSeparatorV2", "ArrayMetadataV2", "ArrayMetadataV3", "ArrayOrderV2", "CodecMetadataV2", "ConsolidatedMetadataV2", "ConsolidatedMetadataV3", "DataTypeMetadataV2", "ExtensionFieldV3", "GroupMetadataV2", "GroupMetadataV3", "MetadataFieldV3", "NamedConfig", "__version__", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/_common.py000066400000000000000000000013011517635743000257400ustar00rootroot00000000000000""" Top-level cross-version primitives for Zarr metadata. Version-specific types live under `zarr_metadata.v2` and `zarr_metadata.v3`. Codec and dtype spec types live under `zarr_metadata.v3.codec` and `zarr_metadata.v3.data_type`. """ from collections.abc import Mapping from typing import NotRequired from typing_extensions import TypedDict class NamedConfig(TypedDict): """ Externally-tagged union member for a metadata field. The `configuration` mapping holds arbitrary JSON-encodable values; it is typed as `Mapping[str, object]` because the type system cannot express or verify JSON-encodability. """ name: str configuration: NotRequired[Mapping[str, object]] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/py.typed000066400000000000000000000000001517635743000254310ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v2/000077500000000000000000000000001517635743000242735ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v2/__init__.py000066400000000000000000000010141517635743000264000ustar00rootroot00000000000000"""Zarr v2 metadata types.""" from zarr_metadata.v2.array import ( ArrayDimensionSeparatorV2, ArrayMetadataV2, ArrayOrderV2, DataTypeMetadataV2, ) from zarr_metadata.v2.codec import CodecMetadataV2 from zarr_metadata.v2.consolidated import ConsolidatedMetadataV2 from zarr_metadata.v2.group import GroupMetadataV2 __all__ = [ "ArrayDimensionSeparatorV2", "ArrayMetadataV2", "ArrayOrderV2", "CodecMetadataV2", "ConsolidatedMetadataV2", "DataTypeMetadataV2", "GroupMetadataV2", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v2/array.py000066400000000000000000000050371517635743000257700ustar00rootroot00000000000000"""Zarr v2 array metadata types.""" from collections.abc import Mapping from typing import Literal, NotRequired from typing_extensions import TypedDict from zarr_metadata.v2.codec import CodecMetadataV2 DataTypeMetadataV2 = str | tuple[tuple[str, str] | tuple[str, str, tuple[int, ...]], ...] """The v2 dtype representation. Either a numpy-style dtype string (e.g. `"ChunkGridMetadata` aliases re-exported here are the canonical type for each grid's permitted JSON shapes. For the underlying `ChunkGridObject`, `ChunkGridConfiguration`, etc., import directly from the leaf submodule. See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-grids """ from zarr_metadata.v3.chunk_grid.rectilinear import RectilinearChunkGridMetadata from zarr_metadata.v3.chunk_grid.regular import RegularChunkGridMetadata __all__ = [ "RectilinearChunkGridMetadata", "RegularChunkGridMetadata", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/chunk_grid/rectilinear.py000066400000000000000000000030651517635743000312700ustar00rootroot00000000000000""" Rectilinear chunk grid (zarr-extensions). See https://github.com/zarr-developers/zarr-extensions/tree/main/chunk-grids/rectilinear """ from typing import Final, Literal from typing_extensions import TypedDict RECTILINEAR_CHUNK_GRID_NAME: Final = "rectilinear" """The `name` field value of the rectilinear chunk grid.""" RectilinearChunkGridName = Literal["rectilinear"] """Literal type of the `name` field of the rectilinear chunk grid.""" RectilinearDimSpec = int | tuple[int | tuple[int, int], ...] """JSON shape for one dimension's rectilinear spec. Either a bare integer (uniform shorthand for a regular dimension within a rectilinear grid), or a tuple of integers and/or `[value, count]` RLE pairs. """ class RectilinearChunkGridConfiguration(TypedDict): """Configuration for the rectilinear chunk grid.""" kind: Literal["inline"] chunk_shapes: tuple[RectilinearDimSpec, ...] class RectilinearChunkGridObject(TypedDict): """Rectilinear chunk grid metadata in object form.""" name: RectilinearChunkGridName configuration: RectilinearChunkGridConfiguration RectilinearChunkGridMetadata = RectilinearChunkGridObject """Permitted JSON shape for rectilinear chunk grid metadata. `kind` and `chunk_shapes` are required, so only the object form is valid; the short-hand-name form is not permitted by the spec for this grid. """ __all__ = [ "RECTILINEAR_CHUNK_GRID_NAME", "RectilinearChunkGridConfiguration", "RectilinearChunkGridMetadata", "RectilinearChunkGridName", "RectilinearChunkGridObject", "RectilinearDimSpec", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/chunk_grid/regular.py000066400000000000000000000022151517635743000304240ustar00rootroot00000000000000""" Regular chunk grid (Zarr v3 core spec). See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#regular-grids """ from typing import Final, Literal from typing_extensions import TypedDict REGULAR_CHUNK_GRID_NAME: Final = "regular" """The `name` field value of the regular chunk grid.""" RegularChunkGridName = Literal["regular"] """Literal type of the `name` field of the regular chunk grid.""" class RegularChunkGridConfiguration(TypedDict): """Configuration for the regular chunk grid.""" chunk_shape: tuple[int, ...] class RegularChunkGridObject(TypedDict): """Regular chunk grid metadata in object form.""" name: RegularChunkGridName configuration: RegularChunkGridConfiguration RegularChunkGridMetadata = RegularChunkGridObject """Permitted JSON shape for regular chunk grid metadata. `chunk_shape` is required and has no default, so only the object form is valid; the short-hand-name form is not permitted by the spec for this grid. """ __all__ = [ "REGULAR_CHUNK_GRID_NAME", "RegularChunkGridConfiguration", "RegularChunkGridMetadata", "RegularChunkGridName", "RegularChunkGridObject", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/000077500000000000000000000000001517635743000301225ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/__init__.py000066400000000000000000000015361517635743000322400ustar00rootroot00000000000000""" Zarr v3 chunk key encoding metadata types. Each chunk key encoding lives in its own submodule: - `default` -- v3 default encoding (`/`-separated) - `v2` -- v2-compatibility encoding (`.`-separated by default) Both are defined by the v3 core spec. The `ChunkKeyEncodingMetadata` aliases re-exported here are the canonical type for each encoding's permitted JSON shapes. For the underlying `ChunkKeyEncodingObject`, `ChunkKeyEncodingConfiguration`, etc., import directly from the leaf submodule. See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-key-encoding """ from zarr_metadata.v3.chunk_key_encoding.default import DefaultChunkKeyEncodingMetadata from zarr_metadata.v3.chunk_key_encoding.v2 import V2ChunkKeyEncodingMetadata __all__ = [ "DefaultChunkKeyEncodingMetadata", "V2ChunkKeyEncodingMetadata", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/default.py000066400000000000000000000034061517635743000321230ustar00rootroot00000000000000""" Default chunk key encoding (Zarr v3 core spec). The chunk key for a chunk with grid index `(k, j, i, ...)` is formed by appending `ckji...` (where `` is `separator`). See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-key-encoding """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict DEFAULT_CHUNK_KEY_ENCODING_NAME: Final = "default" """The `name` field value of the default chunk key encoding.""" DefaultChunkKeyEncodingName = Literal["default"] """Literal type of the `name` field of the default chunk key encoding.""" DefaultChunkKeyEncodingSeparator = Literal["/", "."] """Permitted `separator` values for the default chunk key encoding. Defaults to `"/"` if absent. """ class DefaultChunkKeyEncodingConfiguration(TypedDict): """Configuration for the default chunk key encoding. `separator` is optional and defaults to `"/"` per spec. """ separator: NotRequired[DefaultChunkKeyEncodingSeparator] class DefaultChunkKeyEncodingObject(TypedDict): """Default chunk key encoding metadata in object form.""" name: DefaultChunkKeyEncodingName configuration: NotRequired[DefaultChunkKeyEncodingConfiguration] DefaultChunkKeyEncodingMetadata = DefaultChunkKeyEncodingObject | DefaultChunkKeyEncodingName """Permitted JSON shapes for the default chunk-key encoding metadata. The configuration has no required keys (`separator` defaults to `"/"`), so the short-hand-name form is permitted in addition to the object form. """ __all__ = [ "DEFAULT_CHUNK_KEY_ENCODING_NAME", "DefaultChunkKeyEncodingConfiguration", "DefaultChunkKeyEncodingMetadata", "DefaultChunkKeyEncodingName", "DefaultChunkKeyEncodingObject", "DefaultChunkKeyEncodingSeparator", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/chunk_key_encoding/v2.py000066400000000000000000000032441517635743000310260ustar00rootroot00000000000000""" v2-compatibility chunk key encoding (Zarr v3 core spec). Intended only to allow existing v2 arrays to be converted to v3 without having to rename chunks. Not recommended for new arrays. See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#chunk-key-encoding """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict V2_CHUNK_KEY_ENCODING_NAME: Final = "v2" """The `name` field value of the v2 chunk key encoding.""" V2ChunkKeyEncodingName = Literal["v2"] """Literal type of the `name` field of the v2 chunk key encoding.""" V2ChunkKeyEncodingSeparator = Literal["/", "."] """Permitted `separator` values for the v2 chunk key encoding. Defaults to `"."` if absent. """ class V2ChunkKeyEncodingConfiguration(TypedDict): """Configuration for the v2 chunk key encoding. `separator` is optional and defaults to `"."` per spec. """ separator: NotRequired[V2ChunkKeyEncodingSeparator] class V2ChunkKeyEncodingObject(TypedDict): """v2-compatibility chunk key encoding metadata in object form.""" name: V2ChunkKeyEncodingName configuration: NotRequired[V2ChunkKeyEncodingConfiguration] V2ChunkKeyEncodingMetadata = V2ChunkKeyEncodingObject | V2ChunkKeyEncodingName """Permitted JSON shapes for the v2-compatibility chunk-key encoding metadata. The configuration has no required keys (`separator` defaults to `"."`), so the short-hand-name form is permitted in addition to the object form. """ __all__ = [ "V2_CHUNK_KEY_ENCODING_NAME", "V2ChunkKeyEncodingConfiguration", "V2ChunkKeyEncodingMetadata", "V2ChunkKeyEncodingName", "V2ChunkKeyEncodingObject", "V2ChunkKeyEncodingSeparator", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/000077500000000000000000000000001517635743000253515ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/__init__.py000066400000000000000000000031201517635743000274560ustar00rootroot00000000000000""" Zarr v3 codec spec types. Each codec defined by the spec or by zarr-extensions has its own submodule (`blosc`, `bytes`, `cast_value`, `crc32c`, `gzip`, `scale_offset`, `sharding_indexed`, `transpose`, `zstd`). The `CodecMetadata` aliases re-exported here are the canonical type for each codec's permitted JSON shapes (object form plus, where the spec allows, a bare-string short-hand form). For the underlying `CodecObject`, `CodecConfiguration`, etc., import directly from the leaf submodule. For the field-level "any codec entry" alias (used in array metadata's `codecs` list and in sharding's inner pipelines), import `MetadataFieldV3` from `zarr_metadata.v3`. See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/index.html """ from zarr_metadata.v3.codec.blosc import BloscCodecMetadata from zarr_metadata.v3.codec.bytes import BytesCodecMetadata from zarr_metadata.v3.codec.cast_value import CastValueCodecMetadata from zarr_metadata.v3.codec.crc32c import Crc32cCodecMetadata from zarr_metadata.v3.codec.gzip import GzipCodecMetadata from zarr_metadata.v3.codec.scale_offset import ScaleOffsetCodecMetadata from zarr_metadata.v3.codec.sharding_indexed import ShardingIndexedCodecMetadata from zarr_metadata.v3.codec.transpose import TransposeCodecMetadata from zarr_metadata.v3.codec.zstd import ZstdCodecMetadata __all__ = [ "BloscCodecMetadata", "BytesCodecMetadata", "CastValueCodecMetadata", "Crc32cCodecMetadata", "GzipCodecMetadata", "ScaleOffsetCodecMetadata", "ShardingIndexedCodecMetadata", "TransposeCodecMetadata", "ZstdCodecMetadata", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/blosc.py000066400000000000000000000026131517635743000270270ustar00rootroot00000000000000""" Blosc codec types. See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/blosc/index.html """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict BLOSC_CODEC_NAME: Final = "blosc" """The `name` field value of the `blosc` codec.""" BloscCodecName = Literal["blosc"] """Literal type of the `name` field of the `blosc` codec.""" BloscShuffle = Literal["noshuffle", "shuffle", "bitshuffle"] """Blosc shuffle mode names.""" BloscCName = Literal["lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd"] """Blosc compressor identifiers.""" class BloscCodecConfiguration(TypedDict): """Configuration for the Zarr v3 `blosc` codec.""" cname: BloscCName clevel: int shuffle: BloscShuffle blocksize: int typesize: NotRequired[int] class BloscCodecObject(TypedDict): """`blosc` codec metadata in object form.""" name: BloscCodecName configuration: BloscCodecConfiguration BloscCodecMetadata = BloscCodecObject """Permitted JSON shape for `blosc` codec metadata. The configuration has multiple required keys (`cname`, `clevel`, `shuffle`, `blocksize`), so only the object form is valid; the short-hand-name form is not permitted by the spec for this codec. """ __all__ = [ "BLOSC_CODEC_NAME", "BloscCName", "BloscCodecConfiguration", "BloscCodecMetadata", "BloscCodecName", "BloscCodecObject", "BloscShuffle", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/bytes.py000066400000000000000000000023701517635743000270530ustar00rootroot00000000000000""" Bytes codec types. See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/bytes/index.html """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict BYTES_CODEC_NAME: Final = "bytes" """The `name` field value of the `bytes` codec.""" BytesCodecName = Literal["bytes"] """Literal type of the `name` field of the `bytes` codec.""" Endian = Literal["little", "big"] """Byte order of multi-byte numeric data.""" class BytesCodecConfiguration(TypedDict): """ Configuration for the Zarr v3 `bytes` codec. The `endian` field is required for multi-byte data types. """ endian: NotRequired[Endian] class BytesCodecObject(TypedDict): """`bytes` codec metadata in object form.""" name: BytesCodecName configuration: BytesCodecConfiguration BytesCodecMetadata = BytesCodecObject | BytesCodecName """Permitted JSON shapes for `bytes` codec metadata. The configuration has no required keys (`endian` is conditionally required at runtime based on data type), so the spec's short-hand-name form is permitted in addition to the object form. """ __all__ = [ "BYTES_CODEC_NAME", "BytesCodecConfiguration", "BytesCodecMetadata", "BytesCodecName", "BytesCodecObject", "Endian", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/cast_value.py000066400000000000000000000046671517635743000300660ustar00rootroot00000000000000""" Cast-value codec types. See https://github.com/zarr-developers/zarr-extensions/tree/main/codecs/cast_value """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict from zarr_metadata.v3._common import MetadataFieldV3 CAST_VALUE_CODEC_NAME: Final = "cast_value" """The `name` field value of the `cast_value` codec.""" CastValueCodecName = Literal["cast_value"] """Literal type of the `name` field of the `cast_value` codec.""" RoundingMode = Literal[ "nearest-even", "towards-zero", "towards-positive", "towards-negative", "nearest-away", ] """Permitted values for the `rounding` configuration field. Defaults to `"nearest-even"` if absent. """ OutOfRangeMode = Literal["clamp", "wrap"] """Permitted values for the `out_of_range` configuration field. If absent, out-of-range values are an encoding/decoding error. """ ScalarMapEntry = tuple[object, object] """A single `[input, output]` mapping in a `scalar_map` direction. Each scalar is JSON-encoded per its data type's fill-value rules (so e.g. `"NaN"` and `"+Infinity"` are permitted). """ class ScalarMap(TypedDict): """Optional encode/decode scalar overrides for the cast_value codec.""" encode: NotRequired[tuple[ScalarMapEntry, ...]] decode: NotRequired[tuple[ScalarMapEntry, ...]] class CastValueCodecConfiguration(TypedDict): """ Configuration for the Zarr v3 `cast_value` codec. `data_type` is the target data type that input values are cast to. It is the same shape as the top-level array `data_type` field: either a bare-string primitive name or a `{name, configuration}` envelope. """ data_type: MetadataFieldV3 rounding: NotRequired[RoundingMode] out_of_range: NotRequired[OutOfRangeMode] scalar_map: NotRequired[ScalarMap] class CastValueCodecObject(TypedDict): """`cast_value` codec metadata in object form.""" name: CastValueCodecName configuration: CastValueCodecConfiguration CastValueCodecMetadata = CastValueCodecObject """Permitted JSON shape for `cast_value` codec metadata. `configuration.data_type` is required, so only the object form is valid; the short-hand-name form is not permitted by the spec for this codec. """ __all__ = [ "CAST_VALUE_CODEC_NAME", "CastValueCodecConfiguration", "CastValueCodecMetadata", "CastValueCodecName", "CastValueCodecObject", "OutOfRangeMode", "RoundingMode", "ScalarMap", "ScalarMapEntry", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/crc32c.py000066400000000000000000000023741517635743000270100ustar00rootroot00000000000000""" CRC32C codec types. See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/crc32c/index.html The CRC32C codec has no configuration fields, so the `configuration` key is absent from the metadata. """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict CRC32C_CODEC_NAME: Final = "crc32c" """The `name` field value of the `crc32c` codec.""" Crc32cCodecName = Literal["crc32c"] """Literal type of the `name` field of the `crc32c` codec.""" class Empty(TypedDict, closed=True): # type: ignore[call-arg] """An empty mapping""" class Crc32cCodecObject(TypedDict): """`crc32c` codec metadata in object form. Per spec the codec has no configuration fields. `configuration` is optional and, if present, should be an empty mapping. """ name: Crc32cCodecName configuration: NotRequired[Empty] Crc32cCodecMetadata = Crc32cCodecObject | Crc32cCodecName """Permitted JSON shapes for `crc32c` codec metadata. The spec's Extension definition allows extensions with no required configuration to be encoded as a bare short-hand name. CRC32C has no configuration, so both forms are valid. """ __all__ = [ "CRC32C_CODEC_NAME", "Crc32cCodecMetadata", "Crc32cCodecName", "Crc32cCodecObject", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/gzip.py000066400000000000000000000022661517635743000267020ustar00rootroot00000000000000""" Gzip codec types. See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/gzip/index.html """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict GZIP_CODEC_NAME: Final = "gzip" """The `name` field value of the `gzip` codec.""" GzipCodecName = Literal["gzip"] """Literal type of the `name` field of the `gzip` codec.""" class GzipCodecConfiguration(TypedDict): """ Configuration for the Zarr v3 `gzip` codec. `level` is an integer in the range 0-9; 0 disables compression and 9 is slowest with the best compression ratio. The spec does not mandate a default. """ level: NotRequired[int] class GzipCodecObject(TypedDict): """`gzip` codec metadata in object form.""" name: GzipCodecName configuration: GzipCodecConfiguration GzipCodecMetadata = GzipCodecObject | GzipCodecName """Permitted JSON shapes for `gzip` codec metadata. The configuration has no required keys (`level` has no spec-mandated default but is `NotRequired`), so the short-hand-name form is permitted. """ __all__ = [ "GZIP_CODEC_NAME", "GzipCodecConfiguration", "GzipCodecMetadata", "GzipCodecName", "GzipCodecObject", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/scale_offset.py000066400000000000000000000034721517635743000303660ustar00rootroot00000000000000""" Scale-offset codec types. See https://github.com/zarr-developers/zarr-extensions/tree/main/codecs/scale_offset """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict SCALE_OFFSET_CODEC_NAME: Final = "scale_offset" """The `name` field value of the `scale_offset` codec.""" ScaleOffsetCodecName = Literal["scale_offset"] """Literal type of the `name` field of the `scale_offset` codec.""" class ScaleOffsetCodecConfiguration(TypedDict): """ Configuration for the Zarr v3 `scale_offset` codec. Both fields are optional. A missing `offset` is the additive identity (e.g. 0 for numeric types); a missing `scale` is the multiplicative identity (e.g. 1). Each scalar is JSON-encoded per the input array's fill-value rules, so `"NaN"` and `"+Infinity"` style strings are permitted in addition to numbers. """ offset: NotRequired[object] scale: NotRequired[object] class ScaleOffsetCodecObject(TypedDict): """`scale_offset` codec metadata in object form. `configuration` is itself optional per spec — when both `offset` and `scale` are at their identity defaults, the codec is a no-op and the entire `configuration` field may be omitted. """ name: ScaleOffsetCodecName configuration: NotRequired[ScaleOffsetCodecConfiguration] ScaleOffsetCodecMetadata = ScaleOffsetCodecObject | ScaleOffsetCodecName """Permitted JSON shapes for `scale_offset` codec metadata. The configuration has no required keys (both `offset` and `scale` are optional, and the configuration itself is optional), so the short-hand-name form is permitted in addition to the object form. """ __all__ = [ "SCALE_OFFSET_CODEC_NAME", "ScaleOffsetCodecConfiguration", "ScaleOffsetCodecMetadata", "ScaleOffsetCodecName", "ScaleOffsetCodecObject", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/sharding_indexed.py000066400000000000000000000040121517635743000312170ustar00rootroot00000000000000""" Sharding-indexed codec types. See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/sharding-indexed/index.html """ from typing import Final, Literal, NotRequired from typing_extensions import TypedDict from zarr_metadata.v3._common import MetadataFieldV3 SHARDING_INDEXED_CODEC_NAME: Final = "sharding_indexed" """The `name` field value of the `sharding_indexed` codec.""" ShardingIndexedCodecName = Literal["sharding_indexed"] """Literal type of the `name` field of the `sharding_indexed` codec.""" IndexLocation = Literal["start", "end"] """Position of the shard index within the encoded shard.""" class ShardingIndexedCodecConfiguration(TypedDict): """ Configuration for the Zarr v3 `sharding_indexed` codec. `chunk_shape` is the shape of inner chunks along each dimension; it must evenly divide the shard shape. `codecs` is the codec pipeline applied to each inner chunk; exactly one array-to-bytes codec is required. `index_codecs` is the codec pipeline applied to the shard index; it must be deterministic (no variable-size compression). `index_location` defaults to `"end"` per the spec. """ chunk_shape: tuple[int, ...] codecs: tuple[MetadataFieldV3, ...] index_codecs: tuple[MetadataFieldV3, ...] index_location: NotRequired[IndexLocation] class ShardingIndexedCodecObject(TypedDict): """`sharding_indexed` codec metadata in object form.""" name: ShardingIndexedCodecName configuration: ShardingIndexedCodecConfiguration ShardingIndexedCodecMetadata = ShardingIndexedCodecObject """Permitted JSON shape for `sharding_indexed` codec metadata. The configuration has multiple required keys (`chunk_shape`, `codecs`, `index_codecs`), so only the object form is valid; the short-hand-name form is not permitted by the spec for this codec. """ __all__ = [ "SHARDING_INDEXED_CODEC_NAME", "IndexLocation", "ShardingIndexedCodecConfiguration", "ShardingIndexedCodecMetadata", "ShardingIndexedCodecName", "ShardingIndexedCodecObject", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/transpose.py000066400000000000000000000023241517635743000277420ustar00rootroot00000000000000""" Transpose codec types. See https://zarr-specs.readthedocs.io/en/latest/v3/codecs/transpose/index.html """ from typing import Final, Literal from typing_extensions import TypedDict TRANSPOSE_CODEC_NAME: Final = "transpose" """The `name` field value of the `transpose` codec.""" TransposeCodecName = Literal["transpose"] """Literal type of the `name` field of the `transpose` codec.""" class TransposeCodecConfiguration(TypedDict): """ Configuration for the Zarr v3 `transpose` codec. `order` is a permutation of the dimension indices 0..n-1 that specifies the dimension reordering applied during encoding. """ order: tuple[int, ...] class TransposeCodecObject(TypedDict): """`transpose` codec metadata in object form.""" name: TransposeCodecName configuration: TransposeCodecConfiguration TransposeCodecMetadata = TransposeCodecObject """Permitted JSON shape for `transpose` codec metadata. `order` is required, so only the object form is valid; the short-hand-name form is not permitted by the spec for this codec. """ __all__ = [ "TRANSPOSE_CODEC_NAME", "TransposeCodecConfiguration", "TransposeCodecMetadata", "TransposeCodecName", "TransposeCodecObject", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/codec/zstd.py000066400000000000000000000022251517635743000267100ustar00rootroot00000000000000""" Zstandard codec types. See https://github.com/zarr-developers/zarr-specs/pull/256 (unmerged at time of writing; the configuration shape below reflects the proposed specification). """ from typing import Final, Literal from typing_extensions import TypedDict ZSTD_CODEC_NAME: Final = "zstd" """The `name` field value of the `zstd` codec.""" ZstdCodecName = Literal["zstd"] """Literal type of the `name` field of the `zstd` codec.""" class ZstdCodecConfiguration(TypedDict): """ Configuration for the Zarr v3 `zstd` codec. Both fields are required per the proposed specification. """ level: int checksum: bool class ZstdCodecObject(TypedDict): """`zstd` codec metadata in object form.""" name: ZstdCodecName configuration: ZstdCodecConfiguration ZstdCodecMetadata = ZstdCodecObject """Permitted JSON shape for `zstd` codec metadata. Both `level` and `checksum` are required, so only the object form is valid; the short-hand-name form is not permitted by the spec for this codec. """ __all__ = [ "ZSTD_CODEC_NAME", "ZstdCodecConfiguration", "ZstdCodecMetadata", "ZstdCodecName", "ZstdCodecObject", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/consolidated.py000066400000000000000000000026651517635743000273270ustar00rootroot00000000000000"""Zarr v3 consolidated metadata types. There is no Zarr v3 specification for consolidated metadata. This module models the inline-on-group convention used by the reference Python implementation (and zarrs), where consolidated metadata is embedded as an extension field on a group's `zarr.json`. The shape modeled here (`{kind, must_understand, metadata}` with no `name` field) reflects the original Zarr v3.0 reading of the extension-field rules. Under the strict Zarr v3.1 reading, every extension field must also include a `name: str` key, which would make this shape — and every real-world consolidated metadata document in the wild — out of spec. See `ExtensionFieldV3` and https://github.com/zarr-developers/zarr-specs/issues/371 for the ongoing discussion. """ from collections.abc import Mapping from typing import Literal from typing_extensions import TypedDict from zarr_metadata.v3.array import ArrayMetadataV3 from zarr_metadata.v3.group import GroupMetadataV3 class ConsolidatedMetadataV3(TypedDict): """ Inline consolidated metadata embedded in a v3 group. The `metadata` map contains only v3 array and group entries - v2 entries are excluded by design. Mixing v2 entries into a v3 consolidated metadata document is invalid per spec. """ kind: Literal["inline"] must_understand: Literal[False] metadata: Mapping[str, ArrayMetadataV3 | GroupMetadataV3] __all__ = [ "ConsolidatedMetadataV3", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/000077500000000000000000000000001517635743000262465ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/__init__.py000066400000000000000000000074021517635743000303620ustar00rootroot00000000000000""" Zarr v3 data type spec types. Each v3 data type has its own submodule: - Core primitives: `bool`, `int8`/`16`/`32`/`64`, `uint8`/`16`/`32`/`64`, `float16`/`32`/`64`, `complex64`/`128`, `raw` (for `r`) - zarr-extensions: `bytes`, `string`, `numpy_datetime64`, `numpy_timedelta64`, `struct` The two canonical types per dtype are re-exported here: - `DataTypeName` -- the literal type of the dtype's `data_type` string (or, for named-config dtypes, the literal value of their `name` field) - `FillValue` -- the permitted JSON shape of the `fill_value` field Named-config dtypes (`numpy_datetime64`, `numpy_timedelta64`, `struct`) also expose their envelope TypedDict here. For configuration TypedDicts, branded `HexFloat` / `Base64Bytes` types, and the corresponding validator functions, import directly from the leaf submodule. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from zarr_metadata.v3.data_type.bool import BoolDataTypeName, BoolFillValue from zarr_metadata.v3.data_type.bytes import BytesDataTypeName, BytesFillValue from zarr_metadata.v3.data_type.complex64 import Complex64DataTypeName, Complex64FillValue from zarr_metadata.v3.data_type.complex128 import ( Complex128DataTypeName, Complex128FillValue, ) from zarr_metadata.v3.data_type.float16 import Float16DataTypeName, Float16FillValue from zarr_metadata.v3.data_type.float32 import Float32DataTypeName, Float32FillValue from zarr_metadata.v3.data_type.float64 import Float64DataTypeName, Float64FillValue from zarr_metadata.v3.data_type.int8 import Int8DataTypeName, Int8FillValue from zarr_metadata.v3.data_type.int16 import Int16DataTypeName, Int16FillValue from zarr_metadata.v3.data_type.int32 import Int32DataTypeName, Int32FillValue from zarr_metadata.v3.data_type.int64 import Int64DataTypeName, Int64FillValue from zarr_metadata.v3.data_type.numpy_datetime64 import ( NumpyDatetime64, NumpyDatetime64DataTypeName, NumpyDatetime64FillValue, ) from zarr_metadata.v3.data_type.numpy_timedelta64 import ( NumpyTimedelta64, NumpyTimedelta64DataTypeName, NumpyTimedelta64FillValue, ) from zarr_metadata.v3.data_type.raw import RawBytesDataTypeName, RawBytesFillValue from zarr_metadata.v3.data_type.string import StringDataTypeName, StringFillValue from zarr_metadata.v3.data_type.struct import ( Struct, StructDataTypeName, StructFillValue, ) from zarr_metadata.v3.data_type.uint8 import Uint8DataTypeName, Uint8FillValue from zarr_metadata.v3.data_type.uint16 import Uint16DataTypeName, Uint16FillValue from zarr_metadata.v3.data_type.uint32 import Uint32DataTypeName, Uint32FillValue from zarr_metadata.v3.data_type.uint64 import Uint64DataTypeName, Uint64FillValue __all__ = [ "BoolDataTypeName", "BoolFillValue", "BytesDataTypeName", "BytesFillValue", "Complex64DataTypeName", "Complex64FillValue", "Complex128DataTypeName", "Complex128FillValue", "Float16DataTypeName", "Float16FillValue", "Float32DataTypeName", "Float32FillValue", "Float64DataTypeName", "Float64FillValue", "Int8DataTypeName", "Int8FillValue", "Int16DataTypeName", "Int16FillValue", "Int32DataTypeName", "Int32FillValue", "Int64DataTypeName", "Int64FillValue", "NumpyDatetime64", "NumpyDatetime64DataTypeName", "NumpyDatetime64FillValue", "NumpyTimedelta64", "NumpyTimedelta64DataTypeName", "NumpyTimedelta64FillValue", "RawBytesDataTypeName", "RawBytesFillValue", "StringDataTypeName", "StringFillValue", "Struct", "StructDataTypeName", "StructFillValue", "Uint8DataTypeName", "Uint8FillValue", "Uint16DataTypeName", "Uint16FillValue", "Uint32DataTypeName", "Uint32FillValue", "Uint64DataTypeName", "Uint64FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/bool.py000066400000000000000000000010001517635743000275420ustar00rootroot00000000000000""" Zarr v3 `bool` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal BOOL_DATA_TYPE_NAME: Final = "bool" """The `data_type` value for the `bool` type.""" BoolDataTypeName = Literal["bool"] """Literal type of the `data_type` field for `bool`.""" BoolFillValue = bool """Permitted JSON shape of the `fill_value` field for `bool`: a JSON boolean.""" __all__ = [ "BOOL_DATA_TYPE_NAME", "BoolDataTypeName", "BoolFillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/bytes.py000066400000000000000000000026041517635743000277500ustar00rootroot00000000000000""" Zarr `bytes` data type (variable-length raw bytes, zarr-extensions). See https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/bytes """ import re from typing import Final, Literal, NewType BYTES_DATA_TYPE_NAME: Final = "bytes" """The `data_type` value for the variable-length `bytes` type.""" BytesDataTypeName = Literal["bytes"] """Literal type of the `data_type` field for `bytes`.""" Base64Bytes = NewType("Base64Bytes", str) """A standard-alphabet base64-encoded byte sequence.""" _BASE64_RE: Final = re.compile(r"^[A-Za-z0-9+/]*={0,2}$") def base64_bytes(value: str) -> Base64Bytes: """Validate `value` as a Base64Bytes and brand it. Raises ValueError if `value` is not standard-alphabet base64 (length must be a multiple of 4 once padded; only `A-Z`, `a-z`, `0-9`, `+`, `/`, and trailing `=` padding are permitted). """ if len(value) % 4 != 0 or not _BASE64_RE.fullmatch(value): raise ValueError(f"Expected standard-alphabet base64, got {value!r}") return Base64Bytes(value) BytesFillValue = tuple[int, ...] | Base64Bytes """Permitted JSON shape of the `fill_value` field for `bytes`. Either a JSON array of integers in `[0, 255]` (one per byte), or a `Base64Bytes` string encoding the byte sequence. """ __all__ = [ "BYTES_DATA_TYPE_NAME", "Base64Bytes", "BytesDataTypeName", "BytesFillValue", "base64_bytes", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/complex128.py000066400000000000000000000017511517635743000305260ustar00rootroot00000000000000""" Zarr v3 `complex128` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal from zarr_metadata.v3.data_type.float64 import Float64FillValue COMPLEX128_DATA_TYPE_NAME: Final = "complex128" """The `data_type` value for the `complex128` type.""" Complex128DataTypeName = Literal["complex128"] """Literal type of the `data_type` field for `complex128`.""" Complex128Component = Float64FillValue """One real or imaginary component of a `complex128` fill value. Same shape as a `float64` fill value: a JSON number, a named sentinel, or a `HexFloat64` string. """ Complex128FillValue = tuple[Complex128Component, Complex128Component] """Permitted JSON shape of the `fill_value` field for `complex128`. A two-element JSON array `[real, imag]` where each component is a `Complex128Component`. """ __all__ = [ "COMPLEX128_DATA_TYPE_NAME", "Complex128Component", "Complex128DataTypeName", "Complex128FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/complex64.py000066400000000000000000000017271517635743000304500ustar00rootroot00000000000000""" Zarr v3 `complex64` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal from zarr_metadata.v3.data_type.float32 import Float32FillValue COMPLEX64_DATA_TYPE_NAME: Final = "complex64" """The `data_type` value for the `complex64` type.""" Complex64DataTypeName = Literal["complex64"] """Literal type of the `data_type` field for `complex64`.""" Complex64Component = Float32FillValue """One real or imaginary component of a `complex64` fill value. Same shape as a `float32` fill value: a JSON number, a named sentinel, or a `HexFloat32` string. """ Complex64FillValue = tuple[Complex64Component, Complex64Component] """Permitted JSON shape of the `fill_value` field for `complex64`. A two-element JSON array `[real, imag]` where each component is a `Complex64Component`. """ __all__ = [ "COMPLEX64_DATA_TYPE_NAME", "Complex64Component", "Complex64DataTypeName", "Complex64FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/float16.py000066400000000000000000000044021517635743000300740ustar00rootroot00000000000000""" Zarr v3 `float16` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ import re from typing import Final, Literal, NewType FLOAT16_DATA_TYPE_NAME: Final = "float16" """The `data_type` value for the `float16` type.""" Float16DataTypeName = Literal["float16"] """Literal type of the `data_type` field for `float16`.""" Float16SpecialFillValue = Literal["NaN", "Infinity", "-Infinity"] """Named non-finite fill values permitted by the spec for IEEE 754 floats.""" HexFloat16 = NewType("HexFloat16", str) """A 6-character hex string (`0x` + 4 hex digits) encoding the unsigned-integer representation of a float16.""" _HEX_FLOAT16_RE: Final = re.compile(r"^0x[0-9a-fA-F]{4}$") def hex_float16(value: str) -> HexFloat16: """Validate `value` as a HexFloat16 and brand it. Raises ValueError if `value` is not exactly `0x` followed by 4 hex digits. """ if not _HEX_FLOAT16_RE.fullmatch(value): raise ValueError(f"Expected '0x' followed by 4 hex digits, got {value!r}") return HexFloat16(value) Float16FillValue = float | int | Float16SpecialFillValue | HexFloat16 """Permitted JSON shape of the `fill_value` field for `float16`. Either a JSON number, one of the named non-finite sentinels (`"NaN"`, `"Infinity"`, `"-Infinity"`), or a `HexFloat16` (`0xYYYY` string encoding the unsigned-integer representation of the IEEE 754 value). """ CANONICAL_NAN_HEX_FLOAT16: Final = "0x7e00" """Canonical hex form of the float16 NaN sentinel `"NaN"`. Per spec the named `"NaN"` sentinel denotes the float with sign=0, the most significant mantissa bit set, and all other mantissa bits zero (the IEEE 754 default quiet NaN). Other NaN bit patterns must be encoded with the explicit hex-string form. """ CANONICAL_POSITIVE_INFINITY_HEX_FLOAT16: Final = "0x7c00" """Canonical hex form of the float16 `"Infinity"` sentinel.""" CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT16: Final = "0xfc00" """Canonical hex form of the float16 `"-Infinity"` sentinel.""" __all__ = [ "CANONICAL_NAN_HEX_FLOAT16", "CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT16", "CANONICAL_POSITIVE_INFINITY_HEX_FLOAT16", "FLOAT16_DATA_TYPE_NAME", "Float16DataTypeName", "Float16FillValue", "Float16SpecialFillValue", "HexFloat16", "hex_float16", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/float32.py000066400000000000000000000044231517635743000300750ustar00rootroot00000000000000""" Zarr v3 `float32` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ import re from typing import Final, Literal, NewType FLOAT32_DATA_TYPE_NAME: Final = "float32" """The `data_type` value for the `float32` type.""" Float32DataTypeName = Literal["float32"] """Literal type of the `data_type` field for `float32`.""" Float32SpecialFillValue = Literal["NaN", "Infinity", "-Infinity"] """Named non-finite fill values permitted by the spec for IEEE 754 floats.""" HexFloat32 = NewType("HexFloat32", str) """A 10-character hex string (`0x` + 8 hex digits) encoding the unsigned-integer representation of a float32.""" _HEX_FLOAT32_RE: Final = re.compile(r"^0x[0-9a-fA-F]{8}$") def hex_float32(value: str) -> HexFloat32: """Validate `value` as a HexFloat32 and brand it. Raises ValueError if `value` is not exactly `0x` followed by 8 hex digits. """ if not _HEX_FLOAT32_RE.fullmatch(value): raise ValueError(f"Expected '0x' followed by 8 hex digits, got {value!r}") return HexFloat32(value) Float32FillValue = float | int | Float32SpecialFillValue | HexFloat32 """Permitted JSON shape of the `fill_value` field for `float32`. Either a JSON number, one of the named non-finite sentinels (`"NaN"`, `"Infinity"`, `"-Infinity"`), or a `HexFloat32` (`0xYYYYYYYY` string encoding the unsigned-integer representation of the IEEE 754 value). """ CANONICAL_NAN_HEX_FLOAT32: Final = "0x7fc00000" """Canonical hex form of the float32 NaN sentinel `"NaN"`. Per spec the named `"NaN"` sentinel denotes the float with sign=0, the most significant mantissa bit set, and all other mantissa bits zero (the IEEE 754 default quiet NaN). Other NaN bit patterns must be encoded with the explicit hex-string form. """ CANONICAL_POSITIVE_INFINITY_HEX_FLOAT32: Final = "0x7f800000" """Canonical hex form of the float32 `"Infinity"` sentinel.""" CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT32: Final = "0xff800000" """Canonical hex form of the float32 `"-Infinity"` sentinel.""" __all__ = [ "CANONICAL_NAN_HEX_FLOAT32", "CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT32", "CANONICAL_POSITIVE_INFINITY_HEX_FLOAT32", "FLOAT32_DATA_TYPE_NAME", "Float32DataTypeName", "Float32FillValue", "Float32SpecialFillValue", "HexFloat32", "hex_float32", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/float64.py000066400000000000000000000044701517635743000301040ustar00rootroot00000000000000""" Zarr v3 `float64` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ import re from typing import Final, Literal, NewType FLOAT64_DATA_TYPE_NAME: Final = "float64" """The `data_type` value for the `float64` type.""" Float64DataTypeName = Literal["float64"] """Literal type of the `data_type` field for `float64`.""" Float64SpecialFillValue = Literal["NaN", "Infinity", "-Infinity"] """Named non-finite fill values permitted by the spec for IEEE 754 floats.""" HexFloat64 = NewType("HexFloat64", str) """An 18-character hex string (`0x` + 16 hex digits) encoding the unsigned-integer representation of a float64.""" _HEX_FLOAT64_RE: Final = re.compile(r"^0x[0-9a-fA-F]{16}$") def hex_float64(value: str) -> HexFloat64: """Validate `value` as a HexFloat64 and brand it. Raises ValueError if `value` is not exactly `0x` followed by 16 hex digits. """ if not _HEX_FLOAT64_RE.fullmatch(value): raise ValueError(f"Expected '0x' followed by 16 hex digits, got {value!r}") return HexFloat64(value) Float64FillValue = float | int | Float64SpecialFillValue | HexFloat64 """Permitted JSON shape of the `fill_value` field for `float64`. Either a JSON number, one of the named non-finite sentinels (`"NaN"`, `"Infinity"`, `"-Infinity"`), or a `HexFloat64` (`0xYYYYYYYYYYYYYYYY` string encoding the unsigned-integer representation of the IEEE 754 value). """ CANONICAL_NAN_HEX_FLOAT64: Final = "0x7ff8000000000000" """Canonical hex form of the float64 NaN sentinel `"NaN"`. Per spec the named `"NaN"` sentinel denotes the float with sign=0, the most significant mantissa bit set, and all other mantissa bits zero (the IEEE 754 default quiet NaN). Other NaN bit patterns must be encoded with the explicit hex-string form. """ CANONICAL_POSITIVE_INFINITY_HEX_FLOAT64: Final = "0x7ff0000000000000" """Canonical hex form of the float64 `"Infinity"` sentinel.""" CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT64: Final = "0xfff0000000000000" """Canonical hex form of the float64 `"-Infinity"` sentinel.""" __all__ = [ "CANONICAL_NAN_HEX_FLOAT64", "CANONICAL_NEGATIVE_INFINITY_HEX_FLOAT64", "CANONICAL_POSITIVE_INFINITY_HEX_FLOAT64", "FLOAT64_DATA_TYPE_NAME", "Float64DataTypeName", "Float64FillValue", "Float64SpecialFillValue", "HexFloat64", "hex_float64", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/int16.py000066400000000000000000000010361517635743000275610ustar00rootroot00000000000000""" Zarr v3 `int16` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal INT16_DATA_TYPE_NAME: Final = "int16" """The `data_type` value for the `int16` type.""" Int16DataTypeName = Literal["int16"] """Literal type of the `data_type` field for `int16`.""" Int16FillValue = int """Permitted JSON shape of the `fill_value` field for `int16`: a JSON integer in [-32768, 32767].""" __all__ = [ "INT16_DATA_TYPE_NAME", "Int16DataTypeName", "Int16FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/int32.py000066400000000000000000000010421517635743000275540ustar00rootroot00000000000000""" Zarr v3 `int32` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal INT32_DATA_TYPE_NAME: Final = "int32" """The `data_type` value for the `int32` type.""" Int32DataTypeName = Literal["int32"] """Literal type of the `data_type` field for `int32`.""" Int32FillValue = int """Permitted JSON shape of the `fill_value` field for `int32`: a JSON integer in [-2**31, 2**31 - 1].""" __all__ = [ "INT32_DATA_TYPE_NAME", "Int32DataTypeName", "Int32FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/int64.py000066400000000000000000000010421517635743000275610ustar00rootroot00000000000000""" Zarr v3 `int64` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal INT64_DATA_TYPE_NAME: Final = "int64" """The `data_type` value for the `int64` type.""" Int64DataTypeName = Literal["int64"] """Literal type of the `data_type` field for `int64`.""" Int64FillValue = int """Permitted JSON shape of the `fill_value` field for `int64`: a JSON integer in [-2**63, 2**63 - 1].""" __all__ = [ "INT64_DATA_TYPE_NAME", "Int64DataTypeName", "Int64FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/int8.py000066400000000000000000000010161517635743000275000ustar00rootroot00000000000000""" Zarr v3 `int8` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal INT8_DATA_TYPE_NAME: Final = "int8" """The `data_type` value for the `int8` type.""" Int8DataTypeName = Literal["int8"] """Literal type of the `data_type` field for `int8`.""" Int8FillValue = int """Permitted JSON shape of the `fill_value` field for `int8`: a JSON integer in [-128, 127].""" __all__ = [ "INT8_DATA_TYPE_NAME", "Int8DataTypeName", "Int8FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/numpy_datetime64.py000066400000000000000000000031421517635743000320160ustar00rootroot00000000000000""" Zarr `numpy.datetime64` data type (zarr-extensions). See https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/numpy.datetime64 """ from typing import Final, Literal from typing_extensions import ReadOnly, TypedDict NUMPY_DATETIME64_DATA_TYPE_NAME: Final = "numpy.datetime64" """The `name` field value of the `numpy.datetime64` data type.""" NumpyDatetime64DataTypeName = Literal["numpy.datetime64"] """Literal type of the `name` field of the `numpy.datetime64` data type.""" DateTimeUnit = Literal[ "Y", "M", "W", "D", "h", "m", "s", "ms", "us", "μs", "ns", "ps", "fs", "as", "generic" ] """Time unit codes used by numpy.datetime64.""" class NumpyDatetime64Configuration(TypedDict): """ Configuration for the `numpy.datetime64` data type. Attributes ---------- unit A string encoding a unit of time. scale_factor The multiplier relative to the unit. """ unit: ReadOnly[DateTimeUnit] scale_factor: ReadOnly[int] class NumpyDatetime64(TypedDict): """`numpy.datetime64` data type metadata.""" name: NumpyDatetime64DataTypeName configuration: NumpyDatetime64Configuration NumpyDatetime64FillValue = int | Literal["NaT"] """Permitted JSON shape of the `fill_value` field for `numpy.datetime64`. Either a JSON integer (count of `unit * scale_factor` since the epoch), or the string `"NaT"` (equivalent to the integer `-2**63`). """ __all__ = [ "NUMPY_DATETIME64_DATA_TYPE_NAME", "DateTimeUnit", "NumpyDatetime64", "NumpyDatetime64Configuration", "NumpyDatetime64DataTypeName", "NumpyDatetime64FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/numpy_timedelta64.py000066400000000000000000000031521517635743000321730ustar00rootroot00000000000000""" Zarr `numpy.timedelta64` data type (zarr-extensions). See https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/numpy.timedelta64 """ from typing import Final, Literal from typing_extensions import ReadOnly, TypedDict NUMPY_TIMEDELTA64_DATA_TYPE_NAME: Final = "numpy.timedelta64" """The `name` field value of the `numpy.timedelta64` data type.""" NumpyTimedelta64DataTypeName = Literal["numpy.timedelta64"] """Literal type of the `name` field of the `numpy.timedelta64` data type.""" DateTimeUnit = Literal[ "Y", "M", "W", "D", "h", "m", "s", "ms", "us", "μs", "ns", "ps", "fs", "as", "generic" ] """Time unit codes used by numpy.timedelta64.""" class NumpyTimedelta64Configuration(TypedDict): """ Configuration for the `numpy.timedelta64` data type. Attributes ---------- unit A string encoding a unit of time. scale_factor The multiplier relative to the unit. """ unit: ReadOnly[DateTimeUnit] scale_factor: ReadOnly[int] class NumpyTimedelta64(TypedDict): """`numpy.timedelta64` data type metadata.""" name: NumpyTimedelta64DataTypeName configuration: NumpyTimedelta64Configuration NumpyTimedelta64FillValue = int | Literal["NaT"] """Permitted JSON shape of the `fill_value` field for `numpy.timedelta64`. Either a JSON integer (a count of `unit * scale_factor`), or the string `"NaT"` (equivalent to the integer `-2**63`). """ __all__ = [ "NUMPY_TIMEDELTA64_DATA_TYPE_NAME", "DateTimeUnit", "NumpyTimedelta64", "NumpyTimedelta64Configuration", "NumpyTimedelta64DataTypeName", "NumpyTimedelta64FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/raw.py000066400000000000000000000024531517635743000274150ustar00rootroot00000000000000""" Zarr v3 `r` raw-bytes data type (parameterised by bit count). The `data_type` value is a string of the form `r` where `N` is a positive multiple of 8 (e.g. `r8`, `r16`, `r24`). See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html """ import re from typing import Final, NewType RawBytesDataTypeName = NewType("RawBytesDataTypeName", str) """A spec-conformant `r` raw-bytes name (e.g. `"r8"`, `"r16"`).""" _RAW_BYTES_RE: Final = re.compile(r"^r(\d+)$") def raw_bytes_dtype_name(value: str) -> RawBytesDataTypeName: """Validate `value` as a `r` raw-bytes name and brand it. Raises ValueError if `value` is not `r` followed by a positive multiple of 8. """ match = _RAW_BYTES_RE.fullmatch(value) if match is None: raise ValueError(f"Expected 'r' followed by a positive integer, got {value!r}") bits = int(match.group(1)) if bits == 0 or bits % 8 != 0: raise ValueError(f"Expected 'r' where N is a positive multiple of 8, got {value!r}") return RawBytesDataTypeName(value) RawBytesFillValue = tuple[int, ...] """Permitted JSON shape of the `fill_value` field for `r`. A JSON array of N/8 integers in `[0, 255]` (one per byte). """ __all__ = [ "RawBytesDataTypeName", "RawBytesFillValue", "raw_bytes_dtype_name", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/string.py000066400000000000000000000011161517635743000301250ustar00rootroot00000000000000""" Zarr `string` data type (variable-length utf-8, zarr-extensions). See https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/string """ from typing import Final, Literal STRING_DATA_TYPE_NAME: Final = "string" """The `data_type` value for the `string` type.""" StringDataTypeName = Literal["string"] """Literal type of the `data_type` field for `string`.""" StringFillValue = str """Permitted JSON shape of the `fill_value` field for `string`: a JSON unicode string.""" __all__ = [ "STRING_DATA_TYPE_NAME", "StringDataTypeName", "StringFillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/struct.py000066400000000000000000000030731517635743000301470ustar00rootroot00000000000000""" Zarr `struct` data type (heterogeneous record, zarr-extensions). See https://github.com/zarr-developers/zarr-extensions/blob/main/data-types/struct/README.md """ from collections.abc import Mapping from typing import Final, Literal from typing_extensions import ReadOnly, TypedDict STRUCT_DATA_TYPE_NAME: Final = "struct" """The `name` field value of the `struct` data type.""" StructDataTypeName = Literal["struct"] """Literal type of the `name` field of the `struct` data type.""" class StructField(TypedDict): """ A single field entry inside a structured dtype. Attributes ---------- name The field name (must be unique within a struct and non-empty). data_type The field's data type. Recursive: may be a bare-string primitive or a named-config envelope including another `struct`. """ name: ReadOnly[str] data_type: ReadOnly[object] class StructConfiguration(TypedDict): """Configuration for the `struct` data type.""" fields: ReadOnly[tuple[StructField, ...]] class Struct(TypedDict): """`struct` data type metadata.""" name: StructDataTypeName configuration: StructConfiguration StructFillValue = Mapping[str, object] """Permitted JSON shape of the `fill_value` field for `struct`. A JSON object mapping each field name to that field's fill value. Field fill values are themselves shaped per the field's `data_type`, recursively. """ __all__ = [ "STRUCT_DATA_TYPE_NAME", "Struct", "StructConfiguration", "StructDataTypeName", "StructField", "StructFillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/uint16.py000066400000000000000000000010451517635743000277460ustar00rootroot00000000000000""" Zarr v3 `uint16` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal UINT16_DATA_TYPE_NAME: Final = "uint16" """The `data_type` value for the `uint16` type.""" Uint16DataTypeName = Literal["uint16"] """Literal type of the `data_type` field for `uint16`.""" Uint16FillValue = int """Permitted JSON shape of the `fill_value` field for `uint16`: a JSON integer in [0, 65535].""" __all__ = [ "UINT16_DATA_TYPE_NAME", "Uint16DataTypeName", "Uint16FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/uint32.py000066400000000000000000000010511517635743000277410ustar00rootroot00000000000000""" Zarr v3 `uint32` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal UINT32_DATA_TYPE_NAME: Final = "uint32" """The `data_type` value for the `uint32` type.""" Uint32DataTypeName = Literal["uint32"] """Literal type of the `data_type` field for `uint32`.""" Uint32FillValue = int """Permitted JSON shape of the `fill_value` field for `uint32`: a JSON integer in [0, 2**32 - 1].""" __all__ = [ "UINT32_DATA_TYPE_NAME", "Uint32DataTypeName", "Uint32FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/uint64.py000066400000000000000000000010511517635743000277460ustar00rootroot00000000000000""" Zarr v3 `uint64` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal UINT64_DATA_TYPE_NAME: Final = "uint64" """The `data_type` value for the `uint64` type.""" Uint64DataTypeName = Literal["uint64"] """Literal type of the `data_type` field for `uint64`.""" Uint64FillValue = int """Permitted JSON shape of the `fill_value` field for `uint64`: a JSON integer in [0, 2**64 - 1].""" __all__ = [ "UINT64_DATA_TYPE_NAME", "Uint64DataTypeName", "Uint64FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/data_type/uint8.py000066400000000000000000000010271517635743000276670ustar00rootroot00000000000000""" Zarr v3 `uint8` data type. See https://zarr-specs.readthedocs.io/en/latest/v3/data-types/index.html """ from typing import Final, Literal UINT8_DATA_TYPE_NAME: Final = "uint8" """The `data_type` value for the `uint8` type.""" Uint8DataTypeName = Literal["uint8"] """Literal type of the `data_type` field for `uint8`.""" Uint8FillValue = int """Permitted JSON shape of the `fill_value` field for `uint8`: a JSON integer in [0, 255].""" __all__ = [ "UINT8_DATA_TYPE_NAME", "Uint8DataTypeName", "Uint8FillValue", ] zarr-python-3.2.1/packages/zarr-metadata/src/zarr_metadata/v3/group.py000066400000000000000000000014141517635743000260020ustar00rootroot00000000000000"""Zarr v3 group metadata types. See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#group-metadata """ from collections.abc import Mapping from typing import Literal, NotRequired from typing_extensions import TypedDict from zarr_metadata.v3.array import ExtensionFieldV3 class GroupMetadataV3(TypedDict, extra_items=ExtensionFieldV3): # type: ignore[call-arg] """ Zarr v3 group metadata document (the `zarr.json` content for a group). Extra keys are permitted if they conform to `ExtensionFieldV3`. See https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#group-metadata """ zarr_format: Literal[3] node_type: Literal["group"] attributes: NotRequired[Mapping[str, object]] __all__ = [ "GroupMetadataV3", ] zarr-python-3.2.1/packages/zarr-metadata/tests/000077500000000000000000000000001517635743000215015ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/__init__.py000066400000000000000000000000371517635743000236120ustar00rootroot00000000000000"""Tests for zarr-metadata.""" zarr-python-3.2.1/packages/zarr-metadata/tests/test_version.py000066400000000000000000000007031517635743000245770ustar00rootroot00000000000000"""Verify that `zarr_metadata.__version__` matches the installed distribution metadata, which in turn comes from `pyproject.toml`. This catches the easy mistake of bumping the version in one place and forgetting the other. """ from __future__ import annotations from importlib.metadata import version import zarr_metadata def test_version_matches_distribution_metadata() -> None: assert zarr_metadata.__version__ == version("zarr-metadata") zarr-python-3.2.1/packages/zarr-metadata/tests/v2/000077500000000000000000000000001517635743000220305ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v2/__init__.py000066400000000000000000000000001517635743000241270ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v2/array/000077500000000000000000000000001517635743000231465ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v2/array/__init__.py000066400000000000000000000000001517635743000252450ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v2/array/blosc_compressor_with_filters.json000066400000000000000000000005361517635743000322060ustar00rootroot00000000000000{ "zarr_format": 2, "shape": [200], "chunks": [50], "dtype": " None: ADAPTER.validate_python(json.loads(fixture.read_text())) zarr-python-3.2.1/packages/zarr-metadata/tests/v2/consolidated/000077500000000000000000000000001517635743000245005ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v2/consolidated/__init__.py000066400000000000000000000000001517635743000265770ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v2/consolidated/minimal.json000066400000000000000000000000661517635743000270230ustar00rootroot00000000000000{ "zarr_consolidated_format": 1, "metadata": {} } zarr-python-3.2.1/packages/zarr-metadata/tests/v2/consolidated/test_fixtures.py000066400000000000000000000010511517635743000277570ustar00rootroot00000000000000"""Decode v2 consolidated metadata fixtures via pydantic.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v2.consolidated import ConsolidatedMetadataV2 FIXTURES_DIR = Path(__file__).parent FIXTURES = sorted(FIXTURES_DIR.glob("*.json")) ADAPTER = TypeAdapter(ConsolidatedMetadataV2) @pytest.mark.parametrize("fixture", FIXTURES, ids=lambda p: p.stem) def test_validate(fixture: Path) -> None: ADAPTER.validate_python(json.loads(fixture.read_text())) zarr-python-3.2.1/packages/zarr-metadata/tests/v2/consolidated/with_array_and_group.json000066400000000000000000000005051517635743000316020ustar00rootroot00000000000000{ "zarr_consolidated_format": 1, "metadata": { ".zgroup": {"zarr_format": 2}, "data/.zarray": { "zarr_format": 2, "shape": [100], "chunks": [10], "dtype": " None: ADAPTER.validate_python(json.loads(fixture.read_text())) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/000077500000000000000000000000001517635743000220315ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/__init__.py000066400000000000000000000000001517635743000241300ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/000077500000000000000000000000001517635743000231475ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/__init__.py000066400000000000000000000000001517635743000252460ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/blosc_codec.json000066400000000000000000000010131517635743000262740ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [1024], "data_type": "int32", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [256]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}}, { "name": "blosc", "configuration": { "cname": "zstd", "clevel": 5, "shuffle": "shuffle", "blocksize": 0, "typesize": 4 } } ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/datatype_named_config.json000066400000000000000000000006361517635743000303530ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [10], "data_type": { "name": "numpy.datetime64", "configuration": {"unit": "ns", "scale_factor": 1} }, "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [10]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/gzip_codec.json000066400000000000000000000006001517635743000261440ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [128], "data_type": "uint16", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [64]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}}, {"name": "gzip", "configuration": {"level": 5}} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/rectilinear_grid.json000066400000000000000000000006461517635743000273560ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [100, 100], "data_type": "float64", "chunk_grid": { "name": "rectilinear", "configuration": { "kind": "inline", "chunk_shapes": [ [10, 20, 30, 40], 50 ] } }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0.0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/rectilinear_grid_with_rle.json000066400000000000000000000007161517635743000312510ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [60, 30], "data_type": "int16", "chunk_grid": { "name": "rectilinear", "configuration": { "kind": "inline", "chunk_shapes": [ [[10, 5], 5, 5], [15, 15] ] } }, "chunk_key_encoding": { "name": "default", "configuration": {"separator": "/"} }, "fill_value": 0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/regular_grid_default_encoding.json000066400000000000000000000005741517635743000320700ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [100, 100], "data_type": "int32", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [10, 10]} }, "chunk_key_encoding": { "name": "default", "configuration": {"separator": "/"} }, "fill_value": 0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/regular_grid_v2_encoding.json000066400000000000000000000005061517635743000307660ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [50], "data_type": "uint8", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [25]} }, "chunk_key_encoding": { "name": "v2", "configuration": {"separator": "."} }, "fill_value": 0, "codecs": [ {"name": "bytes"} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/sharding_indexed_codec.json000066400000000000000000000013161517635743000304770ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [1024, 1024], "data_type": "uint16", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [256, 256]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0, "codecs": [ { "name": "sharding_indexed", "configuration": { "chunk_shape": [64, 64], "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}}, {"name": "gzip", "configuration": {"level": 1}} ], "index_codecs": [ {"name": "bytes", "configuration": {"endian": "little"}}, {"name": "crc32c"} ], "index_location": "end" } } ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/test_fixtures.py000066400000000000000000000014741517635743000264370ustar00rootroot00000000000000"""Decode v3 array metadata fixtures via pydantic. Each `*.json` file in this directory is a representative on-disk `zarr.json` that should validate cleanly as `ArrayMetadataV3`. Fixtures are named for the variant they exercise (regular vs rectilinear grid, blosc/gzip/zstd/sharding_indexed codecs, named-config dtypes, optional fields, extra fields). """ from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.array import ArrayMetadataV3 FIXTURES_DIR = Path(__file__).parent FIXTURES = sorted(FIXTURES_DIR.glob("*.json")) ADAPTER = TypeAdapter(ArrayMetadataV3) @pytest.mark.parametrize("fixture", FIXTURES, ids=lambda p: p.stem) def test_validate(fixture: Path) -> None: ADAPTER.validate_python(json.loads(fixture.read_text())) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/transpose_and_crc32c_codecs.json000066400000000000000000000006701517635743000313640ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [10, 20, 30], "data_type": "float32", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [5, 10, 15]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": "NaN", "codecs": [ {"name": "transpose", "configuration": {"order": [2, 1, 0]}}, {"name": "bytes", "configuration": {"endian": "little"}}, {"name": "crc32c"} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/with_extra_field.json000066400000000000000000000007071517635743000273670ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [10], "data_type": "int32", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [10]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ], "my_custom_extension": { "must_understand": false, "purpose": "exercise the extra_items=ExtensionFieldV3 path" } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/with_optionals.json000066400000000000000000000010741517635743000271070ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [10, 20, 30], "data_type": "float64", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [5, 10, 15]} }, "chunk_key_encoding": { "name": "default", "configuration": {"separator": "/"} }, "fill_value": "NaN", "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ], "attributes": { "description": "fixture exercising optional fields", "tags": ["test", "metadata"] }, "dimension_names": ["t", "y", "x"], "storage_transformers": [] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/array/zstd_codec.json000066400000000000000000000005521517635743000261650ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "array", "shape": [128], "data_type": "int8", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [64]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0, "codecs": [ {"name": "bytes"}, {"name": "zstd", "configuration": {"level": 3, "checksum": false}} ] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/000077500000000000000000000000001517635743000241465ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/__init__.py000066400000000000000000000000001517635743000262450ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/rectilinear/000077500000000000000000000000001517635743000264475ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/rectilinear/__init__.py000066400000000000000000000000001517635743000305460ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/rectilinear/cases.json000066400000000000000000000010421517635743000304350ustar00rootroot00000000000000{ "explicit_per_dim": { "name": "rectilinear", "configuration": { "kind": "inline", "chunk_shapes": [ [10, 20, 30, 40], [50] ] } }, "uniform_dim_shorthand": { "name": "rectilinear", "configuration": { "kind": "inline", "chunk_shapes": [ [10, 20, 30, 40], 50 ] } }, "with_rle_pair": { "name": "rectilinear", "configuration": { "kind": "inline", "chunk_shapes": [ [[10, 5], 5, 5], [15, 15] ] } } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/rectilinear/test_fixtures.py000066400000000000000000000010121517635743000317230ustar00rootroot00000000000000"""Validate rectilinear chunk grid fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.chunk_grid.rectilinear import RectilinearChunkGridMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_chunk_grid(case: object) -> None: TypeAdapter(RectilinearChunkGridMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/regular/000077500000000000000000000000001517635743000256075ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/regular/__init__.py000066400000000000000000000000001517635743000277060ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/regular/cases.json000066400000000000000000000004011517635743000275730ustar00rootroot00000000000000{ "1d": { "name": "regular", "configuration": {"chunk_shape": [10]} }, "2d": { "name": "regular", "configuration": {"chunk_shape": [10, 20]} }, "3d": { "name": "regular", "configuration": {"chunk_shape": [5, 10, 15]} } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_grid/regular/test_fixtures.py000066400000000000000000000007721517635743000310770ustar00rootroot00000000000000"""Validate regular chunk grid fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.chunk_grid.regular import RegularChunkGridMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_chunk_grid(case: object) -> None: TypeAdapter(RegularChunkGridMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/000077500000000000000000000000001517635743000256575ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/__init__.py000066400000000000000000000000001517635743000277560ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/default/000077500000000000000000000000001517635743000273035ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/default/__init__.py000066400000000000000000000000001517635743000314020ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/default/cases.json000066400000000000000000000004121517635743000312710ustar00rootroot00000000000000{ "no_configuration": { "name": "default" }, "slash_separator": { "name": "default", "configuration": {"separator": "/"} }, "dot_separator": { "name": "default", "configuration": {"separator": "."} }, "short_hand_name": "default" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/default/test_fixtures.py000066400000000000000000000010401517635743000325600ustar00rootroot00000000000000"""Validate default chunk-key encoding fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.chunk_key_encoding.default import DefaultChunkKeyEncodingMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_chunk_key_encoding(case: object) -> None: TypeAdapter(DefaultChunkKeyEncodingMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/v2/000077500000000000000000000000001517635743000262065ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/v2/__init__.py000066400000000000000000000000001517635743000303050ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/v2/cases.json000066400000000000000000000003661517635743000302040ustar00rootroot00000000000000{ "no_configuration": { "name": "v2" }, "dot_separator": { "name": "v2", "configuration": {"separator": "."} }, "slash_separator": { "name": "v2", "configuration": {"separator": "/"} }, "short_hand_name": "v2" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/chunk_key_encoding/v2/test_fixtures.py000066400000000000000000000010321517635743000314640ustar00rootroot00000000000000"""Validate v2-compatibility chunk-key encoding fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.chunk_key_encoding.v2 import V2ChunkKeyEncodingMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_chunk_key_encoding(case: object) -> None: TypeAdapter(V2ChunkKeyEncodingMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/000077500000000000000000000000001517635743000231065ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/__init__.py000066400000000000000000000000001517635743000252050ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/blosc/000077500000000000000000000000001517635743000242105ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/blosc/__init__.py000066400000000000000000000000001517635743000263070ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/blosc/cases.json000066400000000000000000000005451517635743000262050ustar00rootroot00000000000000{ "with_typesize": { "name": "blosc", "configuration": { "cname": "zstd", "clevel": 5, "shuffle": "shuffle", "blocksize": 0, "typesize": 4 } }, "no_typesize": { "name": "blosc", "configuration": { "cname": "lz4", "clevel": 1, "shuffle": "noshuffle", "blocksize": 0 } } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/blosc/test_fixtures.py000066400000000000000000000007331517635743000274750ustar00rootroot00000000000000"""Validate blosc codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.blosc import BloscCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(BloscCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/bytes/000077500000000000000000000000001517635743000242345ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/bytes/__init__.py000066400000000000000000000000001517635743000263330ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/bytes/cases.json000066400000000000000000000004201517635743000262210ustar00rootroot00000000000000{ "little_endian": { "name": "bytes", "configuration": {"endian": "little"} }, "big_endian": { "name": "bytes", "configuration": {"endian": "big"} }, "no_endian": { "name": "bytes", "configuration": {} }, "short_hand_name": "bytes" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/bytes/test_fixtures.py000066400000000000000000000007331517635743000275210ustar00rootroot00000000000000"""Validate bytes codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.bytes import BytesCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(BytesCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/cast_value/000077500000000000000000000000001517635743000252345ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/cast_value/__init__.py000066400000000000000000000000001517635743000273330ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/cast_value/cases.json000066400000000000000000000023361517635743000272310ustar00rootroot00000000000000{ "minimal": { "name": "cast_value", "configuration": {"data_type": "uint8"} }, "named_config_dtype": { "name": "cast_value", "configuration": { "data_type": { "name": "numpy.datetime64", "configuration": {"unit": "ns", "scale_factor": 1} } } }, "with_rounding": { "name": "cast_value", "configuration": { "data_type": "int16", "rounding": "towards-zero" } }, "with_out_of_range_clamp": { "name": "cast_value", "configuration": { "data_type": "int8", "out_of_range": "clamp" } }, "with_out_of_range_wrap": { "name": "cast_value", "configuration": { "data_type": "uint8", "out_of_range": "wrap" } }, "with_scalar_map_encode_only": { "name": "cast_value", "configuration": { "data_type": "uint8", "scalar_map": { "encode": [["NaN", 0]] } } }, "numpy_compat_full_example": { "name": "cast_value", "configuration": { "data_type": "uint8", "rounding": "towards-zero", "out_of_range": "wrap", "scalar_map": { "encode": [ ["NaN", 0], ["+Infinity", 0], ["-Infinity", 0] ] } } } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/cast_value/test_fixtures.py000066400000000000000000000007551517635743000305250ustar00rootroot00000000000000"""Validate cast_value codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.cast_value import CastValueCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(CastValueCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/crc32c/000077500000000000000000000000001517635743000241655ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/crc32c/__init__.py000066400000000000000000000000001517635743000262640ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/crc32c/cases.json000066400000000000000000000002421517635743000261540ustar00rootroot00000000000000{ "no_configuration": { "name": "crc32c" }, "empty_configuration": { "name": "crc32c", "configuration": {} }, "short_hand_name": "crc32c" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/crc32c/test_fixtures.py000066400000000000000000000007371517635743000274560ustar00rootroot00000000000000"""Validate crc32c codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.crc32c import Crc32cCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(Crc32cCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/gzip/000077500000000000000000000000001517635743000240575ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/gzip/__init__.py000066400000000000000000000000001517635743000261560ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/gzip/cases.json000066400000000000000000000002561517635743000260530ustar00rootroot00000000000000{ "with_level": { "name": "gzip", "configuration": {"level": 5} }, "no_level": { "name": "gzip", "configuration": {} }, "short_hand_name": "gzip" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/gzip/test_fixtures.py000066400000000000000000000007271517635743000273470ustar00rootroot00000000000000"""Validate gzip codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.gzip import GzipCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(GzipCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/scale_offset/000077500000000000000000000000001517635743000255435ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/scale_offset/__init__.py000066400000000000000000000000001517635743000276420ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/scale_offset/cases.json000066400000000000000000000010611517635743000275320ustar00rootroot00000000000000{ "no_configuration": { "name": "scale_offset" }, "empty_configuration": { "name": "scale_offset", "configuration": {} }, "offset_only": { "name": "scale_offset", "configuration": {"offset": 5} }, "scale_only": { "name": "scale_offset", "configuration": {"scale": 0.1} }, "scale_and_offset": { "name": "scale_offset", "configuration": {"offset": 5, "scale": 0.1} }, "string_encoded_scalar": { "name": "scale_offset", "configuration": {"offset": "NaN"} }, "short_hand_name": "scale_offset" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/scale_offset/test_fixtures.py000066400000000000000000000007651517635743000310350ustar00rootroot00000000000000"""Validate scale_offset codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.scale_offset import ScaleOffsetCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(ScaleOffsetCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/sharding_indexed/000077500000000000000000000000001517635743000264055ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/sharding_indexed/__init__.py000066400000000000000000000000001517635743000305040ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/sharding_indexed/cases.json000066400000000000000000000014001517635743000303710ustar00rootroot00000000000000{ "with_index_location": { "name": "sharding_indexed", "configuration": { "chunk_shape": [64, 64], "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}}, {"name": "gzip", "configuration": {"level": 1}} ], "index_codecs": [ {"name": "bytes", "configuration": {"endian": "little"}}, {"name": "crc32c"} ], "index_location": "end" } }, "no_index_location": { "name": "sharding_indexed", "configuration": { "chunk_shape": [128], "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ], "index_codecs": [ {"name": "bytes", "configuration": {"endian": "little"}}, {"name": "crc32c"} ] } } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/sharding_indexed/test_fixtures.py000066400000000000000000000010051517635743000316630ustar00rootroot00000000000000"""Validate sharding_indexed codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.sharding_indexed import ShardingIndexedCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(ShardingIndexedCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/transpose/000077500000000000000000000000001517635743000251245ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/transpose/__init__.py000066400000000000000000000000001517635743000272230ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/transpose/cases.json000066400000000000000000000001361517635743000271150ustar00rootroot00000000000000{ "reversed_3d": { "name": "transpose", "configuration": {"order": [2, 1, 0]} } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/transpose/test_fixtures.py000066400000000000000000000007531517635743000304130ustar00rootroot00000000000000"""Validate transpose codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.transpose import TransposeCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(TransposeCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/zstd/000077500000000000000000000000001517635743000240725ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/zstd/__init__.py000066400000000000000000000000001517635743000261710ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/zstd/cases.json000066400000000000000000000001401517635743000260560ustar00rootroot00000000000000{ "default": { "name": "zstd", "configuration": {"level": 3, "checksum": false} } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/codec/zstd/test_fixtures.py000066400000000000000000000007271517635743000273620ustar00rootroot00000000000000"""Validate zstd codec fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.codec.zstd import ZstdCodecMetadata CASES: dict[str, object] = json.loads((Path(__file__).parent / "cases.json").read_text()) @pytest.mark.parametrize("case", CASES.values(), ids=list(CASES)) def test_codec(case: object) -> None: TypeAdapter(ZstdCodecMetadata).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/consolidated/000077500000000000000000000000001517635743000245015ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/consolidated/__init__.py000066400000000000000000000000001517635743000266000ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/consolidated/minimal.json000066400000000000000000000001051517635743000270160ustar00rootroot00000000000000{ "kind": "inline", "must_understand": false, "metadata": {} } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/consolidated/test_fixtures.py000066400000000000000000000010511517635743000277600ustar00rootroot00000000000000"""Decode v3 consolidated metadata fixtures via pydantic.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.consolidated import ConsolidatedMetadataV3 FIXTURES_DIR = Path(__file__).parent FIXTURES = sorted(FIXTURES_DIR.glob("*.json")) ADAPTER = TypeAdapter(ConsolidatedMetadataV3) @pytest.mark.parametrize("fixture", FIXTURES, ids=lambda p: p.stem) def test_validate(fixture: Path) -> None: ADAPTER.validate_python(json.loads(fixture.read_text())) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/consolidated/with_array_and_group.json000066400000000000000000000010631517635743000316030ustar00rootroot00000000000000{ "kind": "inline", "must_understand": false, "metadata": { "child_group": { "zarr_format": 3, "node_type": "group" }, "child_array": { "zarr_format": 3, "node_type": "array", "shape": [10], "data_type": "int32", "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [10]} }, "chunk_key_encoding": { "name": "default" }, "fill_value": 0, "codecs": [ {"name": "bytes", "configuration": {"endian": "little"}} ] } } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/000077500000000000000000000000001517635743000240035ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/__init__.py000066400000000000000000000000001517635743000261020ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bool/000077500000000000000000000000001517635743000247365ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bool/__init__.py000066400000000000000000000000001517635743000270350ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bool/fill_values.json000066400000000000000000000000451517635743000301350ustar00rootroot00000000000000{ "true": true, "false": false } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bool/test_fixtures.py000066400000000000000000000007731517635743000302270ustar00rootroot00000000000000"""Validate bool fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.bool import BoolFillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(BoolFillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bytes/000077500000000000000000000000001517635743000251315ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bytes/__init__.py000066400000000000000000000000001517635743000272300ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bytes/fill_values.json000066400000000000000000000000551517635743000303310ustar00rootroot00000000000000{ "tuple": [1, 2, 3], "base64": "AQID" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bytes/test_fixtures.py000066400000000000000000000010171517635743000304120ustar00rootroot00000000000000"""Validate variable-length bytes fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.bytes import BytesFillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(BytesFillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/bytes/test_validators.py000066400000000000000000000022321517635743000307110ustar00rootroot00000000000000"""Cover the `base64_bytes` brand validator. The pydantic-driven fixture tests don't enforce the base64 alphabet or length-multiple-of-4 constraint because `Base64Bytes` is a `NewType`, which pydantic treats as plain `str`. Direct coverage of the validator function lives here. """ from __future__ import annotations import pytest from zarr_metadata.v3.data_type.bytes import base64_bytes VALID = [ "", # empty is valid base64 (length 0, multiple of 4) "AQID", # [1, 2, 3] "AAAA", # [0, 0, 0] "////", # [255, 255, 255] "abcd", "AB==", # padding "ABC=", # padding ] INVALID = [ "AB", # length 2, not multiple of 4 "ABC", # length 3, not multiple of 4 "ABCDE", # length 5 "AB-D", # url-safe alphabet, not standard "AB_D", # url-safe alphabet, not standard "AB!D", # not base64 char "AB CD", # whitespace ] @pytest.mark.parametrize("value", VALID) def test_valid(value: str) -> None: assert base64_bytes(value) == value @pytest.mark.parametrize("value", INVALID) def test_invalid(value: str) -> None: with pytest.raises(ValueError, match="standard-alphabet base64"): base64_bytes(value) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex128/000077500000000000000000000000001517635743000257055ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex128/__init__.py000066400000000000000000000000001517635743000300040ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex128/fill_values.json000066400000000000000000000003401517635743000311020ustar00rootroot00000000000000{ "numeric": [1.5, 2.5], "zero": [0.0, 0.0], "with_sentinel_components": ["-Infinity", "NaN"], "mixed_numeric_and_sentinel": [1.0, "Infinity"], "with_hex_components": ["0x7ff8000000000000", "0x0000000000000000"] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex128/test_fixtures.py000066400000000000000000000014361517635743000311730ustar00rootroot00000000000000"""Validate complex128 fill-value fixtures. A v3 complex fill_value is a two-element JSON array `[real, imag]` where each component is shaped per the corresponding float's fill value: a number, one of the named sentinels (`"NaN"`, `"Infinity"`, `"-Infinity"`), or a hex string of the underlying float's bits. """ from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.complex128 import Complex128FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Complex128FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex64/000077500000000000000000000000001517635743000256245ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex64/__init__.py000066400000000000000000000000001517635743000277230ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex64/fill_values.json000066400000000000000000000003201517635743000310170ustar00rootroot00000000000000{ "numeric": [1.5, 2.5], "zero": [0.0, 0.0], "with_sentinel_components": ["-Infinity", "NaN"], "mixed_numeric_and_sentinel": [1.0, "Infinity"], "with_hex_components": ["0x7fc00000", "0x00000000"] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/complex64/test_fixtures.py000066400000000000000000000014321517635743000311060ustar00rootroot00000000000000"""Validate complex64 fill-value fixtures. A v3 complex fill_value is a two-element JSON array `[real, imag]` where each component is shaped per the corresponding float's fill value: a number, one of the named sentinels (`"NaN"`, `"Infinity"`, `"-Infinity"`), or a hex string of the underlying float's bits. """ from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.complex64 import Complex64FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Complex64FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float16/000077500000000000000000000000001517635743000252575ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float16/__init__.py000066400000000000000000000000001517635743000273560ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float16/fill_values.json000066400000000000000000000002241517635743000304550ustar00rootroot00000000000000{ "zero": 0.0, "nan": "NaN", "infinity": "Infinity", "neg_infinity": "-Infinity", "hex_zero": "0x0000", "hex_signaling_nan": "0x7d00" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float16/test_fixtures.py000066400000000000000000000013401517635743000305370ustar00rootroot00000000000000"""Validate float16 fill-value fixtures. A v3 float fill_value is a JSON number, one of the named non-finite sentinels (`"NaN"`, `"Infinity"`, `"-Infinity"`), or a hex string (`"0xYYYY"`) encoding the unsigned-integer representation of the IEEE 754 value. """ from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.float16 import Float16FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Float16FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float16/test_validators.py000066400000000000000000000016011517635743000310360ustar00rootroot00000000000000"""Cover the `hex_float16` brand validator. The pydantic-driven fixture tests don't enforce hex format because `HexFloat16` is a `NewType`, which pydantic treats as plain `str`. Direct coverage of the validator function lives here. """ from __future__ import annotations import pytest from zarr_metadata.v3.data_type.float16 import hex_float16 VALID = ["0x0000", "0x7c00", "0x7d00", "0xffff", "0xFFFF", "0xAbCd"] INVALID = [ "", "0000", # missing 0x "0x000", # too short "0x00000", # too long "0x000g", # non-hex char "0X0000", # uppercase X " 0x0000 ", # whitespace ] @pytest.mark.parametrize("value", VALID) def test_valid(value: str) -> None: assert hex_float16(value) == value @pytest.mark.parametrize("value", INVALID) def test_invalid(value: str) -> None: with pytest.raises(ValueError, match="Expected '0x'"): hex_float16(value) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float32/000077500000000000000000000000001517635743000252555ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float32/__init__.py000066400000000000000000000000001517635743000273540ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float32/fill_values.json000066400000000000000000000003011517635743000304470ustar00rootroot00000000000000{ "zero": 0.0, "nan": "NaN", "infinity": "Infinity", "neg_infinity": "-Infinity", "hex_zero": "0x00000000", "hex_canonical_nan": "0x7fc00000", "hex_signaling_nan": "0x7fa00000" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float32/test_fixtures.py000066400000000000000000000010071517635743000305350ustar00rootroot00000000000000"""Validate float32 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.float32 import Float32FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Float32FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float32/test_validators.py000066400000000000000000000017561517635743000310470ustar00rootroot00000000000000"""Cover the `hex_float32` brand validator. The pydantic-driven fixture tests don't enforce hex format because `HexFloat32` is a `NewType`, which pydantic treats as plain `str`. Direct coverage of the validator function lives here. """ from __future__ import annotations import pytest from zarr_metadata.v3.data_type.float32 import hex_float32 VALID = [ "0x00000000", "0x7fc00000", # canonical NaN "0x7fa00000", # signaling NaN "0xffffffff", "0xFFFFFFFF", "0xDeadBeef", ] INVALID = [ "", "00000000", # missing 0x "0x0000000", # too short "0x000000000", # too long "0x0000000g", # non-hex char "0X00000000", # uppercase X " 0x00000000 ", # whitespace ] @pytest.mark.parametrize("value", VALID) def test_valid(value: str) -> None: assert hex_float32(value) == value @pytest.mark.parametrize("value", INVALID) def test_invalid(value: str) -> None: with pytest.raises(ValueError, match="Expected '0x'"): hex_float32(value) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float64/000077500000000000000000000000001517635743000252625ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float64/__init__.py000066400000000000000000000000001517635743000273610ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float64/fill_values.json000066400000000000000000000003311517635743000304570ustar00rootroot00000000000000{ "zero": 0.0, "nan": "NaN", "infinity": "Infinity", "neg_infinity": "-Infinity", "hex_zero": "0x0000000000000000", "hex_canonical_nan": "0x7ff8000000000000", "hex_signaling_nan": "0x7ff4000000000000" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float64/test_fixtures.py000066400000000000000000000010071517635743000305420ustar00rootroot00000000000000"""Validate float64 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.float64 import Float64FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Float64FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/float64/test_validators.py000066400000000000000000000021161517635743000310430ustar00rootroot00000000000000"""Cover the `hex_float64` brand validator. The pydantic-driven fixture tests don't enforce hex format because `HexFloat64` is a `NewType`, which pydantic treats as plain `str`. Direct coverage of the validator function lives here. """ from __future__ import annotations import pytest from zarr_metadata.v3.data_type.float64 import hex_float64 VALID = [ "0x0000000000000000", "0x7ff8000000000000", # canonical NaN "0x7ff4000000000000", # signaling NaN "0xffffffffffffffff", "0xFFFFFFFFFFFFFFFF", "0xDeadBeefCafeBabe", ] INVALID = [ "", "0000000000000000", # missing 0x "0x000000000000000", # too short "0x00000000000000000", # too long "0x000000000000000g", # non-hex char "0X0000000000000000", # uppercase X " 0x0000000000000000 ", # whitespace ] @pytest.mark.parametrize("value", VALID) def test_valid(value: str) -> None: assert hex_float64(value) == value @pytest.mark.parametrize("value", INVALID) def test_invalid(value: str) -> None: with pytest.raises(ValueError, match="Expected '0x'"): hex_float64(value) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int16/000077500000000000000000000000001517635743000247445ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int16/__init__.py000066400000000000000000000000001517635743000270430ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int16/fill_values.json000066400000000000000000000001031517635743000301360ustar00rootroot00000000000000{ "zero": 0, "min": -32768, "max": 32767, "negative": -1 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int16/test_fixtures.py000066400000000000000000000007771517635743000302410ustar00rootroot00000000000000"""Validate int16 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.int16 import Int16FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Int16FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int32/000077500000000000000000000000001517635743000247425ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int32/__init__.py000066400000000000000000000000001517635743000270410ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int32/fill_values.json000066400000000000000000000001151517635743000301370ustar00rootroot00000000000000{ "zero": 0, "min": -2147483648, "max": 2147483647, "negative": -1 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int32/test_fixtures.py000066400000000000000000000007771517635743000302370ustar00rootroot00000000000000"""Validate int32 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.int32 import Int32FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Int32FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int64/000077500000000000000000000000001517635743000247475ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int64/__init__.py000066400000000000000000000000001517635743000270460ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int64/fill_values.json000066400000000000000000000001371517635743000301500ustar00rootroot00000000000000{ "zero": 0, "min": -9223372036854775808, "max": 9223372036854775807, "negative": -1 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int64/test_fixtures.py000066400000000000000000000007771517635743000302440ustar00rootroot00000000000000"""Validate int64 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.int64 import Int64FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Int64FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int8/000077500000000000000000000000001517635743000246655ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int8/__init__.py000066400000000000000000000000001517635743000267640ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int8/fill_values.json000066400000000000000000000000771517635743000300710ustar00rootroot00000000000000{ "zero": 0, "min": -128, "max": 127, "negative": -1 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/int8/test_fixtures.py000066400000000000000000000007731517635743000301560ustar00rootroot00000000000000"""Validate int8 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.int8 import Int8FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Int8FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_datetime64/000077500000000000000000000000001517635743000272015ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_datetime64/__init__.py000066400000000000000000000000001517635743000313000ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_datetime64/data_type.json000066400000000000000000000001431517635743000320440ustar00rootroot00000000000000{ "name": "numpy.datetime64", "configuration": { "unit": "ns", "scale_factor": 1 } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_datetime64/fill_values.json000066400000000000000000000000431517635743000323760ustar00rootroot00000000000000{ "int": 12345, "nat": "NaT" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_datetime64/test_fixtures.py000066400000000000000000000013361517635743000324660ustar00rootroot00000000000000"""Validate numpy.datetime64 dtype value and fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.numpy_datetime64 import ( NumpyDatetime64, NumpyDatetime64FillValue, ) DIR = Path(__file__).parent FILL_VALUES: dict[str, object] = json.loads((DIR / "fill_values.json").read_text()) def test_data_type() -> None: TypeAdapter(NumpyDatetime64).validate_python(json.loads((DIR / "data_type.json").read_text())) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(NumpyDatetime64FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_timedelta64/000077500000000000000000000000001517635743000273555ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_timedelta64/__init__.py000066400000000000000000000000001517635743000314540ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_timedelta64/data_type.json000066400000000000000000000001431517635743000322200ustar00rootroot00000000000000{ "name": "numpy.timedelta64", "configuration": { "unit": "s", "scale_factor": 1 } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_timedelta64/fill_values.json000066400000000000000000000000401517635743000325470ustar00rootroot00000000000000{ "int": 42, "nat": "NaT" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/numpy_timedelta64/test_fixtures.py000066400000000000000000000013441517635743000326410ustar00rootroot00000000000000"""Validate numpy.timedelta64 dtype value and fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.numpy_timedelta64 import ( NumpyTimedelta64, NumpyTimedelta64FillValue, ) DIR = Path(__file__).parent FILL_VALUES: dict[str, object] = json.loads((DIR / "fill_values.json").read_text()) def test_data_type() -> None: TypeAdapter(NumpyTimedelta64).validate_python(json.loads((DIR / "data_type.json").read_text())) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(NumpyTimedelta64FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/raw/000077500000000000000000000000001517635743000245745ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/raw/__init__.py000066400000000000000000000000001517635743000266730ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/raw/fill_values.json000066400000000000000000000000471517635743000277750ustar00rootroot00000000000000{ "all_zero_4_bytes": [0, 0, 0, 0] } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/raw/test_fixtures.py000066400000000000000000000010201517635743000300470ustar00rootroot00000000000000"""Validate raw-bytes (`r`) fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.raw import RawBytesFillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(RawBytesFillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/raw/test_validators.py000066400000000000000000000020661517635743000303610ustar00rootroot00000000000000"""Cover the `raw_bytes_dtype_name` brand validator. The pydantic-driven fixture tests don't enforce the `r` shape because `RawBytesDataTypeName` is a `NewType`, which pydantic treats as plain `str`. Direct coverage of the validator function lives here. """ from __future__ import annotations import pytest from zarr_metadata.v3.data_type.raw import raw_bytes_dtype_name VALID = ["r8", "r16", "r24", "r256", "r1024"] INVALID_FORMAT = ["", "8", "R8", "r", "r-8", "r8 ", " r8", "r8r8"] INVALID_BITS = ["r0", "r1", "r7", "r9", "r15", "r17"] @pytest.mark.parametrize("value", VALID) def test_valid(value: str) -> None: assert raw_bytes_dtype_name(value) == value @pytest.mark.parametrize("value", INVALID_FORMAT) def test_invalid_format(value: str) -> None: with pytest.raises(ValueError, match="Expected 'r' followed by"): raw_bytes_dtype_name(value) @pytest.mark.parametrize("value", INVALID_BITS) def test_invalid_bit_count(value: str) -> None: with pytest.raises(ValueError, match="positive multiple of 8"): raw_bytes_dtype_name(value) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/string/000077500000000000000000000000001517635743000253115ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/string/__init__.py000066400000000000000000000000001517635743000274100ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/string/fill_values.json000066400000000000000000000001621517635743000305100ustar00rootroot00000000000000{ "empty": "", "ascii": "hello", "unicode": "héllo 世界", "with_escapes": "line1\nline2\t\"quoted\"" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/string/test_fixtures.py000066400000000000000000000010031517635743000305650ustar00rootroot00000000000000"""Validate string fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.string import StringFillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(StringFillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/struct/000077500000000000000000000000001517635743000253275ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/struct/__init__.py000066400000000000000000000000001517635743000274260ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/struct/data_type.json000066400000000000000000000005151517635743000301750ustar00rootroot00000000000000{ "name": "struct", "configuration": { "fields": [ {"name": "x", "data_type": "float32"}, {"name": "y", "data_type": "float32"}, { "name": "when", "data_type": { "name": "numpy.datetime64", "configuration": {"unit": "ns", "scale_factor": 1} } } ] } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/struct/fill_values.json000066400000000000000000000001041517635743000305220ustar00rootroot00000000000000{ "all_fields": { "x": 0.0, "y": 1.0, "when": 0 } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/struct/test_fixtures.py000066400000000000000000000012311517635743000306060ustar00rootroot00000000000000"""Validate struct dtype value and fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.struct import Struct, StructFillValue DIR = Path(__file__).parent FILL_VALUES: dict[str, object] = json.loads((DIR / "fill_values.json").read_text()) def test_data_type() -> None: TypeAdapter(Struct).validate_python(json.loads((DIR / "data_type.json").read_text())) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(StructFillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/test_dtype_names.py000066400000000000000000000062001517635743000277220ustar00rootroot00000000000000"""Validate every primitive Zarr v3 data-type name string. Primitive dtypes are encoded as bare strings in the `data_type` field of a v3 array metadata document (e.g. `"int32"`, `"float64"`). Each must validate as its declared per-dtype `*Name` literal type. """ from __future__ import annotations import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.bool import BOOL_DATA_TYPE_NAME, BoolDataTypeName from zarr_metadata.v3.data_type.bytes import BYTES_DATA_TYPE_NAME, BytesDataTypeName from zarr_metadata.v3.data_type.complex64 import COMPLEX64_DATA_TYPE_NAME, Complex64DataTypeName from zarr_metadata.v3.data_type.complex128 import COMPLEX128_DATA_TYPE_NAME, Complex128DataTypeName from zarr_metadata.v3.data_type.float16 import FLOAT16_DATA_TYPE_NAME, Float16DataTypeName from zarr_metadata.v3.data_type.float32 import FLOAT32_DATA_TYPE_NAME, Float32DataTypeName from zarr_metadata.v3.data_type.float64 import FLOAT64_DATA_TYPE_NAME, Float64DataTypeName from zarr_metadata.v3.data_type.int8 import INT8_DATA_TYPE_NAME, Int8DataTypeName from zarr_metadata.v3.data_type.int16 import INT16_DATA_TYPE_NAME, Int16DataTypeName from zarr_metadata.v3.data_type.int32 import INT32_DATA_TYPE_NAME, Int32DataTypeName from zarr_metadata.v3.data_type.int64 import INT64_DATA_TYPE_NAME, Int64DataTypeName from zarr_metadata.v3.data_type.raw import raw_bytes_dtype_name from zarr_metadata.v3.data_type.string import STRING_DATA_TYPE_NAME, StringDataTypeName from zarr_metadata.v3.data_type.uint8 import UINT8_DATA_TYPE_NAME, Uint8DataTypeName from zarr_metadata.v3.data_type.uint16 import UINT16_DATA_TYPE_NAME, Uint16DataTypeName from zarr_metadata.v3.data_type.uint32 import UINT32_DATA_TYPE_NAME, Uint32DataTypeName from zarr_metadata.v3.data_type.uint64 import UINT64_DATA_TYPE_NAME, Uint64DataTypeName # (name_string, per-dtype literal type) PRIMITIVE_DTYPES = [ (BOOL_DATA_TYPE_NAME, BoolDataTypeName), (INT8_DATA_TYPE_NAME, Int8DataTypeName), (INT16_DATA_TYPE_NAME, Int16DataTypeName), (INT32_DATA_TYPE_NAME, Int32DataTypeName), (INT64_DATA_TYPE_NAME, Int64DataTypeName), (UINT8_DATA_TYPE_NAME, Uint8DataTypeName), (UINT16_DATA_TYPE_NAME, Uint16DataTypeName), (UINT32_DATA_TYPE_NAME, Uint32DataTypeName), (UINT64_DATA_TYPE_NAME, Uint64DataTypeName), (FLOAT16_DATA_TYPE_NAME, Float16DataTypeName), (FLOAT32_DATA_TYPE_NAME, Float32DataTypeName), (FLOAT64_DATA_TYPE_NAME, Float64DataTypeName), (COMPLEX64_DATA_TYPE_NAME, Complex64DataTypeName), (COMPLEX128_DATA_TYPE_NAME, Complex128DataTypeName), (STRING_DATA_TYPE_NAME, StringDataTypeName), (BYTES_DATA_TYPE_NAME, BytesDataTypeName), ] @pytest.mark.parametrize(("name", "literal_type"), PRIMITIVE_DTYPES, ids=lambda x: str(x)) def test_primitive_against_literal(name: str, literal_type: object) -> None: """The dtype name validates against its declared Literal type.""" TypeAdapter(literal_type).validate_python(name) @pytest.mark.parametrize("raw_name", ["r8", "r16", "r24", "r256", "r1024"], ids=str) def test_raw_bytes_name(raw_name: str) -> None: """`r` names pass the raw_bytes_dtype_name validator.""" raw_bytes_dtype_name(raw_name) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint16/000077500000000000000000000000001517635743000251315ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint16/__init__.py000066400000000000000000000000001517635743000272300ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint16/fill_values.json000066400000000000000000000000401517635743000303230ustar00rootroot00000000000000{ "zero": 0, "max": 65535 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint16/test_fixtures.py000066400000000000000000000010031517635743000304050ustar00rootroot00000000000000"""Validate uint16 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.uint16 import Uint16FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Uint16FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint32/000077500000000000000000000000001517635743000251275ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint32/__init__.py000066400000000000000000000000001517635743000272260ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint32/fill_values.json000066400000000000000000000000451517635743000303260ustar00rootroot00000000000000{ "zero": 0, "max": 4294967295 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint32/test_fixtures.py000066400000000000000000000010031517635743000304030ustar00rootroot00000000000000"""Validate uint32 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.uint32 import Uint32FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Uint32FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint64/000077500000000000000000000000001517635743000251345ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint64/__init__.py000066400000000000000000000000001517635743000272330ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint64/fill_values.json000066400000000000000000000000571517635743000303360ustar00rootroot00000000000000{ "zero": 0, "max": 18446744073709551615 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint64/test_fixtures.py000066400000000000000000000010031517635743000304100ustar00rootroot00000000000000"""Validate uint64 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.uint64 import Uint64FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Uint64FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint8/000077500000000000000000000000001517635743000250525ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint8/__init__.py000066400000000000000000000000001517635743000271510ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint8/fill_values.json000066400000000000000000000000361517635743000302510ustar00rootroot00000000000000{ "zero": 0, "max": 255 } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/data_type/uint8/test_fixtures.py000066400000000000000000000007771517635743000303470ustar00rootroot00000000000000"""Validate uint8 fill-value fixtures.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.data_type.uint8 import Uint8FillValue FILL_VALUES: dict[str, object] = json.loads( (Path(__file__).parent / "fill_values.json").read_text() ) @pytest.mark.parametrize("case", FILL_VALUES.values(), ids=list(FILL_VALUES)) def test_fill_value(case: object) -> None: TypeAdapter(Uint8FillValue).validate_python(case) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/group/000077500000000000000000000000001517635743000231655ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/group/__init__.py000066400000000000000000000000001517635743000252640ustar00rootroot00000000000000zarr-python-3.2.1/packages/zarr-metadata/tests/v3/group/minimal.json000066400000000000000000000000571517635743000255100ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "group" } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/group/test_fixtures.py000066400000000000000000000010151517635743000264440ustar00rootroot00000000000000"""Decode v3 group metadata fixtures via pydantic.""" from __future__ import annotations import json from pathlib import Path import pytest from pydantic import TypeAdapter from zarr_metadata.v3.group import GroupMetadataV3 FIXTURES_DIR = Path(__file__).parent FIXTURES = sorted(FIXTURES_DIR.glob("*.json")) ADAPTER = TypeAdapter(GroupMetadataV3) @pytest.mark.parametrize("fixture", FIXTURES, ids=lambda p: p.stem) def test_validate(fixture: Path) -> None: ADAPTER.validate_python(json.loads(fixture.read_text())) zarr-python-3.2.1/packages/zarr-metadata/tests/v3/group/with_attributes.json000066400000000000000000000002031517635743000272740ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "group", "attributes": { "label": "root", "spatial_units": ["meter", "meter"] } } zarr-python-3.2.1/packages/zarr-metadata/tests/v3/group/with_extra_field.json000066400000000000000000000002301517635743000273740ustar00rootroot00000000000000{ "zarr_format": 3, "node_type": "group", "consolidated_metadata": { "must_understand": false, "kind": "inline", "metadata": {} } } zarr-python-3.2.1/pyproject.toml000066400000000000000000000312021517635743000167370ustar00rootroot00000000000000[build-system] requires = ["hatchling>=1.29.0", "hatch-vcs"] build-backend = "hatchling.build" [tool.hatch.build.targets.sdist] exclude = [ "/.github", "/bench", "/docs", ] [project] name = "zarr" description = "An implementation of chunked, compressed, N-dimensional arrays for Python" readme = { file = "README.md", content-type = "text/markdown" } authors = [ { name = "Alistair Miles", email = "alimanfoo@googlemail.com" }, ] maintainers = [ { name = "Davis Bennett", email = "davis.v.bennett@gmail.com" }, { name = "jakirkham" }, { name = "Josh Moore", email = "josh@openmicroscopy.org" }, { name = "Joe Hamman", email = "joe@earthmover.io" }, { name = "Juan Nunez-Iglesias", email = "juan.nunez-iglesias@monash.edu" }, { name = "Martin Durant", email = "mdurant@anaconda.com" }, { name = "Norman Rzepka" }, { name = "Ryan Abernathey" }, { name = "David Stansby" }, { name = "Tom Augspurger", email = "tom.w.augspurger@gmail.com" }, { name = "Deepak Cherian" } ] requires-python = ">=3.12" # If you add a new dependency here, please also add it to .pre-commit-config.yaml dependencies = [ 'packaging>=22.0', 'numpy>=2', 'numcodecs>=0.14', 'google-crc32c>=1.5', 'typing_extensions>=4.13', 'donfig>=0.8', ] dynamic = [ "version", ] classifiers = [ 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'Intended Audience :: Science/Research', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', 'Operating System :: Unix', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.14', ] license = "MIT" license-files = ["LICENSE.txt"] keywords = ["Python", "compressed", "ndimensional-arrays", "zarr"] [project.optional-dependencies] # User-facing extras (shipped in package metadata) remote = [ "fsspec>=2023.10.0", "obstore>=0.5.1", ] gpu = [ "cupy-cuda12x", ] cast-value-rs = ["cast-value-rs"] cli = ["typer"] optional = ["universal-pathlib"] [project.scripts] zarr = "zarr._cli.cli:app" [project.urls] issues = "https://github.com/zarr-developers/zarr-python/issues" changelog = "https://zarr.readthedocs.io/en/stable/release-notes.html" Discussions = "https://github.com/zarr-developers/zarr-python/discussions" documentation = "https://zarr.readthedocs.io/" homepage = "https://github.com/zarr-developers/zarr-python" [dependency-groups] test = [ "coverage>=7.10", "pytest", "pytest-asyncio", "pytest-cov", "pytest-accept", "numpydoc", "hypothesis", "pytest-xdist", "pytest-benchmark", "pytest-codspeed", "tomlkit", "uv", ] remote-tests = [ {include-group = "test"}, "fsspec>=2023.10.0", "obstore>=0.5.1", "botocore", "s3fs>=2023.10.0", "moto[s3,server]", "requests", ] docs = [ # Doc building "mkdocs-material[imaging]>=9.6.14", "mkdocs>=1.6.1,<2", "mkdocstrings>=0.29.1", "mkdocstrings-python>=1.16.10", "mike>=2.1.3", "mkdocs-jupyter>=0.25.1", "mkdocs-redirects>=1.2.0", "markdown-exec[ansi]", "griffe-inherited-docstrings", "ruff", # Changelog generation "towncrier", # Optional dependencies to run examples "numcodecs[msgpack]", "s3fs>=2023.10.0", "astroid<4", "pytest", ] dev = [ {include-group = "test"}, {include-group = "remote-tests"}, {include-group = "docs"}, "universal-pathlib", "mypy", ] [tool.coverage.report] exclude_also = [ 'if TYPE_CHECKING:', ] [tool.coverage.run] omit = [ "bench/compress_normal.py", "src/zarr/testing/conftest.py", # only for downstream projects ] [tool.hatch] version.source = "vcs" [tool.hatch.build] hooks.vcs.version-file = "src/zarr/_version.py" [tool.hatch.envs.test] dependency-groups = ["test"] [tool.hatch.envs.test.env-vars] [[tool.hatch.envs.test.matrix]] python = ["3.12", "3.13", "3.14"] deps = ["minimal", "optional"] [tool.hatch.envs.test.overrides] matrix.deps.features = [ {value = "remote", if = ["optional"]}, {value = "optional", if = ["optional"]}, {value = "cli", if = ["optional"]}, {value = "cast-value-rs", if = ["optional"]}, ] matrix.deps.dependency-groups = [ {value = "remote-tests", if = ["optional"]}, ] [tool.hatch.envs.test.scripts] run-coverage = [ "coverage run --source=src -m pytest --ignore tests/benchmarks --junitxml=junit.xml -o junit_family=legacy {args:}", "coverage xml", ] run-coverage-html = [ "coverage run --source=src -m pytest --ignore tests/benchmarks {args:}", "coverage html", ] run = "pytest --ignore tests/benchmarks" run-verbose = "run-coverage --verbose" run-mypy = "mypy src" run-hypothesis = [ "coverage run --source=src -m pytest -nauto --run-slow-hypothesis tests/test_properties.py tests/test_store/test_stateful* {args:}", "coverage xml", ] run-benchmark = "pytest --benchmark-enable tests/benchmarks" serve-coverage-html = "python -m http.server -d htmlcov 8000" list-env = "pip list" [tool.hatch.envs.gputest] template = "test" extra-dependencies = [ "universal_pathlib", ] features = ["gpu"] [[tool.hatch.envs.gputest.matrix]] python = ["3.12", "3.13"] [tool.hatch.envs.gputest.scripts] run-coverage = [ "coverage run --source=src -m pytest -m gpu --junitxml=junit.xml -o junit_family=legacy --ignore tests/benchmarks {args:}", "coverage xml", ] run = "pytest -m gpu --ignore tests/benchmarks" [tool.hatch.envs.upstream] template = 'test' python = "3.14" extra-dependencies = [ 'packaging @ git+https://github.com/pypa/packaging', 'numpy', # from scientific-python-nightly-wheels 'numcodecs @ git+https://github.com/zarr-developers/numcodecs', 's3fs @ git+https://github.com/fsspec/s3fs', 'universal_pathlib @ git+https://github.com/fsspec/universal_pathlib', 'typing_extensions @ git+https://github.com/python/typing_extensions', 'donfig @ git+https://github.com/pytroll/donfig', 'obstore @ git+https://github.com/developmentseed/obstore@main#subdirectory=obstore', ] [tool.hatch.envs.upstream.env-vars] PIP_INDEX_URL = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/" PIP_EXTRA_INDEX_URL = "https://pypi.org/simple/" PIP_PRE = "1" [tool.hatch.envs.min_deps] description = """Test environment for minimum supported dependencies See Spec 0000 for details and drop schedule: https://scientific-python.org/specs/spec-0000/ """ template = "test" python = "3.12" features = ["remote"] dependency-groups = ["remote-tests"] extra-dependencies = [ 'packaging==22.*', 'numpy==2.0.*', 'numcodecs==0.14.*', # 0.14 needed for zarr3 codecs 'fsspec==2023.10.0', 's3fs==2023.10.0', 'universal_pathlib==0.2.0', 'typing_extensions==4.13.*', 'donfig==0.8.*', 'obstore==0.5.*', ] [tool.hatch.envs.defaults] installer = "uv" [tool.hatch.envs.docs] features = ['remote'] dependency-groups = ['docs'] [tool.hatch.envs.docs.env-vars] DISABLE_MKDOCS_2_WARNING = "true" NO_MKDOCS_2_WARNING = "true" [tool.hatch.envs.docs.scripts] serve = "mkdocs serve --watch src" build = "mkdocs build" check = "mkdocs build --strict" readthedocs = "rm -rf $READTHEDOCS_OUTPUT/html && cp -r site $READTHEDOCS_OUTPUT/html" [tool.hatch.envs.doctest] description = "Test environment for validating executable code blocks in documentation" features = ['remote'] dependency-groups = ['test'] extra-dependencies = [ "s3fs>=2023.10.0", "pytest-examples", ] [tool.hatch.envs.doctest.scripts] test = "pytest tests/test_docs.py -v" list-env = "pip list" [tool.ruff] line-length = 100 force-exclude = true extend-exclude = [ ".bzr", ".direnv", ".eggs", ".git", ".mypy_cache", ".nox", ".pants.d", ".ruff_cache", ".venv", "__pypackages__", "_build", "buck-out", "build", "dist", "venv", "docs", "tests/test_regression/scripts/", # these are scripts that use a different version of python "src/zarr/v2/", "tests/v2/", ] [tool.ruff.lint] extend-select = [ "ANN", # flake8-annotations "B", # flake8-bugbear "C4", # flake8-comprehensions "EXE", # flake8-executable "FA", # flake8-future-annotations "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format "I", # isort "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PERF", # Perflint "PIE", # flake8-pie "PGH", # pygrep-hooks "PT", # flake8-pytest-style "PYI", # flake8-pyi "RET", # flake8-return "RSE", # flake8-raise "RUF", "SIM", # flake8-simplify "SLOT", # flake8-slots "TC", # flake8-type-checking "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle warnings ] ignore = [ "ANN401", "PT011", # TODO: apply this rule "RET505", "RET506", "RUF005", "RUF043", "SIM108", "TRY003", # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", "E114", "E117", "D206", "D300", "Q000", "Q001", "Q002", "Q003", "COM812", "COM819", "TC006", ] [tool.ruff.lint.extend-per-file-ignores] "tests/**" = ["ANN001", "ANN201", "RUF029", "SIM117", "SIM300"] [tool.mypy] python_version = "3.12" ignore_missing_imports = true namespace_packages = false strict = true warn_unreachable = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] [[tool.mypy.overrides]] module = [ "tests.package_with_entrypoint.*", "zarr.testing.stateful", "tests.test_codecs.test_transpose", "tests.test_config", "tests.test_store.test_zip", "tests.test_store.test_local", "tests.test_store.test_fsspec", "tests.test_store.test_memory", "tests.test_codecs.test_codecs", "tests.test_metadata.*", "tests.test_store.test_core", "tests.test_store.test_logging", "tests.test_store.test_object", "tests.test_store.test_stateful", "tests.test_store.test_wrapper", ] strict = false # TODO: Move the next modules up to the strict = false section # and fix the errors [[tool.mypy.overrides]] module = [ "tests.test_group", "tests.test_indexing", "tests.test_properties", "tests.test_sync", "tests.test_regression.scripts.*" ] ignore_errors = true [tool.pytest.ini_options] minversion = "7" testpaths = ["tests", "docs/user-guide"] log_cli_level = "INFO" log_level = "INFO" xfail_strict = true asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" doctest_optionflags = [ "NORMALIZE_WHITESPACE", "ELLIPSIS", "IGNORE_EXCEPTION_DETAIL", ] addopts = [ "--benchmark-columns", "min,mean,stddev,outliers,rounds,iterations", "--benchmark-disable", # benchmark routines run as tests without benchmarking instrumentation "--durations", "10", "-ra", "--strict-config", "--strict-markers", ] filterwarnings = [ "error", "ignore:Unclosed client session None: """ Print version info for use in bug reports. """ import platform from importlib.metadata import version def print_packages(packages: list[str]) -> None: not_installed = [] for package in packages: try: print(f"{package}: {version(package)}") except ModuleNotFoundError: not_installed.append(package) if not_installed: print("\n**Not Installed:**") for package in not_installed: print(package) required = [ "packaging", "numpy", "numcodecs", "typing_extensions", "donfig", ] optional = [ "botocore", "cupy-cuda12x", "fsspec", "numcodecs", "s3fs", "gcsfs", "universal-pathlib", "obstore", ] print(f"platform: {platform.platform()}") print(f"python: {platform.python_version()}") print(f"zarr: {__version__}\n") print("**Required dependencies:**") print_packages(required) print("\n**Optional dependencies:**") print_packages(optional) # The decorator ensures this always returns the same handler (and it is only # attached once). @functools.cache def _ensure_handler() -> logging.Handler: """ The first time this function is called, attach a `StreamHandler` using the same format as `logging.basicConfig` to the Zarr-Python root logger. Return this handler every time this function is called. """ handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) _logger.addHandler(handler) return handler def set_log_level( level: Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], ) -> None: """Set the logging level for Zarr-Python. Zarr-Python uses the standard library `logging` framework under the root logger 'zarr'. This is a helper function to: - set Zarr-Python's root logger level - set the root logger handler's level, creating the handler if it does not exist yet Parameters ---------- level : str The logging level to set. """ _logger.setLevel(level) _ensure_handler().setLevel(level) def set_format(log_format: str) -> None: """Set the format of logging messages from Zarr-Python. Zarr-Python uses the standard library `logging` framework under the root logger 'zarr'. This sets the format of log messages from the root logger's StreamHandler. Parameters ---------- log_format : str A string determining the log format (as defined in the standard library's `logging` module for logging.Formatter) """ _ensure_handler().setFormatter(logging.Formatter(fmt=log_format)) __all__ = [ "Array", "AsyncArray", "AsyncGroup", "Group", "__version__", "array", "config", "consolidate_metadata", "copy", "copy_all", "copy_store", "create", "create_array", "create_group", "create_hierarchy", "empty", "empty_like", "from_array", "full", "full_like", "group", "load", "ones", "ones_like", "open", "open_array", "open_consolidated", "open_group", "open_like", "print_debug_info", "save", "save_array", "save_group", "tree", "zeros", "zeros_like", ] zarr-python-3.2.1/src/zarr/_cli/000077500000000000000000000000001517635743000165005ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/_cli/__init__.py000066400000000000000000000000001517635743000205770ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/_cli/cli.py000066400000000000000000000133341517635743000176250ustar00rootroot00000000000000import logging from enum import StrEnum from typing import Annotated, Literal, cast import typer import zarr import zarr.metadata.migrate_v3 as migrate_metadata from zarr.core.common import ZarrFormat from zarr.core.sync import sync from zarr.storage._common import make_store app = typer.Typer() logger = logging.getLogger(__name__) def _set_logging_level(*, verbose: bool) -> None: if verbose: lvl = "INFO" else: lvl = "WARNING" zarr.set_log_level(cast(Literal["INFO", "WARNING"], lvl)) zarr.set_format("%(message)s") class CLIZarrFormat(StrEnum): v2 = "v2" v3 = "v3" class CLIZarrFormatV3(StrEnum): """Limit CLI choice to only v3""" v3 = "v3" @app.command() # type: ignore[untyped-decorator] def migrate( zarr_format: Annotated[ CLIZarrFormatV3, typer.Argument( help="Zarr format to migrate to. Currently only 'v3' is supported.", ), ], input_store: Annotated[ str, typer.Argument( help=( "Input Zarr to migrate - should be a store, path to directory in file system or name of zip file " "e.g. 'data/example-1.zarr', 's3://example-bucket/example'..." ) ), ], output_store: Annotated[ str | None, typer.Argument( help=( "Output location to write generated metadata (no array data will be copied). If not provided, " "metadata will be written to input_store. Should be a store, path to directory in file system " "or name of zip file e.g. 'data/example-1.zarr', 's3://example-bucket/example'..." ) ), ] = None, dry_run: Annotated[ bool, typer.Option( help="Enable a dry-run: files that would be converted are logged, but no new files are created or changed." ), ] = False, overwrite: Annotated[ bool, typer.Option( help="Remove any existing v3 metadata at the output location, before migration starts." ), ] = False, force: Annotated[ bool, typer.Option( help=( "Only used when --overwrite is given. Allows v3 metadata to be removed when no valid " "v2 metadata exists at the output location." ) ), ] = False, remove_v2_metadata: Annotated[ bool, typer.Option( help="Remove v2 metadata (if any) from the output location, after migration is complete." ), ] = False, ) -> None: """Migrate all v2 metadata in a zarr hierarchy to v3. This will create a zarr.json file for each level (every group / array). v2 files (.zarray, .zattrs etc.) will be left as-is. """ if dry_run: _set_logging_level(verbose=True) logger.info( "Dry run enabled - no new files will be created or changed. Log of files that would be created on a real run:" ) input_zarr_store = sync(make_store(input_store, mode="r+")) if output_store is not None: output_zarr_store = sync(make_store(output_store, mode="w-")) write_store = output_zarr_store else: output_zarr_store = None write_store = input_zarr_store if overwrite: sync(migrate_metadata.remove_metadata(write_store, 3, force=force, dry_run=dry_run)) migrate_metadata.migrate_v2_to_v3( input_store=input_zarr_store, output_store=output_zarr_store, dry_run=dry_run ) if remove_v2_metadata: # There should always be valid v3 metadata at the output location after migration, so force=False sync(migrate_metadata.remove_metadata(write_store, 2, force=False, dry_run=dry_run)) @app.command() # type: ignore[untyped-decorator] def remove_metadata( zarr_format: Annotated[ CLIZarrFormat, typer.Argument(help="Which format's metadata to remove - v2 or v3."), ], store: Annotated[ str, typer.Argument( help="Store or path to directory in file system or name of zip file e.g. 'data/example-1.zarr', 's3://example-bucket/example'..." ), ], force: Annotated[ bool, typer.Option( help=( "Allow metadata to be deleted when no valid alternative exists e.g. allow deletion of v2 metadata, " "when no v3 metadata is present." ) ), ] = False, dry_run: Annotated[ bool, typer.Option( help="Enable a dry-run: files that would be deleted are logged, but no files are removed or changed." ), ] = False, ) -> None: """Remove all v2 (.zarray, .zattrs, .zgroup, .zmetadata) or v3 (zarr.json) metadata files from the given Zarr. Note - this will remove metadata files at all levels of the hierarchy (every group and array). """ if dry_run: _set_logging_level(verbose=True) logger.info( "Dry run enabled - no files will be deleted or changed. Log of files that would be deleted on a real run:" ) input_zarr_store = sync(make_store(store, mode="r+")) sync( migrate_metadata.remove_metadata( store=input_zarr_store, zarr_format=cast(ZarrFormat, int(zarr_format[1:])), force=force, dry_run=dry_run, ) ) @app.callback() # type: ignore[untyped-decorator] def main( verbose: Annotated[ bool, typer.Option( help="enable verbose logging - will print info about metadata files being deleted / saved." ), ] = False, ) -> None: """ See available commands below - access help for individual commands with zarr COMMAND --help. """ _set_logging_level(verbose=verbose) if __name__ == "__main__": app() zarr-python-3.2.1/src/zarr/_compat.py000066400000000000000000000067551517635743000176030ustar00rootroot00000000000000import warnings from collections.abc import Callable from functools import wraps from inspect import Parameter, signature from typing import TYPE_CHECKING, Any import numpy as np from packaging.version import Version from zarr.errors import ZarrFutureWarning if TYPE_CHECKING: from numpy.typing import NDArray # Based off https://github.com/scikit-learn/scikit-learn/blob/e87b32a81c70abed8f2e97483758eb64df8255e9/sklearn/utils/validation.py#L63 def _deprecate_positional_args[T]( func: Callable[..., T] | None = None, *, version: str = "3.1.0" ) -> Callable[..., T]: """Decorator for methods that issues warnings for positional arguments. Using the keyword-only argument syntax in pep 3102, arguments after the * will issue a warning when passed as a positional argument. Parameters ---------- func : callable, default=None Function to check arguments on. version : callable, default="3.1.0" The version when positional arguments will result in error. """ def _inner_deprecate_positional_args(f: Callable[..., T]) -> Callable[..., T]: sig = signature(f) kwonly_args = [] all_args = [] for name, param in sig.parameters.items(): if param.kind == Parameter.POSITIONAL_OR_KEYWORD: all_args.append(name) elif param.kind == Parameter.KEYWORD_ONLY: kwonly_args.append(name) @wraps(f) def inner_f(*args: Any, **kwargs: Any) -> T: extra_args = len(args) - len(all_args) if extra_args <= 0: return f(*args, **kwargs) # extra_args > 0 args_msg = [ f"{name}={arg}" for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:], strict=False) ] formatted_args_msg = ", ".join(args_msg) warnings.warn( ( f"Pass {formatted_args_msg} as keyword args. From version " f"{version} passing these as positional arguments " "will result in an error" ), ZarrFutureWarning, stacklevel=2, ) kwargs.update(zip(sig.parameters, args, strict=False)) return f(**kwargs) return inner_f if func is not None: return _inner_deprecate_positional_args(func) return _inner_deprecate_positional_args # type: ignore[return-value] def _reshape_view(arr: "NDArray[Any]", shape: tuple[int, ...]) -> "NDArray[Any]": """Reshape an array without copying data. This function provides compatibility across NumPy versions for reshaping arrays as views. On NumPy >= 2.1, it uses ``reshape(copy=False)`` which explicitly fails if a view cannot be created. On older versions, it uses direct shape assignment which has the same behavior but is deprecated in 2.5+. Parameters ---------- arr : NDArray The array to reshape. shape : tuple of int The new shape. Returns ------- NDArray A reshaped view of the array. Raises ------ AttributeError If a view cannot be created (the array is not contiguous) on NumPy < 2.1. ValueError If a view cannot be created (the array is not contiguous) on NumPy >= 2.1. """ if Version(np.__version__) >= Version("2.1"): return arr.reshape(shape, copy=False) # type: ignore[call-overload, no-any-return] else: arr.shape = shape return arr zarr-python-3.2.1/src/zarr/abc/000077500000000000000000000000001517635743000163175ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/abc/__init__.py000066400000000000000000000000001517635743000204160ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/abc/buffer.py000066400000000000000000000003041517635743000201370ustar00rootroot00000000000000from zarr.core.buffer.core import ArrayLike, Buffer, BufferPrototype, NDArrayLike, NDBuffer __all__ = [ "ArrayLike", "Buffer", "BufferPrototype", "NDArrayLike", "NDBuffer", ] zarr-python-3.2.1/src/zarr/abc/codec.py000066400000000000000000000400731517635743000177520ustar00rootroot00000000000000from __future__ import annotations from abc import abstractmethod from collections.abc import Mapping from typing import TYPE_CHECKING, Literal, Protocol, TypeGuard, runtime_checkable from typing_extensions import ReadOnly, TypedDict from zarr.abc.metadata import Metadata from zarr.core.buffer import Buffer, NDBuffer from zarr.core.common import NamedConfig, concurrent_map from zarr.core.config import config if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Iterable from typing import Self from zarr.abc.store import ByteGetter, ByteSetter, Store from zarr.core.array_spec import ArraySpec from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType from zarr.core.indexing import SelectorTuple from zarr.core.metadata import ArrayMetadata from zarr.core.metadata.v3 import ChunkGridMetadata __all__ = [ "ArrayArrayCodec", "ArrayBytesCodec", "ArrayBytesCodecPartialDecodeMixin", "ArrayBytesCodecPartialEncodeMixin", "BaseCodec", "BytesBytesCodec", "CodecInput", "CodecOutput", "CodecPipeline", "GetResult", "SupportsSyncCodec", ] class GetResult(TypedDict): """Metadata about a store get operation.""" status: Literal["present", "missing"] type CodecInput = NDBuffer | Buffer type CodecOutput = NDBuffer | Buffer class CodecJSON_V2[TName: str](TypedDict): """The JSON representation of a codec for Zarr V2""" id: ReadOnly[TName] def _check_codecjson_v2(data: object) -> TypeGuard[CodecJSON_V2[str]]: return isinstance(data, Mapping) and "id" in data and isinstance(data["id"], str) CodecJSON_V3 = str | NamedConfig[str, Mapping[str, object]] """The JSON representation of a codec for Zarr V3.""" # The widest type we will *accept* for a codec JSON # This covers v2 and v3 CodecJSON = str | Mapping[str, object] """The widest type of JSON-like input that could specify a codec.""" @runtime_checkable class SupportsSyncCodec[CI: CodecInput, CO: CodecOutput](Protocol): """Protocol for codecs that support synchronous encode/decode. Codecs implementing this protocol provide `_decode_sync` and `_encode_sync` methods that perform encoding/decoding without requiring an async event loop. The type parameters mirror `BaseCodec`: `CI` is the decoded type and `CO` is the encoded type. """ def _decode_sync(self, chunk_data: CO, chunk_spec: ArraySpec) -> CI: ... def _encode_sync(self, chunk_data: CI, chunk_spec: ArraySpec) -> CO | None: ... class BaseCodec[CI: CodecInput, CO: CodecOutput](Metadata): """Generic base class for codecs. Codecs can be registered via zarr.codecs.registry. Warnings -------- This class is not intended to be directly, please use ArrayArrayCodec, ArrayBytesCodec or BytesBytesCodec for subclassing. """ is_fixed_size: bool @abstractmethod def compute_encoded_size(self, input_byte_length: int, chunk_spec: ArraySpec) -> int: """Given an input byte length, this method returns the output byte length. Raises a NotImplementedError for codecs with variable-sized outputs (e.g. compressors). Parameters ---------- input_byte_length : int chunk_spec : ArraySpec Returns ------- int """ ... def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: """Computed the spec of the chunk after it has been encoded by the codec. This is important for codecs that change the shape, data type or fill value of a chunk. The spec will then be used for subsequent codecs in the pipeline. Parameters ---------- chunk_spec : ArraySpec Returns ------- ArraySpec """ return chunk_spec def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: """Fills in codec configuration parameters that can be automatically inferred from the array metadata. Parameters ---------- array_spec : ArraySpec Returns ------- Self """ return self def validate( self, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, ) -> None: """Validates that the codec configuration is compatible with the array metadata. Raises errors when the codec configuration is not compatible. Parameters ---------- shape : tuple[int, ...] The array shape dtype : np.dtype[Any] The array data type chunk_grid : ChunkGridMetadata The array chunk grid metadata """ async def _decode_single(self, chunk_data: CO, chunk_spec: ArraySpec) -> CI: raise NotImplementedError # pragma: no cover async def decode( self, chunks_and_specs: Iterable[tuple[CO | None, ArraySpec]], ) -> Iterable[CI | None]: """Decodes a batch of chunks. Chunks can be None in which case they are ignored by the codec. Parameters ---------- chunks_and_specs : Iterable[tuple[CodecOutput | None, ArraySpec]] Ordered set of encoded chunks with their accompanying chunk spec. Returns ------- Iterable[CI | None] """ return await _batching_helper(self._decode_single, chunks_and_specs) async def _encode_single(self, chunk_data: CI, chunk_spec: ArraySpec) -> CO | None: raise NotImplementedError # pragma: no cover async def encode( self, chunks_and_specs: Iterable[tuple[CI | None, ArraySpec]], ) -> Iterable[CO | None]: """Encodes a batch of chunks. Chunks can be None in which case they are ignored by the codec. Parameters ---------- chunks_and_specs : Iterable[tuple[CI | None, ArraySpec]] Ordered set of to-be-encoded chunks with their accompanying chunk spec. Returns ------- Iterable[CodecOutput | None] """ return await _batching_helper(self._encode_single, chunks_and_specs) class ArrayArrayCodec(BaseCodec[NDBuffer, NDBuffer]): """Base class for array-to-array codecs.""" class ArrayBytesCodec(BaseCodec[NDBuffer, Buffer]): """Base class for array-to-bytes codecs.""" class BytesBytesCodec(BaseCodec[Buffer, Buffer]): """Base class for bytes-to-bytes codecs.""" Codec = ArrayArrayCodec | ArrayBytesCodec | BytesBytesCodec class ArrayBytesCodecPartialDecodeMixin: """Mixin for array-to-bytes codecs that implement partial decoding.""" async def _decode_partial_single( self, byte_getter: ByteGetter, selection: SelectorTuple, chunk_spec: ArraySpec ) -> NDBuffer | None: raise NotImplementedError async def decode_partial( self, batch_info: Iterable[tuple[ByteGetter, SelectorTuple, ArraySpec]], ) -> Iterable[NDBuffer | None]: """Partially decodes a batch of chunks. This method determines parts of a chunk from the slice selection, fetches these parts from the store (via ByteGetter) and decodes them. Parameters ---------- batch_info : Iterable[tuple[ByteGetter, SelectorTuple, ArraySpec]] Ordered set of information about slices of encoded chunks. The slice selection determines which parts of the chunk will be fetched. The ByteGetter is used to fetch the necessary bytes. The chunk spec contains information about the construction of an array from the bytes. Returns ------- Iterable[NDBuffer | None] """ return await concurrent_map( list(batch_info), self._decode_partial_single, config.get("async.concurrency"), ) class ArrayBytesCodecPartialEncodeMixin: """Mixin for array-to-bytes codecs that implement partial encoding.""" async def _encode_partial_single( self, byte_setter: ByteSetter, chunk_array: NDBuffer, selection: SelectorTuple, chunk_spec: ArraySpec, ) -> None: raise NotImplementedError # pragma: no cover async def encode_partial( self, batch_info: Iterable[tuple[ByteSetter, NDBuffer, SelectorTuple, ArraySpec]], ) -> None: """Partially encodes a batch of chunks. This method determines parts of a chunk from the slice selection, encodes them and writes these parts to the store (via ByteSetter). If merging with existing chunk data in the store is necessary, this method will read from the store first and perform the merge. Parameters ---------- batch_info : Iterable[tuple[ByteSetter, NDBuffer, SelectorTuple, ArraySpec]] Ordered set of information about slices of to-be-encoded chunks. The slice selection determines which parts of the chunk will be encoded. The ByteSetter is used to write the necessary bytes and fetch bytes for existing chunk data. The chunk spec contains information about the chunk. """ await concurrent_map( list(batch_info), self._encode_partial_single, config.get("async.concurrency"), ) class CodecPipeline: """Base class for implementing CodecPipeline. A CodecPipeline implements the read and write paths for chunk data. On the read path, it is responsible for fetching chunks from a store (via ByteGetter), decoding them and assembling an output array. On the write path, it encodes the chunks and writes them to a store (via ByteSetter).""" @abstractmethod def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: """Fills in codec configuration parameters that can be automatically inferred from the array metadata. Parameters ---------- array_spec : ArraySpec Returns ------- Self """ ... @classmethod @abstractmethod def from_codecs(cls, codecs: Iterable[Codec]) -> Self: """Creates a codec pipeline from an iterable of codecs. Parameters ---------- codecs : Iterable[Codec] Returns ------- Self """ ... @classmethod def from_array_metadata_and_store(cls, array_metadata: ArrayMetadata, store: Store) -> Self: """Creates a codec pipeline from array metadata and a store path. Raises NotImplementedError by default, indicating the CodecPipeline must be created with from_codecs instead. Parameters ---------- array_metadata : ArrayMetadata store : Store Returns ------- Self """ raise NotImplementedError( f"'{type(cls).__name__}' does not implement CodecPipeline.from_array_metadata_and_store." ) @property @abstractmethod def supports_partial_decode(self) -> bool: ... @property @abstractmethod def supports_partial_encode(self) -> bool: ... @abstractmethod def validate( self, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, ) -> None: """Validates that all codec configurations are compatible with the array metadata. Raises errors when a codec configuration is not compatible. Parameters ---------- shape : tuple[int, ...] The array shape dtype : np.dtype[Any] The array data type chunk_grid : ChunkGridMetadata The array chunk grid metadata """ ... @abstractmethod def compute_encoded_size(self, byte_length: int, array_spec: ArraySpec) -> int: """Given an input byte length, this method returns the output byte length. Raises a NotImplementedError for codecs with variable-sized outputs (e.g. compressors). Parameters ---------- byte_length : int array_spec : ArraySpec Returns ------- int """ ... @abstractmethod async def decode( self, chunk_bytes_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], ) -> Iterable[NDBuffer | None]: """Decodes a batch of chunks. Chunks can be None in which case they are ignored by the codec. Parameters ---------- chunk_bytes_and_specs : Iterable[tuple[Buffer | None, ArraySpec]] Ordered set of encoded chunks with their accompanying chunk spec. Returns ------- Iterable[NDBuffer | None] """ ... @abstractmethod async def encode( self, chunk_arrays_and_specs: Iterable[tuple[NDBuffer | None, ArraySpec]], ) -> Iterable[Buffer | None]: """Encodes a batch of chunks. Chunks can be None in which case they are ignored by the codec. Parameters ---------- chunk_arrays_and_specs : Iterable[tuple[NDBuffer | None, ArraySpec]] Ordered set of to-be-encoded chunks with their accompanying chunk spec. Returns ------- Iterable[Buffer | None] """ ... @abstractmethod async def read( self, batch_info: Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]], out: NDBuffer, drop_axes: tuple[int, ...] = (), ) -> tuple[GetResult, ...]: """Reads chunk data from the store, decodes it and writes it into an output array. Partial decoding may be utilized if the codecs and stores support it. Parameters ---------- batch_info : Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]] Ordered set of information about the chunks. The first slice selection determines which parts of the chunk will be fetched. The second slice selection determines where in the output array the chunk data will be written. The ByteGetter is used to fetch the necessary bytes. The chunk spec contains information about the construction of an array from the bytes. If the Store returns ``None`` for a chunk, then the chunk was not written and the implementation must set the values of that chunk (or ``out``) to the fill value for the array. out : NDBuffer Returns ------- tuple[GetResult, ...] One result per chunk in ``batch_info``. """ ... @abstractmethod async def write( self, batch_info: Iterable[tuple[ByteSetter, ArraySpec, SelectorTuple, SelectorTuple, bool]], value: NDBuffer, drop_axes: tuple[int, ...] = (), ) -> None: """Encodes chunk data and writes it to the store. Merges with existing chunk data by reading first, if necessary. Partial encoding may be utilized if the codecs and stores support it. Parameters ---------- batch_info : Iterable[tuple[ByteSetter, ArraySpec, SelectorTuple, SelectorTuple, bool]] Ordered set of information about the chunks. The first slice selection determines which parts of the chunk will be encoded. The second slice selection determines where in the value array the chunk data is located. The ByteSetter is used to fetch and write the necessary bytes. The chunk spec contains information about the chunk. value : NDBuffer """ ... async def _batching_helper[CI: CodecInput, CO: CodecOutput]( func: Callable[[CI, ArraySpec], Awaitable[CO | None]], batch_info: Iterable[tuple[CI | None, ArraySpec]], ) -> list[CO | None]: return await concurrent_map( list(batch_info), _noop_for_none(func), config.get("async.concurrency"), ) def _noop_for_none[CI: CodecInput, CO: CodecOutput]( func: Callable[[CI, ArraySpec], Awaitable[CO | None]], ) -> Callable[[CI | None, ArraySpec], Awaitable[CO | None]]: async def wrap(chunk: CI | None, chunk_spec: ArraySpec) -> CO | None: if chunk is None: return None return await func(chunk, chunk_spec) return wrap zarr-python-3.2.1/src/zarr/abc/metadata.py000066400000000000000000000025751517635743000204620ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Sequence from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Self from zarr.core.common import JSON from dataclasses import dataclass, fields __all__ = ["Metadata"] @dataclass(frozen=True) class Metadata: def to_dict(self) -> dict[str, JSON]: """ Recursively serialize this model to a dictionary. This method inspects the fields of self and calls `x.to_dict()` for any fields that are instances of `Metadata`. Sequences of `Metadata` are similarly recursed into, and the output of that recursion is collected in a list. """ out_dict = {} for field in fields(self): key = field.name value = getattr(self, key) if isinstance(value, Metadata): out_dict[field.name] = getattr(self, field.name).to_dict() elif isinstance(value, str): out_dict[key] = value elif isinstance(value, Sequence): out_dict[key] = tuple(v.to_dict() if isinstance(v, Metadata) else v for v in value) else: out_dict[key] = value return out_dict @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: """ Create an instance of the model from a dictionary """ return cls(**data) zarr-python-3.2.1/src/zarr/abc/numcodec.py000066400000000000000000000053401517635743000204700ustar00rootroot00000000000000from typing import Any, Protocol, Self, TypeGuard class Numcodec(Protocol): """ A protocol that models the ``numcodecs.abc.Codec`` interface. This protocol should be considered experimental. Expect the type annotations for ``buf`` and ``out`` to narrow in the future. """ codec_id: str def encode(self, buf: Any) -> Any: """Encode data from ``buf``. Parameters ---------- buf : Any Data to be encoded. Returns ------- enc: Any Encoded data. """ ... def decode(self, buf: Any, out: Any | None = None) -> Any: """ Decode data in ``buf``. Parameters ---------- buf : Any Encoded data. out : Any Writeable buffer to store decoded data. If provided, this buffer must be exactly the right size to store the decoded data. Returns ------- dec : Any Decoded data. """ ... def get_config(self) -> Any: """ Return a JSON-serializable configuration dictionary for this codec. Must include an ``'id'`` field with the codec identifier. """ ... @classmethod def from_config(cls, config: Any) -> Self: """ Instantiate a codec from a configuration dictionary. Parameters ---------- config : Any A configuration dictionary for this codec. """ ... def _is_numcodec_cls(obj: object) -> TypeGuard[type[Numcodec]]: """ Check if the given object is a class implements the Numcodec protocol. The @runtime_checkable decorator does not allow issubclass checks for protocols with non-method members (i.e., attributes), so we use this function to manually check for the presence of the required attributes and methods on a given object. """ return ( isinstance(obj, type) and hasattr(obj, "codec_id") and isinstance(obj.codec_id, str) and hasattr(obj, "encode") and callable(obj.encode) and hasattr(obj, "decode") and callable(obj.decode) and hasattr(obj, "get_config") and callable(obj.get_config) and hasattr(obj, "from_config") and callable(obj.from_config) ) def _is_numcodec(obj: object) -> TypeGuard[Numcodec]: """ Check if the given object implements the Numcodec protocol. The @runtime_checkable decorator does not allow issubclass checks for protocols with non-method members (i.e., attributes), so we use this function to manually check for the presence of the required attributes and methods on a given object. """ return _is_numcodec_cls(type(obj)) zarr-python-3.2.1/src/zarr/abc/store.py000066400000000000000000000561361517635743000200400ustar00rootroot00000000000000from __future__ import annotations import asyncio import json from abc import ABC, abstractmethod from dataclasses import dataclass from itertools import starmap from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable from zarr.core.sync import sync if TYPE_CHECKING: from collections.abc import AsyncGenerator, AsyncIterator, Iterable from types import TracebackType from typing import Any, Self from zarr.core.buffer import Buffer, BufferPrototype __all__ = [ "ByteGetter", "ByteSetter", "Store", "SupportsDeleteSync", "SupportsGetSync", "SupportsSetSync", "SupportsSyncStore", "set_or_delete", ] @dataclass(frozen=True, slots=True) class RangeByteRequest: """Request a specific byte range""" start: int """The start of the byte range request (inclusive).""" end: int """The end of the byte range request (exclusive).""" @dataclass(frozen=True, slots=True) class OffsetByteRequest: """Request all bytes starting from a given byte offset""" offset: int """The byte offset for the offset range request.""" @dataclass(frozen=True, slots=True) class SuffixByteRequest: """Request up to the last `n` bytes""" suffix: int """The number of bytes from the suffix to request.""" type ByteRequest = RangeByteRequest | OffsetByteRequest | SuffixByteRequest class Store(ABC): """ Abstract base class for Zarr stores. """ _read_only: bool _is_open: bool def __init__(self, *, read_only: bool = False) -> None: self._is_open = False self._read_only = read_only @classmethod async def open(cls, *args: Any, **kwargs: Any) -> Self: """ Create and open the store. Parameters ---------- *args : Any Positional arguments to pass to the store constructor. **kwargs : Any Keyword arguments to pass to the store constructor. Returns ------- Store The opened store instance. """ store = cls(*args, **kwargs) await store._open() return store def with_read_only(self, read_only: bool = False) -> Store: """ Return a new store with a new read_only setting. The new store points to the same location with the specified new read_only state. The returned Store is not automatically opened, and this store is not automatically closed. Parameters ---------- read_only If True, the store will be created in read-only mode. Defaults to False. Returns ------- A new store of the same type with the new read only attribute. """ raise NotImplementedError( f"with_read_only is not implemented for the {type(self)} store type." ) def __enter__(self) -> Self: """Enter a context manager that will close the store upon exiting.""" return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: """Close the store.""" self.close() async def _open(self) -> None: """ Open the store. Raises ------ ValueError If the store is already open. """ if self._is_open: raise ValueError("store is already open") self._is_open = True async def _ensure_open(self) -> None: """Open the store if it is not already open.""" if not self._is_open: await self._open() async def is_empty(self, prefix: str) -> bool: """ Check if the directory is empty. Parameters ---------- prefix : str Prefix of keys to check. Returns ------- bool True if the store is empty, False otherwise. """ if not self.supports_listing: raise NotImplementedError if prefix != "" and not prefix.endswith("/"): prefix += "/" async for _ in self.list_prefix(prefix): return False return True async def clear(self) -> None: """ Clear the store. Remove all keys and values from the store. """ if not self.supports_deletes: raise NotImplementedError if not self.supports_listing: raise NotImplementedError self._check_writable() await self.delete_dir("") @property def read_only(self) -> bool: """Is the store read-only?""" return self._read_only def _check_writable(self) -> None: """Raise an exception if the store is not writable.""" if self.read_only: raise ValueError("store was opened in read-only mode and does not support writing") @abstractmethod def __eq__(self, value: object) -> bool: """Equality comparison.""" ... @abstractmethod async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None, ) -> Buffer | None: """Retrieve the value associated with a given key. Parameters ---------- key : str prototype : BufferPrototype The prototype of the output buffer. Stores may support a default buffer prototype. byte_range : ByteRequest, optional ByteRequest may be one of the following. If not provided, all data associated with the key is retrieved. - RangeByteRequest(int, int): Request a specific range of bytes in the form (start, end). The end is exclusive. If the given range is zero-length or starts after the end of the object, an error will be returned. Additionally, if the range ends after the end of the object, the entire remainder of the object will be returned. Otherwise, the exact requested range will be returned. - OffsetByteRequest(int): Request all bytes starting from a given byte offset. This is equivalent to bytes={int}- as an HTTP header. - SuffixByteRequest(int): Request the last int bytes. Note that here, int is the size of the request, not the byte offset. This is equivalent to bytes=-{int} as an HTTP header. Returns ------- Buffer """ ... async def _get_bytes( self, key: str, *, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> bytes: """ Retrieve raw bytes from the store asynchronously. This is a convenience method that wraps ``get()`` and converts the result to bytes. Use this when you need the raw byte content of a stored value. Parameters ---------- key : str The key identifying the data to retrieve. prototype : BufferPrototype The buffer prototype to use for reading the data. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Can be a ``RangeByteRequest``, ``OffsetByteRequest``, or ``SuffixByteRequest``. Returns ------- bytes The raw bytes stored at the given key. Raises ------ FileNotFoundError If the key does not exist in the store. See Also -------- get : Lower-level method that returns a Buffer object. get_bytes : Synchronous version of this method. get_json : Asynchronous method for retrieving and parsing JSON data. Examples -------- >>> store = await MemoryStore.open() >>> await store.set("data", Buffer.from_bytes(b"hello world")) >>> data = await store.get_bytes("data", prototype=default_buffer_prototype()) >>> print(data) b'hello world' """ buffer = await self.get(key, prototype, byte_range) if buffer is None: raise FileNotFoundError(key) return buffer.to_bytes() def _get_bytes_sync( self, key: str = "", *, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> bytes: """ Retrieve raw bytes from the store synchronously. This is a synchronous wrapper around ``get_bytes()``. It should only be called from non-async code. For async contexts, use ``get_bytes()`` instead. Parameters ---------- key : str, optional The key identifying the data to retrieve. Defaults to an empty string. prototype : BufferPrototype The buffer prototype to use for reading the data. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Can be a ``RangeByteRequest``, ``OffsetByteRequest``, or ``SuffixByteRequest``. Returns ------- bytes The raw bytes stored at the given key. Raises ------ FileNotFoundError If the key does not exist in the store. Warnings -------- Do not call this method from async functions. Use ``get_bytes()`` instead to avoid blocking the event loop. See Also -------- get_bytes : Asynchronous version of this method. get_json_sync : Synchronous method for retrieving and parsing JSON data. Examples -------- >>> store = MemoryStore() >>> await store.set("data", Buffer.from_bytes(b"hello world")) >>> data = store.get_bytes_sync("data", prototype=default_buffer_prototype()) >>> print(data) b'hello world' """ return sync(self._get_bytes(key, prototype=prototype, byte_range=byte_range)) async def _get_json( self, key: str, *, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Any: """ Retrieve and parse JSON data from the store asynchronously. This is a convenience method that retrieves bytes from the store and parses them as JSON. Parameters ---------- key : str The key identifying the JSON data to retrieve. prototype : BufferPrototype The buffer prototype to use for reading the data. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Can be a ``RangeByteRequest``, ``OffsetByteRequest``, or ``SuffixByteRequest``. Note: Using byte ranges with JSON may result in invalid JSON. Returns ------- Any The parsed JSON data. This follows the behavior of ``json.loads()`` and can be any JSON-serializable type: dict, list, str, int, float, bool, or None. Raises ------ FileNotFoundError If the key does not exist in the store. json.JSONDecodeError If the stored data is not valid JSON. See Also -------- get_bytes : Method for retrieving raw bytes. get_json_sync : Synchronous version of this method. Examples -------- >>> store = await MemoryStore.open() >>> metadata = {"zarr_format": 3, "node_type": "array"} >>> await store.set("zarr.json", Buffer.from_bytes(json.dumps(metadata).encode())) >>> data = await store.get_json("zarr.json", prototype=default_buffer_prototype()) >>> print(data) {'zarr_format': 3, 'node_type': 'array'} """ return json.loads(await self._get_bytes(key, prototype=prototype, byte_range=byte_range)) def _get_json_sync( self, key: str = "", *, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Any: """ Retrieve and parse JSON data from the store synchronously. This is a synchronous wrapper around ``get_json()``. It should only be called from non-async code. For async contexts, use ``get_json()`` instead. Parameters ---------- key : str, optional The key identifying the JSON data to retrieve. Defaults to an empty string. prototype : BufferPrototype The buffer prototype to use for reading the data. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Can be a ``RangeByteRequest``, ``OffsetByteRequest``, or ``SuffixByteRequest``. Note: Using byte ranges with JSON may result in invalid JSON. Returns ------- Any The parsed JSON data. This follows the behavior of ``json.loads()`` and can be any JSON-serializable type: dict, list, str, int, float, bool, or None. Raises ------ FileNotFoundError If the key does not exist in the store. json.JSONDecodeError If the stored data is not valid JSON. Warnings -------- Do not call this method from async functions. Use ``get_json()`` instead to avoid blocking the event loop. See Also -------- get_json : Asynchronous version of this method. get_bytes_sync : Synchronous method for retrieving raw bytes without parsing. Examples -------- >>> store = MemoryStore() >>> metadata = {"zarr_format": 3, "node_type": "array"} >>> store.set("zarr.json", Buffer.from_bytes(json.dumps(metadata).encode())) >>> data = store.get_json_sync("zarr.json", prototype=default_buffer_prototype()) >>> print(data) {'zarr_format': 3, 'node_type': 'array'} """ return sync(self._get_json(key, prototype=prototype, byte_range=byte_range)) @abstractmethod async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: """Retrieve possibly partial values from given key_ranges. Parameters ---------- prototype : BufferPrototype The prototype of the output buffer. Stores may support a default buffer prototype. key_ranges : Iterable[tuple[str, tuple[int | None, int | None]]] Ordered set of key, range pairs, a key may occur multiple times with different ranges Returns ------- list of values, in the order of the key_ranges, may contain null/none for missing keys """ ... @abstractmethod async def exists(self, key: str) -> bool: """Check if a key exists in the store. Parameters ---------- key : str Returns ------- bool """ ... @property @abstractmethod def supports_writes(self) -> bool: """Does the store support writes?""" ... @abstractmethod async def set(self, key: str, value: Buffer) -> None: """Store a (key, value) pair. Parameters ---------- key : str value : Buffer """ ... async def set_if_not_exists(self, key: str, value: Buffer) -> None: """ Store a key to ``value`` if the key is not already present. Parameters ---------- key : str value : Buffer """ # Note for implementers: the default implementation provided here # is not safe for concurrent writers. There's a race condition between # the `exists` check and the `set` where another writer could set some # value at `key` or delete `key`. if not await self.exists(key): await self.set(key, value) async def _set_many(self, values: Iterable[tuple[str, Buffer]]) -> None: """ Insert multiple (key, value) pairs into storage. """ await asyncio.gather(*starmap(self.set, values)) @property def supports_consolidated_metadata(self) -> bool: """ Does the store support consolidated metadata?. If it doesn't an error will be raised on requests to consolidate the metadata. Returning `False` can be useful for stores which implement their own consolidation mechanism outside of the zarr-python implementation. """ return True @property @abstractmethod def supports_deletes(self) -> bool: """Does the store support deletes?""" ... @abstractmethod async def delete(self, key: str) -> None: """Remove a key from the store Parameters ---------- key : str """ ... @property def supports_partial_writes(self) -> Literal[False]: """Does the store support partial writes? Partial writes are no longer used by Zarr, so this is always false. """ return False @property @abstractmethod def supports_listing(self) -> bool: """Does the store support listing?""" ... @abstractmethod def list(self) -> AsyncIterator[str]: """Retrieve all keys in the store. Returns ------- AsyncIterator[str] """ # This method should be async, like overridden methods in child classes. # However, that's not straightforward: # https://stackoverflow.com/questions/68905848 @abstractmethod def list_prefix(self, prefix: str) -> AsyncIterator[str]: """ Retrieve all keys in the store that begin with a given prefix. Keys are returned relative to the root of the store. Parameters ---------- prefix : str Returns ------- AsyncIterator[str] """ # This method should be async, like overridden methods in child classes. # However, that's not straightforward: # https://stackoverflow.com/questions/68905848 @abstractmethod def list_dir(self, prefix: str) -> AsyncIterator[str]: """ Retrieve all keys and prefixes with a given prefix and which do not contain the character “/” after the given prefix. Parameters ---------- prefix : str Returns ------- AsyncIterator[str] """ # This method should be async, like overridden methods in child classes. # However, that's not straightforward: # https://stackoverflow.com/questions/68905848 async def delete_dir(self, prefix: str) -> None: """ Remove all keys and prefixes in the store that begin with a given prefix. """ if not self.supports_deletes: raise NotImplementedError if not self.supports_listing: raise NotImplementedError self._check_writable() if prefix != "" and not prefix.endswith("/"): prefix += "/" async for key in self.list_prefix(prefix): await self.delete(key) def close(self) -> None: """Close the store.""" self._is_open = False async def _get_many( self, requests: Iterable[tuple[str, BufferPrototype, ByteRequest | None]] ) -> AsyncGenerator[tuple[str, Buffer | None], None]: """ Retrieve a collection of objects from storage. In general this method does not guarantee that objects will be retrieved in the order in which they were requested, so this method yields tuple[str, Buffer | None] instead of just Buffer | None """ for req in requests: yield (req[0], await self.get(*req)) async def getsize(self, key: str) -> int: """ Return the size, in bytes, of a value in a Store. Parameters ---------- key : str Returns ------- nbytes : int The size of the value (in bytes). Raises ------ FileNotFoundError When the given key does not exist in the store. """ # Note to implementers: this default implementation is very inefficient since # it requires reading the entire object. Many systems will have ways to get the # size of an object without reading it. # avoid circular import from zarr.core.buffer.core import default_buffer_prototype value = await self.get(key, prototype=default_buffer_prototype()) if value is None: raise FileNotFoundError(key) return len(value) async def getsize_prefix(self, prefix: str) -> int: """ Return the size, in bytes, of all values under a prefix. Parameters ---------- prefix : str The prefix of the directory to measure. Returns ------- nbytes : int The sum of the sizes of the values in the directory (in bytes). See Also -------- zarr.Array.nbytes_stored Store.getsize Notes ----- ``getsize_prefix`` is just provided as a potentially faster alternative to listing all the keys under a prefix calling [`Store.getsize`][zarr.abc.store.Store.getsize] on each. In general, ``prefix`` should be the path of an Array or Group in the Store. Implementations may differ on the behavior when some other ``prefix`` is provided. """ # TODO: Overlap listing keys with getsize calls. # Currently, we load the list of keys into memory and only then move # on to getting sizes. Ideally we would overlap those two, which should # improve tail latency and might reduce memory pressure (since not all keys # would be in memory at once). # avoid circular import from zarr.core.common import concurrent_map from zarr.core.config import config keys = [(x,) async for x in self.list_prefix(prefix)] limit = config.get("async.concurrency") sizes = await concurrent_map(keys, self.getsize, limit=limit) return sum(sizes) @runtime_checkable class ByteGetter(Protocol): async def get( self, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: ... @runtime_checkable class ByteSetter(Protocol): async def get( self, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: ... async def set(self, value: Buffer) -> None: ... async def delete(self) -> None: ... async def set_if_not_exists(self, default: Buffer) -> None: ... @runtime_checkable class SupportsGetSync(Protocol): def get_sync( self, key: str, *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: ... @runtime_checkable class SupportsSetSync(Protocol): def set_sync(self, key: str, value: Buffer) -> None: ... @runtime_checkable class SupportsDeleteSync(Protocol): def delete_sync(self, key: str) -> None: ... @runtime_checkable class SupportsSyncStore(SupportsGetSync, SupportsSetSync, SupportsDeleteSync, Protocol): ... async def set_or_delete(byte_setter: ByteSetter, value: Buffer | None) -> None: """Set or delete a value in a byte setter Parameters ---------- byte_setter : ByteSetter value : Buffer | None Notes ----- If value is None, the key will be deleted. """ if value is None: await byte_setter.delete() else: await byte_setter.set(value) zarr-python-3.2.1/src/zarr/api/000077500000000000000000000000001517635743000163435ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/api/__init__.py000066400000000000000000000000001517635743000204420ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/api/asynchronous.py000066400000000000000000001337211517635743000214570ustar00rootroot00000000000000from __future__ import annotations import asyncio import dataclasses import warnings from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, cast import numpy as np import numpy.typing as npt from typing_extensions import deprecated from zarr.abc.store import Store from zarr.core.array import ( DEFAULT_FILL_VALUE, Array, AsyncArray, CompressorLike, create_array, from_array, get_array_metadata, ) from zarr.core.array_spec import ArrayConfigLike, parse_array_config from zarr.core.buffer import NDArrayLike from zarr.core.common import ( JSON, AccessModeLiteral, DimensionNamesLike, MemoryOrder, ZarrFormat, _default_zarr_format, _warn_write_empty_chunks_kwarg, ) from zarr.core.dtype import ZDTypeLike, get_data_type_from_native_dtype from zarr.core.group import ( AsyncGroup, ConsolidatedMetadata, GroupMetadata, create_hierarchy, ) from zarr.core.metadata import ArrayMetadataDict, ArrayV2Metadata from zarr.errors import ( ArrayNotFoundError, GroupNotFoundError, NodeTypeValidationError, ZarrDeprecationWarning, ZarrRuntimeWarning, ZarrUserWarning, ) from zarr.storage import StorePath from zarr.storage._common import make_store_path if TYPE_CHECKING: from collections.abc import Iterable from zarr.abc.codec import Codec from zarr.abc.numcodec import Numcodec from zarr.core.buffer import NDArrayLikeOrScalar from zarr.core.chunk_key_encodings import ChunkKeyEncoding from zarr.core.metadata.v2 import CompressorLikev2 from zarr.storage import StoreLike from zarr.types import AnyArray, AnyAsyncArray # TODO: this type could use some more thought type ArrayLike = AnyAsyncArray | AnyArray | npt.NDArray[Any] PathLike = str __all__ = [ "array", "consolidate_metadata", "copy", "copy_all", "copy_store", "create", "create_array", "create_hierarchy", "empty", "empty_like", "from_array", "full", "full_like", "group", "load", "ones", "ones_like", "open", "open_array", "open_consolidated", "open_group", "open_like", "save", "save_array", "save_group", "tree", "zeros", "zeros_like", ] _READ_MODES: tuple[AccessModeLiteral, ...] = ("r", "r+", "a") _CREATE_MODES: tuple[AccessModeLiteral, ...] = ("a", "w", "w-") _OVERWRITE_MODES: tuple[AccessModeLiteral, ...] = ("w",) def _infer_overwrite(mode: AccessModeLiteral) -> bool: """ Check that an ``AccessModeLiteral`` is compatible with overwriting an existing Zarr node. """ return mode in _OVERWRITE_MODES def _get_shape_chunks(a: ArrayLike | Any) -> tuple[tuple[int, ...] | None, tuple[int, ...] | None]: """Helper function to get the shape and chunks from an array-like object""" shape = None chunks = None if hasattr(a, "shape") and isinstance(a.shape, tuple): shape = a.shape if hasattr(a, "chunks") and isinstance(a.chunks, tuple) and (len(a.chunks) == len(a.shape)): chunks = a.chunks elif hasattr(a, "chunklen"): # bcolz carray chunks = (a.chunklen,) + a.shape[1:] return shape, chunks class _LikeArgs(TypedDict): shape: NotRequired[tuple[int, ...]] chunks: NotRequired[tuple[int, ...]] dtype: NotRequired[np.dtype[np.generic]] order: NotRequired[Literal["C", "F"]] filters: NotRequired[tuple[Numcodec, ...] | None] compressor: NotRequired[CompressorLikev2] codecs: NotRequired[tuple[Codec, ...]] def _like_args(a: ArrayLike) -> _LikeArgs: """Set default values for shape and chunks if they are not present in the array-like object""" new: _LikeArgs = {} shape, chunks = _get_shape_chunks(a) if shape is not None: new["shape"] = shape if chunks is not None: new["chunks"] = chunks if hasattr(a, "dtype"): new["dtype"] = a.dtype if isinstance(a, AsyncArray | Array): if isinstance(a.metadata, ArrayV2Metadata): new["order"] = a.order new["compressor"] = a.metadata.compressor new["filters"] = a.metadata.filters else: # TODO: Remove type: ignore statement when type inference improves. # mypy cannot correctly infer the type of a.metadata here for some reason. new["codecs"] = a.metadata.codecs else: # TODO: set default values compressor/codecs # to do this, we may need to evaluate if this is a v2 or v3 array # new["compressor"] = "default" pass return new async def consolidate_metadata( store: StoreLike, path: str | None = None, zarr_format: ZarrFormat | None = None, ) -> AsyncGroup: """ Consolidate the metadata of all nodes in a hierarchy. Upon completion, the metadata of the root node in the Zarr hierarchy will be updated to include all the metadata of child nodes. For Stores that do not support consolidated metadata, this operation raises a ``TypeError``. Parameters ---------- store : StoreLike The store-like object whose metadata you wish to consolidate. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. path : str, optional A path to a group in the store to consolidate at. Only children below that group will be consolidated. By default, the root node is used so all the metadata in the store is consolidated. zarr_format : {2, 3, None}, optional The zarr format of the hierarchy. By default the zarr format is inferred. Returns ------- group: AsyncGroup The group, with the ``consolidated_metadata`` field set to include the metadata of each child node. If the Store doesn't support consolidated metadata, this function raises a `TypeError`. See ``Store.supports_consolidated_metadata``. """ store_path = await make_store_path(store, path=path) if not store_path.store.supports_consolidated_metadata: store_name = type(store_path.store).__name__ raise TypeError( f"The Zarr Store in use ({store_name}) doesn't support consolidated metadata", ) group = await AsyncGroup.open(store_path, zarr_format=zarr_format, use_consolidated=False) group.store_path.store._check_writable() members_metadata = { k: v.metadata async for k, v in group.members(max_depth=None, use_consolidated_for_children=False) } # While consolidating, we want to be explicit about when child groups # are empty by inserting an empty dict for consolidated_metadata.metadata for k, v in members_metadata.items(): if isinstance(v, GroupMetadata) and v.consolidated_metadata is None: v = dataclasses.replace(v, consolidated_metadata=ConsolidatedMetadata(metadata={})) members_metadata[k] = v if any(m.zarr_format == 3 for m in members_metadata.values()): warnings.warn( "Consolidated metadata is currently not part in the Zarr format 3 specification. It " "may not be supported by other zarr implementations and may change in the future.", category=ZarrUserWarning, stacklevel=1, ) ConsolidatedMetadata._flat_to_nested(members_metadata) consolidated_metadata = ConsolidatedMetadata(metadata=members_metadata) metadata = dataclasses.replace(group.metadata, consolidated_metadata=consolidated_metadata) group = dataclasses.replace( group, metadata=metadata, ) await group._save_metadata() return group async def copy(*args: Any, **kwargs: Any) -> tuple[int, int, int]: """ Not implemented. """ raise NotImplementedError async def copy_all(*args: Any, **kwargs: Any) -> tuple[int, int, int]: """ Not implemented. """ raise NotImplementedError async def copy_store(*args: Any, **kwargs: Any) -> tuple[int, int, int]: """ Not implemented. """ raise NotImplementedError async def load( *, store: StoreLike, path: str | None = None, zarr_format: ZarrFormat | None = None, ) -> NDArrayLikeOrScalar | dict[str, NDArrayLikeOrScalar]: """Load data from an array or group into memory. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. path : str or None, optional The path within the store from which to load. Returns ------- out If the path contains an array, out will be a numpy array. If the path contains a group, out will be a dict-like object where keys are array names and values are numpy arrays. See Also -------- save Notes ----- If loading data from a group of arrays, data will not be immediately loaded into memory. Rather, arrays will be loaded into memory as they are requested. """ obj = await open(store=store, path=path, zarr_format=zarr_format) if isinstance(obj, AsyncArray): return await obj.getitem(slice(None)) else: raise NotImplementedError("loading groups not yet supported") async def open( *, store: StoreLike | None = None, mode: AccessModeLiteral | None = None, zarr_format: ZarrFormat | None = None, path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to open_array ) -> AnyAsyncArray | AsyncGroup: """Convenience function to open a group or array using file-mode-like semantics. Parameters ---------- store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. mode : {'r', 'r+', 'a', 'w', 'w-'}, optional Persistence mode: 'r' means read only (must exist); 'r+' means read/write (must exist); 'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists). If the store is read-only, the default is 'r'; otherwise, it is 'a'. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str or None, optional The path within the store to open. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs Additional parameters are passed through to `zarr.open_array` or `zarr.open_group`. Returns ------- z : array or group Return type depends on what exists in the given store. """ if mode is None: if isinstance(store, (Store, StorePath)) and store.read_only: mode = "r" else: mode = "a" store_path = await make_store_path(store, mode=mode, path=path, storage_options=storage_options) # TODO: the mode check below seems wrong! if "shape" not in kwargs and mode in {"a", "r", "r+", "w"}: try: metadata_dict = await get_array_metadata(store_path, zarr_format=zarr_format) # TODO: remove this cast when we fix typing for array metadata dicts _metadata_dict = cast("ArrayMetadataDict", metadata_dict) # for v2, the above would already have raised an exception if not an array zarr_format = _metadata_dict["zarr_format"] is_v3_array = zarr_format == 3 and _metadata_dict.get("node_type") == "array" if is_v3_array or zarr_format == 2: return AsyncArray( store_path=store_path, metadata=_metadata_dict, config=kwargs.get("config") ) except (AssertionError, FileNotFoundError, NodeTypeValidationError): pass return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs) try: return await open_array(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs) except (KeyError, NodeTypeValidationError): # KeyError for a missing key # NodeTypeValidationError for failing to parse node metadata as an array when it's # actually a group return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs) async def open_consolidated( *args: Any, use_consolidated: Literal[True] = True, **kwargs: Any ) -> AsyncGroup: """ Alias for [`open_group`][zarr.api.asynchronous.open_group] with ``use_consolidated=True``. """ if use_consolidated is not True: raise TypeError( "'use_consolidated' must be 'True' in 'open_consolidated'. Use 'open' with " "'use_consolidated=False' to bypass consolidated metadata." ) return await open_group(*args, use_consolidated=use_consolidated, **kwargs) async def save( store: StoreLike, *args: NDArrayLike, zarr_format: ZarrFormat | None = None, path: str | None = None, **kwargs: Any, # TODO: type kwargs as valid args to save ) -> None: """Convenience function to save an array or group of arrays to the local file system. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. *args : ndarray NumPy arrays with data to save. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str or None, optional The path within the group where the arrays will be saved. **kwargs NumPy arrays with data to save. """ if len(args) == 0 and len(kwargs) == 0: raise ValueError("at least one array must be provided") if len(args) == 1 and len(kwargs) == 0: await save_array(store, args[0], zarr_format=zarr_format, path=path) else: await save_group(store, *args, zarr_format=zarr_format, path=path, **kwargs) async def save_array( store: StoreLike, arr: NDArrayLike, *, zarr_format: ZarrFormat | None = None, path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to create ) -> None: """Convenience function to save a NumPy array to the local file system, following a similar API to the NumPy save() function. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. arr : ndarray NumPy array with data to save. zarr_format : {2, 3, None}, optional The zarr format to use when saving. The default is ``None``, which will use the default Zarr format defined in the global configuration object. path : str or None, optional The path within the store where the array will be saved. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs Passed through to [`create`][zarr.api.asynchronous.create], e.g., compressor. """ if zarr_format is None: zarr_format = _default_zarr_format() if not isinstance(arr, NDArrayLike): raise TypeError("arr argument must be numpy or other NDArrayLike array") mode = kwargs.pop("mode", "a") store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) if np.isscalar(arr): arr = np.array(arr) shape = arr.shape chunks = getattr(arr, "chunks", None) # for array-likes with chunks attribute overwrite = kwargs.pop("overwrite", None) or _infer_overwrite(mode) zarr_dtype = get_data_type_from_native_dtype(arr.dtype) new = await AsyncArray._create( store_path, zarr_format=zarr_format, shape=shape, dtype=zarr_dtype, chunks=chunks, overwrite=overwrite, **kwargs, ) await new.setitem(slice(None), arr) async def save_group( store: StoreLike, *args: NDArrayLike, zarr_format: ZarrFormat | None = None, path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: NDArrayLike, ) -> None: """Convenience function to save several NumPy arrays to the local file system, following a similar API to the NumPy savez()/savez_compressed() functions. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. *args : ndarray NumPy arrays with data to save. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str or None, optional Path within the store where the group will be saved. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs NumPy arrays with data to save. """ store_path = await make_store_path(store, path=path, mode="w", storage_options=storage_options) if zarr_format is None: zarr_format = _default_zarr_format() for arg in args: if not isinstance(arg, NDArrayLike): raise TypeError( "All arguments must be numpy or other NDArrayLike arrays (except store, path, storage_options, and zarr_format)" ) for k, v in kwargs.items(): if not isinstance(v, NDArrayLike): raise TypeError(f"Keyword argument '{k}' must be a numpy or other NDArrayLike array") if len(args) == 0 and len(kwargs) == 0: raise ValueError("at least one array must be provided") aws = [] for i, arr in enumerate(args): aws.append( save_array( store_path, arr, zarr_format=zarr_format, path=f"arr_{i}", storage_options=storage_options, ) ) for k, arr in kwargs.items(): aws.append(save_array(store_path, arr, zarr_format=zarr_format, path=k)) await asyncio.gather(*aws) @deprecated("Use AsyncGroup.tree instead.", category=ZarrDeprecationWarning) async def tree(grp: AsyncGroup, expand: bool | None = None, level: int | None = None) -> Any: """Provide a rich display of the hierarchy. !!! warning "Deprecated" `zarr.tree()` is deprecated since v3.0.0 and will be removed in a future release. Use `group.tree()` instead. Parameters ---------- grp : Group Zarr or h5py group. expand : bool, optional Only relevant for HTML representation. If True, tree will be fully expanded. level : int, optional Maximum depth to descend into hierarchy. Returns ------- TreeRepr A pretty-printable object displaying the hierarchy. """ return await grp.tree(expand=expand, level=level) async def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyAsyncArray: """Create an array filled with `data`. Parameters ---------- data : array_like The data to fill the array with. **kwargs Passed through to [`create`][zarr.api.asynchronous.create]. Returns ------- array : array The new array. """ if isinstance(data, Array): return await from_array(data=data, **kwargs) # ensure data is array-like if not hasattr(data, "shape") or not hasattr(data, "dtype"): data = np.asanyarray(data) # setup dtype kw_dtype = kwargs.get("dtype") if kw_dtype is None and hasattr(data, "dtype"): kwargs["dtype"] = data.dtype else: kwargs["dtype"] = kw_dtype # setup shape and chunks data_shape, data_chunks = _get_shape_chunks(data) kwargs["shape"] = data_shape kw_chunks = kwargs.get("chunks") if kw_chunks is None: kwargs["chunks"] = data_chunks else: kwargs["chunks"] = kw_chunks read_only = kwargs.pop("read_only", False) if read_only: raise ValueError("read_only=True is no longer supported when creating new arrays") # instantiate array z = await create(**kwargs) # fill with data await z.setitem(Ellipsis, data) return z async def group( *, # Note: this is a change from v2 store: StoreLike | None = None, overwrite: bool = False, chunk_store: StoreLike | None = None, # not used cache_attrs: bool | None = None, # not used, default changed synchronizer: Any | None = None, # not used path: str | None = None, zarr_format: ZarrFormat | None = None, meta_array: Any | None = None, # not used attributes: dict[str, JSON] | None = None, storage_options: dict[str, Any] | None = None, ) -> AsyncGroup: """Create a group. Parameters ---------- store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. overwrite : bool, optional If True, delete any pre-existing data in `store` at `path` before creating the group. chunk_store : StoreLike or None, default=None Separate storage for chunks. Not implemented. cache_attrs : bool, optional If True (default), user attributes will be cached for attribute read operations. If False, user attributes are reloaded from the store prior to all attribute read operations. synchronizer : object, optional Array synchronizer. path : str, optional Group path within store. meta_array : array-like, optional An array instance to use for determining arrays to create and return to users. Use `numpy.empty(())` by default. zarr_format : {2, 3, None}, optional The zarr format to use when saving. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. Returns ------- g : group The new group. """ mode: AccessModeLiteral if overwrite: mode = "w" else: mode = "a" return await open_group( store=store, mode=mode, chunk_store=chunk_store, cache_attrs=cache_attrs, synchronizer=synchronizer, path=path, zarr_format=zarr_format, meta_array=meta_array, attributes=attributes, storage_options=storage_options, ) async def create_group( *, store: StoreLike, path: str | None = None, overwrite: bool = False, zarr_format: ZarrFormat | None = None, attributes: dict[str, Any] | None = None, storage_options: dict[str, Any] | None = None, ) -> AsyncGroup: """Create a group. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. path : str, optional Group path within store. overwrite : bool, optional If True, pre-existing data at ``path`` will be deleted before creating the group. zarr_format : {2, 3, None}, optional The zarr format to use when saving. If no ``zarr_format`` is provided, the default format will be used. This default can be changed by modifying the value of ``default_zarr_format`` in [`zarr.config`][zarr.config]. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. Returns ------- AsyncGroup The new group. """ if zarr_format is None: zarr_format = _default_zarr_format() mode: Literal["a"] = "a" store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) return await AsyncGroup.from_store( store=store_path, zarr_format=zarr_format, overwrite=overwrite, attributes=attributes, ) async def open_group( store: StoreLike | None = None, *, # Note: this is a change from v2 mode: AccessModeLiteral = "a", cache_attrs: bool | None = None, # not used, default changed synchronizer: Any = None, # not used path: str | None = None, chunk_store: StoreLike | None = None, # not used storage_options: dict[str, Any] | None = None, zarr_format: ZarrFormat | None = None, meta_array: Any | None = None, # not used attributes: dict[str, JSON] | None = None, use_consolidated: bool | str | None = None, ) -> AsyncGroup: """Open a group using file-mode-like semantics. Parameters ---------- store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. mode : {'r', 'r+', 'a', 'w', 'w-'}, optional Persistence mode: 'r' means read only (must exist); 'r+' means read/write (must exist); 'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists). cache_attrs : bool, optional If True (default), user attributes will be cached for attribute read operations. If False, user attributes are reloaded from the store prior to all attribute read operations. synchronizer : object, optional Array synchronizer. path : str, optional Group path within store. chunk_store : StoreLike or None, default=None Separate storage for chunks. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. meta_array : array-like, optional An array instance to use for determining arrays to create and return to users. Use `numpy.empty(())` by default. attributes : dict A dictionary of JSON-serializable values with user-defined attributes. use_consolidated : bool or str, default None Whether to use consolidated metadata. By default, consolidated metadata is used if it's present in the store (in the ``zarr.json`` for Zarr format 3 and in the ``.zmetadata`` file for Zarr format 2). To explicitly require consolidated metadata, set ``use_consolidated=True``, which will raise an exception if consolidated metadata is not found. To explicitly *not* use consolidated metadata, set ``use_consolidated=False``, which will fall back to using the regular, non consolidated metadata. Zarr format 2 allowed configuring the key storing the consolidated metadata (``.zmetadata`` by default). Specify the custom key as ``use_consolidated`` to load consolidated metadata from a non-default key. Returns ------- g : group The new group. """ if cache_attrs is not None: warnings.warn("cache_attrs is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if synchronizer is not None: warnings.warn("synchronizer is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if meta_array is not None: warnings.warn("meta_array is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if chunk_store is not None: warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) store_path = await make_store_path(store, mode=mode, storage_options=storage_options, path=path) if attributes is None: attributes = {} try: if mode in _READ_MODES: return await AsyncGroup.open( store_path, zarr_format=zarr_format, use_consolidated=use_consolidated ) except (KeyError, FileNotFoundError): pass if mode in _CREATE_MODES: overwrite = _infer_overwrite(mode) _zarr_format = zarr_format or _default_zarr_format() return await AsyncGroup.from_store( store_path, zarr_format=_zarr_format, overwrite=overwrite, attributes=attributes, ) msg = f"No group found in store {store!r} at path {store_path.path!r}" raise GroupNotFoundError(msg) async def create( shape: tuple[int, ...] | int, *, # Note: this is a change from v2 chunks: tuple[int, ...] | int | bool | None = None, dtype: ZDTypeLike | None = None, compressor: CompressorLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, store: StoreLike | None = None, synchronizer: Any | None = None, overwrite: bool = False, path: PathLike | None = None, chunk_store: StoreLike | None = None, filters: Iterable[dict[str, JSON] | Numcodec] | None = None, cache_metadata: bool | None = None, cache_attrs: bool | None = None, read_only: bool | None = None, object_codec: Codec | None = None, # TODO: type has changed dimension_separator: Literal[".", "/"] | None = None, write_empty_chunks: bool | None = None, zarr_format: ZarrFormat | None = None, meta_array: Any | None = None, # TODO: need type attributes: dict[str, JSON] | None = None, # v3 only chunk_shape: tuple[int, ...] | int | None = None, chunk_key_encoding: ( ChunkKeyEncoding | tuple[Literal["default"], Literal[".", "/"]] | tuple[Literal["v2"], Literal[".", "/"]] | None ) = None, codecs: Iterable[Codec | dict[str, JSON]] | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, config: ArrayConfigLike | None = None, **kwargs: Any, ) -> AnyAsyncArray: """Create an array. Parameters ---------- shape : int or tuple of ints Array shape. chunks : int or tuple of ints, optional Chunk shape. If True, will be guessed from ``shape`` and ``dtype``. If False, will be set to ``shape``, i.e., single chunk for the whole array. If an int, the chunk size in each dimension will be given by the value of ``chunks``. Default is True. dtype : str or dtype, optional NumPy dtype. compressor : Codec, optional Primary compressor to compress chunk data. Zarr format 2 only. Zarr format 3 arrays should use ``codecs`` instead. If neither ``compressor`` nor ``filters`` are provided, the default compressor [`zarr.codecs.ZstdCodec`][] is used. If ``compressor`` is set to ``None``, no compression is used. fill_value : Any, optional Fill value for the array. order : {'C', 'F'}, optional Deprecated in favor of the ``config`` keyword argument. Pass ``{'order': }`` to ``create`` instead of using this parameter. Memory layout to be used within each chunk. If not specified, the ``array.order`` parameter in the global config will be used. store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. synchronizer : object, optional Array synchronizer. overwrite : bool, optional If True, delete all pre-existing data in ``store`` at ``path`` before creating the array. path : str, optional Path under which array is stored. chunk_store : StoreLike or None, default=None Separate storage for chunks. If not provided, ``store`` will be used for storage of both chunks and metadata. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. cache_metadata : bool, optional If True, array configuration metadata will be cached for the lifetime of the object. If False, array metadata will be reloaded prior to all data access and modification operations (may incur overhead depending on storage and data access pattern). cache_attrs : bool, optional If True (default), user attributes will be cached for attribute read operations. If False, user attributes are reloaded from the store prior to all attribute read operations. read_only : bool, optional True if array should be protected against modification. object_codec : Codec, optional A codec to encode object arrays, only needed if dtype=object. dimension_separator : {'.', '/'}, optional Separator placed between the dimensions of a chunk. Zarr format 2 only. Zarr format 3 arrays should use ``chunk_key_encoding`` instead. write_empty_chunks : bool, optional Deprecated in favor of the ``config`` keyword argument. Pass ``{'write_empty_chunks': }`` to ``create`` instead of using this parameter. If True, all chunks will be stored regardless of their contents. If False, each chunk is compared to the array's fill value prior to storing. If a chunk is uniformly equal to the fill value, then that chunk is not be stored, and the store entry for that chunk's key is deleted. zarr_format : {2, 3, None}, optional The Zarr format to use when creating an array. The default is ``None``, which instructs Zarr to choose the default Zarr format value defined in the runtime configuration. meta_array : array-like, optional Not implemented. attributes : dict[str, JSON], optional A dictionary of user attributes to store with the array. chunk_shape : int or tuple of ints, optional The shape of the Array's chunks (default is None). Zarr format 3 only. Zarr format 2 arrays should use `chunks` instead. chunk_key_encoding : ChunkKeyEncoding, optional A specification of how the chunk keys are represented in storage. Zarr format 3 only. Zarr format 2 arrays should use `dimension_separator` instead. Default is ``("default", "/")``. codecs : Sequence of Codecs or dicts, optional An iterable of Codec or dict serializations of Codecs. Zarr V3 only. The elements of ``codecs`` specify the transformation from array values to stored bytes. Zarr format 3 only. Zarr format 2 arrays should use ``filters`` and ``compressor`` instead. If no codecs are provided, default codecs will be used based on the data type of the array. For most data types, the default codecs are the tuple ``(BytesCodec(), ZstdCodec())``; data types that require a special [`zarr.abc.codec.ArrayBytesCodec`][], like variable-length strings or bytes, will use the [`zarr.abc.codec.ArrayBytesCodec`][] required for the data type instead of [`zarr.codecs.BytesCodec`][]. dimension_names : Iterable[str | None] | None = None An iterable of dimension names. Zarr format 3 only. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. config : ArrayConfigLike, optional Runtime configuration of the array. If provided, will override the default values from `zarr.config.array`. Returns ------- z : array The array. """ if zarr_format is None: zarr_format = _default_zarr_format() if synchronizer is not None: warnings.warn("synchronizer is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if chunk_store is not None: warnings.warn("chunk_store is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if cache_metadata is not None: warnings.warn("cache_metadata is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if cache_attrs is not None: warnings.warn("cache_attrs is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if object_codec is not None: warnings.warn("object_codec is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if read_only is not None: warnings.warn("read_only is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if meta_array is not None: warnings.warn("meta_array is not yet implemented", ZarrRuntimeWarning, stacklevel=2) if write_empty_chunks is not None: _warn_write_empty_chunks_kwarg() mode = kwargs.pop("mode", None) if mode is None: mode = "a" store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) config_parsed = parse_array_config(config) if write_empty_chunks is not None: if config is not None: msg = ( "Both write_empty_chunks and config keyword arguments are set. " "This is redundant. When both are set, write_empty_chunks will be used instead " "of the value in config." ) warnings.warn(ZarrUserWarning(msg), stacklevel=1) config_parsed = dataclasses.replace(config_parsed, write_empty_chunks=write_empty_chunks) return await AsyncArray._create( store_path, shape=shape, chunks=chunks, dtype=dtype, compressor=compressor, fill_value=fill_value, overwrite=overwrite, filters=filters, dimension_separator=dimension_separator, order=order, zarr_format=zarr_format, chunk_shape=chunk_shape, chunk_key_encoding=chunk_key_encoding, codecs=codecs, dimension_names=dimension_names, attributes=attributes, config=config_parsed, **kwargs, ) async def empty(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an empty array with the specified shape. The contents will be filled with the specified fill value or zeros if no fill value is provided. Parameters ---------- shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Notes ----- The contents of an empty Zarr array are not defined. On attempting to retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ return await create(shape=shape, **kwargs) async def empty_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create an empty array like `a`. The contents will be filled with the array's fill value or zeros if no fill value is provided. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Returns ------- Array The new array. Notes ----- The contents of an empty Zarr array are not defined. On attempting to retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ like_kwargs = _like_args(a) | kwargs if isinstance(a, (AsyncArray | Array)): like_kwargs.setdefault("fill_value", a.metadata.fill_value) return await empty(**like_kwargs) # type: ignore[arg-type] # TODO: add type annotations for fill_value and kwargs async def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> AnyAsyncArray: """Create an array, with `fill_value` being used as the default value for uninitialized portions of the array. Parameters ---------- shape : int or tuple of int Shape of the empty array. fill_value : scalar Fill value. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Returns ------- Array The new array. """ return await create(shape=shape, fill_value=fill_value, **kwargs) # TODO: add type annotations for kwargs async def full_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create a filled array like `a`. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ like_kwargs = _like_args(a) | kwargs if isinstance(a, (AsyncArray | Array)): like_kwargs.setdefault("fill_value", a.metadata.fill_value) return await full(**like_kwargs) # type: ignore[arg-type] async def ones(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with one being used as the default value for uninitialized portions of the array. Parameters ---------- shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ return await create(shape=shape, fill_value=1, **kwargs) async def ones_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create an array of ones like `a`. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ like_kwargs = _like_args(a) | kwargs return await ones(**like_kwargs) # type: ignore[arg-type] async def open_array( *, # note: this is a change from v2 store: StoreLike | None = None, zarr_format: ZarrFormat | None = None, path: PathLike = "", storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to save ) -> AnyAsyncArray: """Open an array using file-mode-like semantics. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str, optional Path in store to array. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs Any keyword arguments to pass to [`create`][zarr.api.asynchronous.create]. Returns ------- AsyncArray The opened array. """ mode = kwargs.pop("mode", None) store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) if "write_empty_chunks" in kwargs: _warn_write_empty_chunks_kwarg() try: return await AsyncArray.open(store_path, zarr_format=zarr_format) except FileNotFoundError as err: if not store_path.read_only and mode in _CREATE_MODES: overwrite = _infer_overwrite(mode) _zarr_format = zarr_format or _default_zarr_format() return await create( store=store_path, zarr_format=_zarr_format, overwrite=overwrite, **kwargs, ) msg = f"No array found in store {store_path.store} at path {store_path.path}" raise ArrayNotFoundError(msg) from err async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyAsyncArray: """Open a persistent array like `a`. Parameters ---------- a : Array The shape and data-type of a define these same attributes of the returned array. path : str The path to the new array. **kwargs Any keyword arguments to pass to the array constructor. Returns ------- AsyncArray The opened array. """ like_kwargs = _like_args(a) | kwargs if isinstance(a, (AsyncArray | Array)): like_kwargs.setdefault("fill_value", a.metadata.fill_value) return await open_array(path=path, **like_kwargs) # type: ignore[arg-type] async def zeros(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with zero being used as the default value for uninitialized portions of the array. Parameters ---------- shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ return await create(shape=shape, fill_value=0, **kwargs) async def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create an array of zeros like `a`. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Returns ------- Array The new array. """ like_kwargs = _like_args(a) | kwargs return await zeros(**like_kwargs) # type: ignore[arg-type] zarr-python-3.2.1/src/zarr/api/synchronous.py000066400000000000000000001516051517635743000213170ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal from typing_extensions import deprecated import zarr.api.asynchronous as async_api import zarr.core.array from zarr.core.array import DEFAULT_FILL_VALUE, Array, AsyncArray, CompressorLike from zarr.core.group import Group from zarr.core.sync import sync from zarr.core.sync_group import create_hierarchy from zarr.errors import ZarrDeprecationWarning if TYPE_CHECKING: from collections.abc import Iterable import numpy as np import numpy.typing as npt from zarr.abc.codec import Codec from zarr.abc.numcodec import Numcodec from zarr.api.asynchronous import ArrayLike, PathLike from zarr.core.array import ( CompressorsLike, FiltersLike, SerializerLike, ShardsLike, ) from zarr.core.array_spec import ArrayConfigLike from zarr.core.buffer import NDArrayLike, NDArrayLikeOrScalar from zarr.core.chunk_key_encodings import ChunkKeyEncoding, ChunkKeyEncodingLike from zarr.core.common import ( JSON, AccessModeLiteral, ChunksLike, DimensionNamesLike, MemoryOrder, ShapeLike, ZarrFormat, ) from zarr.core.dtype import ZDTypeLike from zarr.storage import StoreLike from zarr.types import AnyArray __all__ = [ "array", "consolidate_metadata", "copy", "copy_all", "copy_store", "create", "create_array", "create_hierarchy", "empty", "empty_like", "from_array", "full", "full_like", "group", "load", "ones", "ones_like", "open", "open_array", "open_consolidated", "open_group", "open_like", "save", "save_array", "save_group", "tree", "zeros", "zeros_like", ] def consolidate_metadata( store: StoreLike, path: str | None = None, zarr_format: ZarrFormat | None = None, ) -> Group: """ Consolidate the metadata of all nodes in a hierarchy. Upon completion, the metadata of the root node in the Zarr hierarchy will be updated to include all the metadata of child nodes. For Stores that do not use consolidated metadata, this operation raises a `TypeError`. Parameters ---------- store : StoreLike The store-like object whose metadata you wish to consolidate. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. path : str, optional A path to a group in the store to consolidate at. Only children below that group will be consolidated. By default, the root node is used so all the metadata in the store is consolidated. zarr_format : {2, 3, None}, optional The zarr format of the hierarchy. By default the zarr format is inferred. Returns ------- group: Group The group, with the ``consolidated_metadata`` field set to include the metadata of each child node. If the Store doesn't support consolidated metadata, this function raises a `TypeError`. See ``Store.supports_consolidated_metadata``. """ return Group(sync(async_api.consolidate_metadata(store, path=path, zarr_format=zarr_format))) def copy(*args: Any, **kwargs: Any) -> tuple[int, int, int]: """ Not implemented. """ return sync(async_api.copy(*args, **kwargs)) def copy_all(*args: Any, **kwargs: Any) -> tuple[int, int, int]: """ Not implemented. """ return sync(async_api.copy_all(*args, **kwargs)) def copy_store(*args: Any, **kwargs: Any) -> tuple[int, int, int]: """ Not implemented. """ return sync(async_api.copy_store(*args, **kwargs)) def load( store: StoreLike, path: str | None = None, zarr_format: ZarrFormat | None = None, ) -> NDArrayLikeOrScalar | dict[str, NDArrayLikeOrScalar]: """Load data from an array or group into memory. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. path : str or None, optional The path within the store from which to load. Returns ------- out If the path contains an array, out will be a numpy array. If the path contains a group, out will be a dict-like object where keys are array names and values are numpy arrays. See Also -------- save, savez Notes ----- If loading data from a group of arrays, data will not be immediately loaded into memory. Rather, arrays will be loaded into memory as they are requested. """ return sync(async_api.load(store=store, zarr_format=zarr_format, path=path)) def open( store: StoreLike | None = None, *, mode: AccessModeLiteral | None = None, zarr_format: ZarrFormat | None = None, path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to async_api.open ) -> AnyArray | Group: """Open a group or array using file-mode-like semantics. Parameters ---------- store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. mode : {'r', 'r+', 'a', 'w', 'w-'}, optional Persistence mode: 'r' means read only (must exist); 'r+' means read/write (must exist); 'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists). If the store is read-only, the default is 'r'; otherwise, it is 'a'. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str or None, optional The path within the store to open. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs Additional parameters are passed through to `zarr.open_array` or `zarr.open_group`. Returns ------- z : array or group Return type depends on what exists in the given store. """ obj = sync( async_api.open( store=store, mode=mode, zarr_format=zarr_format, path=path, storage_options=storage_options, **kwargs, ) ) if isinstance(obj, AsyncArray): return Array(obj) else: return Group(obj) def open_consolidated(*args: Any, use_consolidated: Literal[True] = True, **kwargs: Any) -> Group: """ Alias for [`open_group`][zarr.api.synchronous.open_group] with ``use_consolidated=True``. """ return Group( sync(async_api.open_consolidated(*args, use_consolidated=use_consolidated, **kwargs)) ) def save( store: StoreLike, *args: NDArrayLike, zarr_format: ZarrFormat | None = None, path: str | None = None, **kwargs: Any, # TODO: type kwargs as valid args to async_api.save ) -> None: """Save an array or group of arrays to the local file system. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. *args : ndarray NumPy arrays with data to save. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str or None, optional The path within the group where the arrays will be saved. **kwargs NumPy arrays with data to save. """ return sync(async_api.save(store, *args, zarr_format=zarr_format, path=path, **kwargs)) def save_array( store: StoreLike, arr: NDArrayLike, *, zarr_format: ZarrFormat | None = None, path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to async_api.save_array ) -> None: """Save a NumPy array to the local file system. Follows a similar API to the NumPy save() function. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. arr : ndarray NumPy array with data to save. zarr_format : {2, 3, None}, optional The zarr format to use when saving. The default is ``None``, which will use the default Zarr format defined in the global configuration object. path : str or None, optional The path within the store where the array will be saved. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs Passed through to [`create`][zarr.api.asynchronous.create], e.g., compressor. """ return sync( async_api.save_array( store=store, arr=arr, zarr_format=zarr_format, path=path, storage_options=storage_options, **kwargs, ) ) def save_group( store: StoreLike, *args: NDArrayLike, zarr_format: ZarrFormat | None = None, path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: NDArrayLike, ) -> None: """Save several NumPy arrays to the local file system. Follows a similar API to the NumPy savez()/savez_compressed() functions. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. *args : ndarray NumPy arrays with data to save. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str or None, optional Path within the store where the group will be saved. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs NumPy arrays with data to save. """ return sync( async_api.save_group( store, *args, zarr_format=zarr_format, path=path, storage_options=storage_options, **kwargs, ) ) @deprecated("Use Group.tree instead.", category=ZarrDeprecationWarning) def tree(grp: Group, expand: bool | None = None, level: int | None = None) -> Any: """Provide a rich display of the hierarchy. !!! warning "Deprecated" `zarr.tree()` is deprecated since v3.0.0 and will be removed in a future release. Use `group.tree()` instead. Parameters ---------- grp : Group Zarr or h5py group. expand : bool, optional Only relevant for HTML representation. If True, tree will be fully expanded. level : int, optional Maximum depth to descend into hierarchy. Returns ------- TreeRepr A pretty-printable object displaying the hierarchy. """ return sync(async_api.tree(grp._async_group, expand=expand, level=level)) # TODO: add type annotations for kwargs def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyArray: """Create an array filled with `data`. Parameters ---------- data : array_like The data to fill the array with. **kwargs Passed through to [`create`][zarr.api.asynchronous.create]. Returns ------- array : Array The new array. """ return Array(sync(async_api.array(data=data, **kwargs))) def group( store: StoreLike | None = None, *, overwrite: bool = False, chunk_store: StoreLike | None = None, # not used cache_attrs: bool | None = None, # not used, default changed synchronizer: Any | None = None, # not used path: str | None = None, zarr_format: ZarrFormat | None = None, meta_array: Any | None = None, # not used attributes: dict[str, JSON] | None = None, storage_options: dict[str, Any] | None = None, ) -> Group: """Create a group. Parameters ---------- store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. overwrite : bool, optional If True, delete any pre-existing data in `store` at `path` before creating the group. chunk_store : StoreLike or None, default=None Separate storage for chunks. Not implemented. cache_attrs : bool, optional If True (default), user attributes will be cached for attribute read operations. If False, user attributes are reloaded from the store prior to all attribute read operations. synchronizer : object, optional Array synchronizer. path : str, optional Group path within store. meta_array : array-like, optional An array instance to use for determining arrays to create and return to users. Use `numpy.empty(())` by default. zarr_format : {2, 3, None}, optional The zarr format to use when saving. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. Returns ------- g : Group The new group. """ return Group( sync( async_api.group( store=store, overwrite=overwrite, chunk_store=chunk_store, cache_attrs=cache_attrs, synchronizer=synchronizer, path=path, zarr_format=zarr_format, meta_array=meta_array, attributes=attributes, storage_options=storage_options, ) ) ) def open_group( store: StoreLike | None = None, *, mode: AccessModeLiteral = "a", cache_attrs: bool | None = None, # default changed, not used in async api synchronizer: Any = None, # not used in async api path: str | None = None, chunk_store: StoreLike | None = None, # not used in async api storage_options: dict[str, Any] | None = None, # not used in async api zarr_format: ZarrFormat | None = None, meta_array: Any | None = None, # not used in async api attributes: dict[str, JSON] | None = None, use_consolidated: bool | str | None = None, ) -> Group: """Open a group using file-mode-like semantics. Parameters ---------- store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. mode : {'r', 'r+', 'a', 'w', 'w-'}, optional Persistence mode: 'r' means read only (must exist); 'r+' means read/write (must exist); 'a' means read/write (create if doesn't exist); 'w' means create (overwrite if exists); 'w-' means create (fail if exists). cache_attrs : bool, optional If True (default), user attributes will be cached for attribute read operations. If False, user attributes are reloaded from the store prior to all attribute read operations. synchronizer : object, optional Array synchronizer. path : str, optional Group path within store. chunk_store : StoreLike or None, default=None Separate storage for chunks. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. meta_array : array-like, optional An array instance to use for determining arrays to create and return to users. Use `numpy.empty(())` by default. attributes : dict A dictionary of JSON-serializable values with user-defined attributes. use_consolidated : bool or str, default None Whether to use consolidated metadata. By default, consolidated metadata is used if it's present in the store (in the ``zarr.json`` for Zarr format 3 and in the ``.zmetadata`` file for Zarr format 2). To explicitly require consolidated metadata, set ``use_consolidated=True``, which will raise an exception if consolidated metadata is not found. To explicitly *not* use consolidated metadata, set ``use_consolidated=False``, which will fall back to using the regular, non consolidated metadata. Zarr format 2 allowed configuring the key storing the consolidated metadata (``.zmetadata`` by default). Specify the custom key as ``use_consolidated`` to load consolidated metadata from a non-default key. Returns ------- g : Group The new group. """ return Group( sync( async_api.open_group( store=store, mode=mode, cache_attrs=cache_attrs, synchronizer=synchronizer, path=path, chunk_store=chunk_store, storage_options=storage_options, zarr_format=zarr_format, meta_array=meta_array, attributes=attributes, use_consolidated=use_consolidated, ) ) ) def create_group( store: StoreLike, *, path: str | None = None, zarr_format: ZarrFormat | None = None, overwrite: bool = False, attributes: dict[str, Any] | None = None, storage_options: dict[str, Any] | None = None, ) -> Group: """Create a group. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. path : str, optional Group path within store. overwrite : bool, optional If True, pre-existing data at ``path`` will be deleted before creating the group. zarr_format : {2, 3, None}, optional The zarr format to use when saving. If no ``zarr_format`` is provided, the default format will be used. This default can be changed by modifying the value of ``default_zarr_format`` in [`zarr.config`][zarr.config]. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. Returns ------- Group The new group. """ return Group( sync( async_api.create_group( store=store, path=path, overwrite=overwrite, storage_options=storage_options, zarr_format=zarr_format, attributes=attributes, ) ) ) # TODO: add type annotations for kwargs def create( shape: tuple[int, ...] | int, *, # Note: this is a change from v2 chunks: tuple[int, ...] | int | bool | None = None, dtype: ZDTypeLike | None = None, compressor: CompressorLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, # TODO: need type order: MemoryOrder | None = None, store: StoreLike | None = None, synchronizer: Any | None = None, overwrite: bool = False, path: PathLike | None = None, chunk_store: StoreLike | None = None, filters: Iterable[dict[str, JSON] | Numcodec] | None = None, cache_metadata: bool | None = None, cache_attrs: bool | None = None, read_only: bool | None = None, object_codec: Codec | None = None, # TODO: type has changed dimension_separator: Literal[".", "/"] | None = None, write_empty_chunks: bool | None = None, # TODO: default has changed zarr_format: ZarrFormat | None = None, meta_array: Any | None = None, # TODO: need type attributes: dict[str, JSON] | None = None, # v3 only chunk_shape: tuple[int, ...] | int | None = None, chunk_key_encoding: ( ChunkKeyEncoding | tuple[Literal["default"], Literal[".", "/"]] | tuple[Literal["v2"], Literal[".", "/"]] | None ) = None, codecs: Iterable[Codec | dict[str, JSON]] | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, config: ArrayConfigLike | None = None, **kwargs: Any, ) -> AnyArray: """Create an array. Parameters ---------- shape : int or tuple of ints Array shape. chunks : int or tuple of ints, optional Chunk shape. If True, will be guessed from ``shape`` and ``dtype``. If False, will be set to ``shape``, i.e., single chunk for the whole array. If an int, the chunk size in each dimension will be given by the value of ``chunks``. Default is True. dtype : str or dtype, optional NumPy dtype. compressor : Codec, optional Primary compressor to compress chunk data. Zarr format 2 only. Zarr format 3 arrays should use ``codecs`` instead. If neither ``compressor`` nor ``filters`` are provided, the default compressor [`zarr.codecs.ZstdCodec`][] is used. If ``compressor`` is set to ``None``, no compression is used. fill_value : Any, optional Fill value for the array. order : {'C', 'F'}, optional Deprecated in favor of the ``config`` keyword argument. Pass ``{'order': }`` to ``create`` instead of using this parameter. Memory layout to be used within each chunk. If not specified, the ``array.order`` parameter in the global config will be used. store : StoreLike or None, default=None StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. synchronizer : object, optional Array synchronizer. overwrite : bool, optional If True, delete all pre-existing data in ``store`` at ``path`` before creating the array. path : str, optional Path under which array is stored. chunk_store : StoreLike or None, default=None Separate storage for chunks. If not provided, ``store`` will be used for storage of both chunks and metadata. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. cache_metadata : bool, optional If True, array configuration metadata will be cached for the lifetime of the object. If False, array metadata will be reloaded prior to all data access and modification operations (may incur overhead depending on storage and data access pattern). cache_attrs : bool, optional If True (default), user attributes will be cached for attribute read operations. If False, user attributes are reloaded from the store prior to all attribute read operations. read_only : bool, optional True if array should be protected against modification. object_codec : Codec, optional A codec to encode object arrays, only needed if dtype=object. dimension_separator : {'.', '/'}, optional Separator placed between the dimensions of a chunk. Zarr format 2 only. Zarr format 3 arrays should use ``chunk_key_encoding`` instead. write_empty_chunks : bool, optional Deprecated in favor of the ``config`` keyword argument. Pass ``{'write_empty_chunks': }`` to ``create`` instead of using this parameter. If True, all chunks will be stored regardless of their contents. If False, each chunk is compared to the array's fill value prior to storing. If a chunk is uniformly equal to the fill value, then that chunk is not be stored, and the store entry for that chunk's key is deleted. zarr_format : {2, 3, None}, optional The Zarr format to use when creating an array. The default is ``None``, which instructs Zarr to choose the default Zarr format value defined in the runtime configuration. meta_array : array-like, optional Not implemented. attributes : dict[str, JSON], optional A dictionary of user attributes to store with the array. chunk_shape : int or tuple of ints, optional The shape of the Array's chunks (default is None). Zarr format 3 only. Zarr format 2 arrays should use `chunks` instead. chunk_key_encoding : ChunkKeyEncoding, optional A specification of how the chunk keys are represented in storage. Zarr format 3 only. Zarr format 2 arrays should use `dimension_separator` instead. Default is ``("default", "/")``. codecs : Sequence of Codecs or dicts, optional An iterable of Codec or dict serializations of Codecs. Zarr V3 only. The elements of ``codecs`` specify the transformation from array values to stored bytes. Zarr format 3 only. Zarr format 2 arrays should use ``filters`` and ``compressor`` instead. If no codecs are provided, default codecs will be used based on the data type of the array. For most data types, the default codecs are the tuple ``(BytesCodec(), ZstdCodec())``; data types that require a special [`zarr.abc.codec.ArrayBytesCodec`][], like variable-length strings or bytes, will use the [`zarr.abc.codec.ArrayBytesCodec`][] required for the data type instead of [`zarr.codecs.BytesCodec`][]. dimension_names : Iterable[str | None] | None = None An iterable of dimension names. Zarr format 3 only. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. config : ArrayConfigLike, optional Runtime configuration of the array. If provided, will override the default values from `zarr.config.array`. Returns ------- z : Array The array. """ return Array( sync( async_api.create( shape=shape, chunks=chunks, dtype=dtype, compressor=compressor, fill_value=fill_value, order=order, store=store, synchronizer=synchronizer, overwrite=overwrite, path=path, chunk_store=chunk_store, filters=filters, cache_metadata=cache_metadata, cache_attrs=cache_attrs, read_only=read_only, object_codec=object_codec, dimension_separator=dimension_separator, write_empty_chunks=write_empty_chunks, zarr_format=zarr_format, meta_array=meta_array, attributes=attributes, chunk_shape=chunk_shape, chunk_key_encoding=chunk_key_encoding, codecs=codecs, dimension_names=dimension_names, storage_options=storage_options, config=config, **kwargs, ) ) ) def create_array( store: StoreLike, *, name: str | None = None, shape: ShapeLike | None = None, dtype: ZDTypeLike | None = None, data: np.ndarray[Any, np.dtype[Any]] | None = None, chunks: ChunksLike | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", serializer: SerializerLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, zarr_format: ZarrFormat | None = 3, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, ) -> AnyArray: """Create an array. This function wraps [zarr.core.array.create_array][]. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. name : str or None, optional The name of the array within the store. If ``name`` is ``None``, the array will be located at the root of the store. shape : ShapeLike, optional Shape of the array. Must be ``None`` if ``data`` is provided. dtype : ZDTypeLike | None Data type of the array. Must be ``None`` if ``data`` is provided. data : np.ndarray, optional Array-like data to use for initializing the array. If this parameter is provided, the ``shape`` and ``dtype`` parameters must be ``None``. chunks : tuple[int, ...] | Sequence[Sequence[int]] | Literal["auto"], default="auto" Chunk shape of the array. If chunks is "auto", a chunk shape is guessed based on the shape of the array and the dtype. A nested list of per-dimension edge sizes creates a rectilinear grid. Rectilinear chunk grids are experimental and must be explicitly enabled with ``zarr.config.set({'array.rectilinear_chunks': True})`` while the feature is stabilizing. shards : tuple[int, ...], optional Shard shape of the array. The default value of ``None`` results in no sharding at all. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec], optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. For Zarr format 3, a "compressor" is a codec that takes a bytestream, and returns another bytestream. Multiple compressors my be provided for Zarr format 3. If no ``compressors`` are provided, a default set of compressors will be used. These defaults can be changed by modifying the value of ``array.v3_default_compressors`` in [`zarr.config`][zarr.config]. Use ``None`` to omit default compressors. For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may be provided for Zarr format 2. If no ``compressor`` is provided, a default compressor will be used. in [`zarr.config`][zarr.config]. Use ``None`` to omit the default compressor. serializer : dict[str, JSON] | ArrayBytesCodec, optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. If no ``serializer`` is provided, a default serializer will be used. These defaults can be changed by modifying the value of ``array.v3_default_serializer`` in [`zarr.config`][zarr.config]. fill_value : Any, optional Fill value for the array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If no ``order`` is provided, a default order will be used. This default can be changed by modifying the value of ``array.order`` in [`zarr.config`][zarr.config]. zarr_format : {2, 3}, optional The zarr format to use when saving. attributes : dict, optional Attributes for the array. chunk_key_encoding : ChunkKeyEncodingLike, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. dimension_names : Iterable[str], optional The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. storage_options : dict, optional If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. If ``True``, all existing paths in the store will be deleted. config : ArrayConfigLike, optional Runtime configuration for the array. write_data : bool If a pre-existing array-like object was provided to this function via the ``data`` parameter then ``write_data`` determines whether the values in that array-like object should be written to the Zarr array created by this function. If ``write_data`` is ``False``, then the array will be left empty. Returns ------- Array The array. Examples -------- ```python import zarr store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(100,100), chunks=(10,10), dtype='i4', fill_value=0) # ``` """ return Array( sync( zarr.core.array.create_array( store, name=name, shape=shape, dtype=dtype, data=data, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=zarr_format, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, storage_options=storage_options, overwrite=overwrite, config=config, write_data=write_data, ) ) ) def from_array( store: StoreLike, *, data: AnyArray | npt.ArrayLike, write_data: bool = True, name: str | None = None, chunks: ChunksLike | Literal["auto", "keep"] = "keep", shards: ShardsLike | None | Literal["keep"] = "keep", filters: FiltersLike | Literal["keep"] = "keep", compressors: CompressorsLike | Literal["keep"] = "keep", serializer: SerializerLike | Literal["keep"] = "keep", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, zarr_format: ZarrFormat | None = None, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, ) -> AnyArray: """Create an array from an existing array or array-like. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. data : Array | array-like The array to copy. write_data : bool, default True Whether to copy the data from the input array to the new array. If ``write_data`` is ``False``, the new array will be created with the same metadata as the input array, but without any data. name : str or None, optional The name of the array within the store. If ``name`` is ``None``, the array will be located at the root of the store. chunks : tuple[int, ...] or Sequence[Sequence[int]] or "auto" or "keep", optional Chunk shape of the array. Following values are supported: - "auto": Automatically determine the chunk shape based on the array's shape and dtype. - "keep": Retain the chunk grid of the data array if it is a zarr Array. - tuple[int, ...]: A tuple of integers representing the chunk shape (regular grid). - Sequence[Sequence[int]]: Per-dimension chunk edge lists (rectilinear grid). Rectilinear chunk grids are experimental and must be explicitly enabled with ``zarr.config.set({'array.rectilinear_chunks': True})`` while the feature is stabilizing. If not specified, defaults to "keep" if data is a zarr Array, otherwise "auto". shards : tuple[int, ...], optional Shard shape of the array. Following values are supported: - "auto": Automatically determine the shard shape based on the array's shape and chunk shape. - "keep": Retain the shard shape of the data array if it is a zarr Array. - tuple[int, ...]: A tuple of integers representing the shard shape. - None: No sharding. If not specified, defaults to "keep" if data is a zarr Array, otherwise None. filters : Iterable[Codec] | Literal["auto", "keep"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"keep"`` instructs Zarr to infer ``filters`` from ``data``. If that inference is not possible, Zarr will fall back to the behavior specified by ``"auto"``, which is to choose default filters based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are the empty tuple ``()``. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters is a tuple with a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec] or "auto" or "keep", optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. For Zarr format 3, a "compressor" is a codec that takes a bytestream, and returns another bytestream. Multiple compressors my be provided for Zarr format 3. For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may be provided for Zarr format 2. Following values are supported: - Iterable[Codec]: List of compressors to apply to the array. - "auto": Automatically determine the compressors based on the array's dtype. - "keep": Retain the compressors of the input array if it is a zarr Array. If no ``compressors`` are provided, defaults to "keep" if data is a zarr Array, otherwise "auto". serializer : dict[str, JSON] | ArrayBytesCodec or "auto" or "keep", optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. Following values are supported: - dict[str, JSON]: A dict representation of an ``ArrayBytesCodec``. - ArrayBytesCodec: An instance of ``ArrayBytesCodec``. - "auto": a default serializer will be used. These defaults can be changed by modifying the value of ``array.v3_default_serializer`` in [`zarr.config`][zarr.config]. - "keep": Retain the serializer of the input array if it is a zarr Array. fill_value : Any, optional Fill value for the array. If not specified, defaults to the fill value of the data array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If not specified, defaults to the memory order of the data array. zarr_format : {2, 3}, optional The zarr format to use when saving. If not specified, defaults to the zarr format of the data array. attributes : dict, optional Attributes for the array. If not specified, defaults to the attributes of the data array. chunk_key_encoding : ChunkKeyEncoding, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. If not specified and the data array has the same zarr format as the target array, the chunk key encoding of the data array is used. dimension_names : Iterable[str | None] | None The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. If not specified, defaults to the dimension names of the data array. storage_options : dict, optional If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. config : ArrayConfig or ArrayConfigLike, optional Runtime configuration for the array. Returns ------- Array The array. Examples -------- Create an array from an existing Array: ```python import zarr store = zarr.storage.MemoryStore() store2 = zarr.storage.LocalStore('example_from_array.zarr') arr = zarr.create_array( store=store, shape=(100,100), chunks=(10,10), dtype='int32', fill_value=0) arr2 = zarr.from_array(store2, data=arr, overwrite=True) # ``` Create an array from an existing NumPy array: ```python import zarr import numpy as np arr3 = zarr.from_array( zarr.storage.MemoryStore(), data=np.arange(10000, dtype='i4').reshape(100, 100), ) # ``` Create an array from any array-like object: ```python import zarr arr4 = zarr.from_array( zarr.storage.MemoryStore(), data=[[1, 2], [3, 4]], ) # arr4[...] # array([[1, 2],[3, 4]]) ``` Create an array from an existing Array without copying the data: ```python import zarr arr4 = zarr.from_array( zarr.storage.MemoryStore(), data=[[1, 2], [3, 4]], ) arr5 = zarr.from_array( zarr.storage.MemoryStore(), data=arr4, write_data=False, ) # arr5[...] # array([[0, 0],[0, 0]]) ``` """ return Array( sync( zarr.core.array.from_array( store, data=data, write_data=write_data, name=name, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=zarr_format, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, storage_options=storage_options, overwrite=overwrite, config=config, ) ) ) # TODO: add type annotations for kwargs def empty(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an empty array with the specified shape. The contents will be filled with the array's fill value or zeros if no fill value is provided. Parameters ---------- shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Returns ------- Array The new array. Notes ----- The contents of an empty Zarr array are not defined. On attempting to retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ return Array(sync(async_api.empty(shape, **kwargs))) # TODO: move ArrayLike to common module # TODO: add type annotations for kwargs def empty_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create an empty array like another array. The contents will be filled with the array's fill value or zeros if no fill value is provided. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Returns ------- Array The new array. Notes ----- The contents of an empty Zarr array are not defined. On attempting to retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ return Array(sync(async_api.empty_like(a, **kwargs))) # TODO: add type annotations for kwargs and fill_value def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> AnyArray: """Create an array with a default fill value. Parameters ---------- shape : int or tuple of int Shape of the empty array. fill_value : scalar Fill value. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Returns ------- Array The new array. """ return Array(sync(async_api.full(shape=shape, fill_value=fill_value, **kwargs))) # TODO: move ArrayLike to common module # TODO: add type annotations for kwargs def full_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create a filled array like another array. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ return Array(sync(async_api.full_like(a, **kwargs))) # TODO: add type annotations for kwargs def ones(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array with a fill value of one. Parameters ---------- shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ return Array(sync(async_api.ones(shape, **kwargs))) # TODO: add type annotations for kwargs def ones_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create an array of ones like another array. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ return Array(sync(async_api.ones_like(a, **kwargs))) # TODO: update this once async_api.open_array is fully implemented def open_array( store: StoreLike | None = None, *, zarr_format: ZarrFormat | None = None, path: PathLike = "", storage_options: dict[str, Any] | None = None, **kwargs: Any, ) -> AnyArray: """Open an array using file-mode-like semantics. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. zarr_format : {2, 3, None}, optional The zarr format to use when saving. path : str, optional Path in store to array. storage_options : dict If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs Any keyword arguments to pass to [`create`][zarr.api.asynchronous.create]. Returns ------- AsyncArray The opened array. """ return Array( sync( async_api.open_array( store=store, zarr_format=zarr_format, path=path, storage_options=storage_options, **kwargs, ) ) ) # TODO: add type annotations for kwargs def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyArray: """Open a persistent array like another array. Parameters ---------- a : Array The shape and data-type of a define these same attributes of the returned array. path : str The path to the new array. **kwargs Any keyword arguments to pass to the array constructor. Returns ------- AsyncArray The opened array. """ return Array(sync(async_api.open_like(a, path=path, **kwargs))) # TODO: add type annotations for kwargs def zeros(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array with a fill value of zero. Parameters ---------- shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. Returns ------- Array The new array. """ return Array(sync(async_api.zeros(shape=shape, **kwargs))) # TODO: add type annotations for kwargs def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create an array of zeros like another array. Parameters ---------- a : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. Returns ------- Array The new array. """ return Array(sync(async_api.zeros_like(a, **kwargs))) zarr-python-3.2.1/src/zarr/buffer/000077500000000000000000000000001517635743000170435ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/buffer/__init__.py000066400000000000000000000004331517635743000211540ustar00rootroot00000000000000""" Implementations of the Zarr Buffer interface. See Also ======== zarr.abc.buffer: Abstract base class for the Zarr Buffer interface. """ from zarr.buffer import cpu, gpu from zarr.core.buffer import default_buffer_prototype __all__ = ["cpu", "default_buffer_prototype", "gpu"] zarr-python-3.2.1/src/zarr/buffer/cpu.py000066400000000000000000000004161517635743000202050ustar00rootroot00000000000000from zarr.core.buffer.cpu import ( Buffer, NDBuffer, as_numpy_array_wrapper, buffer_prototype, numpy_buffer_prototype, ) __all__ = [ "Buffer", "NDBuffer", "as_numpy_array_wrapper", "buffer_prototype", "numpy_buffer_prototype", ] zarr-python-3.2.1/src/zarr/buffer/gpu.py000066400000000000000000000002111517635743000202020ustar00rootroot00000000000000from zarr.core.buffer.gpu import Buffer, NDBuffer, buffer_prototype __all__ = [ "Buffer", "NDBuffer", "buffer_prototype", ] zarr-python-3.2.1/src/zarr/codecs/000077500000000000000000000000001517635743000170325ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/codecs/__init__.py000066400000000000000000000070501517635743000211450ustar00rootroot00000000000000from __future__ import annotations from zarr.codecs.blosc import BloscCname, BloscCodec, BloscShuffle from zarr.codecs.bytes import BytesCodec, Endian from zarr.codecs.cast_value import CastValue from zarr.codecs.crc32c_ import Crc32cCodec from zarr.codecs.gzip import GzipCodec from zarr.codecs.numcodecs import ( BZ2, CRC32, CRC32C, LZ4, LZMA, ZFPY, Adler32, AsType, BitRound, Blosc, Delta, FixedScaleOffset, Fletcher32, GZip, JenkinsLookup3, PackBits, PCodec, Quantize, Shuffle, Zlib, Zstd, ) from zarr.codecs.scale_offset import ScaleOffset from zarr.codecs.sharding import ShardingCodec, ShardingCodecIndexLocation from zarr.codecs.transpose import TransposeCodec from zarr.codecs.vlen_utf8 import VLenBytesCodec, VLenUTF8Codec from zarr.codecs.zstd import ZstdCodec from zarr.registry import register_codec __all__ = [ "BloscCname", "BloscCodec", "BloscShuffle", "BytesCodec", "CastValue", "Crc32cCodec", "Endian", "GzipCodec", "ScaleOffset", "ShardingCodec", "ShardingCodecIndexLocation", "TransposeCodec", "VLenBytesCodec", "VLenUTF8Codec", "ZstdCodec", ] register_codec("blosc", BloscCodec) register_codec("cast_value", CastValue) register_codec("bytes", 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("vlen-utf8", VLenUTF8Codec) register_codec("vlen-bytes", VLenBytesCodec) register_codec("transpose", TransposeCodec) # Register all the codecs formerly contained in numcodecs.zarr3 register_codec("numcodecs.bz2", BZ2, qualname="zarr.codecs.numcodecs.BZ2") register_codec("numcodecs.crc32", CRC32, qualname="zarr.codecs.numcodecs.CRC32") register_codec("numcodecs.crc32c", CRC32C, qualname="zarr.codecs.numcodecs.CRC32C") register_codec("numcodecs.lz4", LZ4, qualname="zarr.codecs.numcodecs.LZ4") register_codec("numcodecs.lzma", LZMA, qualname="zarr.codecs.numcodecs.LZMA") register_codec("numcodecs.zfpy", ZFPY, qualname="zarr.codecs.numcodecs.ZFPY") register_codec("numcodecs.adler32", Adler32, qualname="zarr.codecs.numcodecs.Adler32") register_codec("numcodecs.astype", AsType, qualname="zarr.codecs.numcodecs.AsType") register_codec("numcodecs.bitround", BitRound, qualname="zarr.codecs.numcodecs.BitRound") register_codec("numcodecs.blosc", Blosc, qualname="zarr.codecs.numcodecs.Blosc") register_codec("numcodecs.delta", Delta, qualname="zarr.codecs.numcodecs.Delta") register_codec( "numcodecs.fixedscaleoffset", FixedScaleOffset, qualname="zarr.codecs.numcodecs.FixedScaleOffset", ) register_codec("numcodecs.fletcher32", Fletcher32, qualname="zarr.codecs.numcodecs.Fletcher32") register_codec("numcodecs.gzip", GZip, qualname="zarr.codecs.numcodecs.GZip") register_codec( "numcodecs.jenkins_lookup3", JenkinsLookup3, qualname="zarr.codecs.numcodecs.JenkinsLookup3" ) register_codec("numcodecs.pcodec", PCodec, qualname="zarr.codecs.numcodecs.PCodec") register_codec("numcodecs.packbits", PackBits, qualname="zarr.codecs.numcodecs.PackBits") register_codec("numcodecs.quantize", Quantize, qualname="zarr.codecs.numcodecs.Quantize") register_codec("numcodecs.shuffle", Shuffle, qualname="zarr.codecs.numcodecs.Shuffle") register_codec("numcodecs.zlib", Zlib, qualname="zarr.codecs.numcodecs.Zlib") register_codec("numcodecs.zstd", Zstd, qualname="zarr.codecs.numcodecs.Zstd") zarr-python-3.2.1/src/zarr/codecs/_v2.py000066400000000000000000000071011517635743000200710ustar00rootroot00000000000000from __future__ import annotations import asyncio from dataclasses import dataclass from typing import TYPE_CHECKING import numpy as np from numcodecs.compat import ensure_bytes, ensure_ndarray_like from zarr.abc.codec import ArrayBytesCodec from zarr.registry import get_ndbuffer_class if TYPE_CHECKING: from zarr.abc.numcodec import Numcodec from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer, NDBuffer @dataclass(frozen=True) class V2Codec(ArrayBytesCodec): filters: tuple[Numcodec, ...] | None compressor: Numcodec | None is_fixed_size = False async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> NDBuffer: cdata = chunk_bytes.as_array_like() # decompress if self.compressor: chunk = await asyncio.to_thread(self.compressor.decode, cdata) else: chunk = cdata # apply filters if self.filters: for f in reversed(self.filters): chunk = await asyncio.to_thread(f.decode, chunk) # view as numpy array with correct dtype chunk = ensure_ndarray_like(chunk) # special case object dtype, because incorrect handling can lead to # segfaults and other bad things happening if chunk_spec.dtype.dtype_cls is not np.dtypes.ObjectDType: try: chunk = chunk.view(chunk_spec.dtype.to_native_dtype()) except TypeError: # this will happen if the dtype of the chunk # does not match the dtype of the array spec i.g. if # the dtype of the chunk_spec is a string dtype, but the chunk # is an object array. In this case, we need to convert the object # array to the correct dtype. chunk = np.array(chunk).astype(chunk_spec.dtype.to_native_dtype()) elif chunk.dtype != object: # If we end up here, someone must have hacked around with the filters. # We cannot deal with object arrays unless there is an object # codec in the filter chain, i.e., a filter that converts from object # array to something else during encoding, and converts back to object # array during decoding. raise RuntimeError("cannot read object array without object codec") # ensure correct chunk shape chunk = chunk.reshape(-1, order="A") chunk = chunk.reshape(chunk_spec.shape, order=chunk_spec.order) return get_ndbuffer_class().from_ndarray_like(chunk) async def _encode_single( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> Buffer | None: chunk = chunk_array.as_ndarray_like() # ensure contiguous and correct order chunk = chunk.astype(chunk_spec.dtype.to_native_dtype(), order=chunk_spec.order, copy=False) # apply filters if self.filters: for f in self.filters: chunk = await asyncio.to_thread(f.encode, chunk) # check object encoding if ensure_ndarray_like(chunk).dtype == object: raise RuntimeError("cannot write object array without object codec") # compress if self.compressor: cdata = await asyncio.to_thread(self.compressor.encode, chunk) else: cdata = chunk cdata = ensure_bytes(cdata) return chunk_spec.prototype.buffer.from_bytes(cdata) def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int: raise NotImplementedError zarr-python-3.2.1/src/zarr/codecs/blosc.py000066400000000000000000000255021517635743000205120ustar00rootroot00000000000000from __future__ import annotations import asyncio from dataclasses import dataclass, field, replace from enum import Enum from functools import cached_property from typing import TYPE_CHECKING, Final, Literal, NotRequired, TypedDict import numcodecs from numcodecs.blosc import Blosc from packaging.version import Version from zarr.abc.codec import BytesBytesCodec from zarr.core.buffer.cpu import as_numpy_array_wrapper from zarr.core.common import JSON, NamedRequiredConfig, parse_enum, parse_named_configuration from zarr.core.dtype.common import HasItemSize if TYPE_CHECKING: from typing import Self from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer Shuffle = Literal["noshuffle", "shuffle", "bitshuffle"] """The shuffle values permitted for the blosc codec""" SHUFFLE: Final = ("noshuffle", "shuffle", "bitshuffle") CName = Literal["lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd"] """The codec identifiers used in the blosc codec """ class BloscConfigV2(TypedDict): """Configuration for the V2 Blosc codec""" cname: CName clevel: int shuffle: int blocksize: int typesize: NotRequired[int] class BloscConfigV3(TypedDict): """Configuration for the V3 Blosc codec""" cname: CName clevel: int shuffle: Shuffle blocksize: int typesize: int class BloscJSON_V3(NamedRequiredConfig[Literal["blosc"], BloscConfigV3]): """ The JSON form of the Blosc codec in Zarr V3. """ class BloscShuffle(Enum): """ Enum for shuffle filter used by blosc. """ noshuffle = "noshuffle" shuffle = "shuffle" bitshuffle = "bitshuffle" @classmethod def from_int(cls, num: int) -> BloscShuffle: blosc_shuffle_int_to_str = { 0: "noshuffle", 1: "shuffle", 2: "bitshuffle", } if num not in blosc_shuffle_int_to_str: raise ValueError(f"Value must be between 0 and 2. Got {num}.") return BloscShuffle[blosc_shuffle_int_to_str[num]] class BloscCname(Enum): """ Enum for compression library used by blosc. """ lz4 = "lz4" lz4hc = "lz4hc" blosclz = "blosclz" zstd = "zstd" snappy = "snappy" zlib = "zlib" # See https://zarr.readthedocs.io/en/stable/user-guide/performance.html#configuring-blosc numcodecs.blosc.use_threads = False def parse_typesize(data: JSON) -> int: if isinstance(data, int): if data > 0: return data else: raise ValueError( f"Value must be greater than 0. Got {data}, which is less or equal to 0." ) raise TypeError(f"Value must be an int. Got {type(data)} instead.") # todo: real validation def parse_clevel(data: JSON) -> int: if isinstance(data, int): return data raise TypeError(f"Value should be an int. Got {type(data)} instead.") def parse_blocksize(data: JSON) -> int: if isinstance(data, int): return data raise TypeError(f"Value should be an int. Got {type(data)} instead.") @dataclass(frozen=True) class BloscCodec(BytesBytesCodec): """ Blosc compression codec for zarr. Blosc is a high-performance compressor optimized for binary data. It uses a combination of blocking, shuffling, and fast compression algorithms to achieve excellent compression ratios and speed. Attributes ---------- is_fixed_size : bool Always False for Blosc codec, as compression produces variable-sized output. typesize : int The data type size in bytes used for shuffle filtering. cname : BloscCname The compression algorithm being used (lz4, lz4hc, blosclz, snappy, zlib, or zstd). clevel : int The compression level (0-9). shuffle : BloscShuffle The shuffle filter mode (noshuffle, shuffle, or bitshuffle). blocksize : int The size of compressed blocks in bytes (0 for automatic). Parameters ---------- typesize : int, optional The data type size in bytes. This affects how the shuffle filter processes the data. If None, defaults to 1 and the attribute is marked as tunable. Default: 1. cname : BloscCname or {'lz4', 'lz4hc', 'blosclz', 'snappy', 'zlib', 'zstd'}, optional The compression algorithm to use. Default: 'zstd'. clevel : int, optional The compression level, from 0 (no compression) to 9 (maximum compression). Higher values provide better compression at the cost of speed. Default: 5. shuffle : BloscShuffle or {'noshuffle', 'shuffle', 'bitshuffle'}, optional The shuffle filter to apply before compression: - 'noshuffle': No shuffling - 'shuffle': Byte shuffling (better for typesize > 1) - 'bitshuffle': Bit shuffling (better for typesize == 1) If None, defaults to 'bitshuffle' and the attribute is marked as tunable. Default: 'bitshuffle'. blocksize : int, optional The requested size of compressed blocks in bytes. A value of 0 means automatic block size selection. Default: 0. Notes ----- **Tunable attributes**: If `typesize` or `shuffle` are set to None during initialization, they are marked as tunable attributes. This means they can be adjusted later based on the data type of the array being compressed. **Thread Safety**: This codec sets `numcodecs.blosc.use_threads = False` at module import time to avoid threading issues in asyncio contexts. Examples -------- Create a Blosc codec with default settings: >>> codec = BloscCodec() >>> codec.typesize 1 >>> codec.shuffle Create a codec with specific compression settings: >>> codec = BloscCodec(cname='zstd', clevel=9, shuffle='shuffle') >>> codec.cname See Also -------- BloscShuffle : Enum for shuffle filter options BloscCname : Enum for compression algorithm options """ # This attribute tracks parameters were set to None at init time, and thus tunable _tunable_attrs: set[Literal["typesize", "shuffle"]] = field(init=False) is_fixed_size = False typesize: int cname: BloscCname clevel: int shuffle: BloscShuffle blocksize: int def __init__( self, *, typesize: int | None = None, cname: BloscCname | CName = BloscCname.zstd, clevel: int = 5, shuffle: BloscShuffle | Shuffle | None = None, blocksize: int = 0, ) -> None: object.__setattr__(self, "_tunable_attrs", set()) # If typesize was set to None, replace it with a valid typesize # and flag the typesize attribute as safe to replace later if typesize is None: typesize = 1 self._tunable_attrs.update({"typesize"}) # If shuffle was set to None, replace it with a valid shuffle # and flag the shuffle attribute as safe to replace later if shuffle is None: shuffle = BloscShuffle.bitshuffle self._tunable_attrs.update({"shuffle"}) typesize_parsed = parse_typesize(typesize) cname_parsed = parse_enum(cname, BloscCname) clevel_parsed = parse_clevel(clevel) shuffle_parsed = parse_enum(shuffle, BloscShuffle) blocksize_parsed = parse_blocksize(blocksize) object.__setattr__(self, "typesize", typesize_parsed) object.__setattr__(self, "cname", cname_parsed) object.__setattr__(self, "clevel", clevel_parsed) object.__setattr__(self, "shuffle", shuffle_parsed) object.__setattr__(self, "blocksize", blocksize_parsed) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration(data, "blosc") return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: result: BloscJSON_V3 = { "name": "blosc", "configuration": { "typesize": self.typesize, "cname": self.cname.value, "clevel": self.clevel, "shuffle": self.shuffle.value, "blocksize": self.blocksize, }, } return result # type: ignore[return-value] def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: """ Create a new codec with typesize and shuffle parameters adjusted according to the size of each element in the data type associated with array_spec. Parameters are only updated if they were set to None when self.__init__ was called. """ item_size = 1 if isinstance(array_spec.dtype, HasItemSize): item_size = array_spec.dtype.item_size new_codec = self if "typesize" in self._tunable_attrs: new_codec = replace(new_codec, typesize=item_size) if "shuffle" in self._tunable_attrs: new_codec = replace( new_codec, shuffle=(BloscShuffle.bitshuffle if item_size == 1 else BloscShuffle.shuffle), ) return new_codec @cached_property def _blosc_codec(self) -> Blosc: map_shuffle_str_to_int = { BloscShuffle.noshuffle: 0, BloscShuffle.shuffle: 1, BloscShuffle.bitshuffle: 2, } config_dict: BloscConfigV2 = { "cname": self.cname.name, # type: ignore[typeddict-item] "clevel": self.clevel, "shuffle": map_shuffle_str_to_int[self.shuffle], "blocksize": self.blocksize, } # See https://github.com/zarr-developers/numcodecs/pull/713 if Version(numcodecs.__version__) >= Version("0.16.0"): config_dict["typesize"] = self.typesize return Blosc.from_config(config_dict) def _decode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: return as_numpy_array_wrapper(self._blosc_codec.decode, chunk_bytes, chunk_spec.prototype) async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: return await asyncio.to_thread(self._decode_sync, chunk_bytes, chunk_spec) def _encode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: # Since blosc only support host memory, we convert the input and output of the encoding # between numpy array and buffer return chunk_spec.prototype.buffer.from_bytes( self._blosc_codec.encode(chunk_bytes.as_numpy_array()) ) async def _encode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: return await asyncio.to_thread(self._encode_sync, chunk_bytes, chunk_spec) def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int: raise NotImplementedError zarr-python-3.2.1/src/zarr/codecs/bytes.py000066400000000000000000000114111517635743000205300ustar00rootroot00000000000000from __future__ import annotations import sys import warnings from dataclasses import dataclass, replace from enum import Enum from typing import TYPE_CHECKING from zarr.abc.codec import ArrayBytesCodec from zarr.core.buffer import Buffer, NDBuffer from zarr.core.common import JSON, parse_enum, parse_named_configuration from zarr.core.dtype.common import HasEndianness from zarr.core.dtype.npy.structured import Struct if TYPE_CHECKING: from typing import Self from zarr.core.array_spec import ArraySpec class Endian(Enum): """ Enum for endian type used by bytes codec. """ big = "big" little = "little" default_system_endian = Endian(sys.byteorder) @dataclass(frozen=True) class BytesCodec(ArrayBytesCodec): """bytes codec""" is_fixed_size = True endian: Endian | None def __init__(self, *, endian: Endian | str | None = default_system_endian) -> None: endian_parsed = None if endian is None else parse_enum(endian, Endian) object.__setattr__(self, "endian", endian_parsed) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( data, "bytes", require_configuration=False ) configuration_parsed = configuration_parsed or {} return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: if self.endian is None: return {"name": "bytes"} else: return {"name": "bytes", "configuration": {"endian": self.endian.value}} def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: if isinstance(array_spec.dtype, Struct): if array_spec.dtype.has_multi_byte_fields(): if self.endian is None: warnings.warn( "Missing 'endian' for structured dtype with multi-byte fields. " "Assuming little-endian for legacy compatibility.", UserWarning, stacklevel=2, ) return replace(self, endian=Endian.little) else: if self.endian is not None: return replace(self, endian=None) elif not isinstance(array_spec.dtype, HasEndianness): if self.endian is not None: return replace(self, endian=None) elif self.endian is None: raise ValueError( "The `endian` configuration needs to be specified for multi-byte data types." ) return self def _decode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> NDBuffer: # TODO: remove endianness enum in favor of literal union endian_str = self.endian.value if self.endian is not None else None if isinstance(chunk_spec.dtype, HasEndianness): dtype = replace(chunk_spec.dtype, endianness=endian_str).to_native_dtype() # type: ignore[call-arg] else: dtype = chunk_spec.dtype.to_native_dtype() as_array_like = chunk_bytes.as_array_like() chunk_array = chunk_spec.prototype.nd_buffer.from_ndarray_like( as_array_like.view(dtype=dtype) # type: ignore[attr-defined] ) # ensure correct chunk shape if chunk_array.shape != chunk_spec.shape: chunk_array = chunk_array.reshape( chunk_spec.shape, ) return chunk_array async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> NDBuffer: return self._decode_sync(chunk_bytes, chunk_spec) def _encode_sync( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> Buffer | None: assert isinstance(chunk_array, NDBuffer) if ( chunk_array.dtype.itemsize > 1 and self.endian is not None and self.endian != chunk_array.byteorder ): # type-ignore is a numpy bug # see https://github.com/numpy/numpy/issues/26473 new_dtype = chunk_array.dtype.newbyteorder(self.endian.name) # type: ignore[arg-type] chunk_array = chunk_array.astype(new_dtype) nd_array = chunk_array.as_ndarray_like() # Flatten the nd-array (only copy if needed) and reinterpret as bytes nd_array = nd_array.ravel().view(dtype="B") return chunk_spec.prototype.buffer.from_array_like(nd_array) async def _encode_single( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> Buffer | None: return self._encode_sync(chunk_array, chunk_spec) def compute_encoded_size(self, input_byte_length: int, _chunk_spec: ArraySpec) -> int: return input_byte_length zarr-python-3.2.1/src/zarr/codecs/cast_value.py000066400000000000000000000351101517635743000215320ustar00rootroot00000000000000"""Cast-value array-to-array codec. Value-converts array elements to a new data type during encoding, and back to the original data type during decoding, with configurable rounding, out-of-range handling, and explicit scalar mappings. Requires the optional ``cast-value-rs`` package for the actual casting logic. Install it with: ``pip install cast-value-rs``. """ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass, replace from typing import TYPE_CHECKING, Final, Literal, TypedDict, cast import numpy as np from zarr.abc.codec import ArrayArrayCodec from zarr.core.common import JSON, parse_named_configuration from zarr.core.dtype import get_data_type_from_json if TYPE_CHECKING: from typing import NotRequired, Self from zarr.core.array_spec import ArraySpec from zarr.core.buffer import NDBuffer from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType from zarr.core.metadata.v3 import ChunkGridMetadata class ScalarMapJSON(TypedDict): encode: NotRequired[list[tuple[object, object]]] 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. """ encode: Mapping[str | float | int, str | float | int] decode: Mapping[str | float | int, str | float | int] # see https://github.com/zarr-developers/zarr-extensions/tree/main/codecs/cast_value CAST_VALUE_INT_DTYPES: Final[set[str]] = { # signed "int2", "int4", "int8", "int16", "int32", "int64", # unsigned "uint2", "uint4", "uint8", "uint16", "uint32", "uint64", } """Integer dtype identifiers permitted as the source or target of `cast_value`. Membership in this set drives the `out_of_range="wrap"` rule, which the spec restricts to integral targets that use two's-complement representation for modular arithmetic. """ CAST_VALUE_FLOAT_DTYPES: Final[set[str]] = { "float4_e2m1fn", "float6_e2m3fn", "float6_e3m2fn", "float8_e3m4", "float8_e4m3", "float8_e4m3b11fnuz", "float8_e4m3fnuz", "float8_e5m2", "float8_e5m2fnuz", "float8_e8m0fnu", "bfloat16", "float16", "float32", "float64", } """Floating-point dtype identifiers permitted as the source or target of `cast_value`.""" PERMITTED_DATA_TYPE_NAMES: Final[set[str]] = CAST_VALUE_INT_DTYPES | CAST_VALUE_FLOAT_DTYPES """All dtype identifiers the `cast_value` codec is defined for.""" def parse_scalar_map(obj: ScalarMapJSON | ScalarMap) -> ScalarMap: """ Parse a scalar map into its normalized dict-of-dicts form. Accepts either the JSON form (lists of tuples) or an already-normalized form (dicts). For example, ``{"encode": [("NaN", 0)]}`` becomes ``{"encode": {"NaN": 0}}``. """ result: ScalarMap = {} for direction in ("encode", "decode"): if direction in obj: entries = obj[direction] if entries is not None: if isinstance(entries, Mapping): result[direction] = entries else: result[direction] = dict(entries) # type: ignore[arg-type] return result # --------------------------------------------------------------------------- # Backend: cast-value-rs # --------------------------------------------------------------------------- try: from cast_value_rs import cast_array as cast_array_rs _HAS_RUST_BACKEND = True except ModuleNotFoundError: _HAS_RUST_BACKEND = False def _check_representable( value: JSON, zdtype: ZDType[TBaseDType, TBaseScalar], label: str, ) -> None: """Raise ``ValueError`` if *value* cannot be parsed by *zdtype*.""" try: zdtype.from_json_scalar(value, zarr_format=3) except (TypeError, ValueError, OverflowError) as e: raise ValueError( f"{label} {value!r} is not representable in dtype {zdtype.to_native_dtype()}." ) from e # --------------------------------------------------------------------------- # Codec # --------------------------------------------------------------------------- @dataclass(frozen=True) class CastValue(ArrayArrayCodec): """Cast-value array-to-array codec. Value-converts array elements to a new data type during encoding, and back to the original data type during decoding. Requires the `cast-value-rs` package for the actual casting logic. Parameters ---------- data_type : str or ZDType Target zarr v3 data type. Strings are looked up by spec name (e.g. "uint8", "float32"); a `ZDType` instance is used as-is. rounding : RoundingMode How to round when exact representation is impossible. Default is "nearest-even". out_of_range : OutOfRangeMode or None What to do when a value is outside the target's range. `None` means error; "clamp" clips to range; "wrap" uses modular arithmetic (only valid for integer types). Default is `None`. scalar_map : ScalarMap, ScalarMapJSON, or None Explicit mapping from input scalars to output scalars. Default is `None`. Attributes ---------- dtype : ZDType Resolved target data type (a `ZDType` instance, regardless of whether the constructor received a string or a `ZDType`). rounding : RoundingMode The rounding mode, as supplied to the constructor. out_of_range : OutOfRangeMode or None The out-of-range behaviour, as supplied to the constructor. scalar_map : ScalarMap or None Parsed scalar map (always normalized to `ScalarMap` form). References ---------- - The `cast_value` codec spec: https://github.com/zarr-developers/zarr-extensions/tree/main/codecs/cast_value """ is_fixed_size = True dtype: ZDType[TBaseDType, TBaseScalar] rounding: RoundingMode out_of_range: OutOfRangeMode | None scalar_map: ScalarMap | None def __init__( self, *, data_type: str | ZDType[TBaseDType, TBaseScalar], rounding: RoundingMode = "nearest-even", out_of_range: OutOfRangeMode | None = None, scalar_map: ScalarMapJSON | ScalarMap | None = None, ) -> None: if isinstance(data_type, str): zdtype = get_data_type_from_json(data_type, zarr_format=3) else: zdtype = data_type if zdtype.to_json(zarr_format=3) not in PERMITTED_DATA_TYPE_NAMES: raise ValueError( f"Invalid target data type {data_type!r}. " f"cast_value codec only supports integer and floating-point data types. " f"Got {zdtype}." ) object.__setattr__(self, "dtype", zdtype) object.__setattr__(self, "rounding", rounding) object.__setattr__(self, "out_of_range", out_of_range) if scalar_map is not None: parsed = parse_scalar_map(scalar_map) else: parsed = None object.__setattr__(self, "scalar_map", parsed) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( data, "cast_value", require_configuration=True ) return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: config: dict[str, JSON] = {"data_type": cast("JSON", self.dtype.to_json(zarr_format=3))} if self.rounding != "nearest-even": config["rounding"] = self.rounding 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]]] = {} 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()] config["scalar_map"] = cast("JSON", json_map) return {"name": "cast_value", "configuration": config} def validate( self, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, ) -> None: # `dtype` is the source (the array's dtype); `self.dtype` is the # cast target. The spec requires both to be permitted, and rules # like `out_of_range="wrap"` apply to the target. source_name = dtype.to_json(zarr_format=3) target_name = self.dtype.to_json(zarr_format=3) for role, name in (("source", source_name), ("target", target_name)): if name not in PERMITTED_DATA_TYPE_NAMES: raise ValueError( f"The cast_value codec only supports integer and floating-point data types. " f"Got {role} dtype {name}." ) if self.out_of_range == "wrap" and target_name not in CAST_VALUE_INT_DTYPES: raise ValueError( f"out_of_range='wrap' is only valid for integer target types. " f"Got target dtype {target_name}." ) if self.scalar_map is not None: self._validate_scalar_map(dtype, self.dtype) def _validate_scalar_map( self, source_zdtype: ZDType[TBaseDType, TBaseScalar], target_zdtype: ZDType[TBaseDType, TBaseScalar], ) -> None: """Validate that scalar map entries are compatible with source/target dtypes.""" assert self.scalar_map is not None # For encode: keys are source values, values are target values. # For decode: keys are target values, values are source values. direction_dtypes: dict[ str, tuple[ZDType[TBaseDType, TBaseScalar], ZDType[TBaseDType, TBaseScalar]] ] = { "encode": (source_zdtype, target_zdtype), "decode": (target_zdtype, source_zdtype), } for direction, (key_zdtype, val_zdtype) in direction_dtypes.items(): if direction not in self.scalar_map: continue sub_map = self.scalar_map[direction] # type: ignore[literal-required] for k, v in sub_map.items(): _check_representable(k, key_zdtype, f"scalar_map {direction} key") _check_representable(v, val_zdtype, f"scalar_map {direction} value") def _do_cast( self, arr: np.ndarray, # type: ignore[type-arg] *, target_dtype: np.dtype, # type: ignore[type-arg] scalar_map: Mapping[str | float | int, str | float | int] | None, ) -> np.ndarray: # type: ignore[type-arg] if not _HAS_RUST_BACKEND: raise ImportError( "The cast_value codec requires the 'cast-value-rs' package. " "Install it with: pip install cast-value-rs" ) scalar_map_entries: dict[float | int, float | int] | None = None if scalar_map is not None: src_dtype = arr.dtype to_src = int if np.issubdtype(src_dtype, np.integer) else float to_tgt = int if np.issubdtype(target_dtype, np.integer) else float scalar_map_entries = {to_src(k): to_tgt(v) for k, v in scalar_map.items()} return cast_array_rs( # type: ignore[no-any-return] arr, target_dtype=target_dtype, rounding_mode=self.rounding, out_of_range_mode=self.out_of_range, scalar_map_entries=scalar_map_entries, ) def _get_scalar_map( self, direction: str ) -> Mapping[str | float | int, str | float | int] | None: """Extract the encode or decode mapping from scalar_map, or None.""" if self.scalar_map is None: return None return self.scalar_map.get(direction) # type: ignore[return-value] def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: """ Update the fill value of the output spec by applying casting procedure. """ target_zdtype = self.dtype target_native = target_zdtype.to_native_dtype() source_native = chunk_spec.dtype.to_native_dtype() fill = chunk_spec.fill_value fill_arr = np.array([fill], dtype=source_native) new_fill_arr = self._do_cast( fill_arr, target_dtype=target_native, scalar_map=self._get_scalar_map("encode") ) new_fill = target_native.type(new_fill_arr[0]) return replace(chunk_spec, dtype=target_zdtype, fill_value=new_fill) def _encode_sync( self, chunk_array: NDBuffer, _chunk_spec: ArraySpec, ) -> NDBuffer | None: arr = chunk_array.as_ndarray_like() target_native = self.dtype.to_native_dtype() result = self._do_cast( np.asarray(arr), target_dtype=target_native, scalar_map=self._get_scalar_map("encode") ) return chunk_array.__class__.from_ndarray_like(result) async def _encode_single( self, chunk_data: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer | None: return self._encode_sync(chunk_data, chunk_spec) def _decode_sync( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer: arr = chunk_array.as_ndarray_like() target_native = chunk_spec.dtype.to_native_dtype() result = self._do_cast( np.asarray(arr), target_dtype=target_native, scalar_map=self._get_scalar_map("decode") ) return chunk_array.__class__.from_ndarray_like(result) async def _decode_single( self, chunk_data: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer: return self._decode_sync(chunk_data, chunk_spec) def compute_encoded_size(self, input_byte_length: int, chunk_spec: ArraySpec) -> int: dtype_name = chunk_spec.dtype.to_json(zarr_format=3) if dtype_name not in PERMITTED_DATA_TYPE_NAMES: raise ValueError( "cast_value codec only supports fixed-size integer and floating-point data types. " f"Got source dtype: {chunk_spec.dtype}." ) source_itemsize = chunk_spec.dtype.to_native_dtype().itemsize target_itemsize = self.dtype.to_native_dtype().itemsize if source_itemsize == 0 or target_itemsize == 0: raise ValueError( "cast_value codec requires fixed-size data types. " f"Got source itemsize={source_itemsize}, target itemsize={target_itemsize}." ) num_elements = input_byte_length // source_itemsize return num_elements * target_itemsize zarr-python-3.2.1/src/zarr/codecs/crc32c_.py000066400000000000000000000050141517635743000206220ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, cast import google_crc32c import numpy as np import typing_extensions from zarr.abc.codec import BytesBytesCodec from zarr.core.common import JSON, parse_named_configuration if TYPE_CHECKING: from typing import Self from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer @dataclass(frozen=True) class Crc32cCodec(BytesBytesCodec): """crc32c codec""" is_fixed_size = True @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: parse_named_configuration(data, "crc32c", require_configuration=False) return cls() def to_dict(self) -> dict[str, JSON]: return {"name": "crc32c"} def _decode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: data = chunk_bytes.as_numpy_array() crc32_bytes = data[-4:] inner_bytes = data[:-4] # Need to do a manual cast until https://github.com/numpy/numpy/issues/26783 is resolved computed_checksum = np.uint32( google_crc32c.value(cast("typing_extensions.Buffer", inner_bytes)) ).tobytes() stored_checksum = bytes(crc32_bytes) if computed_checksum != stored_checksum: raise ValueError( f"Stored and computed checksum do not match. Stored: {stored_checksum!r}. Computed: {computed_checksum!r}." ) return chunk_spec.prototype.buffer.from_array_like(inner_bytes) async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: return self._decode_sync(chunk_bytes, chunk_spec) def _encode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: data = chunk_bytes.as_numpy_array() # Calculate the checksum and "cast" it to a numpy array checksum = np.array( [google_crc32c.value(cast("typing_extensions.Buffer", data))], dtype=np.uint32 ) # Append the checksum (as bytes) to the data return chunk_spec.prototype.buffer.from_array_like(np.append(data, checksum.view("B"))) async def _encode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: return self._encode_sync(chunk_bytes, chunk_spec) def compute_encoded_size(self, input_byte_length: int, _chunk_spec: ArraySpec) -> int: return input_byte_length + 4 zarr-python-3.2.1/src/zarr/codecs/gzip.py000066400000000000000000000047071517635743000203650ustar00rootroot00000000000000from __future__ import annotations import asyncio from dataclasses import dataclass from functools import cached_property from typing import TYPE_CHECKING from numcodecs.gzip import GZip from zarr.abc.codec import BytesBytesCodec from zarr.core.buffer.cpu import as_numpy_array_wrapper from zarr.core.common import JSON, parse_named_configuration if TYPE_CHECKING: from typing import Self from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer def parse_gzip_level(data: JSON) -> int: if not isinstance(data, (int)): raise TypeError(f"Expected int, got {type(data)}") if data not in range(10): raise ValueError( f"Expected an integer from the inclusive range (0, 9). Got {data} instead." ) return data @dataclass(frozen=True) class GzipCodec(BytesBytesCodec): """gzip codec""" is_fixed_size = False level: int = 5 def __init__(self, *, level: int = 5) -> None: level_parsed = parse_gzip_level(level) object.__setattr__(self, "level", level_parsed) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration(data, "gzip") return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: return {"name": "gzip", "configuration": {"level": self.level}} @cached_property def _gzip_codec(self) -> GZip: return GZip(self.level) def _decode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: return as_numpy_array_wrapper(self._gzip_codec.decode, chunk_bytes, chunk_spec.prototype) async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: return await asyncio.to_thread(self._decode_sync, chunk_bytes, chunk_spec) def _encode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: return as_numpy_array_wrapper(self._gzip_codec.encode, chunk_bytes, chunk_spec.prototype) async def _encode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: return await asyncio.to_thread(self._encode_sync, chunk_bytes, chunk_spec) def compute_encoded_size( self, _input_byte_length: int, _chunk_spec: ArraySpec, ) -> int: raise NotImplementedError zarr-python-3.2.1/src/zarr/codecs/numcodecs/000077500000000000000000000000001517635743000210125ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/codecs/numcodecs/__init__.py000066400000000000000000000016051517635743000231250ustar00rootroot00000000000000from __future__ import annotations from zarr.codecs.numcodecs._codecs import ( BZ2, CRC32, CRC32C, LZ4, LZMA, ZFPY, Adler32, AsType, BitRound, Blosc, Delta, FixedScaleOffset, Fletcher32, GZip, JenkinsLookup3, PackBits, PCodec, Quantize, Shuffle, Zlib, Zstd, _NumcodecsArrayArrayCodec, _NumcodecsArrayBytesCodec, _NumcodecsBytesBytesCodec, _NumcodecsCodec, ) __all__ = [ "BZ2", "CRC32", "CRC32C", "LZ4", "LZMA", "ZFPY", "Adler32", "AsType", "BitRound", "Blosc", "Delta", "FixedScaleOffset", "Fletcher32", "GZip", "JenkinsLookup3", "PCodec", "PackBits", "Quantize", "Shuffle", "Zlib", "Zstd", "_NumcodecsArrayArrayCodec", "_NumcodecsArrayBytesCodec", "_NumcodecsBytesBytesCodec", "_NumcodecsCodec", ] zarr-python-3.2.1/src/zarr/codecs/numcodecs/_codecs.py000066400000000000000000000266041517635743000227730ustar00rootroot00000000000000""" This module provides compatibility for [numcodecs][] in Zarr version 3. These codecs were previously defined in [numcodecs][], and have now been moved to `zarr`. ```python import numpy as np import zarr import zarr.codecs.numcodecs as numcodecs array = zarr.create_array( store="data_numcodecs.zarr", shape=(1024, 1024), chunks=(64, 64), dtype="uint32", filters=[numcodecs.Delta(dtype="uint32")], compressors=[numcodecs.BZ2(level=5)], overwrite=True) array[:] = np.arange(np.prod(array.shape), dtype=array.dtype).reshape(*array.shape) ``` !!! note Please note that the codecs in [zarr.codecs.numcodecs][] are not part of the Zarr version 3 specification. Using these codecs might cause interoperability issues with other Zarr implementations. """ from __future__ import annotations import asyncio import math from dataclasses import dataclass, replace from functools import cached_property from typing import TYPE_CHECKING, Any, Self import numpy as np from zarr.abc.codec import ArrayArrayCodec, ArrayBytesCodec, BytesBytesCodec from zarr.abc.metadata import Metadata from zarr.core.buffer.cpu import as_numpy_array_wrapper from zarr.core.common import JSON, parse_named_configuration, product from zarr.dtype import UInt8, ZDType, parse_dtype from zarr.registry import get_numcodec if TYPE_CHECKING: from zarr.abc.numcodec import Numcodec from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer, BufferPrototype, NDBuffer CODEC_PREFIX = "numcodecs." def _expect_name_prefix(codec_name: str) -> str: if not codec_name.startswith(CODEC_PREFIX): raise ValueError( f"Expected name to start with '{CODEC_PREFIX}'. Got {codec_name} instead." ) # pragma: no cover return codec_name.removeprefix(CODEC_PREFIX) def _parse_codec_configuration(data: dict[str, JSON]) -> dict[str, JSON]: parsed_name, parsed_configuration = parse_named_configuration(data) if not parsed_name.startswith(CODEC_PREFIX): raise ValueError( f"Expected name to start with '{CODEC_PREFIX}'. Got {parsed_name} instead." ) # pragma: no cover id = _expect_name_prefix(parsed_name) return {"id": id, **parsed_configuration} @dataclass(frozen=True) class _NumcodecsCodec(Metadata): codec_name: str codec_config: dict[str, JSON] def __init_subclass__(cls, *, codec_name: str | None = None, **kwargs: Any) -> None: """To be used only when creating the actual public-facing codec class.""" super().__init_subclass__(**kwargs) if codec_name is not None: namespace = codec_name cls_name = f"{CODEC_PREFIX}{namespace}.{cls.__name__}" cls.codec_name = f"{CODEC_PREFIX}{namespace}" cls.__doc__ = f""" See [{cls_name}][] for more details and parameters. """ def __init__(self, **codec_config: JSON) -> None: if not self.codec_name: raise ValueError( "The codec name needs to be supplied through the `codec_name` attribute." ) # pragma: no cover unprefixed_codec_name = _expect_name_prefix(self.codec_name) if "id" not in codec_config: codec_config = {"id": unprefixed_codec_name, **codec_config} elif codec_config["id"] != unprefixed_codec_name: raise ValueError( f"Codec id does not match {unprefixed_codec_name}. Got: {codec_config['id']}." ) # pragma: no cover object.__setattr__(self, "codec_config", codec_config) @cached_property def _codec(self) -> Numcodec: return get_numcodec(self.codec_config) # type: ignore[arg-type] @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: codec_config = _parse_codec_configuration(data) return cls(**codec_config) def to_dict(self) -> dict[str, JSON]: codec_config = self.codec_config.copy() codec_config.pop("id", None) return { "name": self.codec_name, "configuration": codec_config, } def compute_encoded_size(self, input_byte_length: int, chunk_spec: ArraySpec) -> int: raise NotImplementedError # pragma: no cover # Override __repr__ because dynamically constructed classes don't seem to work otherwise def __repr__(self) -> str: codec_config = self.codec_config.copy() codec_config.pop("id", None) return f"{self.__class__.__name__}(codec_name={self.codec_name!r}, codec_config={codec_config!r})" class _NumcodecsBytesBytesCodec(_NumcodecsCodec, BytesBytesCodec): def __init__(self, **codec_config: JSON) -> None: super().__init__(**codec_config) async def _decode_single(self, chunk_data: Buffer, chunk_spec: ArraySpec) -> Buffer: return await asyncio.to_thread( as_numpy_array_wrapper, self._codec.decode, chunk_data, chunk_spec.prototype, ) def _encode(self, chunk_data: Buffer, prototype: BufferPrototype) -> Buffer: encoded = self._codec.encode(chunk_data.as_array_like()) if isinstance(encoded, np.ndarray): # Required for checksum codecs return prototype.buffer.from_bytes(encoded.tobytes()) return prototype.buffer.from_bytes(encoded) async def _encode_single(self, chunk_data: Buffer, chunk_spec: ArraySpec) -> Buffer: return await asyncio.to_thread(self._encode, chunk_data, chunk_spec.prototype) class _NumcodecsArrayArrayCodec(_NumcodecsCodec, ArrayArrayCodec): def __init__(self, **codec_config: JSON) -> None: super().__init__(**codec_config) async def _decode_single(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> NDBuffer: chunk_ndarray = chunk_data.as_ndarray_like() out = await asyncio.to_thread(self._codec.decode, chunk_ndarray) return chunk_spec.prototype.nd_buffer.from_ndarray_like(out.reshape(chunk_spec.shape)) async def _encode_single(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> NDBuffer: chunk_ndarray = chunk_data.as_ndarray_like() out = await asyncio.to_thread(self._codec.encode, chunk_ndarray) return chunk_spec.prototype.nd_buffer.from_ndarray_like(out) class _NumcodecsArrayBytesCodec(_NumcodecsCodec, ArrayBytesCodec): def __init__(self, **codec_config: JSON) -> None: super().__init__(**codec_config) async def _decode_single(self, chunk_data: Buffer, chunk_spec: ArraySpec) -> NDBuffer: chunk_bytes = chunk_data.to_bytes() out = await asyncio.to_thread(self._codec.decode, chunk_bytes) return chunk_spec.prototype.nd_buffer.from_ndarray_like(out.reshape(chunk_spec.shape)) async def _encode_single(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> Buffer: chunk_ndarray = chunk_data.as_ndarray_like() out = await asyncio.to_thread(self._codec.encode, chunk_ndarray) return chunk_spec.prototype.buffer.from_bytes(out) # bytes-to-bytes codecs class Blosc(_NumcodecsBytesBytesCodec, codec_name="blosc"): pass class LZ4(_NumcodecsBytesBytesCodec, codec_name="lz4"): pass class Zstd(_NumcodecsBytesBytesCodec, codec_name="zstd"): pass class Zlib(_NumcodecsBytesBytesCodec, codec_name="zlib"): pass class GZip(_NumcodecsBytesBytesCodec, codec_name="gzip"): pass class BZ2(_NumcodecsBytesBytesCodec, codec_name="bz2"): pass class LZMA(_NumcodecsBytesBytesCodec, codec_name="lzma"): pass class Shuffle(_NumcodecsBytesBytesCodec, codec_name="shuffle"): def evolve_from_array_spec(self, array_spec: ArraySpec) -> Shuffle: if self.codec_config.get("elementsize") is None: dtype = array_spec.dtype.to_native_dtype() return Shuffle(**{**self.codec_config, "elementsize": dtype.itemsize}) return self # pragma: no cover # array-to-array codecs ("filters") class Delta(_NumcodecsArrayArrayCodec, codec_name="delta"): def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: if astype := self.codec_config.get("astype"): dtype = parse_dtype(np.dtype(astype), zarr_format=3) # type: ignore[call-overload] return replace(chunk_spec, dtype=dtype) return chunk_spec class BitRound(_NumcodecsArrayArrayCodec, codec_name="bitround"): pass class FixedScaleOffset(_NumcodecsArrayArrayCodec, codec_name="fixedscaleoffset"): def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: if astype := self.codec_config.get("astype"): dtype = parse_dtype(np.dtype(astype), zarr_format=3) # type: ignore[call-overload] return replace(chunk_spec, dtype=dtype) return chunk_spec def evolve_from_array_spec(self, array_spec: ArraySpec) -> FixedScaleOffset: if self.codec_config.get("dtype") is None: dtype = array_spec.dtype.to_native_dtype() return FixedScaleOffset(**{**self.codec_config, "dtype": str(dtype)}) return self class Quantize(_NumcodecsArrayArrayCodec, codec_name="quantize"): def __init__(self, **codec_config: JSON) -> None: super().__init__(**codec_config) def evolve_from_array_spec(self, array_spec: ArraySpec) -> Quantize: if self.codec_config.get("dtype") is None: dtype = array_spec.dtype.to_native_dtype() return Quantize(**{**self.codec_config, "dtype": str(dtype)}) return self class PackBits(_NumcodecsArrayArrayCodec, codec_name="packbits"): def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: return replace( chunk_spec, shape=(1 + math.ceil(product(chunk_spec.shape) / 8),), dtype=UInt8(), ) # todo: remove this type: ignore when this class can be defined w.r.t. # a single zarr dtype API def validate(self, *, dtype: ZDType[Any, Any], **_kwargs: Any) -> None: # this is bugged and will fail _dtype = dtype.to_native_dtype() if _dtype != np.dtype("bool"): raise ValueError(f"Packbits filter requires bool dtype. Got {dtype}.") class AsType(_NumcodecsArrayArrayCodec, codec_name="astype"): def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: dtype = parse_dtype(np.dtype(self.codec_config["encode_dtype"]), zarr_format=3) # type: ignore[arg-type] return replace(chunk_spec, dtype=dtype) def evolve_from_array_spec(self, array_spec: ArraySpec) -> AsType: if self.codec_config.get("decode_dtype") is None: # TODO: remove these coverage exemptions the correct way, i.e. with tests dtype = array_spec.dtype.to_native_dtype() # pragma: no cover return AsType(**{**self.codec_config, "decode_dtype": str(dtype)}) # pragma: no cover return self # bytes-to-bytes checksum codecs class _NumcodecsChecksumCodec(_NumcodecsBytesBytesCodec): def compute_encoded_size(self, input_byte_length: int, chunk_spec: ArraySpec) -> int: return input_byte_length + 4 # pragma: no cover class CRC32(_NumcodecsChecksumCodec, codec_name="crc32"): pass class CRC32C(_NumcodecsChecksumCodec, codec_name="crc32c"): pass class Adler32(_NumcodecsChecksumCodec, codec_name="adler32"): pass class Fletcher32(_NumcodecsChecksumCodec, codec_name="fletcher32"): pass class JenkinsLookup3(_NumcodecsChecksumCodec, codec_name="jenkins_lookup3"): pass # array-to-bytes codecs class PCodec(_NumcodecsArrayBytesCodec, codec_name="pcodec"): pass class ZFPY(_NumcodecsArrayBytesCodec, codec_name="zfpy"): pass zarr-python-3.2.1/src/zarr/codecs/scale_offset.py000066400000000000000000000435441517635743000220530ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, replace from typing import TYPE_CHECKING, Any, cast import numpy as np import numpy.typing as npt from zarr.abc.codec import ArrayArrayCodec from zarr.core.common import JSON, parse_named_configuration if TYPE_CHECKING: from typing import Self from zarr.core.array_spec import ArraySpec from zarr.core.buffer import NDBuffer from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType from zarr.core.metadata.v3 import ChunkGridMetadata _WIDE_INT = np.dtype(np.int64) def _encode_fits_natively(dtype: np.dtype[Any], offset: int, scale: int) -> bool: """Static range proof: is ``(x - offset) * scale`` always in range for every ``x`` in dtype? Uses Python ints (unbounded) to avoid overflow in the proof itself. """ info = np.iinfo(dtype) d_lo = int(info.min) - offset d_hi = int(info.max) - offset # Taking min/max of both products handles negative scale without a sign branch. products = (d_lo * scale, d_hi * scale) lo, hi = min(products), max(products) return info.min <= lo and hi <= info.max def _decode_fits_natively(dtype: np.dtype[Any], offset: int, scale: int) -> bool: """Static range proof for decode: is ``x // scale + offset`` always in range?""" info = np.iinfo(dtype) # x // scale is bounded by the extremes of x / scale (integer division stays within that range) if scale > 0: q_lo, q_hi = int(info.min) // scale, int(info.max) // scale else: q_lo, q_hi = int(info.max) // scale, int(info.min) // scale lo, hi = q_lo + offset, q_hi + offset return info.min <= lo and hi <= info.max def _check_int_range( values: npt.NDArray[np.integer[Any]], target: np.dtype[np.integer[Any]] ) -> None: """Raise if any value is outside the representable range of ``target``. Uses a single min/max pass instead of two ``np.any`` passes. """ info = np.iinfo(target) lo, hi = values.min(), values.max() if lo < info.min or hi > info.max: raise ValueError( f"scale_offset produced a value outside the range of dtype {target} " f"[{info.min}, {info.max}]." ) def _check_exact_division( arr: npt.NDArray[np.integer[Any]], scale: np.integer[Any], scale_repr: object ) -> None: """Raise ValueError if ``arr`` has any element not exactly divisible by ``scale``.""" if np.any(arr % scale): raise ValueError( f"scale_offset decode produced a non-zero remainder when dividing by " f"scale={scale_repr!r}; result is not exactly representable in dtype {arr.dtype}." ) def _encode_int_native( arr: npt.NDArray[np.integer[Any]], offset: np.integer[Any], scale: np.integer[Any] ) -> npt.NDArray[np.integer[Any]]: """Compute ``(arr - offset) * scale`` directly in ``arr.dtype``. This is the fast path; it exists only as a separate function to make the contract with ``_encode_fits_natively`` explicit: the caller must have already proved that no ``x`` in ``arr.dtype``'s range can overflow, so we can skip widening and range-checking entirely. Using it without that proof would silently wrap on overflow. """ return cast("npt.NDArray[np.integer[Any]]", (arr - offset) * scale) def _encode_int_widened( arr: npt.NDArray[np.integer[Any]], offset: np.integer[Any], scale: np.integer[Any] ) -> npt.NDArray[np.integer[Any]]: """Overflow-checked integer encode for int8..int64 and uint8..uint32. Exists because numpy integer arithmetic silently wraps on overflow, which the spec forbids. We widen to int64, perform the arithmetic there (int64 holds the product of any two values from these dtypes), range-check against the target dtype, then cast back. uint64 cannot use this path because its range exceeds int64 — see ``_encode_uint64``. """ wide_arr = arr.astype(_WIDE_INT, copy=False) result = (wide_arr - _WIDE_INT.type(offset)) * _WIDE_INT.type(scale) _check_int_range(result, arr.dtype) return result.astype(arr.dtype, copy=False) def _encode_float( arr: npt.NDArray[np.floating[Any]], offset: np.floating[Any], scale: np.floating[Any] ) -> npt.NDArray[np.floating[Any]]: """Encode float arrays in-dtype, guarding only against silent promotion. Float arithmetic doesn't need widening — float64 is already the widest supported dtype, and ``inf``/``nan`` from overflow are representable IEEE 754 values, so no range check is required by the spec. The one thing that can still go wrong is numpy promoting the result to a wider float dtype (e.g. float32 * float64 scalar -> float64), which would violate the spec's "arithmetic semantics of the input array's data type" clause. """ result = cast("npt.NDArray[np.floating[Any]]", (arr - offset) * scale) if result.dtype != arr.dtype: raise ValueError( f"scale_offset changed dtype from {arr.dtype} to {result.dtype}. " f"Arithmetic must preserve the data type." ) return result def _check_py_int_range( result: np.ndarray[tuple[Any, ...], np.dtype[Any]], target: np.dtype[np.unsignedinteger[Any]], ) -> None: """Range-check an ``object``-dtype ndarray holding Python ints against ``target``'s iinfo. Exists as a uint64-specific counterpart to ``_check_int_range``. That one compares numpy integers against ``iinfo``; here the values are unbounded Python ints produced by ``_encode_uint64`` / ``_decode_uint64``, so we rely on Python's arbitrary-precision comparison to detect values outside the target dtype's range. """ info = np.iinfo(target) # np.min/np.max on an object array returns a Python int (which compares correctly with iinfo). # Works uniformly for 0-d arrays where .flat iteration is awkward. lo = np.min(result) hi = np.max(result) if lo < int(info.min) or hi > int(info.max): raise ValueError( f"scale_offset produced a value outside the range of dtype {target} " f"[{info.min}, {info.max}]." ) def _encode_uint64( arr: npt.NDArray[np.unsignedinteger[Any]], offset: int, scale: int ) -> npt.NDArray[np.unsignedinteger[Any]]: """Encode uint64 via Python-int arithmetic in an ``object``-dtype array. Exists because uint64's range [0, 2**64) exceeds int64, so the int64 widening used by ``_encode_int_widened`` would itself overflow. Python ints are unbounded, so computing via ``object`` dtype is correct by construction. The trade-off is speed: object-dtype arithmetic is interpreted per element and is roughly 10x slower than ufunc paths. """ obj = arr.astype(object, copy=False) # np.asarray restores ndarray-ness in the 0-d/scalar edge case. result = np.asarray((obj - offset) * scale, dtype=object) _check_py_int_range(result, arr.dtype) return cast("npt.NDArray[np.unsignedinteger[Any]]", result.astype(arr.dtype, copy=False)) def _decode_uint64( arr: npt.NDArray[np.unsignedinteger[Any]], offset: int, scale: int ) -> npt.NDArray[np.unsignedinteger[Any]]: """Decode uint64 via Python-int arithmetic. See ``_encode_uint64`` for why.""" obj = arr.astype(object, copy=False) result = np.asarray((obj // scale) + offset, dtype=object) _check_py_int_range(result, arr.dtype) return cast("npt.NDArray[np.unsignedinteger[Any]]", result.astype(arr.dtype, copy=False)) def _decode_int_native( arr: npt.NDArray[np.integer[Any]], offset: np.integer[Any], scale: np.integer[Any] ) -> npt.NDArray[np.integer[Any]]: """Compute ``arr // scale + offset`` directly in ``arr.dtype``. Fast-path counterpart to ``_encode_int_native``; same contract. Caller must have proved via ``_decode_fits_natively`` that the result can't overflow. Divisibility is checked upstream in ``_decode`` before this is called, so ``//`` is exact here. """ return cast("npt.NDArray[np.integer[Any]]", (arr // scale) + offset) def _decode_int_widened( arr: npt.NDArray[np.integer[Any]], offset: np.integer[Any], scale: np.integer[Any] ) -> npt.NDArray[np.integer[Any]]: """Overflow-checked integer decode for int8..int64 and uint8..uint32. Counterpart to ``_encode_int_widened``. Widens to int64 so the addition of ``offset`` after division can't silently wrap, then range-checks against the target dtype. """ wide_arr = arr.astype(_WIDE_INT, copy=False) result = (wide_arr // _WIDE_INT.type(scale)) + _WIDE_INT.type(offset) _check_int_range(result, arr.dtype) return result.astype(arr.dtype, copy=False) def _decode_float( arr: npt.NDArray[np.floating[Any]], offset: np.floating[Any], scale: np.floating[Any] ) -> npt.NDArray[np.floating[Any]]: """Decode float arrays in-dtype, guarding only against silent promotion. Counterpart to ``_encode_float``; same reasoning. ``arr / scale`` is true division and always well-defined for floats (including ``0/0 = nan`` and ``x/0 = ±inf``), so no range or exactness check is needed. """ result = cast("npt.NDArray[np.floating[Any]]", (arr / scale) + offset) if result.dtype != arr.dtype: raise ValueError( f"scale_offset changed dtype from {arr.dtype} to {result.dtype}. " f"Arithmetic must preserve the data type." ) return result def _encode( arr: np.ndarray[tuple[Any, ...], np.dtype[Any]], offset: np.generic, scale: np.generic, ) -> np.ndarray[tuple[Any, ...], np.dtype[Any]]: """Compute ``(arr - offset) * scale`` without silent overflow, returning ``arr.dtype``.""" # uint64 is split out first because its full range (up to 2**64-1) doesn't fit in int64, # so the widening strategy used for every other integer dtype would itself overflow. if arr.dtype == np.uint64: u_arr = cast("npt.NDArray[np.unsignedinteger[Any]]", arr) return _encode_uint64(u_arr, int(offset), int(scale)) if np.issubdtype(arr.dtype, np.integer): i_arr = cast("npt.NDArray[np.integer[Any]]", arr) i_offset = cast("np.integer[Any]", offset) i_scale = cast("np.integer[Any]", scale) # Fast path: if a static proof shows no ``x`` in the dtype's range can overflow, # skip the int64 widening and run the arithmetic directly in the input dtype. if _encode_fits_natively(arr.dtype, int(offset), int(scale)): return _encode_int_native(i_arr, i_offset, i_scale) return _encode_int_widened(i_arr, i_offset, i_scale) # Float path: arithmetic stays in-dtype (no widening); only guard against numpy # silently promoting a narrower float to a wider one via scalar type mismatch. f_arr = cast("npt.NDArray[np.floating[Any]]", arr) f_offset = cast("np.floating[Any]", offset) f_scale = cast("np.floating[Any]", scale) return _encode_float(f_arr, f_offset, f_scale) def _decode( arr: np.ndarray[tuple[Any, ...], np.dtype[Any]], offset: np.generic, scale: np.generic, *, scale_repr: object, ) -> np.ndarray[tuple[Any, ...], np.dtype[Any]]: """Compute ``arr / scale + offset`` without silent overflow, returning ``arr.dtype``.""" # uint64: same reasoning as _encode — its range exceeds int64, so the Python-int path is the # only correct option. Exactness check runs first so non-divisible inputs fail before the # slower object-dtype arithmetic. if arr.dtype == np.uint64: u_arr = cast("npt.NDArray[np.unsignedinteger[Any]]", arr) _check_exact_division(u_arr, cast("np.integer[Any]", scale), scale_repr) return _decode_uint64(u_arr, int(offset), int(scale)) if np.issubdtype(arr.dtype, np.integer): i_arr = cast("npt.NDArray[np.integer[Any]]", arr) i_offset = cast("np.integer[Any]", offset) i_scale = cast("np.integer[Any]", scale) # The spec requires decode to use true division and error if the result isn't # representable. For integers that means the remainder must be zero; if any element # isn't exactly divisible we fail here rather than silently truncating via //. _check_exact_division(i_arr, i_scale, scale_repr) # Fast path mirrors _encode: static proof that ``x // scale + offset`` stays in dtype. if _decode_fits_natively(arr.dtype, int(offset), int(scale)): return _decode_int_native(i_arr, i_offset, i_scale) return _decode_int_widened(i_arr, i_offset, i_scale) # Float path: division is well-defined; only guard against dtype promotion. f_arr = cast("npt.NDArray[np.floating[Any]]", arr) f_offset = cast("np.floating[Any]", offset) f_scale = cast("np.floating[Any]", scale) return _decode_float(f_arr, f_offset, f_scale) @dataclass(frozen=True) class ScaleOffset(ArrayArrayCodec): """Scale-offset array-to-array codec. Encodes values with `out = (in - offset) * scale` and decodes with `out = (in / scale) + offset`, using the input array's data type semantics. Intermediate or final values that are not representable in that dtype are reported as errors (integer overflow, unsigned underflow, non-exact integer division). Parameters ---------- offset : int, float, or str Value subtracted during encoding. Strings preserve the exact JSON representation when round-tripping metadata. Default is 0. scale : int, float, or str Value multiplied during encoding (after offset subtraction). Strings preserve the exact JSON representation when round-tripping metadata. Default is 1. Attributes ---------- offset : int, float, or str The offset value, as supplied to the constructor. scale : int, float, or str The scale value, as supplied to the constructor. References ---------- - The `scale_offset` codec spec: https://github.com/zarr-developers/zarr-extensions/tree/main/codecs/scale_offset """ is_fixed_size = True offset: int | float | str scale: int | float | str def __init__(self, *, offset: object = 0, scale: object = 1) -> None: if not isinstance(offset, int | float | str): raise TypeError(f"offset must be a number or string, got {type(offset).__name__}") if not isinstance(scale, int | float | str): raise TypeError(f"scale must be a number or string, got {type(scale).__name__}") object.__setattr__(self, "offset", offset) object.__setattr__(self, "scale", scale) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( data, "scale_offset", 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"} 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} def validate( self, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, ) -> None: native = dtype.to_native_dtype() if not np.issubdtype(native, np.integer) and not np.issubdtype(native, np.floating): raise ValueError( f"scale_offset codec only supports integer and floating-point data types. " f"Got {dtype}." ) if self.scale == 0: raise ValueError("scale_offset scale must be non-zero.") for name, value in [("offset", self.offset), ("scale", self.scale)]: try: dtype.from_json_scalar(value, zarr_format=3) except (TypeError, ValueError, OverflowError) as e: raise ValueError( f"scale_offset {name} value {value!r} is not representable in dtype {native}." ) from e def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: zdtype = chunk_spec.dtype fill = np.asarray(zdtype.cast_scalar(chunk_spec.fill_value)) offset = cast("np.generic", zdtype.from_json_scalar(self.offset, zarr_format=3)) scale = cast("np.generic", zdtype.from_json_scalar(self.scale, zarr_format=3)) new_fill = _encode(fill, offset, scale) return replace(chunk_spec, fill_value=new_fill.reshape(()).item()) def _decode_sync( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer: arr = cast("np.ndarray[tuple[Any, ...], np.dtype[Any]]", chunk_array.as_ndarray_like()) zdtype = chunk_spec.dtype offset = cast("np.generic", zdtype.from_json_scalar(self.offset, zarr_format=3)) scale = cast("np.generic", zdtype.from_json_scalar(self.scale, zarr_format=3)) result = _decode(arr, offset, scale, scale_repr=self.scale) return chunk_spec.prototype.nd_buffer.from_ndarray_like(result) async def _decode_single( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer: return self._decode_sync(chunk_array, chunk_spec) def _encode_sync( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer | None: arr = cast("np.ndarray[tuple[Any, ...], np.dtype[Any]]", chunk_array.as_ndarray_like()) zdtype = chunk_spec.dtype offset = cast("np.generic", zdtype.from_json_scalar(self.offset, zarr_format=3)) scale = cast("np.generic", zdtype.from_json_scalar(self.scale, zarr_format=3)) result = _encode(arr, offset, scale) return chunk_spec.prototype.nd_buffer.from_ndarray_like(result) async def _encode_single( self, chunk_array: NDBuffer, _chunk_spec: ArraySpec, ) -> NDBuffer | None: return self._encode_sync(chunk_array, _chunk_spec) def compute_encoded_size(self, input_byte_length: int, _chunk_spec: ArraySpec) -> int: return input_byte_length zarr-python-3.2.1/src/zarr/codecs/sharding.py000066400000000000000000000712161517635743000212120ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable, Mapping, MutableMapping, Sequence from dataclasses import dataclass, replace from enum import Enum from functools import lru_cache from operator import itemgetter from typing import TYPE_CHECKING, Any, NamedTuple, cast import numpy as np import numpy.typing as npt from zarr.abc.codec import ( ArrayBytesCodec, ArrayBytesCodecPartialDecodeMixin, ArrayBytesCodecPartialEncodeMixin, Codec, CodecPipeline, ) from zarr.abc.store import ( ByteGetter, ByteRequest, ByteSetter, RangeByteRequest, SuffixByteRequest, ) from zarr.codecs.bytes import BytesCodec from zarr.codecs.crc32c_ import Crc32cCodec from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import ( Buffer, BufferPrototype, NDBuffer, default_buffer_prototype, numpy_buffer_prototype, ) from zarr.core.chunk_grids import ChunkGrid from zarr.core.common import ( ShapeLike, parse_enum, parse_named_configuration, parse_shapelike, product, ) from zarr.core.dtype.npy.int import UInt64 from zarr.core.indexing import ( BasicIndexer, ChunkProjection, SelectorTuple, _morton_order, _morton_order_keys, c_order_iter, get_indexer, morton_order_iter, ) from zarr.core.metadata.v3 import ( ChunkGridMetadata, RectilinearChunkGridMetadata, RegularChunkGridMetadata, parse_codecs, ) from zarr.registry import get_ndbuffer_class, get_pipeline_class from zarr.storage._utils import _normalize_byte_range_index if TYPE_CHECKING: from collections.abc import Iterator from typing import Self from zarr.core.common import JSON from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType MAX_UINT_64 = 2**64 - 1 ShardMapping = Mapping[tuple[int, ...], Buffer | None] ShardMutableMapping = MutableMapping[tuple[int, ...], Buffer | None] class ShardingCodecIndexLocation(Enum): """ Enum for index location used by the sharding codec. """ start = "start" end = "end" def parse_index_location(data: object) -> ShardingCodecIndexLocation: return parse_enum(data, ShardingCodecIndexLocation) @dataclass(frozen=True) class _ShardingByteGetter(ByteGetter): shard_dict: ShardMapping chunk_coords: tuple[int, ...] async def get( self, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: assert prototype == default_buffer_prototype(), ( f"prototype is not supported within shards currently. diff: {prototype} != {default_buffer_prototype()}" ) value = self.shard_dict.get(self.chunk_coords) if value is None: return None if byte_range is None: return value start, stop = _normalize_byte_range_index(value, byte_range) return value[start:stop] @dataclass(frozen=True) class _ShardingByteSetter(_ShardingByteGetter, ByteSetter): shard_dict: ShardMutableMapping async def set(self, value: Buffer, byte_range: ByteRequest | None = None) -> None: assert byte_range is None, "byte_range is not supported within shards" self.shard_dict[self.chunk_coords] = value async def delete(self) -> None: del self.shard_dict[self.chunk_coords] async def set_if_not_exists(self, default: Buffer) -> None: self.shard_dict.setdefault(self.chunk_coords, default) class _ShardIndex(NamedTuple): # dtype uint64, shape (chunks_per_shard_0, chunks_per_shard_1, ..., 2) offsets_and_lengths: npt.NDArray[np.uint64] @property def chunks_per_shard(self) -> tuple[int, ...]: result = tuple(self.offsets_and_lengths.shape[0:-1]) # The cast is required until https://github.com/numpy/numpy/pull/27211 is merged return cast("tuple[int, ...]", result) def _localize_chunk(self, chunk_coords: tuple[int, ...]) -> tuple[int, ...]: return tuple( chunk_i % shard_i for chunk_i, shard_i in zip(chunk_coords, self.offsets_and_lengths.shape, strict=False) ) def is_all_empty(self) -> bool: return bool(np.array_equiv(self.offsets_and_lengths, MAX_UINT_64)) def get_full_chunk_map(self) -> npt.NDArray[np.bool_]: return np.not_equal(self.offsets_and_lengths[..., 0], MAX_UINT_64) def get_chunk_slice(self, chunk_coords: tuple[int, ...]) -> tuple[int, int] | None: localized_chunk = self._localize_chunk(chunk_coords) chunk_start, chunk_len = self.offsets_and_lengths[localized_chunk] if (chunk_start, chunk_len) == (MAX_UINT_64, MAX_UINT_64): return None else: return (int(chunk_start), int(chunk_start + chunk_len)) def get_chunk_slices_vectorized( self, chunk_coords_array: npt.NDArray[np.integer[Any]] ) -> tuple[npt.NDArray[np.uint64], npt.NDArray[np.uint64], npt.NDArray[np.bool_]]: """Get chunk slices for multiple coordinates at once. Parameters ---------- chunk_coords_array : ndarray of shape (n_chunks, n_dims) Array of chunk coordinates to look up. Returns ------- starts : ndarray of shape (n_chunks,) Start byte positions for each chunk. ends : ndarray of shape (n_chunks,) End byte positions for each chunk. valid : ndarray of shape (n_chunks,) Boolean mask indicating which chunks are non-empty. """ # Localize coordinates via modulo (vectorized) shard_shape = np.array(self.offsets_and_lengths.shape[:-1], dtype=np.uint64) localized = chunk_coords_array.astype(np.uint64) % shard_shape # Build index tuple for advanced indexing index_tuple = tuple(localized[:, i] for i in range(localized.shape[1])) # Fetch all offsets and lengths at once offsets_and_lengths = self.offsets_and_lengths[index_tuple] starts = offsets_and_lengths[:, 0] lengths = offsets_and_lengths[:, 1] # Check for valid (non-empty) chunks valid = starts != MAX_UINT_64 # Compute end positions ends = starts + lengths return starts, ends, valid def set_chunk_slice(self, chunk_coords: tuple[int, ...], chunk_slice: slice | None) -> None: localized_chunk = self._localize_chunk(chunk_coords) if chunk_slice is None: self.offsets_and_lengths[localized_chunk] = (MAX_UINT_64, MAX_UINT_64) else: self.offsets_and_lengths[localized_chunk] = ( chunk_slice.start, chunk_slice.stop - chunk_slice.start, ) def is_dense(self, chunk_byte_length: int) -> bool: sorted_offsets_and_lengths = sorted( [ (offset, length) for offset, length in self.offsets_and_lengths if offset != MAX_UINT_64 ], key=itemgetter(0), ) # Are all non-empty offsets unique? if len( {offset for offset, _ in sorted_offsets_and_lengths if offset != MAX_UINT_64} ) != len(sorted_offsets_and_lengths): return False return all( offset % chunk_byte_length == 0 and length == chunk_byte_length for offset, length in sorted_offsets_and_lengths ) @classmethod def create_empty(cls, chunks_per_shard: tuple[int, ...]) -> _ShardIndex: offsets_and_lengths = np.zeros(chunks_per_shard + (2,), dtype=" _ShardReader: shard_index_size = codec._shard_index_size(chunks_per_shard) obj = cls() obj.buf = buf if codec.index_location == ShardingCodecIndexLocation.start: shard_index_bytes = obj.buf[:shard_index_size] else: shard_index_bytes = obj.buf[-shard_index_size:] obj.index = await codec._decode_shard_index(shard_index_bytes, chunks_per_shard) return obj @classmethod def create_empty( cls, chunks_per_shard: tuple[int, ...], buffer_prototype: BufferPrototype | None = None ) -> _ShardReader: if buffer_prototype is None: buffer_prototype = default_buffer_prototype() index = _ShardIndex.create_empty(chunks_per_shard) obj = cls() obj.buf = buffer_prototype.buffer.create_zero_length() obj.index = index return obj def __getitem__(self, chunk_coords: tuple[int, ...]) -> Buffer: chunk_byte_slice = self.index.get_chunk_slice(chunk_coords) if chunk_byte_slice: return self.buf[chunk_byte_slice[0] : chunk_byte_slice[1]] raise KeyError def __len__(self) -> int: return int(self.index.offsets_and_lengths.size / 2) def __iter__(self) -> Iterator[tuple[int, ...]]: return c_order_iter(self.index.offsets_and_lengths.shape[:-1]) def to_dict_vectorized( self, chunk_coords_array: npt.NDArray[np.integer[Any]], ) -> dict[tuple[int, ...], Buffer | None]: """Build a dict of chunk coordinates to buffers using vectorized lookup. Parameters ---------- chunk_coords_array : ndarray of shape (n_chunks, n_dims) Array of chunk coordinates for vectorized index lookup. Returns ------- dict mapping chunk coordinate tuples to Buffer or None """ starts, ends, valid = self.index.get_chunk_slices_vectorized(chunk_coords_array) chunks_per_shard = tuple(self.index.offsets_and_lengths.shape[:-1]) chunk_coords_keys = _morton_order_keys(chunks_per_shard) result: dict[tuple[int, ...], Buffer | None] = {} for i, coords in enumerate(chunk_coords_keys): if valid[i]: result[coords] = self.buf[int(starts[i]) : int(ends[i])] else: result[coords] = None return result @dataclass(frozen=True) class ShardingCodec( ArrayBytesCodec, ArrayBytesCodecPartialDecodeMixin, ArrayBytesCodecPartialEncodeMixin ): """Sharding codec""" chunk_shape: tuple[int, ...] codecs: tuple[Codec, ...] index_codecs: tuple[Codec, ...] index_location: ShardingCodecIndexLocation = ShardingCodecIndexLocation.end def __init__( self, *, chunk_shape: ShapeLike, codecs: Iterable[Codec | dict[str, JSON]] = (BytesCodec(),), index_codecs: Iterable[Codec | dict[str, JSON]] = (BytesCodec(), Crc32cCodec()), index_location: ShardingCodecIndexLocation | str = ShardingCodecIndexLocation.end, ) -> None: chunk_shape_parsed = parse_shapelike(chunk_shape) codecs_parsed = parse_codecs(codecs) index_codecs_parsed = parse_codecs(index_codecs) index_location_parsed = parse_index_location(index_location) object.__setattr__(self, "chunk_shape", chunk_shape_parsed) object.__setattr__(self, "codecs", codecs_parsed) object.__setattr__(self, "index_codecs", index_codecs_parsed) object.__setattr__(self, "index_location", index_location_parsed) # Use instance-local lru_cache to avoid memory leaks # numpy void scalars are not hashable, which means an array spec with a fill value that is # a numpy void scalar will break the lru_cache. This is commented for now but should be # fixed. See https://github.com/zarr-developers/zarr-python/issues/3054 # object.__setattr__(self, "_get_chunk_spec", lru_cache()(self._get_chunk_spec)) object.__setattr__(self, "_get_index_chunk_spec", lru_cache()(self._get_index_chunk_spec)) object.__setattr__(self, "_get_chunks_per_shard", lru_cache()(self._get_chunks_per_shard)) # todo: typedict return type def __getstate__(self) -> dict[str, Any]: return self.to_dict() def __setstate__(self, state: dict[str, Any]) -> None: config = state["configuration"] object.__setattr__(self, "chunk_shape", parse_shapelike(config["chunk_shape"])) object.__setattr__(self, "codecs", parse_codecs(config["codecs"])) object.__setattr__(self, "index_codecs", parse_codecs(config["index_codecs"])) object.__setattr__(self, "index_location", parse_index_location(config["index_location"])) # Use instance-local lru_cache to avoid memory leaks # object.__setattr__(self, "_get_chunk_spec", lru_cache()(self._get_chunk_spec)) object.__setattr__(self, "_get_index_chunk_spec", lru_cache()(self._get_index_chunk_spec)) object.__setattr__(self, "_get_chunks_per_shard", lru_cache()(self._get_chunks_per_shard)) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration(data, "sharding_indexed") return cls(**configuration_parsed) # type: ignore[arg-type] @property def codec_pipeline(self) -> CodecPipeline: return get_pipeline_class().from_codecs(self.codecs) def to_dict(self) -> dict[str, JSON]: return { "name": "sharding_indexed", "configuration": { "chunk_shape": self.chunk_shape, "codecs": tuple(s.to_dict() for s in self.codecs), "index_codecs": tuple(s.to_dict() for s in self.index_codecs), "index_location": self.index_location.value, }, } def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: shard_spec = self._get_chunk_spec(array_spec) evolved_codecs = tuple(c.evolve_from_array_spec(array_spec=shard_spec) for c in self.codecs) if evolved_codecs != self.codecs: return replace(self, codecs=evolved_codecs) return self def validate( self, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, ) -> None: if len(self.chunk_shape) != len(shape): raise ValueError( "The shard's `chunk_shape` and array's `shape` need to have the same number of dimensions." ) if isinstance(chunk_grid, RegularChunkGridMetadata): edges_per_dim: tuple[tuple[int, ...], ...] = tuple((s,) for s in chunk_grid.chunk_shape) elif isinstance(chunk_grid, RectilinearChunkGridMetadata): edges_per_dim = tuple( (s,) if isinstance(s, int) else s for s in chunk_grid.chunk_shapes ) else: raise TypeError( f"Sharding is only compatible with regular and rectilinear chunk grids, " f"got {type(chunk_grid)}" ) for i, (edges, inner) in enumerate(zip(edges_per_dim, self.chunk_shape, strict=False)): for edge in set(edges): if edge % inner != 0: raise ValueError( f"Chunk edge length {edge} in dimension {i} is not " f"divisible by the shard's inner chunk size {inner}." ) async def _decode_single( self, shard_bytes: Buffer, shard_spec: ArraySpec, ) -> NDBuffer: shard_shape = shard_spec.shape chunk_shape = self.chunk_shape chunks_per_shard = self._get_chunks_per_shard(shard_spec) chunk_spec = self._get_chunk_spec(shard_spec) indexer = BasicIndexer( tuple(slice(0, s) for s in shard_shape), shape=shard_shape, chunk_grid=ChunkGrid.from_sizes(shard_shape, chunk_shape), ) # setup output array out = chunk_spec.prototype.nd_buffer.empty( shape=shard_shape, dtype=shard_spec.dtype.to_native_dtype(), order=shard_spec.order, ) shard_dict = await _ShardReader.from_bytes(shard_bytes, self, chunks_per_shard) if shard_dict.index.is_all_empty(): out.fill(shard_spec.fill_value) return out # decoding chunks and writing them into the output buffer await self.codec_pipeline.read( [ ( _ShardingByteGetter(shard_dict, chunk_coords), chunk_spec, chunk_selection, out_selection, is_complete_shard, ) for chunk_coords, chunk_selection, out_selection, is_complete_shard in indexer ], out, ) return out async def _decode_partial_single( self, byte_getter: ByteGetter, selection: SelectorTuple, shard_spec: ArraySpec, ) -> NDBuffer | None: shard_shape = shard_spec.shape chunk_shape = self.chunk_shape chunks_per_shard = self._get_chunks_per_shard(shard_spec) chunk_spec = self._get_chunk_spec(shard_spec) indexer = get_indexer( selection, shape=shard_shape, chunk_grid=ChunkGrid.from_sizes(shard_shape, chunk_shape), ) # setup output array out = shard_spec.prototype.nd_buffer.empty( shape=indexer.shape, dtype=shard_spec.dtype.to_native_dtype(), order=shard_spec.order, ) indexed_chunks = list(indexer) all_chunk_coords = {chunk_coords for chunk_coords, *_ in indexed_chunks} # reading bytes of all requested chunks shard_dict: ShardMapping = {} if self._is_total_shard(all_chunk_coords, chunks_per_shard): # read entire shard shard_dict_maybe = await self._load_full_shard_maybe( byte_getter=byte_getter, prototype=chunk_spec.prototype, chunks_per_shard=chunks_per_shard, ) if shard_dict_maybe is None: return None shard_dict = shard_dict_maybe else: # read some chunks within the shard shard_index = await self._load_shard_index_maybe(byte_getter, chunks_per_shard) if shard_index is None: return None shard_dict = {} for chunk_coords in all_chunk_coords: chunk_byte_slice = shard_index.get_chunk_slice(chunk_coords) if chunk_byte_slice: chunk_bytes = await byte_getter.get( prototype=chunk_spec.prototype, byte_range=RangeByteRequest(chunk_byte_slice[0], chunk_byte_slice[1]), ) if chunk_bytes: shard_dict[chunk_coords] = chunk_bytes # decoding chunks and writing them into the output buffer await self.codec_pipeline.read( [ ( _ShardingByteGetter(shard_dict, chunk_coords), chunk_spec, chunk_selection, out_selection, is_complete_shard, ) for chunk_coords, chunk_selection, out_selection, is_complete_shard in indexer ], out, ) if hasattr(indexer, "sel_shape"): return out.reshape(indexer.sel_shape) else: return out async def _encode_single( self, shard_array: NDBuffer, shard_spec: ArraySpec, ) -> Buffer | None: shard_shape = shard_spec.shape chunk_shape = self.chunk_shape chunks_per_shard = self._get_chunks_per_shard(shard_spec) chunk_spec = self._get_chunk_spec(shard_spec) indexer = list( BasicIndexer( tuple(slice(0, s) for s in shard_shape), shape=shard_shape, chunk_grid=ChunkGrid.from_sizes(shard_shape, chunk_shape), ) ) shard_builder = dict.fromkeys(morton_order_iter(chunks_per_shard)) await self.codec_pipeline.write( [ ( _ShardingByteSetter(shard_builder, chunk_coords), chunk_spec, chunk_selection, out_selection, is_complete_shard, ) for chunk_coords, chunk_selection, out_selection, is_complete_shard in indexer ], shard_array, ) return await self._encode_shard_dict( shard_builder, chunks_per_shard=chunks_per_shard, buffer_prototype=default_buffer_prototype(), ) async def _encode_partial_single( self, byte_setter: ByteSetter, shard_array: NDBuffer, selection: SelectorTuple, shard_spec: ArraySpec, ) -> None: shard_shape = shard_spec.shape chunk_shape = self.chunk_shape chunks_per_shard = self._get_chunks_per_shard(shard_spec) chunk_spec = self._get_chunk_spec(shard_spec) indexer = list( get_indexer( selection, shape=shard_shape, chunk_grid=ChunkGrid.from_sizes(shard_shape, chunk_shape), ) ) if self._is_complete_shard_write(indexer, chunks_per_shard): shard_dict = dict.fromkeys(morton_order_iter(chunks_per_shard)) else: shard_reader = await self._load_full_shard_maybe( byte_getter=byte_setter, prototype=chunk_spec.prototype, chunks_per_shard=chunks_per_shard, ) shard_reader = shard_reader or _ShardReader.create_empty(chunks_per_shard) # Use vectorized lookup for better performance shard_dict = shard_reader.to_dict_vectorized( np.asarray(_morton_order(chunks_per_shard)) ) await self.codec_pipeline.write( [ ( _ShardingByteSetter(shard_dict, chunk_coords), chunk_spec, chunk_selection, out_selection, is_complete_shard, ) for chunk_coords, chunk_selection, out_selection, is_complete_shard in indexer ], shard_array, ) buf = await self._encode_shard_dict( shard_dict, chunks_per_shard=chunks_per_shard, buffer_prototype=default_buffer_prototype(), ) if buf is None: await byte_setter.delete() else: await byte_setter.set(buf) async def _encode_shard_dict( self, map: ShardMapping, chunks_per_shard: tuple[int, ...], buffer_prototype: BufferPrototype, ) -> Buffer | None: index = _ShardIndex.create_empty(chunks_per_shard) buffers = [] template = buffer_prototype.buffer.create_zero_length() chunk_start = 0 for chunk_coords in morton_order_iter(chunks_per_shard): value = map.get(chunk_coords) if value is None: continue if len(value) == 0: continue chunk_length = len(value) buffers.append(value) index.set_chunk_slice(chunk_coords, slice(chunk_start, chunk_start + chunk_length)) chunk_start += chunk_length if len(buffers) == 0: return None index_bytes = await self._encode_shard_index(index) if self.index_location == ShardingCodecIndexLocation.start: empty_chunks_mask = index.offsets_and_lengths[..., 0] == MAX_UINT_64 index.offsets_and_lengths[~empty_chunks_mask, 0] += len(index_bytes) index_bytes = await self._encode_shard_index( index ) # encode again with corrected offsets buffers.insert(0, index_bytes) else: buffers.append(index_bytes) return template.combine(buffers) def _is_total_shard( self, all_chunk_coords: set[tuple[int, ...]], chunks_per_shard: tuple[int, ...] ) -> bool: return len(all_chunk_coords) == product(chunks_per_shard) and all( chunk_coords in all_chunk_coords for chunk_coords in c_order_iter(chunks_per_shard) ) def _is_complete_shard_write( self, indexed_chunks: Sequence[ChunkProjection], chunks_per_shard: tuple[int, ...], ) -> bool: all_chunk_coords = {chunk_coords for chunk_coords, *_ in indexed_chunks} return self._is_total_shard(all_chunk_coords, chunks_per_shard) and all( is_complete_chunk for *_, is_complete_chunk in indexed_chunks ) async def _decode_shard_index( self, index_bytes: Buffer, chunks_per_shard: tuple[int, ...] ) -> _ShardIndex: index_array = next( iter( await get_pipeline_class() .from_codecs(self.index_codecs) .decode( [(index_bytes, self._get_index_chunk_spec(chunks_per_shard))], ) ) ) # This cannot be None because we have the bytes already index_array = cast(NDBuffer, index_array) return _ShardIndex(index_array.as_numpy_array()) async def _encode_shard_index(self, index: _ShardIndex) -> Buffer: index_bytes = next( iter( await get_pipeline_class() .from_codecs(self.index_codecs) .encode( [ ( get_ndbuffer_class().from_numpy_array(index.offsets_and_lengths), self._get_index_chunk_spec(index.chunks_per_shard), ) ], ) ) ) assert index_bytes is not None assert isinstance(index_bytes, Buffer) return index_bytes def _shard_index_size(self, chunks_per_shard: tuple[int, ...]) -> int: return ( get_pipeline_class() .from_codecs(self.index_codecs) .compute_encoded_size( 16 * product(chunks_per_shard), self._get_index_chunk_spec(chunks_per_shard) ) ) def _get_index_chunk_spec(self, chunks_per_shard: tuple[int, ...]) -> ArraySpec: return ArraySpec( shape=chunks_per_shard + (2,), dtype=UInt64(endianness="little"), fill_value=MAX_UINT_64, config=ArrayConfig( order="C", write_empty_chunks=False ), # Note: this is hard-coded for simplicity -- it is not surfaced into user code, prototype=default_buffer_prototype(), ) def _get_chunk_spec(self, shard_spec: ArraySpec) -> ArraySpec: return ArraySpec( shape=self.chunk_shape, dtype=shard_spec.dtype, fill_value=shard_spec.fill_value, config=shard_spec.config, prototype=shard_spec.prototype, ) def _get_chunks_per_shard(self, shard_spec: ArraySpec) -> tuple[int, ...]: return tuple( s // c for s, c in zip( shard_spec.shape, self.chunk_shape, strict=False, ) ) async def _load_shard_index_maybe( self, byte_getter: ByteGetter, chunks_per_shard: tuple[int, ...] ) -> _ShardIndex | None: shard_index_size = self._shard_index_size(chunks_per_shard) if self.index_location == ShardingCodecIndexLocation.start: index_bytes = await byte_getter.get( prototype=numpy_buffer_prototype(), byte_range=RangeByteRequest(0, shard_index_size), ) else: index_bytes = await byte_getter.get( prototype=numpy_buffer_prototype(), byte_range=SuffixByteRequest(shard_index_size) ) if index_bytes is not None: return await self._decode_shard_index(index_bytes, chunks_per_shard) return None async def _load_shard_index( self, byte_getter: ByteGetter, chunks_per_shard: tuple[int, ...] ) -> _ShardIndex: return ( await self._load_shard_index_maybe(byte_getter, chunks_per_shard) ) or _ShardIndex.create_empty(chunks_per_shard) async def _load_full_shard_maybe( self, byte_getter: ByteGetter, prototype: BufferPrototype, chunks_per_shard: tuple[int, ...] ) -> _ShardReader | None: shard_bytes = await byte_getter.get(prototype=prototype) return ( await _ShardReader.from_bytes(shard_bytes, self, chunks_per_shard) if shard_bytes else None ) def compute_encoded_size(self, input_byte_length: int, shard_spec: ArraySpec) -> int: chunks_per_shard = self._get_chunks_per_shard(shard_spec) return input_byte_length + self._shard_index_size(chunks_per_shard) zarr-python-3.2.1/src/zarr/codecs/transpose.py000066400000000000000000000105771517635743000214340ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable from dataclasses import dataclass, replace from typing import TYPE_CHECKING, cast import numpy as np from zarr.abc.codec import ArrayArrayCodec from zarr.core.array_spec import ArraySpec from zarr.core.common import JSON, parse_named_configuration if TYPE_CHECKING: from typing import Self from zarr.core.buffer import NDBuffer from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType from zarr.core.metadata.v3 import ChunkGridMetadata def parse_transpose_order(data: JSON | Iterable[int]) -> tuple[int, ...]: if not isinstance(data, Iterable): raise TypeError(f"Expected an iterable. Got {data} instead.") if not all(isinstance(a, int) for a in data): raise TypeError(f"Expected an iterable of integers. Got {data} instead.") return tuple(cast("Iterable[int]", data)) @dataclass(frozen=True) class TransposeCodec(ArrayArrayCodec): """Transpose codec""" is_fixed_size = True order: tuple[int, ...] def __init__(self, *, order: Iterable[int]) -> None: order_parsed = parse_transpose_order(order) object.__setattr__(self, "order", order_parsed) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration(data, "transpose") return cls(**configuration_parsed) # type: ignore[arg-type] def to_dict(self) -> dict[str, JSON]: return {"name": "transpose", "configuration": {"order": tuple(self.order)}} def validate( self, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, ) -> None: if len(self.order) != len(shape): raise ValueError( f"The `order` tuple must have as many entries as there are dimensions in the array. Got {self.order}." ) if len(self.order) != len(set(self.order)): raise ValueError( f"There must not be duplicates in the `order` tuple. Got {self.order}." ) if not all(0 <= x < len(shape) for x in self.order): raise ValueError( f"All entries in the `order` tuple must be between 0 and the number of dimensions in the array. Got {self.order}." ) def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: ndim = array_spec.ndim if len(self.order) != ndim: raise ValueError( f"The `order` tuple must have as many entries as there are dimensions in the array. Got {self.order}." ) if len(self.order) != len(set(self.order)): raise ValueError( f"There must not be duplicates in the `order` tuple. Got {self.order}." ) if not all(0 <= x < ndim for x in self.order): raise ValueError( f"All entries in the `order` tuple must be between 0 and the number of dimensions in the array. Got {self.order}." ) order = tuple(self.order) if order != self.order: return replace(self, order=order) return self def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec: return ArraySpec( shape=tuple(chunk_spec.shape[self.order[i]] for i in range(chunk_spec.ndim)), dtype=chunk_spec.dtype, fill_value=chunk_spec.fill_value, config=chunk_spec.config, prototype=chunk_spec.prototype, ) def _decode_sync( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer: inverse_order = tuple(int(i) for i in np.argsort(self.order)) return chunk_array.transpose(inverse_order) async def _decode_single( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> NDBuffer: return self._decode_sync(chunk_array, chunk_spec) def _encode_sync( self, chunk_array: NDBuffer, _chunk_spec: ArraySpec, ) -> NDBuffer | None: return chunk_array.transpose(self.order) async def _encode_single( self, chunk_array: NDBuffer, _chunk_spec: ArraySpec, ) -> NDBuffer | None: return self._encode_sync(chunk_array, _chunk_spec) def compute_encoded_size(self, input_byte_length: int, _chunk_spec: ArraySpec) -> int: return input_byte_length zarr-python-3.2.1/src/zarr/codecs/vlen_utf8.py000066400000000000000000000106701517635743000213220ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING import numpy as np from numcodecs.vlen import VLenBytes, VLenUTF8 from zarr._compat import _reshape_view from zarr.abc.codec import ArrayBytesCodec from zarr.core.buffer import Buffer, NDBuffer from zarr.core.common import JSON, parse_named_configuration if TYPE_CHECKING: from typing import Self from zarr.core.array_spec import ArraySpec # can use a global because there are no parameters _vlen_utf8_codec = VLenUTF8() _vlen_bytes_codec = VLenBytes() @dataclass(frozen=True) class VLenUTF8Codec(ArrayBytesCodec): """Variable-length UTF8 codec""" @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( data, "vlen-utf8", require_configuration=False ) configuration_parsed = configuration_parsed or {} return cls(**configuration_parsed) def to_dict(self) -> dict[str, JSON]: return {"name": "vlen-utf8", "configuration": {}} def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: return self def _decode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> NDBuffer: assert isinstance(chunk_bytes, Buffer) raw_bytes = chunk_bytes.as_array_like() decoded = _vlen_utf8_codec.decode(raw_bytes) assert decoded.dtype == np.object_ decoded = _reshape_view(decoded, chunk_spec.shape) as_string_dtype = decoded.astype(chunk_spec.dtype.to_native_dtype(), copy=False) return chunk_spec.prototype.nd_buffer.from_numpy_array(as_string_dtype) async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> NDBuffer: return self._decode_sync(chunk_bytes, chunk_spec) def _encode_sync( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> Buffer | None: assert isinstance(chunk_array, NDBuffer) return chunk_spec.prototype.buffer.from_bytes( _vlen_utf8_codec.encode(chunk_array.as_numpy_array()) ) async def _encode_single( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> Buffer | None: return self._encode_sync(chunk_array, chunk_spec) def compute_encoded_size(self, input_byte_length: int, _chunk_spec: ArraySpec) -> int: # what is input_byte_length for an object dtype? raise NotImplementedError("compute_encoded_size is not implemented for VLen codecs") @dataclass(frozen=True) class VLenBytesCodec(ArrayBytesCodec): @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration( data, "vlen-bytes", require_configuration=False ) configuration_parsed = configuration_parsed or {} return cls(**configuration_parsed) def to_dict(self) -> dict[str, JSON]: return {"name": "vlen-bytes", "configuration": {}} def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: return self def _decode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> NDBuffer: assert isinstance(chunk_bytes, Buffer) raw_bytes = chunk_bytes.as_array_like() decoded = _vlen_bytes_codec.decode(raw_bytes) assert decoded.dtype == np.object_ decoded = _reshape_view(decoded, chunk_spec.shape) return chunk_spec.prototype.nd_buffer.from_numpy_array(decoded) async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> NDBuffer: return self._decode_sync(chunk_bytes, chunk_spec) def _encode_sync( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> Buffer | None: assert isinstance(chunk_array, NDBuffer) return chunk_spec.prototype.buffer.from_bytes( _vlen_bytes_codec.encode(chunk_array.as_numpy_array()) ) async def _encode_single( self, chunk_array: NDBuffer, chunk_spec: ArraySpec, ) -> Buffer | None: return self._encode_sync(chunk_array, chunk_spec) def compute_encoded_size(self, input_byte_length: int, _chunk_spec: ArraySpec) -> int: # what is input_byte_length for an object dtype? raise NotImplementedError("compute_encoded_size is not implemented for VLen codecs") zarr-python-3.2.1/src/zarr/codecs/zstd.py000066400000000000000000000063741517635743000204020ustar00rootroot00000000000000from __future__ import annotations import asyncio from dataclasses import dataclass from functools import cached_property from typing import TYPE_CHECKING import numcodecs from numcodecs.zstd import Zstd from packaging.version import Version from zarr.abc.codec import BytesBytesCodec from zarr.core.buffer.cpu import as_numpy_array_wrapper from zarr.core.common import JSON, parse_named_configuration if TYPE_CHECKING: from typing import Self from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer def parse_zstd_level(data: JSON) -> int: if isinstance(data, int): if data >= 23: raise ValueError(f"Value must be less than or equal to 22. Got {data} instead.") return data raise TypeError(f"Got value with type {type(data)}, but expected an int.") def parse_checksum(data: JSON) -> bool: if isinstance(data, bool): return data raise TypeError(f"Expected bool. Got {type(data)}.") @dataclass(frozen=True) class ZstdCodec(BytesBytesCodec): """zstd codec""" is_fixed_size = False level: int = 0 checksum: bool = False def __init__(self, *, level: int = 0, checksum: bool = False) -> None: # numcodecs 0.13.0 introduces the checksum attribute for the zstd codec _numcodecs_version = Version(numcodecs.__version__) if _numcodecs_version < Version("0.13.0"): raise RuntimeError( "numcodecs version >= 0.13.0 is required to use the zstd codec. " f"Version {_numcodecs_version} is currently installed." ) level_parsed = parse_zstd_level(level) checksum_parsed = parse_checksum(checksum) object.__setattr__(self, "level", level_parsed) object.__setattr__(self, "checksum", checksum_parsed) @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, configuration_parsed = parse_named_configuration(data, "zstd") 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}} @cached_property def _zstd_codec(self) -> Zstd: config_dict = {"level": self.level, "checksum": self.checksum} return Zstd.from_config(config_dict) def _decode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: return as_numpy_array_wrapper(self._zstd_codec.decode, chunk_bytes, chunk_spec.prototype) async def _decode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer: return await asyncio.to_thread(self._decode_sync, chunk_bytes, chunk_spec) def _encode_sync( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: return as_numpy_array_wrapper(self._zstd_codec.encode, chunk_bytes, chunk_spec.prototype) async def _encode_single( self, chunk_bytes: Buffer, chunk_spec: ArraySpec, ) -> Buffer | None: return await asyncio.to_thread(self._encode_sync, chunk_bytes, chunk_spec) def compute_encoded_size(self, _input_byte_length: int, _chunk_spec: ArraySpec) -> int: raise NotImplementedError zarr-python-3.2.1/src/zarr/core/000077500000000000000000000000001517635743000165225ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/core/__init__.py000066400000000000000000000004331517635743000206330ustar00rootroot00000000000000""" The ``zarr.core`` module is considered private API and should not be imported directly by 3rd-party code. """ from __future__ import annotations from zarr.core.buffer import Buffer, NDBuffer # noqa: F401 from zarr.core.codec_pipeline import BatchedCodecPipeline # noqa: F401 zarr-python-3.2.1/src/zarr/core/_info.py000066400000000000000000000116301517635743000201670ustar00rootroot00000000000000from __future__ import annotations import dataclasses import textwrap from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: from zarr.abc.codec import ArrayArrayCodec, ArrayBytesCodec, BytesBytesCodec from zarr.abc.numcodec import Numcodec from zarr.core.common import ZarrFormat from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType @dataclasses.dataclass(kw_only=True) class GroupInfo: """ Visual summary for a Group. Note that this method and its properties is not part of Zarr's public API. """ _name: str _type: Literal["Group"] = "Group" _zarr_format: ZarrFormat _read_only: bool _store_type: str _count_members: int | None = None _count_arrays: int | None = None _count_groups: int | None = None def __repr__(self) -> str: template = textwrap.dedent("""\ Name : {_name} Type : {_type} Zarr format : {_zarr_format} Read-only : {_read_only} Store type : {_store_type}""") if self._count_members is not None: template += "\nNo. members : {_count_members}" if self._count_arrays is not None: template += "\nNo. arrays : {_count_arrays}" if self._count_groups is not None: template += "\nNo. groups : {_count_groups}" return template.format(**dataclasses.asdict(self)) def human_readable_size(size: int) -> str: if size < 2**10: return f"{size}" elif size < 2**20: return f"{size / float(2**10):.1f}K" elif size < 2**30: return f"{size / float(2**20):.1f}M" elif size < 2**40: return f"{size / float(2**30):.1f}G" elif size < 2**50: return f"{size / float(2**40):.1f}T" else: return f"{size / float(2**50):.1f}P" def byte_info(size: int) -> str: if size < 2**10: return str(size) else: return f"{size} ({human_readable_size(size)})" @dataclasses.dataclass(kw_only=True, frozen=True, slots=True) class ArrayInfo: """ Visual summary for an Array. Note that this method and its properties is not part of Zarr's public API. """ _type: Literal["Array"] = "Array" _zarr_format: ZarrFormat _data_type: ZDType[TBaseDType, TBaseScalar] _fill_value: object _shape: tuple[int, ...] _shard_shape: tuple[int, ...] | None = None _chunk_shape: tuple[int, ...] | None = None _order: Literal["C", "F"] _read_only: bool _store_type: str _filters: tuple[Numcodec, ...] | tuple[ArrayArrayCodec, ...] = () _serializer: ArrayBytesCodec | None = None _compressors: tuple[Numcodec, ...] | tuple[BytesBytesCodec, ...] = () _count_bytes: int | None = None _count_bytes_stored: int | None = None _count_chunks_initialized: int | None = None def __repr__(self) -> str: template = textwrap.dedent("""\ Type : {_type} Zarr format : {_zarr_format} Data type : {_data_type} Fill value : {_fill_value} Shape : {_shape}""") if self._shard_shape is not None: template += textwrap.dedent(""" Shard shape : {_shard_shape}""") template += textwrap.dedent(""" Chunk shape : {_chunk_shape} Order : {_order} Read-only : {_read_only} Store type : {_store_type}""") # We can't use dataclasses.asdict, because we only want a shallow dict kwargs = {field.name: getattr(self, field.name) for field in dataclasses.fields(self)} if self._chunk_shape is None: # for non-regular chunk grids kwargs["_chunk_shape"] = "" template += "\nFilters : {_filters}" if self._serializer is not None: template += "\nSerializer : {_serializer}" template += "\nCompressors : {_compressors}" if self._count_bytes is not None: template += "\nNo. bytes : {_count_bytes}" kwargs["_count_bytes"] = byte_info(self._count_bytes) if self._count_bytes_stored is not None: template += "\nNo. bytes stored : {_count_bytes_stored}" kwargs["_count_bytes_stored"] = byte_info(self._count_bytes_stored) if ( self._count_bytes is not None and self._count_bytes_stored is not None and self._count_bytes_stored > 0 ): template += "\nStorage ratio : {_storage_ratio}" kwargs["_storage_ratio"] = f"{self._count_bytes / self._count_bytes_stored:.1f}" if self._count_chunks_initialized is not None: if self._shard_shape is not None: template += "\nShards Initialized : {_count_chunks_initialized}" else: template += "\nChunks Initialized : {_count_chunks_initialized}" return template.format(**kwargs) zarr-python-3.2.1/src/zarr/core/_tree.py000066400000000000000000000117221517635743000201750ustar00rootroot00000000000000import sys from collections import deque from collections.abc import Sequence from html import escape as html_escape from typing import Any from zarr.core.group import AsyncGroup class TreeRepr: """ A simple object with a tree-like repr for the Zarr Group. Note that this object and it's implementation isn't considered part of Zarr's public API. """ def __init__(self, text: str, html: str, truncated: str = "") -> None: self._text = text self._html = html self._truncated = truncated def __repr__(self) -> str: if self._truncated: return self._truncated + self._text return self._text def _repr_mimebundle_( self, include: Sequence[str] | None = None, exclude: Sequence[str] | None = None, **kwargs: Any, ) -> dict[str, str]: text = self._truncated + self._text if self._truncated else self._text # For jupyter support. html_body = self._truncated + self._html if self._truncated else self._html html = ( '
"
            f"{html_body}
\n" ) return {"text/plain": text, "text/html": html} async def group_tree_async( group: AsyncGroup, max_depth: int | None = None, *, max_nodes: int = 500, plain: bool = False, ) -> TreeRepr: members: list[tuple[str, Any]] = [] truncated = False async for item in group.members(max_depth=max_depth): if len(members) == max_nodes: truncated = True break members.append(item) members.sort(key=lambda key_node: key_node[0]) # Set up styling tokens: ANSI bold for terminals, HTML for Jupyter, # or empty strings when plain=True (useful for LLMs, logging, files). if plain: ansi_open = ansi_close = html_open = html_close = "" else: # Avoid emitting ANSI escape codes when output is piped or in CI. use_ansi = sys.stdout.isatty() ansi_open = "\x1b[1m" if use_ansi else "" ansi_close = "\x1b[0m" if use_ansi else "" html_open = "" html_close = "" # Group members by parent key so we can render the tree level by level. nodes: dict[str, list[tuple[str, Any]]] = {} for key, node in members: if key.count("/") == 0: parent_key = "" else: parent_key = key.rsplit("/", 1)[0] nodes.setdefault(parent_key, []).append((key, node)) # Render the tree iteratively (not recursively) to avoid hitting # Python's recursion limit on deeply nested hierarchies. # Each stack frame is (prefix_string, remaining_children_at_this_level). text_lines = [f"{ansi_open}{group.name}{ansi_close}"] html_lines = [f"{html_open}{html_escape(group.name)}{html_close}"] stack = [("", deque(nodes.get("", [])))] while stack: prefix, remaining = stack[-1] if not remaining: stack.pop() continue key, node = remaining.popleft() name = key.rsplit("/")[-1] escaped_name = html_escape(name) # if we popped the last item then remaining will # now be empty - that's how we got past the if not remaining # above, but this can still be true. is_last = not remaining connector = "└── " if is_last else "├── " if isinstance(node, AsyncGroup): text_lines.append(f"{prefix}{connector}{ansi_open}{name}{ansi_close}") html_lines.append(f"{prefix}{connector}{html_open}{escaped_name}{html_close}") else: text_lines.append( f"{prefix}{connector}{ansi_open}{name}{ansi_close} {node.shape} {node.dtype}" ) html_lines.append( f"{prefix}{connector}{html_open}{escaped_name}{html_close}" f" {html_escape(str(node.shape))} {html_escape(str(node.dtype))}" ) # Descend into children with an accumulated prefix: # Example showing how prefix accumulates: # / # ├── a prefix = "" # │ ├── b prefix = "" + "│ " # │ │ └── x prefix = "" + "│ " + "│ " # │ └── c prefix = "" + "│ " # └── d prefix = "" # └── e prefix = "" + " " if children := nodes.get(key, []): if is_last: child_prefix = prefix + " " else: child_prefix = prefix + "│ " stack.append((child_prefix, deque(children))) text = "\n".join(text_lines) + "\n" html = "\n".join(html_lines) + "\n" note = ( f"Truncated at max_nodes={max_nodes}, some nodes and their children may be missing\n" if truncated else "" ) return TreeRepr(text, html, truncated=note) zarr-python-3.2.1/src/zarr/core/array.py000066400000000000000000006472031517635743000202260ustar00rootroot00000000000000from __future__ import annotations import json import warnings from asyncio import gather from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field, replace from itertools import starmap from logging import getLogger from typing import ( TYPE_CHECKING, Any, Literal, TypedDict, cast, overload, ) from warnings import warn import numpy as np from typing_extensions import deprecated import zarr from zarr.abc.codec import ArrayArrayCodec, ArrayBytesCodec, BytesBytesCodec, Codec from zarr.abc.numcodec import Numcodec, _is_numcodec from zarr.codecs._v2 import V2Codec from zarr.codecs.bytes import BytesCodec from zarr.codecs.vlen_utf8 import VLenBytesCodec, VLenUTF8Codec from zarr.codecs.zstd import ZstdCodec from zarr.core._info import ArrayInfo from zarr.core.array_spec import ArrayConfig, ArrayConfigLike, ArraySpec, parse_array_config from zarr.core.attributes import Attributes from zarr.core.buffer import ( BufferPrototype, NDArrayLike, NDArrayLikeOrScalar, NDBuffer, default_buffer_prototype, ) from zarr.core.buffer.cpu import buffer_prototype as cpu_buffer_prototype from zarr.core.chunk_grids import ( ChunkGrid, _auto_partition, normalize_chunks, ) from zarr.core.chunk_key_encodings import ( ChunkKeyEncoding, ChunkKeyEncodingLike, DefaultChunkKeyEncoding, V2ChunkKeyEncoding, parse_chunk_key_encoding, ) from zarr.core.common import ( JSON, ZARR_JSON, ZARRAY_JSON, ZATTRS_JSON, ChunksLike, DimensionNamesLike, MemoryOrder, ShapeLike, ZarrFormat, _default_zarr_format, _warn_order_kwarg, ceildiv, concurrent_map, parse_shapelike, product, ) from zarr.core.config import config as zarr_config from zarr.core.dtype import ( Structured, VariableLengthBytes, VariableLengthUTF8, ZDType, ZDTypeLike, parse_dtype, ) from zarr.core.dtype.common import HasEndianness, HasItemSize, HasObjectCodec from zarr.core.indexing import ( AsyncOIndex, AsyncVIndex, BasicIndexer, BasicSelection, BlockIndex, BlockIndexer, CoordinateIndexer, CoordinateSelection, Fields, Indexer, MaskIndexer, MaskSelection, OIndex, OrthogonalIndexer, OrthogonalSelection, Selection, VIndex, _iter_grid, _iter_regions, check_fields, check_no_multi_fields, is_pure_fancy_indexing, is_pure_orthogonal_indexing, is_scalar, pop_fields, ) from zarr.core.metadata import ( ArrayMetadata, ArrayMetadataDict, ArrayMetadataJSON_V3, ArrayV2Metadata, ArrayV2MetadataDict, ArrayV3Metadata, ) from zarr.core.metadata.io import save_metadata from zarr.core.metadata.v2 import ( CompressorLikev2, get_object_codec_id, parse_compressor, parse_filters, ) from zarr.core.metadata.v3 import ( ChunkGridMetadata, RectilinearChunkGridMetadata, RegularChunkGridMetadata, parse_node_type_array, resolve_chunks, ) from zarr.core.sync import sync from zarr.errors import ( ArrayNotFoundError, ChunkNotFoundError, MetadataValidationError, ZarrDeprecationWarning, ZarrUserWarning, ) from zarr.registry import ( _parse_array_array_codec, _parse_array_bytes_codec, _parse_bytes_bytes_codec, get_pipeline_class, ) from zarr.storage._common import StorePath, ensure_no_existing_node, make_store_path from zarr.storage._utils import _relativize_path if TYPE_CHECKING: from collections.abc import Iterator from typing import Self import numpy.typing as npt from zarr.abc.codec import CodecPipeline from zarr.abc.store import Store from zarr.codecs.sharding import ShardingCodecIndexLocation from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar from zarr.storage import StoreLike from zarr.types import AnyArray, AnyAsyncArray, ArrayV2, ArrayV3, AsyncArrayV2, AsyncArrayV3 # Array and AsyncArray are defined in the base ``zarr`` namespace __all__ = [ "DEFAULT_FILL_VALUE", "DefaultFillValue", "create_codec_pipeline", "parse_array_metadata", ] logger = getLogger(__name__) class DefaultFillValue: """ Sentinel class to indicate that the default fill value should be used. This class exists because conventional values used to convey "defaultness" like ``None`` or ``"auto"` are ambiguous when specifying the fill value parameter of a Zarr array. The value ``None`` is ambiguous because it is a valid fill value for Zarr V2 (resulting in ``"fill_value": null`` in array metadata). A string like ``"auto"`` is ambiguous because such a string is a valid fill value for an array with a string data type. An instance of this class lies outside the space of valid fill values, which means it can umambiguously express that the default fill value should be used. """ DEFAULT_FILL_VALUE = DefaultFillValue() def _chunk_sizes_from_shape( array_shape: tuple[int, ...], chunk_shape: tuple[int, ...] ) -> tuple[tuple[int, ...], ...]: """Compute dask-style chunk sizes from an array shape and uniform chunk shape.""" result: list[tuple[int, ...]] = [] for s, c in zip(array_shape, chunk_shape, strict=True): nchunks = ceildiv(s, c) sizes = tuple(min(c, s - i * c) for i in range(nchunks)) result.append(sizes) return tuple(result) def parse_array_metadata(data: Any) -> ArrayMetadata: if isinstance(data, ArrayMetadata): return data elif isinstance(data, dict): zarr_format = data.get("zarr_format") if zarr_format == 3: meta_out = ArrayV3Metadata.from_dict(data) if len(meta_out.storage_transformers) > 0: msg = ( f"Array metadata contains storage transformers: {meta_out.storage_transformers}." "Arrays with storage transformers are not supported in zarr-python at this time." ) raise ValueError(msg) return meta_out elif zarr_format == 2: return ArrayV2Metadata.from_dict(data) else: raise ValueError(f"Invalid zarr_format: {zarr_format}. Expected 2 or 3") raise TypeError # pragma: no cover def create_codec_pipeline(metadata: ArrayMetadata, *, store: Store | None = None) -> CodecPipeline: if store is not None: try: return get_pipeline_class().from_array_metadata_and_store( array_metadata=metadata, store=store ) except NotImplementedError: pass if isinstance(metadata, ArrayV3Metadata): return get_pipeline_class().from_codecs(metadata.codecs) elif isinstance(metadata, ArrayV2Metadata): v2_codec = V2Codec(filters=metadata.filters, compressor=metadata.compressor) return get_pipeline_class().from_codecs([v2_codec]) raise TypeError # pragma: no cover async def get_array_metadata( store_path: StorePath, zarr_format: ZarrFormat | None = 3 ) -> dict[str, JSON]: if zarr_format == 2: zarray_bytes, zattrs_bytes = await gather( (store_path / ZARRAY_JSON).get(prototype=cpu_buffer_prototype), (store_path / ZATTRS_JSON).get(prototype=cpu_buffer_prototype), ) if zarray_bytes is None: msg = ( "A Zarr V2 array metadata document was not found in store " f"{store_path.store!r} at path {store_path.path!r}." ) raise ArrayNotFoundError(msg) elif zarr_format == 3: zarr_json_bytes = await (store_path / ZARR_JSON).get(prototype=cpu_buffer_prototype) if zarr_json_bytes is None: msg = ( "A Zarr V3 array metadata document was not found in store " f"{store_path.store!r} at path {store_path.path!r}." ) raise ArrayNotFoundError(msg) elif zarr_format is None: zarr_json_bytes, zarray_bytes, zattrs_bytes = await gather( (store_path / ZARR_JSON).get(prototype=cpu_buffer_prototype), (store_path / ZARRAY_JSON).get(prototype=cpu_buffer_prototype), (store_path / ZATTRS_JSON).get(prototype=cpu_buffer_prototype), ) if zarr_json_bytes is not None and zarray_bytes is not None: # warn and favor v3 msg = f"Both zarr.json (Zarr format 3) and .zarray (Zarr format 2) metadata objects exist at {store_path}. Zarr v3 will be used." warnings.warn(msg, category=ZarrUserWarning, stacklevel=1) if zarr_json_bytes is None and zarray_bytes is None: msg = ( f"Neither Zarr V3 nor Zarr V2 array metadata documents " f"were found in store {store_path.store!r} at path {store_path.path!r}." ) raise ArrayNotFoundError(msg) # set zarr_format based on which keys were found if zarr_json_bytes is not None: zarr_format = 3 else: zarr_format = 2 else: msg = f"Invalid value for 'zarr_format'. Expected 2, 3, or None. Got '{zarr_format}'." # type: ignore[unreachable] raise MetadataValidationError(msg) metadata_dict: dict[str, JSON] if zarr_format == 2: # V2 arrays are comprised of a .zarray and .zattrs objects assert zarray_bytes is not None metadata_dict = json.loads(zarray_bytes.to_bytes()) zattrs_dict = json.loads(zattrs_bytes.to_bytes()) if zattrs_bytes is not None else {} metadata_dict["attributes"] = zattrs_dict else: # V3 arrays are comprised of a zarr.json object assert zarr_json_bytes is not None metadata_dict = json.loads(zarr_json_bytes.to_bytes()) parse_node_type_array(metadata_dict.get("node_type")) return metadata_dict @dataclass(frozen=True) class AsyncArray[T_ArrayMetadata: (ArrayV2Metadata, ArrayV3Metadata)]: """ An asynchronous array class representing a chunked array stored in a Zarr store. Parameters ---------- metadata : ArrayMetadata The metadata of the array. store_path : StorePath The path to the Zarr store. config : ArrayConfigLike, optional The runtime configuration of the array, by default None. Attributes ---------- metadata : ArrayMetadata The metadata of the array. store_path : StorePath The path to the Zarr store. codec_pipeline : CodecPipeline The codec pipeline used for encoding and decoding chunks. config : ArrayConfig The runtime configuration of the array. """ metadata: T_ArrayMetadata store_path: StorePath codec_pipeline: CodecPipeline = field(init=False) _chunk_grid: ChunkGrid = field(init=False) config: ArrayConfig @overload def __init__( self: AsyncArrayV2, metadata: ArrayV2Metadata | ArrayV2MetadataDict, store_path: StorePath, config: ArrayConfigLike | None = None, ) -> None: ... @overload def __init__( self: AsyncArrayV3, metadata: ArrayV3Metadata | ArrayMetadataJSON_V3, store_path: StorePath, config: ArrayConfigLike | None = None, ) -> None: ... def __init__( self, metadata: ArrayMetadata | ArrayMetadataDict, store_path: StorePath, config: ArrayConfigLike | None = None, ) -> None: metadata_parsed = parse_array_metadata(metadata) config_parsed = parse_array_config(config) object.__setattr__(self, "metadata", metadata_parsed) object.__setattr__(self, "store_path", store_path) object.__setattr__(self, "config", config_parsed) object.__setattr__(self, "_chunk_grid", ChunkGrid.from_metadata(metadata_parsed)) object.__setattr__( self, "codec_pipeline", create_codec_pipeline(metadata=metadata_parsed, store=store_path.store), ) @classmethod async def _create( cls, store: StoreLike, *, # v2 and v3 shape: ShapeLike, dtype: ZDTypeLike | ZDType[TBaseDType, TBaseScalar], zarr_format: ZarrFormat = 3, fill_value: Any | None = DEFAULT_FILL_VALUE, attributes: dict[str, JSON] | None = None, # v3 only chunk_shape: ShapeLike | None = None, chunk_key_encoding: ( ChunkKeyEncodingLike | tuple[Literal["default"], Literal[".", "/"]] | tuple[Literal["v2"], Literal[".", "/"]] | None ) = None, codecs: Iterable[Codec | dict[str, JSON]] | None = None, dimension_names: DimensionNamesLike = None, # v2 only chunks: ShapeLike | None = None, dimension_separator: Literal[".", "/"] | None = None, order: MemoryOrder | None = None, filters: Iterable[dict[str, JSON] | Numcodec] | None = None, compressor: CompressorLike = "auto", # runtime overwrite: bool = False, data: npt.ArrayLike | None = None, config: ArrayConfigLike | None = None, ) -> AnyAsyncArray: """Method to create a new asynchronous array instance. Deprecated in favor of [`zarr.api.asynchronous.create_array`][]. """ dtype_parsed = parse_dtype(dtype, zarr_format=zarr_format) store_path = await make_store_path(store) shape = parse_shapelike(shape) if chunks is not None and chunk_shape is not None: raise ValueError("Only one of chunk_shape or chunks can be provided.") from zarr.core.chunk_grids import _is_rectilinear_chunks _raw_chunks = chunks if chunks is not None else chunk_shape config_parsed = parse_array_config(config) result: AnyAsyncArray if zarr_format == 3: if dimension_separator is not None: raise ValueError( "dimension_separator cannot be used for arrays with zarr_format 3. Use chunk_key_encoding instead." ) if filters is not None: raise ValueError( "filters cannot be used for arrays with zarr_format 3. Use array-to-array codecs instead." ) if compressor != "auto": raise ValueError( "compressor cannot be used for arrays with zarr_format 3. Use bytes-to-bytes codecs instead." ) if order is not None: _warn_order_kwarg() item_size = 1 if isinstance(dtype_parsed, HasItemSize): item_size = dtype_parsed.item_size chunk_grid = resolve_chunks(_raw_chunks, shape, item_size) result = await cls._create_v3( store_path, shape=shape, dtype=dtype_parsed, fill_value=fill_value, chunk_key_encoding=chunk_key_encoding, codecs=codecs, dimension_names=dimension_names, attributes=attributes, overwrite=overwrite, config=config_parsed, chunk_grid=chunk_grid, ) elif zarr_format == 2: if codecs is not None: raise ValueError( "codecs cannot be used for arrays with zarr_format 2. Use filters and compressor instead." ) if chunk_key_encoding is not None: raise ValueError( "chunk_key_encoding cannot be used for arrays with zarr_format 2. Use dimension_separator instead." ) if dimension_names is not None: raise ValueError("dimension_names cannot be used for arrays with zarr_format 2.") if _is_rectilinear_chunks(_raw_chunks): raise ValueError("Zarr format 2 does not support rectilinear chunk grids.") item_size = 1 if isinstance(dtype_parsed, HasItemSize): item_size = dtype_parsed.item_size if chunks: _chunks = normalize_chunks(chunks, shape, item_size) else: _chunks = normalize_chunks(chunk_shape, shape, item_size) if order is None: order_parsed = config_parsed.order else: order_parsed = order config_parsed = replace(config_parsed, order=order) result = await cls._create_v2( store_path, shape=shape, dtype=dtype_parsed, chunks=_chunks, dimension_separator=dimension_separator, fill_value=fill_value, order=order_parsed, config=config_parsed, filters=filters, compressor=compressor, attributes=attributes, overwrite=overwrite, ) else: raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover if data is not None: # insert user-provided data await result.setitem(..., data) return result @staticmethod def _create_metadata_v3( shape: ShapeLike, dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, fill_value: Any | None = DEFAULT_FILL_VALUE, chunk_key_encoding: ChunkKeyEncodingLike | None = None, codecs: Iterable[Codec | dict[str, JSON]] | None = None, dimension_names: DimensionNamesLike = None, attributes: dict[str, JSON] | None = None, ) -> ArrayV3Metadata: """Create an instance of ArrayV3Metadata.""" filters: tuple[ArrayArrayCodec, ...] compressors: tuple[BytesBytesCodec, ...] shape = parse_shapelike(shape) if codecs is None: filters = default_filters_v3(dtype) serializer = default_serializer_v3(dtype) compressors = default_compressors_v3(dtype) codecs_parsed = (*filters, serializer, *compressors) else: codecs_parsed = tuple(codecs) chunk_key_encoding_parsed: ChunkKeyEncodingLike if chunk_key_encoding is None: chunk_key_encoding_parsed = {"name": "default", "separator": "/"} else: chunk_key_encoding_parsed = chunk_key_encoding if isinstance(fill_value, DefaultFillValue) or fill_value is None: # Use dtype's default scalar for DefaultFillValue sentinel # For v3, None is converted to DefaultFillValue behavior fill_value_parsed = dtype.default_scalar() else: fill_value_parsed = fill_value return ArrayV3Metadata( shape=shape, data_type=dtype, chunk_grid=chunk_grid, chunk_key_encoding=chunk_key_encoding_parsed, fill_value=fill_value_parsed, codecs=codecs_parsed, # type: ignore[arg-type] dimension_names=tuple(dimension_names) if dimension_names else None, attributes=attributes or {}, ) @classmethod async def _create_v3( cls, store_path: StorePath, *, shape: ShapeLike, dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, config: ArrayConfig, fill_value: Any | None = DEFAULT_FILL_VALUE, chunk_key_encoding: ( ChunkKeyEncodingLike | tuple[Literal["default"], Literal[".", "/"]] | tuple[Literal["v2"], Literal[".", "/"]] | None ) = None, codecs: Iterable[Codec | dict[str, JSON]] | None = None, dimension_names: DimensionNamesLike = None, attributes: dict[str, JSON] | None = None, overwrite: bool = False, ) -> AsyncArrayV3: if overwrite: if store_path.store.supports_deletes: await store_path.delete_dir() else: await ensure_no_existing_node(store_path, zarr_format=3) else: await ensure_no_existing_node(store_path, zarr_format=3) if isinstance(chunk_key_encoding, tuple): chunk_key_encoding = ( V2ChunkKeyEncoding(separator=chunk_key_encoding[1]) if chunk_key_encoding[0] == "v2" else DefaultChunkKeyEncoding(separator=chunk_key_encoding[1]) ) metadata = cls._create_metadata_v3( shape=shape, dtype=dtype, chunk_grid=chunk_grid, fill_value=fill_value, chunk_key_encoding=chunk_key_encoding, codecs=codecs, dimension_names=dimension_names, attributes=attributes, ) array = cls(metadata=metadata, store_path=store_path, config=config) await array._save_metadata(metadata, ensure_parents=True) return array @staticmethod def _create_metadata_v2( shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunks: tuple[int, ...], order: MemoryOrder, dimension_separator: Literal[".", "/"] | None = None, fill_value: Any | None = DEFAULT_FILL_VALUE, filters: Iterable[dict[str, JSON] | Numcodec] | None = None, compressor: CompressorLikev2 = None, attributes: dict[str, JSON] | None = None, ) -> ArrayV2Metadata: if dimension_separator is None: dimension_separator = "." # Handle DefaultFillValue sentinel if isinstance(fill_value, DefaultFillValue): fill_value_parsed: Any = dtype.default_scalar() else: # For v2, preserve None as-is (backward compatibility) fill_value_parsed = fill_value return ArrayV2Metadata( shape=shape, dtype=dtype, chunks=chunks, order=order, dimension_separator=dimension_separator, fill_value=fill_value_parsed, compressor=compressor, filters=filters, attributes=attributes, ) @classmethod async def _create_v2( cls, store_path: StorePath, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunks: tuple[int, ...], order: MemoryOrder, config: ArrayConfig, dimension_separator: Literal[".", "/"] | None = None, fill_value: Any | None = DEFAULT_FILL_VALUE, filters: Iterable[dict[str, JSON] | Numcodec] | None = None, compressor: CompressorLike = "auto", attributes: dict[str, JSON] | None = None, overwrite: bool = False, ) -> AsyncArrayV2: if overwrite: if store_path.store.supports_deletes: await store_path.delete_dir() else: await ensure_no_existing_node(store_path, zarr_format=2) else: await ensure_no_existing_node(store_path, zarr_format=2) compressor_parsed: CompressorLikev2 if compressor == "auto": compressor_parsed = default_compressor_v2(dtype) elif isinstance(compressor, BytesBytesCodec): raise ValueError( "Cannot use a BytesBytesCodec as a compressor for zarr v2 arrays. " "Use a numcodecs codec directly instead." ) else: compressor_parsed = compressor if filters is None: filters = default_filters_v2(dtype) metadata = cls._create_metadata_v2( shape=shape, dtype=dtype, chunks=chunks, order=order, dimension_separator=dimension_separator, fill_value=fill_value, filters=filters, compressor=compressor_parsed, attributes=attributes, ) array = cls(metadata=metadata, store_path=store_path, config=config) await array._save_metadata(metadata, ensure_parents=True) return array @classmethod def from_dict( cls, store_path: StorePath, data: dict[str, JSON], ) -> AnyAsyncArray: """ Create a Zarr array from a dictionary, with support for both Zarr format 2 and 3 metadata. Parameters ---------- store_path : StorePath The path within the store where the array should be created. data : dict A dictionary representing the array data. This dictionary should include necessary metadata for the array, such as shape, dtype, and other attributes. The format of the metadata will determine whether a Zarr format 2 or 3 array is created. Returns ------- AsyncArrayV3 or AsyncArrayV2 The created Zarr array, either using Zarr format 2 or 3 metadata based on the provided data. Raises ------ ValueError If the dictionary data is invalid or incompatible with either Zarr format 2 or 3 array creation. """ metadata = parse_array_metadata(data) return cls(metadata=metadata, store_path=store_path) @classmethod async def open( cls, store: StoreLike, zarr_format: ZarrFormat | None = 3, ) -> AnyAsyncArray: """ Async method to open an existing Zarr array from a given store. Parameters ---------- store : StoreLike The store containing the Zarr array. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. zarr_format : ZarrFormat | None, optional The Zarr format version (default is 3). Returns ------- AsyncArray The opened Zarr array. Examples -------- ```python import asyncio import zarr from zarr.core.array import AsyncArray async def example(): store = zarr.storage.MemoryStore() # First create an array to open await zarr.api.asynchronous.create_array( store=store, shape=(100, 100), dtype="int32" ) # Now open it async_arr = await AsyncArray.open(store) return async_arr async_arr = asyncio.run(example()) # ``` """ store_path = await make_store_path(store) metadata_dict = await get_array_metadata(store_path, zarr_format=zarr_format) # TODO: remove this cast when we have better type hints _metadata_dict = cast("ArrayMetadataJSON_V3", metadata_dict) return cls(store_path=store_path, metadata=_metadata_dict) @property def store(self) -> Store: return self.store_path.store @property @deprecated("Use AsyncArray.config instead.", category=ZarrDeprecationWarning) def _config(self) -> ArrayConfig: return self.config @property def ndim(self) -> int: """Returns the number of dimensions in the Array. Returns ------- int The number of dimensions in the Array. """ return len(self.metadata.shape) @property def shape(self) -> tuple[int, ...]: """Returns the shape of the Array. Returns ------- tuple The shape of the Array. """ return self.metadata.shape @property def chunks(self) -> tuple[int, ...]: """Returns the chunk shape of the Array. If sharding is used the inner chunk shape is returned. Only defined for arrays using a regular chunk grid. If array uses a rectilinear chunk grid, `NotImplementedError` is raised. Returns ------- tuple[int, ...]: The chunk shape of the Array. """ # TODO: move sharding awareness out of metadata return self.metadata.chunks @property def read_chunk_sizes(self) -> tuple[tuple[int, ...], ...]: """Per-dimension data sizes of chunks used for reading, clipped to the array extent. Boundary chunks that extend past the array shape are clipped, so the last size along a dimension may be smaller than the declared chunk size. This matches the dask ``Array.chunks`` convention. When sharding is used, returns the inner chunk sizes. Otherwise, returns the outer chunk sizes (same as ``write_chunk_sizes``). Returns ------- tuple[tuple[int, ...], ...] One inner tuple per dimension containing the data size of each chunk (not the encoded buffer size). Examples -------- >>> arr = zarr.create_array(store, shape=(100, 80), chunks=(30, 40)) >>> arr.read_chunk_sizes ((30, 30, 30, 10), (40, 40)) """ from zarr.codecs.sharding import ShardingCodec codecs: tuple[Codec, ...] = getattr(self.metadata, "codecs", ()) if len(codecs) == 1 and isinstance(codecs[0], ShardingCodec): inner_chunk_shape = codecs[0].chunk_shape return _chunk_sizes_from_shape(self.shape, inner_chunk_shape) return self._chunk_grid.chunk_sizes @property def write_chunk_sizes(self) -> tuple[tuple[int, ...], ...]: """Per-dimension data sizes of storage chunks, clipped to the array extent. Always returns the outer chunk sizes, regardless of sharding. Boundary chunks that extend past the array shape are clipped, so the last size along a dimension may be smaller than the declared chunk size. This matches the dask ``Array.chunks`` convention. Returns ------- tuple[tuple[int, ...], ...] One inner tuple per dimension containing the data size of each chunk (not the encoded buffer size). Examples -------- >>> arr = zarr.create_array(store, shape=(100, 80), chunks=(30, 40)) >>> arr.write_chunk_sizes ((30, 30, 30, 10), (40, 40)) """ return self._chunk_grid.chunk_sizes @property def shards(self) -> tuple[int, ...] | None: """Returns the shard shape of the Array. Returns None if sharding is not used. Only defined for arrays using a regular chunk grid. If array uses a rectilinear chunk grid, `NotImplementedError` is raised. Returns ------- tuple[int, ...]: The shard shape of the Array. """ return self.metadata.shards @property def size(self) -> int: """Returns the total number of elements in the array Returns ------- int Total number of elements in the array """ return np.prod(self.metadata.shape).item() @property def filters(self) -> tuple[Numcodec, ...] | tuple[ArrayArrayCodec, ...]: """ Filters that are applied to each chunk of the array, in order, before serializing that chunk to bytes. """ if self.metadata.zarr_format == 2: filters = self.metadata.filters if filters is None: return () return filters return tuple( codec for codec in self.metadata.inner_codecs if isinstance(codec, ArrayArrayCodec) ) @property def serializer(self) -> ArrayBytesCodec | None: """ Array-to-bytes codec to use for serializing the chunks into bytes. """ if self.metadata.zarr_format == 2: return None return next( codec for codec in self.metadata.inner_codecs if isinstance(codec, ArrayBytesCodec) ) @property @deprecated("Use AsyncArray.compressors instead.", category=ZarrDeprecationWarning) def compressor(self) -> Numcodec | None: """ Compressor that is applied to each chunk of the array. !!! warning "Deprecated" `Array.compressor` is deprecated since v3.0.0 and will be removed in a future release. Use [`Array.compressors`][zarr.AsyncArray.compressors] instead. """ if self.metadata.zarr_format == 2: return self.metadata.compressor raise TypeError("`compressor` is not available for Zarr format 3 arrays.") @property def compressors(self) -> tuple[Numcodec, ...] | tuple[BytesBytesCodec, ...]: """ Compressors that are applied to each chunk of the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. """ if self.metadata.zarr_format == 2: if self.metadata.compressor is not None: return (self.metadata.compressor,) return () return tuple( codec for codec in self.metadata.inner_codecs if isinstance(codec, BytesBytesCodec) ) @property def _zdtype(self) -> ZDType[TBaseDType, TBaseScalar]: """ The zarr-specific representation of the array data type """ if self.metadata.zarr_format == 2: return self.metadata.dtype else: return self.metadata.data_type @property def dtype(self) -> TBaseDType: """Returns the data type of the array. Returns ------- np.dtype Data type of the array """ return self._zdtype.to_native_dtype() @property def order(self) -> MemoryOrder: """Returns the memory order of the array. Returns ------- bool Memory order of the array """ if self.metadata.zarr_format == 2: return self.metadata.order else: return self.config.order @property def attrs(self) -> dict[str, JSON]: """Returns the attributes of the array. Returns ------- dict Attributes of the array """ return self.metadata.attributes @property def read_only(self) -> bool: """Returns True if the array is read-only. Returns ------- bool True if the array is read-only """ # Backwards compatibility for 2.x return self.store_path.read_only @property def path(self) -> str: """Storage path. Returns ------- str The path to the array in the Zarr store. """ return self.store_path.path @property def name(self) -> str: """Array name following h5py convention. Returns ------- str The name of the array. """ # follow h5py convention: add leading slash name = self.path if not name.startswith("/"): name = "/" + name return name @property def basename(self) -> str: """Final component of name. Returns ------- str The basename or final component of the array name. """ return self.name.split("/")[-1] @property def cdata_shape(self) -> tuple[int, ...]: """ The number of chunks along each dimension. When sharding is used, this counts inner chunks (not shards) per dimension. Returns ------- tuple[int, ...] The number of chunks along each dimension. """ return self._chunk_grid_shape @property def _chunk_grid_shape(self) -> tuple[int, ...]: """ The number of chunks along each dimension. When sharding is used, this counts inner chunks (not shards) per dimension. Returns ------- tuple[int, ...] The number of chunks along each dimension. """ # TODO: refactor — extract a sharding_codec property on ArrayV3Metadata # to replace the repeated `len == 1 and isinstance` pattern. from zarr.codecs.sharding import ShardingCodec codecs: tuple[Codec, ...] = getattr(self.metadata, "codecs", ()) if len(codecs) == 1 and isinstance(codecs[0], ShardingCodec): # When sharding, count inner chunks across the whole array chunk_shape = codecs[0].chunk_shape return tuple(starmap(ceildiv, zip(self.shape, chunk_shape, strict=True))) return self._chunk_grid.grid_shape @property def _shard_grid_shape(self) -> tuple[int, ...]: """ The shape of the shard grid for this array. When no shards are present this will automatically fall back to the chunk grid. Returns ------- tuple[int, ...] The shape of the shard grid for this array. """ if self.shards is None: shard_shape = self.chunks else: shard_shape = self.shards return tuple(starmap(ceildiv, zip(self.shape, shard_shape, strict=True))) @property def nchunks(self) -> int: """ The number of chunks in this array. Note that if a sharding codec is used, then the number of chunks may exceed the number of stored objects supporting this array. Returns ------- int The total number of chunks in the array. """ return product(self._chunk_grid_shape) @property def _nshards(self) -> int: """ The number of shards in this array. If no shards are present this will fall back to giving the number of chunks Returns ------- int The total number of shards or if absent, chunks in the array. """ return product(self._shard_grid_shape) @overload def with_config(self: AsyncArrayV2, config: ArrayConfigLike) -> AsyncArrayV2: ... @overload def with_config(self: AsyncArrayV3, config: ArrayConfigLike) -> AsyncArrayV3: ... def with_config(self, config: ArrayConfigLike) -> Self: """ Return a copy of this Array with a new runtime configuration. Parameters ---------- config : ArrayConfigLike The runtime config for the new Array. Any keys not specified will be inherited from the current array's config. Returns ------- A new Array """ if isinstance(config, ArrayConfig): new_config = config else: # Merge new config with existing config, so missing keys are inherited # from the current array rather than from global defaults new_config = ArrayConfig(**{**self.config.to_dict(), **config}) # type: ignore[arg-type] return type(self)(metadata=self.metadata, store_path=self.store_path, config=new_config) async def nchunks_initialized(self) -> int: """ Calculate the number of chunks that have been initialized in storage. This value is calculated as the product of the number of initialized shards and the number of chunks per shard. For arrays that do not use sharding, the number of chunks per shard is effectively 1, and in that case the number of chunks initialized is the same as the number of stored objects associated with an array. Returns ------- nchunks_initialized : int The number of chunks that have been initialized. Notes ----- On [`AsyncArray`][zarr.AsyncArray] this is an asynchronous method, unlike the (synchronous) property [`Array.nchunks_initialized`][zarr.Array.nchunks_initialized]. Examples -------- ```python import asyncio import zarr.api.asynchronous async def example(): arr = await zarr.api.asynchronous.create(shape=(10,), chunks=(1,)) count = await arr.nchunks_initialized() print(f"Initial: {count}") #> Initial: 0 await arr.setitem(slice(5), 1) count = await arr.nchunks_initialized() print(f"After write: {count}") #> After write: 5 return count result = asyncio.run(example()) ``` """ return await _nchunks_initialized(self) async def _nshards_initialized(self) -> int: """ Calculate the number of shards that have been initialized in storage. This is the number of shards that have been persisted to the storage backend. Returns ------- nshards_initialized : int The number of shards that have been initialized. Notes ----- On [`AsyncArray`][zarr.AsyncArray] this is an asynchronous method, unlike the (synchronous) property [`Array._nshards_initialized`][zarr.Array._nshards_initialized]. Examples -------- ```python import asyncio import zarr.api.asynchronous async def example(): arr = await zarr.api.asynchronous.create(shape=(10,), chunks=(2,)) count = await arr._nshards_initialized() print(f"Initial: {count}") #> Initial: 0 await arr.setitem(slice(5), 1) count = await arr._nshards_initialized() print(f"After write: {count}") #> After write: 3 return count result = asyncio.run(example()) ``` """ return await _nshards_initialized(self) async def nbytes_stored(self) -> int: return await _nbytes_stored(self.store_path) def _iter_chunk_coords( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[int, ...]]: """ Create an iterator over the coordinates of chunks in chunk grid space. If the `origin` keyword is used, iteration will start at the chunk index specified by `origin`. The default behavior is to start at the origin of the grid coordinate space. If the `selection_shape` keyword is used, iteration will be bounded over a contiguous region ranging from `[origin, origin selection_shape]`, where the upper bound is exclusive as per python indexing conventions. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's chunk grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in chunk grid coordinates. Yields ------ chunk_coords: tuple[int, ...] The coordinates of each chunk in the selection. """ return _iter_chunk_coords( array=self, origin=origin, selection_shape=selection_shape, ) def _iter_shard_coords( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[int, ...]]: """ Create an iterator over the coordinates of shards in shard grid space. This will fall back to chunk grid space in case no shards are present. Note that If the `origin` keyword is used, iteration will start at the shard index specified by `origin`. The default behavior is to start at the origin of the grid coordinate space. If the `selection_shape` keyword is used, iteration will be bounded over a contiguous region ranging from `[origin, origin selection_shape]`, where the upper bound is exclusive as per python indexing conventions. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's shard grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in shard grid coordinates. Yields ------ chunk_coords: tuple[int, ...] The coordinates of each shard in the selection or chunk in case of no shard being present. """ return _iter_shard_coords( array=self, origin=origin, selection_shape=selection_shape, ) def _iter_shard_keys( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[str]: """ Iterate over the keys of the stored objects supporting this array. Although only stored objects, e.g. shards should have keys, in case no shards are present this automatically falls back to chunks. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's chunk grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in shard grid coordinates. Yields ------ key: str The storage key of each shard in the selection or in case of no shard present of each chunk although the latter case as technically incorrect. """ # Iterate over the coordinates of chunks in chunk grid space. return _iter_shard_keys( array=self, origin=origin, selection_shape=selection_shape, ) def _iter_chunk_regions( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[slice, ...]]: """ Iterate over the regions spanned by each chunk. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's chunk grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in chunk grid coordinates. Yields ------ region: tuple[slice, ...] A tuple of slice objects representing the region spanned by each chunk in the selection. """ return _iter_chunk_regions( array=self, origin=origin, selection_shape=selection_shape, ) def _iter_shard_regions( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[slice, ...]]: """ Iterate over the regions spanned by each shard. This will automatically fall back to chunks if no shards are present. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's shard grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in shard grid coordinates. Yields ------ region: tuple[slice, ...] A tuple of slice objects representing the region spanned by each shard in the selection or chunk in the absence of shards. """ return _iter_shard_regions(array=self, origin=origin, selection_shape=selection_shape) @property def nbytes(self) -> int: """ The total number of bytes that can be stored in the chunks of this array. Notes ----- This value is calculated by multiplying the number of elements in the array and the size of each element, the latter of which is determined by the dtype of the array. For this reason, ``nbytes`` will likely be inaccurate for arrays with variable-length dtypes. It is not possible to determine the size of an array with variable-length elements from the shape and dtype alone. """ return self.size * self.dtype.itemsize async def _get_selection( self, indexer: Indexer, *, prototype: BufferPrototype, out: NDBuffer | None = None, fields: Fields | None = None, ) -> NDArrayLikeOrScalar: return await _get_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, indexer, prototype=prototype, out=out, fields=fields, ) async def getitem( self, selection: BasicSelection, *, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """ Asynchronous function that retrieves a subset of the array's data based on the provided selection. Parameters ---------- selection : BasicSelection A selection object specifying the subset of data to retrieve. prototype : BufferPrototype, optional A buffer prototype to use for the retrieved data (default is None). Returns ------- NDArrayLikeOrScalar The retrieved subset of the array's data. Examples -------- ```python import asyncio import zarr.api.asynchronous async def example(): store = zarr.storage.MemoryStore() async_arr = await zarr.api.asynchronous.create_array( store=store, shape=(100,100), chunks=(10,10), dtype='i4', fill_value=0) result = await async_arr.getitem((0,1)) print(result) #> 0 return result value = asyncio.run(example()) ``` """ return await _getitem( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, selection, prototype=prototype, ) async def get_orthogonal_selection( self, selection: OrthogonalSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: return await _get_orthogonal_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, selection, out=out, fields=fields, prototype=prototype, ) async def get_mask_selection( self, mask: MaskSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: return await _get_mask_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, mask, out=out, fields=fields, prototype=prototype, ) async def get_coordinate_selection( self, selection: CoordinateSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: return await _get_coordinate_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, selection, out=out, fields=fields, prototype=prototype, ) async def _save_metadata(self, metadata: ArrayMetadata, ensure_parents: bool = False) -> None: """ Asynchronously save the array metadata. """ await save_metadata(self.store_path, metadata, ensure_parents=ensure_parents) async def _set_selection( self, indexer: Indexer, value: npt.ArrayLike, *, prototype: BufferPrototype, fields: Fields | None = None, ) -> None: return await _set_selection( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, indexer, value, prototype=prototype, fields=fields, ) async def setitem( self, selection: BasicSelection, value: npt.ArrayLike, prototype: BufferPrototype | None = None, ) -> None: """ Asynchronously set values in the array using basic indexing. Parameters ---------- selection : BasicSelection The selection defining the region of the array to set. value : numpy.typing.ArrayLike The values to be written into the selected region of the array. prototype : BufferPrototype or None, optional A prototype buffer that defines the structure and properties of the array chunks being modified. If None, the default buffer prototype is used. Default is None. Returns ------- None This method does not return any value. Raises ------ IndexError If the selection is out of bounds for the array. ValueError If the values are not compatible with the array's dtype or shape. Notes ----- - This method is asynchronous and should be awaited. - Supports basic indexing, where the selection is contiguous and does not involve advanced indexing. """ return await _setitem( self.store_path, self.metadata, self.codec_pipeline, self.config, self._chunk_grid, selection, value, prototype=prototype, ) @property def oindex(self) -> AsyncOIndex[T_ArrayMetadata]: """Shortcut for orthogonal (outer) indexing, see [get_orthogonal_selection][zarr.Array.get_orthogonal_selection] and [set_orthogonal_selection][zarr.Array.set_orthogonal_selection] for documentation and examples.""" return AsyncOIndex(self) @property def vindex(self) -> AsyncVIndex[T_ArrayMetadata]: """Shortcut for vectorized (inner) indexing, see [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_mask_selection][zarr.Array.get_mask_selection] and [set_mask_selection][zarr.Array.set_mask_selection] for documentation and examples.""" return AsyncVIndex(self) async def resize(self, new_shape: ShapeLike, delete_outside_chunks: bool = True) -> None: """ Asynchronously resize the array to a new shape. Parameters ---------- new_shape : tuple[int, ...] The desired new shape of the array. delete_outside_chunks : bool, optional If True (default), chunks that fall outside the new shape will be deleted. If False, the data in those chunks will be preserved. Returns ------- AsyncArray The resized array. Raises ------ ValueError If the new shape is incompatible with the current array's chunking configuration. Notes ----- - This method is asynchronous and should be awaited. """ return await _resize(self, new_shape, delete_outside_chunks) async def append(self, data: npt.ArrayLike, axis: int = 0) -> tuple[int, ...]: """Append `data` to `axis`. Parameters ---------- data : array-like Data to be appended. axis : int Axis along which to append. Returns ------- new_shape : tuple Notes ----- The size of all dimensions other than `axis` must match between this array and `data`. """ return await _append(self, data, axis) async def update_attributes(self, new_attributes: dict[str, JSON]) -> Self: """ Asynchronously update the array's attributes. Parameters ---------- new_attributes : dict of str to JSON A dictionary of new attributes to update or add to the array. The keys represent attribute names, and the values must be JSON-compatible. Returns ------- AsyncArray The array with the updated attributes. Raises ------ ValueError If the attributes are invalid or incompatible with the array's metadata. Notes ----- - This method is asynchronous and should be awaited. - The updated attributes will be merged with existing attributes, and any conflicts will be overwritten by the new values. """ await _update_attributes(self, new_attributes) return self def __repr__(self) -> str: return f"" @property def info(self) -> Any: """ Return the statically known information for an array. Returns ------- ArrayInfo Related ------- [zarr.AsyncArray.info_complete][] - All information about a group, including dynamic information like the number of bytes and chunks written. Examples -------- >>> arr = await zarr.api.asynchronous.create( ... path="array", shape=(3, 4, 5), chunks=(2, 2, 2)) ... ) >>> arr.info Type : Array Zarr format : 3 Data type : DataType.float64 Shape : (3, 4, 5) Chunk shape : (2, 2, 2) Order : C Read-only : False Store type : MemoryStore Codecs : [{'endian': }] No. bytes : 480 """ return self._info() async def info_complete(self) -> Any: """ Return all the information for an array, including dynamic information like a storage size. In addition to the static information, this provides - The count of chunks initialized - The sum of the bytes written Returns ------- ArrayInfo Related ------- [zarr.AsyncArray.info][] - A property giving just the statically known information about an array. """ return await _info_complete(self) def _info( self, count_chunks_initialized: int | None = None, count_bytes_stored: int | None = None ) -> Any: chunk_shape = self.chunks if self._chunk_grid.is_regular else None return ArrayInfo( _zarr_format=self.metadata.zarr_format, _data_type=self._zdtype, _fill_value=self.metadata.fill_value, _shape=self.shape, _order=self.order, _shard_shape=self.shards, _chunk_shape=chunk_shape, _read_only=self.read_only, _compressors=self.compressors, _filters=self.filters, _serializer=self.serializer, _store_type=type(self.store_path.store).__name__, _count_bytes=self.nbytes, _count_bytes_stored=count_bytes_stored, _count_chunks_initialized=count_chunks_initialized, ) # TODO: Array can be a frozen data class again once property setters (e.g. shape) are removed @dataclass(frozen=False) class Array[T_ArrayMetadata: (ArrayV2Metadata, ArrayV3Metadata)]: """ A Zarr array. """ _async_array: AsyncArray[T_ArrayMetadata] @property def async_array(self) -> AsyncArray[T_ArrayMetadata]: """An asynchronous version of the current array. Useful for batching requests. Returns ------- An asynchronous array whose metadata + store matches that of this synchronous array. """ return self._async_array @property def config(self) -> ArrayConfig: """ The runtime configuration for this array. This is a read-only property. To modify the runtime configuration, use `Array.with_config` to create a new `Array` with the modified configuration. Returns ------- An `ArrayConfig` object that defines the runtime configuration for the array. """ return self.async_array.config @property def _chunk_grid(self) -> ChunkGrid: """The chunk grid for this array, bound to the array's shape.""" return self.async_array._chunk_grid @classmethod def _create( cls, store: StoreLike, *, # v2 and v3 shape: tuple[int, ...], dtype: ZDTypeLike, zarr_format: ZarrFormat = 3, fill_value: Any | None = DEFAULT_FILL_VALUE, attributes: dict[str, JSON] | None = None, # v3 only chunk_shape: tuple[int, ...] | None = None, chunk_key_encoding: ( ChunkKeyEncoding | tuple[Literal["default"], Literal[".", "/"]] | tuple[Literal["v2"], Literal[".", "/"]] | None ) = None, codecs: Iterable[Codec | dict[str, JSON]] | None = None, dimension_names: DimensionNamesLike = None, # v2 only chunks: tuple[int, ...] | None = None, dimension_separator: Literal[".", "/"] | None = None, order: MemoryOrder | None = None, filters: list[dict[str, JSON]] | None = None, compressor: CompressorLike = "auto", # runtime overwrite: bool = False, config: ArrayConfigLike | None = None, ) -> Self: """Creates a new Array instance from an initialized store. Deprecated in favor of [`zarr.create_array`][]. """ async_array = sync( AsyncArray._create( store=store, shape=shape, dtype=dtype, zarr_format=zarr_format, attributes=attributes, fill_value=fill_value, chunk_shape=chunk_shape, chunk_key_encoding=chunk_key_encoding, codecs=codecs, dimension_names=dimension_names, chunks=chunks, dimension_separator=dimension_separator, order=order, filters=filters, compressor=compressor, overwrite=overwrite, config=config, ), ) return cls(async_array) @classmethod def from_dict( cls, store_path: StorePath, data: dict[str, JSON], ) -> Self: """ Create a Zarr array from a dictionary. Parameters ---------- store_path : StorePath The path within the store where the array should be created. data : dict A dictionary representing the array data. This dictionary should include necessary metadata for the array, such as shape, dtype, fill value, and attributes. Returns ------- Array The created Zarr array. Raises ------ ValueError If the dictionary data is invalid or missing required fields for array creation. """ async_array = AsyncArray.from_dict(store_path=store_path, data=data) return cls(async_array) @classmethod def open( cls, store: StoreLike, ) -> Self: """Opens an existing Array from a store. Parameters ---------- store : StoreLike Store containing the Array. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. Returns ------- Array Array opened from the store. """ async_array = sync(AsyncArray.open(store)) return cls(async_array) @property def store(self) -> Store: return self.async_array.store @property def ndim(self) -> int: """Returns the number of dimensions in the array. Returns ------- int The number of dimensions in the array. """ return self.async_array.ndim @property def shape(self) -> tuple[int, ...]: """Returns the shape of the array. Returns ------- tuple[int, ...] The shape of the array. """ return self.async_array.shape @shape.setter def shape(self, value: tuple[int, ...]) -> None: """Sets the shape of the array by calling resize.""" self.resize(value) @property def chunks(self) -> tuple[int, ...]: """Returns a tuple of integers describing the length of each dimension of a chunk of the array. If sharding is used the inner chunk shape is returned. Only defined for arrays using a regular chunk grid. If array uses a rectilinear chunk grid, `NotImplementedError` is raised. Returns ------- tuple A tuple of integers representing the length of each dimension of a chunk. """ return self.async_array.chunks @property def read_chunk_sizes(self) -> tuple[tuple[int, ...], ...]: """Per-dimension data sizes of chunks used for reading, clipped to the array extent. Boundary chunks that extend past the array shape are clipped, so the last size along a dimension may be smaller than the declared chunk size. This matches the dask ``Array.chunks`` convention. When sharding is used, returns the inner chunk sizes. Otherwise, returns the outer chunk sizes (same as ``write_chunk_sizes``). Returns ------- tuple[tuple[int, ...], ...] One inner tuple per dimension containing the data size of each chunk (not the encoded buffer size). Examples -------- >>> arr = zarr.open_array(store) >>> arr.read_chunk_sizes ((30, 30, 30, 10), (40, 40)) """ return self.async_array.read_chunk_sizes @property def write_chunk_sizes(self) -> tuple[tuple[int, ...], ...]: """Per-dimension data sizes of storage chunks, clipped to the array extent. Always returns the outer chunk sizes, regardless of sharding. Boundary chunks that extend past the array shape are clipped, so the last size along a dimension may be smaller than the declared chunk size. This matches the dask ``Array.chunks`` convention. Returns ------- tuple[tuple[int, ...], ...] One inner tuple per dimension containing the data size of each chunk (not the encoded buffer size). Examples -------- >>> arr = zarr.open_array(store) >>> arr.write_chunk_sizes ((30, 30, 30, 10), (40, 40)) """ return self.async_array.write_chunk_sizes @property def shards(self) -> tuple[int, ...] | None: """Returns a tuple of integers describing the length of each dimension of a shard of the array. Returns None if sharding is not used. Only defined for arrays using a regular chunk grid. If array uses a rectilinear chunk grid, `NotImplementedError` is raised. Returns ------- tuple | None A tuple of integers representing the length of each dimension of a shard or None if sharding is not used. """ return self.async_array.shards @property def size(self) -> int: """Returns the total number of elements in the array. Returns ------- int Total number of elements in the array. """ return self.async_array.size @property def dtype(self) -> np.dtype[Any]: """Returns the NumPy data type. Returns ------- np.dtype The NumPy data type. """ return self.async_array.dtype @property def attrs(self) -> Attributes: """Returns a [MutableMapping][collections.abc.MutableMapping] containing user-defined attributes. Returns ------- attrs A [MutableMapping][collections.abc.MutableMapping] object containing user-defined attributes. Notes ----- Note that attribute values must be JSON serializable. """ return Attributes(self) @property def path(self) -> str: """Storage path.""" return self.async_array.path @property def name(self) -> str: """Array name following h5py convention.""" return self.async_array.name @property def basename(self) -> str: """Final component of name.""" return self.async_array.basename @property def metadata(self) -> ArrayMetadata: return self.async_array.metadata @property def store_path(self) -> StorePath: return self.async_array.store_path @property def order(self) -> MemoryOrder: return self.async_array.order @property def read_only(self) -> bool: return self.async_array.read_only @property def fill_value(self) -> Any: return self.metadata.fill_value @property def filters(self) -> tuple[Numcodec, ...] | tuple[ArrayArrayCodec, ...]: """ Filters that are applied to each chunk of the array, in order, before serializing that chunk to bytes. """ return self.async_array.filters @property def serializer(self) -> None | ArrayBytesCodec: """ Array-to-bytes codec to use for serializing the chunks into bytes. """ return self.async_array.serializer @property @deprecated("Use Array.compressors instead.", category=ZarrDeprecationWarning) def compressor(self) -> Numcodec | None: """ Compressor that is applied to each chunk of the array. !!! warning "Deprecated" `array.compressor` is deprecated since v3.0.0 and will be removed in a future release. Use [`array.compressors`][zarr.Array.compressors] instead. """ return self.async_array.compressor @property def compressors(self) -> tuple[Numcodec, ...] | tuple[BytesBytesCodec, ...]: """ Compressors that are applied to each chunk of the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. """ return self.async_array.compressors @property def cdata_shape(self) -> tuple[int, ...]: """ The number of chunks along each dimension. When sharding is used, this counts inner chunks (not shards) per dimension. """ return self.async_array._chunk_grid_shape @property def _chunk_grid_shape(self) -> tuple[int, ...]: """ The number of chunks along each dimension. When sharding is used, this counts inner chunks (not shards) per dimension. Returns ------- tuple[int, ...] The number of chunks along each dimension. """ return self.async_array._chunk_grid_shape @property def _shard_grid_shape(self) -> tuple[int, ...]: """ The shape of the shard grid for this array. """ return self.async_array._shard_grid_shape @property def nchunks(self) -> int: """ The number of chunks in this array. Note that if a sharding codec is used, then the number of chunks may exceed the number of stored objects supporting this array. """ return self.async_array.nchunks @property def _nshards(self) -> int: """ The number of shards in the stored representation of this array. """ return self.async_array._nshards @overload def with_config(self: ArrayV2, config: ArrayConfigLike) -> ArrayV2: ... @overload def with_config(self: ArrayV3, config: ArrayConfigLike) -> ArrayV3: ... def with_config(self, config: ArrayConfigLike) -> Self: """ Return a copy of this Array with a new runtime configuration. Parameters ---------- config : ArrayConfigLike The runtime config for the new Array. Any keys not specified will be inherited from the current array's config. Returns ------- A new Array """ return type(self)(self._async_array.with_config(config)) @property def nbytes(self) -> int: """ The total number of bytes that can be stored in the chunks of this array. Notes ----- This value is calculated by multiplying the number of elements in the array and the size of each element, the latter of which is determined by the dtype of the array. For this reason, ``nbytes`` will likely be inaccurate for arrays with variable-length dtypes. It is not possible to determine the size of an array with variable-length elements from the shape and dtype alone. """ return self.async_array.nbytes @property def nchunks_initialized(self) -> int: """ Calculate the number of chunks that have been initialized in storage. This value is calculated as the product of the number of initialized shards and the number of chunks per shard. For arrays that do not use sharding, the number of chunks per shard is effectively 1, and in that case the number of chunks initialized is the same as the number of stored objects associated with an array. For a direct count of the number of initialized stored objects, see ``nshards_initialized``. Returns ------- nchunks_initialized : int The number of chunks that have been initialized. Examples -------- >>> arr = zarr.create_array(store={}, shape=(10,), chunks=(1,), shards=(2,)) >>> arr.nchunks_initialized 0 >>> arr[:5] = 1 >>> arr.nchunks_initialized 6 """ return sync(self.async_array.nchunks_initialized()) @property def _nshards_initialized(self) -> int: """ Calculate the number of shards that have been initialized, i.e. the number of shards that have been persisted to the storage backend. Returns ------- nshards_initialized : int The number of shards that have been initialized. Examples -------- >>> arr = await zarr.create(shape=(10,), chunks=(2,)) >>> arr._nshards_initialized 0 >>> arr[:5] = 1 >>> arr._nshard_initialized 3 """ return sync(self.async_array._nshards_initialized()) def nbytes_stored(self) -> int: """ Determine the size, in bytes, of the array actually written to the store. Returns ------- size : int """ return sync(self.async_array.nbytes_stored()) def _iter_shard_keys( self, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[str]: """ Iterate over the storage keys of each shard, relative to an optional origin, and optionally limited to a contiguous region in chunk grid coordinates. If no shards are present this falls back to chunks, though in this case these are then actually not storage keys. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's shard grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in shard grid coordinates. Yields ------ str The storage key of each shard in the selection or chunk though chunks technically do not have storage keys. """ return self.async_array._iter_shard_keys(origin=origin, selection_shape=selection_shape) def _iter_chunk_coords( self, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[int, ...]]: """ Create an iterator over the coordinates of chunks in chunk grid space. If the `origin` keyword is used, iteration will start at the chunk index specified by `origin`. The default behavior is to start at the origin of the grid coordinate space. If the `selection_shape` keyword is used, iteration will be bounded over a contiguous region ranging from `[origin, origin + selection_shape]`, where the upper bound is exclusive as per python indexing conventions. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's chunk grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in chunk grid coordinates. Yields ------ tuple[int, ...] The coordinates of each chunk in the selection. """ return self.async_array._iter_chunk_coords(origin=origin, selection_shape=selection_shape) def _iter_shard_coords( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[int, ...]]: """ Create an iterator over the coordinates of shards in shard grid space. If the `origin` keyword is used, iteration will start at the shard index specified by `origin`. The default behavior is to start at the origin of the grid coordinate space. If the `selection_shape` keyword is used, iteration will be bounded over a contiguous region ranging from `[origin, origin selection_shape]`, where the upper bound is exclusive as per python indexing conventions. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's shard grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in shard grid coordinates. Yields ------ tuple[int, ...] The coordinates of each shard in the selection. """ return self.async_array._iter_shard_coords(origin=origin, selection_shape=selection_shape) def _iter_chunk_regions( self, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[slice, ...]]: """ Iterate over the regions spanned by each chunk. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's chunk grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in chunk grid coordinates. Yields ------ tuple[slice, ...] A tuple of slice objects representing the region spanned by each chunk in the selection. """ return self.async_array._iter_chunk_regions(origin=origin, selection_shape=selection_shape) def _iter_shard_regions( self, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None ) -> Iterator[tuple[slice, ...]]: """ Iterate over the regions spanned by each shard or chunk if no shard is present. Parameters ---------- origin : Sequence[int] | None, default=None The origin of the selection relative to the array's chunk grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in chunk grid coordinates. Yields ------ tuple[slice, ...] A tuple of slice objects representing the region spanned by each shard or if no shard is present, chunk in the selection. """ return self.async_array._iter_shard_regions(origin=origin, selection_shape=selection_shape) def __array__( self, dtype: npt.DTypeLike | None = None, copy: bool | None = None ) -> NDArrayLike: """ This method is used by numpy when converting zarr.Array into a numpy array. For more information, see https://numpy.org/devdocs/user/basics.interoperability.html#the-array-method """ if copy is False: msg = "`copy=False` is not supported. This method always creates a copy." raise ValueError(msg) arr = self[...] arr_np = np.array(arr, dtype=dtype) if dtype is not None: arr_np = arr_np.astype(dtype) return arr_np def __getitem__(self, selection: Selection) -> NDArrayLikeOrScalar: """Retrieve data for an item or region of the array. Parameters ---------- selection : tuple An integer index or slice or tuple of int/slice objects specifying the requested item or region for each dimension of the array. Returns ------- NDArrayLikeOrScalar An array-like or scalar containing the data for the requested region. Examples -------- Setup a 1-dimensional array:: >>> import zarr >>> import numpy as np >>> data = np.arange(100, dtype="uint16") >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=(10,), >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve a single item:: >>> z[5] 5 Retrieve a region via slicing:: >>> z[:5] array([0, 1, 2, 3, 4]) >>> z[-5:] array([95, 96, 97, 98, 99]) >>> z[5:10] array([5, 6, 7, 8, 9]) >>> z[5:10:2] array([5, 7, 9]) >>> z[::2] array([ 0, 2, 4, ..., 94, 96, 98]) Load the entire array into memory:: >>> z[...] array([ 0, 1, 2, ..., 97, 98, 99]) Setup a 2-dimensional array:: >>> data = np.arange(100, dtype="uint16").reshape(10, 10) >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=(10, 10), >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve an item:: >>> z[2, 2] 22 Retrieve a region via slicing:: >>> z[1:3, 1:3] array([[11, 12], [21, 22]]) >>> z[1:3, :] array([[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]]) >>> z[:, 1:3] array([[ 1, 2], [11, 12], [21, 22], [31, 32], [41, 42], [51, 52], [61, 62], [71, 72], [81, 82], [91, 92]]) >>> z[0:5:2, 0:5:2] array([[ 0, 2, 4], [20, 22, 24], [40, 42, 44]]) >>> z[::2, ::2] array([[ 0, 2, 4, 6, 8], [20, 22, 24, 26, 28], [40, 42, 44, 46, 48], [60, 62, 64, 66, 68], [80, 82, 84, 86, 88]]) Load the entire array into memory:: >>> z[...] array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], [50, 51, 52, 53, 54, 55, 56, 57, 58, 59], [60, 61, 62, 63, 64, 65, 66, 67, 68, 69], [70, 71, 72, 73, 74, 75, 76, 77, 78, 79], [80, 81, 82, 83, 84, 85, 86, 87, 88, 89], [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]]) Notes ----- Slices with step > 1 are supported, but slices with negative step are not. For arrays with a structured dtype, see Zarr format 2 for examples of how to use fields Currently the implementation for __getitem__ is provided by [`vindex`][zarr.Array.vindex] if the indexing is pure fancy indexing (ie a broadcast-compatible tuple of integer array indices), or by [`set_basic_selection`][zarr.Array.set_basic_selection] otherwise. Effectively, this means that the following indexing modes are supported: - integer indexing - slice indexing - mixed slice and integer indexing - boolean indexing - fancy indexing (vectorized list of integers) For specific indexing options including outer indexing, see the methods listed under Related. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection] [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__setitem__][zarr.Array.__setitem__] """ fields, pure_selection = pop_fields(selection) if is_pure_fancy_indexing(pure_selection, self.ndim): return self.vindex[cast("CoordinateSelection | MaskSelection", selection)] elif is_pure_orthogonal_indexing(pure_selection, self.ndim): return self.get_orthogonal_selection(pure_selection, fields=fields) else: return self.get_basic_selection(cast("BasicSelection", pure_selection), fields=fields) def __setitem__(self, selection: Selection, value: npt.ArrayLike) -> None: """Modify data for an item or region of the array. Parameters ---------- selection : tuple An integer index or slice or tuple of int/slice specifying the requested region for each dimension of the array. value : npt.ArrayLike An array-like containing the data to be stored in the selection. Examples -------- Setup a 1-dimensional array:: >>> import zarr >>> z = zarr.zeros( >>> shape=(100,), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(5,), >>> dtype="i4", >>> ) Set all array elements to the same scalar value:: >>> z[...] = 42 >>> z[...] array([42, 42, 42, ..., 42, 42, 42]) Set a portion of the array:: >>> z[:10] = np.arange(10) >>> z[-10:] = np.arange(10)[::-1] >>> z[...] array([ 0, 1, 2, ..., 2, 1, 0]) Setup a 2-dimensional array:: >>> z = zarr.zeros( >>> shape=(5, 5), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(5, 5), >>> dtype="i4", >>> ) Set all array elements to the same scalar value:: >>> z[...] = 42 Set a portion of the array:: >>> z[0, :] = np.arange(z.shape[1]) >>> z[:, 0] = np.arange(z.shape[0]) >>> z[...] array([[ 0, 1, 2, 3, 4], [ 1, 42, 42, 42, 42], [ 2, 42, 42, 42, 42], [ 3, 42, 42, 42, 42], [ 4, 42, 42, 42, 42]]) Notes ----- Slices with step > 1 are supported, but slices with negative step are not. For arrays with a structured dtype, see Zarr format 2 for examples of how to use fields Currently the implementation for __setitem__ is provided by [`vindex`][zarr.Array.vindex] if the indexing is pure fancy indexing (ie a broadcast-compatible tuple of integer array indices), or by [`set_basic_selection`][zarr.Array.set_basic_selection] otherwise. Effectively, this means that the following indexing modes are supported: - integer indexing - slice indexing - mixed slice and integer indexing - boolean indexing - fancy indexing (vectorized list of integers) For specific indexing options including outer indexing, see the methods listed under Related. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__] """ # Converting a zarr Array to numpy here avoids a SyncError that occurs when # value.__getitem__ is called inside the async codec pipeline (which already # runs within a running event loop). np.asarray triggers Array.__array__, # which reads the data synchronously before we enter the async context. if isinstance(value, Array): value = np.asarray(value) fields, pure_selection = pop_fields(selection) if is_pure_fancy_indexing(pure_selection, self.ndim): self.vindex[cast("CoordinateSelection | MaskSelection", selection)] = value elif is_pure_orthogonal_indexing(pure_selection, self.ndim): self.set_orthogonal_selection(pure_selection, value, fields=fields) else: self.set_basic_selection(cast("BasicSelection", pure_selection), value, fields=fields) def get_basic_selection( self, selection: BasicSelection = Ellipsis, *, out: NDBuffer | None = None, prototype: BufferPrototype | None = None, fields: Fields | None = None, ) -> NDArrayLikeOrScalar: """Retrieve data for an item or region of the array. Parameters ---------- selection : tuple A tuple specifying the requested item or region for each dimension of the array. May be any combination of int and/or slice or ellipsis for multidimensional arrays. out : NDBuffer, optional If given, load the selected data directly into this buffer. prototype : BufferPrototype, optional The prototype of the buffer to use for the output data. If not provided, the default buffer prototype is used. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to extract data for. Returns ------- NDArrayLikeOrScalar An array-like or scalar containing the data for the requested region. Examples -------- Setup a 1-dimensional array:: >>> import zarr >>> import numpy as np >>> data = np.arange(100, dtype="uint16") >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=(3,), >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve a single item:: >>> z.get_basic_selection(5) 5 Retrieve a region via slicing:: >>> z.get_basic_selection(slice(5)) array([0, 1, 2, 3, 4]) >>> z.get_basic_selection(slice(-5, None)) array([95, 96, 97, 98, 99]) >>> z.get_basic_selection(slice(5, 10)) array([5, 6, 7, 8, 9]) >>> z.get_basic_selection(slice(5, 10, 2)) array([5, 7, 9]) >>> z.get_basic_selection(slice(None, None, 2)) array([ 0, 2, 4, ..., 94, 96, 98]) Setup a 3-dimensional array:: >>> data = np.arange(1000).reshape(10, 10, 10) >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=(5, 5, 5), >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve an item:: >>> z.get_basic_selection((1, 2, 3)) 123 Retrieve a region via slicing and Ellipsis:: >>> z.get_basic_selection((slice(1, 3), slice(1, 3), 0)) array([[110, 120], [210, 220]]) >>> z.get_basic_selection(0, (slice(1, 3), slice(None))) array([[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]]) >>> z.get_basic_selection((..., 5)) array([[ 2 12 22 32 42 52 62 72 82 92] [102 112 122 132 142 152 162 172 182 192] ... [802 812 822 832 842 852 862 872 882 892] [902 912 922 932 942 952 962 972 982 992]] Notes ----- Slices with step > 1 are supported, but slices with negative step are not. For arrays with a structured dtype, see Zarr format 2 for examples of how to use the `fields` parameter. This method provides the implementation for accessing data via the square bracket notation (__getitem__). See [`__getitem__`][zarr.Array.__getitem__] for examples using the alternative notation. Related ------- [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() return sync( self.async_array._get_selection( BasicIndexer(selection, self.shape, self._chunk_grid), out=out, fields=fields, prototype=prototype, ) ) def set_basic_selection( self, selection: BasicSelection, value: npt.ArrayLike, *, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> None: """Modify data for an item or region of the array. Parameters ---------- selection : tuple A tuple specifying the requested item or region for each dimension of the array. May be any combination of int and/or slice or ellipsis for multidimensional arrays. value : npt.ArrayLike An array-like containing values to be stored into the array. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to set data for. prototype : BufferPrototype, optional The prototype of the buffer used for setting the data. If not provided, the default buffer prototype is used. Examples -------- Setup a 1-dimensional array:: >>> import zarr >>> z = zarr.zeros( >>> shape=(100,), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(100,), >>> dtype="i4", >>> ) Set all array elements to the same scalar value:: >>> z.set_basic_selection(..., 42) >>> z[...] array([42, 42, 42, ..., 42, 42, 42]) Set a portion of the array:: >>> z.set_basic_selection(slice(10), np.arange(10)) >>> z.set_basic_selection(slice(-10, None), np.arange(10)[::-1]) >>> z[...] array([ 0, 1, 2, ..., 2, 1, 0]) Setup a 2-dimensional array:: >>> z = zarr.zeros( >>> shape=(5, 5), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(5, 5), >>> dtype="i4", >>> ) Set all array elements to the same scalar value:: >>> z.set_basic_selection(..., 42) Set a portion of the array:: >>> z.set_basic_selection((0, slice(None)), np.arange(z.shape[1])) >>> z.set_basic_selection((slice(None), 0), np.arange(z.shape[0])) >>> z[...] array([[ 0, 1, 2, 3, 4], [ 1, 42, 42, 42, 42], [ 2, 42, 42, 42, 42], [ 3, 42, 42, 42, 42], [ 4, 42, 42, 42, 42]]) Notes ----- For arrays with a structured dtype, see Zarr format 2 for examples of how to use the `fields` parameter. This method provides the underlying implementation for modifying data via square bracket notation, see [`__setitem__`][zarr.Array.__setitem__] for equivalent examples using the alternative notation. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = BasicIndexer(selection, self.shape, self._chunk_grid) sync(self.async_array._set_selection(indexer, value, fields=fields, prototype=prototype)) def get_orthogonal_selection( self, selection: OrthogonalSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """Retrieve data by making a selection for each dimension of the array. For example, if an array has 2 dimensions, allows selecting specific rows and/or columns. The selection for each dimension can be either an integer (indexing a single item), a slice, an array of integers, or a Boolean array where True values indicate a selection. Parameters ---------- selection : tuple A selection for each dimension of the array. May be any combination of int, slice, integer array or Boolean array. out : NDBuffer, optional If given, load the selected data directly into this buffer. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to extract data for. prototype : BufferPrototype, optional The prototype of the buffer to use for the output data. If not provided, the default buffer prototype is used. Returns ------- NDArrayLikeOrScalar An array-like or scalar containing the data for the requested selection. Examples -------- Setup a 2-dimensional array:: >>> import zarr >>> import numpy as np >>> data = np.arange(100).reshape(10, 10) >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=data.shape, >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve rows and columns via any combination of int, slice, integer array and/or Boolean array:: >>> z.get_orthogonal_selection(([1, 4], slice(None))) array([[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]]) >>> z.get_orthogonal_selection((slice(None), [1, 4])) array([[ 1, 4], [11, 14], [21, 24], [31, 34], [41, 44], [51, 54], [61, 64], [71, 74], [81, 84], [91, 94]]) >>> z.get_orthogonal_selection(([1, 4], [1, 4])) array([[11, 14], [41, 44]]) >>> sel = np.zeros(z.shape[0], dtype=bool) >>> sel[1] = True >>> sel[4] = True >>> z.get_orthogonal_selection((sel, sel)) array([[11, 14], [41, 44]]) For convenience, the orthogonal selection functionality is also available via the `oindex` property, e.g.:: >>> z.oindex[[1, 4], :] array([[10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]]) >>> z.oindex[:, [1, 4]] array([[ 1, 4], [11, 14], [21, 24], [31, 34], [41, 44], [51, 54], [61, 64], [71, 74], [81, 84], [91, 94]]) >>> z.oindex[[1, 4], [1, 4]] array([[11, 14], [41, 44]]) >>> sel = np.zeros(z.shape[0], dtype=bool) >>> sel[1] = True >>> sel[4] = True >>> z.oindex[sel, sel] array([[11, 14], [41, 44]]) Notes ----- Orthogonal indexing is also known as outer indexing. Slices with step > 1 are supported, but slices with negative step are not. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = OrthogonalIndexer(selection, self.shape, self._chunk_grid) return sync( self.async_array._get_selection( indexer=indexer, out=out, fields=fields, prototype=prototype ) ) def set_orthogonal_selection( self, selection: OrthogonalSelection, value: npt.ArrayLike, *, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> None: """Modify data via a selection for each dimension of the array. Parameters ---------- selection : tuple A selection for each dimension of the array. May be any combination of int, slice, integer array or Boolean array. value : npt.ArrayLike An array-like array containing the data to be stored in the array. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to set data for. prototype : BufferPrototype, optional The prototype of the buffer used for setting the data. If not provided, the default buffer prototype is used. Examples -------- Setup a 2-dimensional array:: >>> import zarr >>> z = zarr.zeros( >>> shape=(5, 5), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(5, 5), >>> dtype="i4", >>> ) Set data for a selection of rows:: >>> z.set_orthogonal_selection(([1, 4], slice(None)), 1) >>> z[...] array([[0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [1, 1, 1, 1, 1]]) Set data for a selection of columns:: >>> z.set_orthogonal_selection((slice(None), [1, 4]), 2) >>> z[...] array([[0, 2, 0, 0, 2], [1, 2, 1, 1, 2], [0, 2, 0, 0, 2], [0, 2, 0, 0, 2], [1, 2, 1, 1, 2]]) Set data for a selection of rows and columns:: >>> z.set_orthogonal_selection(([1, 4], [1, 4]), 3) >>> z[...] array([[0, 2, 0, 0, 2], [1, 3, 1, 1, 3], [0, 2, 0, 0, 2], [0, 2, 0, 0, 2], [1, 3, 1, 1, 3]]) Set data from a 2D array:: >>> values = np.arange(10).reshape(2, 5) >>> z.set_orthogonal_selection(([0, 3], ...), values) >>> z[...] array([[0, 1, 2, 3, 4], [1, 3, 1, 1, 3], [0, 2, 0, 0, 2], [5, 6, 7, 8, 9], [1, 3, 1, 1, 3]]) For convenience, this functionality is also available via the `oindex` property. E.g.:: >>> z.oindex[[1, 4], [1, 4]] = 4 >>> z[...] array([[0, 1, 2, 3, 4], [1, 4, 1, 1, 4], [0, 2, 0, 0, 2], [5, 6, 7, 8, 9], [1, 4, 1, 1, 4]]) Notes ----- Orthogonal indexing is also known as outer indexing. Slices with step > 1 are supported, but slices with negative step are not. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = OrthogonalIndexer(selection, self.shape, self._chunk_grid) return sync( self.async_array._set_selection(indexer, value, fields=fields, prototype=prototype) ) def get_mask_selection( self, mask: MaskSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """Retrieve a selection of individual items, by providing a Boolean array of the same shape as the array against which the selection is being made, where True values indicate a selected item. Parameters ---------- mask : ndarray, bool A Boolean array of the same shape as the array against which the selection is being made. out : NDBuffer, optional If given, load the selected data directly into this buffer. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to extract data for. prototype : BufferPrototype, optional The prototype of the buffer to use for the output data. If not provided, the default buffer prototype is used. Returns ------- NDArrayLikeOrScalar An array-like or scalar containing the data for the requested selection. Examples -------- Setup a 2-dimensional array:: >>> import zarr >>> import numpy as np >>> data = np.arange(100).reshape(10, 10) >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=data.shape, >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve items by specifying a mask:: >>> sel = np.zeros_like(z, dtype=bool) >>> sel[1, 1] = True >>> sel[4, 4] = True >>> z.get_mask_selection(sel) array([11, 44]) For convenience, the mask selection functionality is also available via the `vindex` property, e.g.:: >>> z.vindex[sel] array([11, 44]) Notes ----- Mask indexing is a form of vectorized or inner indexing, and is equivalent to coordinate indexing. Internally the mask array is converted to coordinate arrays by calling `np.nonzero`. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = MaskIndexer(mask, self.shape, self._chunk_grid) return sync( self.async_array._get_selection( indexer=indexer, out=out, fields=fields, prototype=prototype ) ) def set_mask_selection( self, mask: MaskSelection, value: npt.ArrayLike, *, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> None: """Modify a selection of individual items, by providing a Boolean array of the same shape as the array against which the selection is being made, where True values indicate a selected item. Parameters ---------- mask : ndarray, bool A Boolean array of the same shape as the array against which the selection is being made. value : npt.ArrayLike An array-like containing values to be stored into the array. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to set data for. Examples -------- Setup a 2-dimensional array:: >>> import zarr >>> z = zarr.zeros( >>> shape=(5, 5), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(5, 5), >>> dtype="i4", >>> ) Set data for a selection of items:: >>> sel = np.zeros_like(z, dtype=bool) >>> sel[1, 1] = True >>> sel[4, 4] = True >>> z.set_mask_selection(sel, 1) >>> z[...] array([[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 1]]) For convenience, this functionality is also available via the `vindex` property. E.g.:: >>> z.vindex[sel] = 2 >>> z[...] array([[0, 0, 0, 0, 0], [0, 2, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 2]]) Notes ----- Mask indexing is a form of vectorized or inner indexing, and is equivalent to coordinate indexing. Internally the mask array is converted to coordinate arrays by calling `np.nonzero`. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = MaskIndexer(mask, self.shape, self._chunk_grid) sync(self.async_array._set_selection(indexer, value, fields=fields, prototype=prototype)) def get_coordinate_selection( self, selection: CoordinateSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """Retrieve a selection of individual items, by providing the indices (coordinates) for each selected item. Parameters ---------- selection : tuple An integer (coordinate) array for each dimension of the array. out : NDBuffer, optional If given, load the selected data directly into this buffer. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to extract data for. prototype : BufferPrototype, optional The prototype of the buffer to use for the output data. If not provided, the default buffer prototype is used. Returns ------- NDArrayLikeOrScalar An array-like or scalar containing the data for the requested coordinate selection. Examples -------- Setup a 2-dimensional array:: >>> import zarr >>> import numpy as np >>> data = np.arange(0, 100, dtype="uint16").reshape((10, 10)) >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=(3, 3), >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve items by specifying their coordinates:: >>> z.get_coordinate_selection(([1, 4], [1, 4])) array([11, 44]) For convenience, the coordinate selection functionality is also available via the `vindex` property, e.g.:: >>> z.vindex[[1, 4], [1, 4]] array([11, 44]) Notes ----- Coordinate indexing is also known as point selection, and is a form of vectorized or inner indexing. Slices are not supported. Coordinate arrays must be provided for all dimensions of the array. Coordinate arrays may be multidimensional, in which case the output array will also be multidimensional. Coordinate arrays are broadcast against each other before being applied. The shape of the output will be the same as the shape of each coordinate array after broadcasting. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = CoordinateIndexer(selection, self.shape, self._chunk_grid) out_array = sync( self.async_array._get_selection( indexer=indexer, out=out, fields=fields, prototype=prototype ) ) if hasattr(out_array, "shape"): # restore shape out_array = np.array(out_array).reshape(indexer.sel_shape) return out_array def set_coordinate_selection( self, selection: CoordinateSelection, value: npt.ArrayLike, *, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> None: """Modify a selection of individual items, by providing the indices (coordinates) for each item to be modified. Parameters ---------- selection : tuple An integer (coordinate) array for each dimension of the array. value : npt.ArrayLike An array-like containing values to be stored into the array. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to set data for. Examples -------- Setup a 2-dimensional array:: >>> import zarr >>> z = zarr.zeros( >>> shape=(5, 5), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(5, 5), >>> dtype="i4", >>> ) Set data for a selection of items:: >>> z.set_coordinate_selection(([1, 4], [1, 4]), 1) >>> z[...] array([[0, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 1]]) For convenience, this functionality is also available via the `vindex` property. E.g.:: >>> z.vindex[[1, 4], [1, 4]] = 2 >>> z[...] array([[0, 0, 0, 0, 0], [0, 2, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 2]]) Notes ----- Coordinate indexing is also known as point selection, and is a form of vectorized or inner indexing. Slices are not supported. Coordinate arrays must be provided for all dimensions of the array. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() # setup indexer indexer = CoordinateIndexer(selection, self.shape, self._chunk_grid) # handle value - need ndarray-like flatten value if not is_scalar(value, self.dtype): try: from numcodecs.compat import ensure_ndarray_like value = ensure_ndarray_like(value) # TODO replace with agnostic except TypeError: # Handle types like `list` or `tuple` value = np.array(value) # TODO replace with agnostic if hasattr(value, "shape") and len(value.shape) > 1: value = np.array(value).reshape(-1) if not is_scalar(value, self.dtype) and ( isinstance(value, NDArrayLike) and indexer.shape != value.shape ): raise ValueError( f"Attempting to set a selection of {indexer.sel_shape[0]} " f"elements with an array of {value.shape[0]} elements." ) sync(self.async_array._set_selection(indexer, value, fields=fields, prototype=prototype)) def get_block_selection( self, selection: BasicSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """Retrieve a selection of individual items, by providing the indices (coordinates) for each selected item. Parameters ---------- selection : int or slice or tuple of int or slice An integer (coordinate) or slice for each dimension of the array. out : NDBuffer, optional If given, load the selected data directly into this buffer. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to extract data for. prototype : BufferPrototype, optional The prototype of the buffer to use for the output data. If not provided, the default buffer prototype is used. Returns ------- NDArrayLikeOrScalar An array-like or scalar containing the data for the requested block selection. Examples -------- Setup a 2-dimensional array:: >>> import zarr >>> import numpy as np >>> data = np.arange(0, 100, dtype="uint16").reshape((10, 10)) >>> z = zarr.create_array( >>> StorePath(MemoryStore(mode="w")), >>> shape=data.shape, >>> chunks=(3, 3), >>> dtype=data.dtype, >>> ) >>> z[:] = data Retrieve items by specifying their block coordinates:: >>> z.get_block_selection((1, slice(None))) array([[30, 31, 32, 33, 34, 35, 36, 37, 38, 39], [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], [50, 51, 52, 53, 54, 55, 56, 57, 58, 59]]) Which is equivalent to:: >>> z[3:6, :] array([[30, 31, 32, 33, 34, 35, 36, 37, 38, 39], [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], [50, 51, 52, 53, 54, 55, 56, 57, 58, 59]]) For convenience, the block selection functionality is also available via the `blocks` property, e.g.:: >>> z.blocks[1] array([[30, 31, 32, 33, 34, 35, 36, 37, 38, 39], [40, 41, 42, 43, 44, 45, 46, 47, 48, 49], [50, 51, 52, 53, 54, 55, 56, 57, 58, 59]]) Notes ----- Block indexing is a convenience indexing method to work on individual chunks with chunk index slicing. It has the same concept as Dask's `Array.blocks` indexing. Slices are supported. However, only with a step size of one. Block index arrays may be multidimensional to index multidimensional arrays. For example:: >>> z.blocks[0, 1:3] array([[ 3, 4, 5, 6, 7, 8], [13, 14, 15, 16, 17, 18], [23, 24, 25, 26, 27, 28]]) Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = BlockIndexer(selection, self.shape, self._chunk_grid) return sync( self.async_array._get_selection( indexer=indexer, out=out, fields=fields, prototype=prototype ) ) def set_block_selection( self, selection: BasicSelection, value: npt.ArrayLike, *, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> None: """Modify a selection of individual blocks, by providing the chunk indices (coordinates) for each block to be modified. Parameters ---------- selection : tuple An integer (coordinate) or slice for each dimension of the array. value : npt.ArrayLike An array-like containing the data to be stored in the block selection. fields : str or sequence of str, optional For arrays with a structured dtype, one or more fields can be specified to set data for. prototype : BufferPrototype, optional The prototype of the buffer used for setting the data. If not provided, the default buffer prototype is used. Examples -------- Set up a 2-dimensional array:: >>> import zarr >>> z = zarr.zeros( >>> shape=(6, 6), >>> store=StorePath(MemoryStore(mode="w")), >>> chunk_shape=(2, 2), >>> dtype="i4", >>> ) Set data for a selection of items:: >>> z.set_block_selection((1, 0), 1) >>> z[...] array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]) For convenience, this functionality is also available via the `blocks` property. E.g.:: >>> z.blocks[2, 1] = 4 >>> z[...] array([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0], [0, 0, 4, 4, 0, 0], [0, 0, 4, 4, 0, 0]]) >>> z.blocks[:, 2] = 7 >>> z[...] array([[0, 0, 0, 0, 7, 7], [0, 0, 0, 0, 7, 7], [1, 1, 0, 0, 7, 7], [1, 1, 0, 0, 7, 7], [0, 0, 4, 4, 7, 7], [0, 0, 4, 4, 7, 7]]) Notes ----- Block indexing is a convenience indexing method to work on individual chunks with chunk index slicing. It has the same concept as Dask's `Array.blocks` indexing. Slices are supported. However, only with a step size of one. Related ------- [get_basic_selection][zarr.Array.get_basic_selection], [set_basic_selection][zarr.Array.set_basic_selection], [get_mask_selection][zarr.Array.get_mask_selection], [set_mask_selection][zarr.Array.set_mask_selection], [get_orthogonal_selection][zarr.Array.get_orthogonal_selection], [set_orthogonal_selection][zarr.Array.set_orthogonal_selection], [get_coordinate_selection][zarr.Array.get_coordinate_selection], [get_block_selection][zarr.Array.get_block_selection], [set_block_selection][zarr.Array.set_block_selection], [vindex][zarr.Array.vindex], [oindex][zarr.Array.oindex], [blocks][zarr.Array.blocks], [__getitem__][zarr.Array.__getitem__], [__setitem__][zarr.Array.__setitem__] """ if prototype is None: prototype = default_buffer_prototype() indexer = BlockIndexer(selection, self.shape, self._chunk_grid) sync(self.async_array._set_selection(indexer, value, fields=fields, prototype=prototype)) @property def vindex(self) -> VIndex: """Shortcut for vectorized (inner) indexing, see [get_coordinate_selection][zarr.Array.get_coordinate_selection], [set_coordinate_selection][zarr.Array.set_coordinate_selection], [get_mask_selection][zarr.Array.get_mask_selection] and [set_mask_selection][zarr.Array.set_mask_selection] for documentation and examples.""" return VIndex(self) @property def oindex(self) -> OIndex: """Shortcut for orthogonal (outer) indexing, see [get_orthogonal_selection][zarr.Array.get_orthogonal_selection] and [set_orthogonal_selection][zarr.Array.set_orthogonal_selection] for documentation and examples.""" return OIndex(self) @property def blocks(self) -> BlockIndex: """Shortcut for blocked chunked indexing, see [get_block_selection][zarr.Array.get_block_selection] and [set_block_selection][zarr.Array.set_block_selection] for documentation and examples.""" return BlockIndex(self) def resize(self, new_shape: ShapeLike) -> None: """ Change the shape of the array by growing or shrinking one or more dimensions. This is an in-place operation that modifies the array. Parameters ---------- new_shape : tuple New shape of the array. Notes ----- If one or more dimensions are shrunk, any chunks falling outside the new array shape will be deleted from the underlying store. However, it is noteworthy that the chunks partially falling inside the new array (i.e. boundary chunks) will remain intact, and therefore, the data falling outside the new array but inside the boundary chunks would be restored by a subsequent resize operation that grows the array size. Examples -------- ```python import zarr z = zarr.zeros(shape=(10000, 10000), chunk_shape=(1000, 1000), dtype="int32",) z.shape #> (10000, 10000) z.resize((20000, 1000)) z.shape #> (20000, 1000) z.resize((50, 50)) z.shape #>(50, 50) ``` """ sync(self.async_array.resize(new_shape)) def append(self, data: npt.ArrayLike, axis: int = 0) -> tuple[int, ...]: """Append `data` to `axis`. Parameters ---------- data : array-like Data to be appended. axis : int Axis along which to append. Returns ------- new_shape : tuple Notes ----- The size of all dimensions other than `axis` must match between this array and `data`. Examples -------- >>> import numpy as np >>> import zarr >>> a = np.arange(10000000, dtype='i4').reshape(10000, 1000) >>> z = zarr.array(a, chunks=(1000, 100)) >>> z.shape (10000, 1000) >>> z.append(a) (20000, 1000) >>> z.append(np.vstack([a, a]), axis=1) (20000, 2000) >>> z.shape (20000, 2000) """ return sync(self.async_array.append(data, axis=axis)) def update_attributes(self, new_attributes: dict[str, JSON]) -> Self: """ Update the array's attributes. Parameters ---------- new_attributes : dict A dictionary of new attributes to update or add to the array. The keys represent attribute names, and the values must be JSON-compatible. Returns ------- Array The array with the updated attributes. Raises ------ ValueError If the attributes are invalid or incompatible with the array's metadata. Notes ----- - The updated attributes will be merged with existing attributes, and any conflicts will be overwritten by the new values. """ new_array = sync(self.async_array.update_attributes(new_attributes)) return type(self)(new_array) def __repr__(self) -> str: return f"" @property def info(self) -> Any: """ Return the statically known information for an array. Returns ------- ArrayInfo Related ------- [zarr.Array.info_complete][] - All information about a group, including dynamic information like the number of bytes and chunks written. Examples -------- >>> arr = zarr.create(shape=(10,), chunks=(2,), dtype="float32") >>> arr.info Type : Array Zarr format : 3 Data type : DataType.float32 Shape : (10,) Chunk shape : (2,) Order : C Read-only : False Store type : MemoryStore Codecs : [BytesCodec(endian=)] No. bytes : 40 """ return self.async_array.info def info_complete(self) -> Any: """ Returns all the information about an array, including information from the Store. In addition to the statically known information like ``name`` and ``zarr_format``, this includes additional information like the size of the array in bytes and the number of chunks written. Note that this method will need to read metadata from the store. Returns ------- ArrayInfo Related ------- [zarr.Array.info][] - The statically known subset of metadata about an array. """ return sync(self.async_array.info_complete()) async def _shards_initialized( array: AnyAsyncArray, ) -> tuple[str, ...]: """ Return the keys of the shards that have been persisted to the storage backend. This will fall back to chunks in case no shards are present. Parameters ---------- array : AsyncArray The array to inspect. Returns ------- chunks_initialized : tuple[str, ...] The keys of the shards or if these are not present, chunks that have been initialized. Related ------- [nchunks_initialized][zarr.Array.nchunks_initialized] """ store_contents = [ x async for x in array.store_path.store.list_prefix(prefix=array.store_path.path) ] store_contents_relative = [ _relativize_path(path=key, prefix=array.store_path.path) for key in store_contents ] return tuple( chunk_key for chunk_key in array._iter_shard_keys() if chunk_key in store_contents_relative ) type FiltersLike = ( Iterable[dict[str, JSON] | ArrayArrayCodec | Numcodec] | ArrayArrayCodec | Iterable[Numcodec] | Numcodec | Literal["auto"] | None ) # Union of acceptable types for users to pass in for both v2 and v3 compressors type CompressorLike = dict[str, JSON] | BytesBytesCodec | Numcodec | Literal["auto"] | None type CompressorsLike = ( Iterable[dict[str, JSON] | BytesBytesCodec | Numcodec] | Mapping[str, JSON] | BytesBytesCodec | Numcodec | Literal["auto"] | None ) type SerializerLike = dict[str, JSON] | ArrayBytesCodec | Literal["auto"] class ShardsConfigParam(TypedDict): shape: tuple[int, ...] index_location: ShardingCodecIndexLocation | None type ShardsLike = tuple[int, ...] | Sequence[Sequence[int]] | ShardsConfigParam | Literal["auto"] async def from_array( store: StoreLike, *, data: AnyArray | npt.ArrayLike, write_data: bool = True, name: str | None = None, chunks: ChunksLike | Literal["auto", "keep"] = "keep", shards: ShardsLike | None | Literal["keep"] = "keep", filters: FiltersLike | Literal["keep"] = "keep", compressors: CompressorsLike | Literal["keep"] = "keep", serializer: SerializerLike | Literal["keep"] = "keep", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, zarr_format: ZarrFormat | None = None, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, ) -> AnyAsyncArray: """Create an array from an existing array or array-like. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. data : Array | array-like The array to copy. write_data : bool, default True Whether to copy the data from the input array to the new array. If ``write_data`` is ``False``, the new array will be created with the same metadata as the input array, but without any data. name : str or None, optional The name of the array within the store. If ``name`` is ``None``, the array will be located at the root of the store. chunks : tuple[int, ...] or Sequence[Sequence[int]] or "auto" or "keep", optional Chunk shape of the array. Following values are supported: - "auto": Automatically determine the chunk shape based on the array's shape and dtype. - "keep": Retain the chunk grid of the data array if it is a zarr Array. - tuple[int, ...]: A tuple of integers representing the chunk shape (regular grid). - Sequence[Sequence[int]]: Per-dimension chunk edge lists (rectilinear grid). Rectilinear chunk grids are experimental and must be explicitly enabled with ``zarr.config.set({'array.rectilinear_chunks': True})`` while the feature is stabilizing. If not specified, defaults to "keep" if data is a zarr Array, otherwise "auto". shards : tuple[int, ...], optional Shard shape of the array. Following values are supported: - "auto": Automatically determine the shard shape based on the array's shape and chunk shape. - "keep": Retain the shard shape of the data array if it is a zarr Array. - tuple[int, ...]: A tuple of integers representing the shard shape. - None: No sharding. If not specified, defaults to "keep" if data is a zarr Array, otherwise None. filters : Iterable[Codec] | Literal["auto", "keep"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"keep"`` instructs Zarr to infer ``filters`` from ``data``. If that inference is not possible, Zarr will fall back to the behavior specified by ``"auto"``, which is to choose default filters based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are the empty tuple ``()``. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters is a tuple with a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec] or "auto" or "keep", optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. For Zarr format 3, a "compressor" is a codec that takes a bytestream, and returns another bytestream. Multiple compressors my be provided for Zarr format 3. For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may be provided for Zarr format 2. Following values are supported: - Iterable[Codec]: List of compressors to apply to the array. - "auto": Automatically determine the compressors based on the array's dtype. - "keep": Retain the compressors of the input array if it is a zarr Array. If no ``compressors`` are provided, defaults to "keep" if data is a zarr Array, otherwise "auto". serializer : dict[str, JSON] | ArrayBytesCodec or "auto" or "keep", optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. Following values are supported: - dict[str, JSON]: A dict representation of an ``ArrayBytesCodec``. - ArrayBytesCodec: An instance of ``ArrayBytesCodec``. - "auto": a default serializer will be used. These defaults can be changed by modifying the value of ``array.v3_default_serializer`` in [`zarr.config`][zarr.config]. - "keep": Retain the serializer of the input array if it is a zarr Array. fill_value : Any, optional Fill value for the array. If not specified, defaults to the fill value of the data array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If not specified, defaults to the memory order of the data array. zarr_format : {2, 3}, optional The zarr format to use when saving. If not specified, defaults to the zarr format of the data array. attributes : dict, optional Attributes for the array. If not specified, defaults to the attributes of the data array. chunk_key_encoding : ChunkKeyEncoding, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. If not specified and the data array has the same zarr format as the target array, the chunk key encoding of the data array is used. dimension_names : Iterable[str | None] | None The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. If not specified, defaults to the dimension names of the data array. storage_options : dict, optional If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. config : ArrayConfig or ArrayConfigLike, optional Runtime configuration for the array. Returns ------- AsyncArray The array. Examples -------- Create an array from an existing Array:: >>> import zarr >>> store = zarr.storage.MemoryStore() >>> store2 = zarr.storage.LocalStore('example.zarr') >>> arr = zarr.create_array( >>> store=store, >>> shape=(100,100), >>> chunks=(10,10), >>> dtype='int32', >>> fill_value=0) >>> arr2 = await zarr.api.asynchronous.from_array(store2, data=arr) Create an array from an existing NumPy array:: >>> arr3 = await zarr.api.asynchronous.from_array( >>> zarr.storage.MemoryStore(), >>> data=np.arange(10000, dtype='i4').reshape(100, 100), >>> ) Create an array from any array-like object:: >>> arr4 = await zarr.api.asynchronous.from_array( >>> zarr.storage.MemoryStore(), >>> data=[[1, 2], [3, 4]], >>> ) >>> await arr4.getitem(...) array([[1, 2],[3, 4]]) Create an array from an existing Array without copying the data:: >>> arr5 = await zarr.api.asynchronous.from_array( >>> zarr.storage.MemoryStore(), >>> data=Array(arr4), >>> write_data=False, >>> ) >>> await arr5.getitem(...) array([[0, 0],[0, 0]]) """ mode: Literal["a"] = "a" config_parsed = parse_array_config(config) store_path = await make_store_path(store, path=name, mode=mode, storage_options=storage_options) ( chunks, shards, filters, compressors, serializer, fill_value, order, zarr_format, chunk_key_encoding, dimension_names, ) = _parse_keep_array_attr( data=data, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=zarr_format, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, ) if not hasattr(data, "dtype") or not hasattr(data, "shape"): data = np.array(data) result = await init_array( store_path=store_path, shape=data.shape, dtype=data.dtype, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=zarr_format, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, overwrite=overwrite, config=config_parsed, ) if write_data: if isinstance(data, Array): async def _copy_array_region( chunk_coords: tuple[int, ...] | slice, _data: AnyArray ) -> None: arr = await _data.async_array.getitem(chunk_coords) await result.setitem(chunk_coords, arr) # Stream data from the source array to the new array await concurrent_map( [(region, data) for region in result._iter_shard_regions()], _copy_array_region, zarr.core.config.config.get("async.concurrency"), ) else: async def _copy_arraylike_region(chunk_coords: slice, _data: NDArrayLike) -> None: await result.setitem(chunk_coords, _data[chunk_coords]) # Stream data from the source array to the new array await concurrent_map( [(region, data) for region in result._iter_shard_regions()], _copy_arraylike_region, zarr.core.config.config.get("async.concurrency"), ) return result async def init_array( *, store_path: StorePath, shape: ShapeLike, dtype: ZDTypeLike, chunks: ChunksLike | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", serializer: SerializerLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, zarr_format: ZarrFormat | None = 3, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, overwrite: bool = False, config: ArrayConfigLike | None = None, ) -> AnyAsyncArray: """Create and persist an array metadata document. Parameters ---------- store_path : StorePath StorePath instance. The path attribute is the name of the array to initialize. shape : tuple[int, ...] Shape of the array. dtype : ZDTypeLike Data type of the array. chunks : tuple[int, ...], optional Chunk shape of the array. If not specified, default are guessed based on the shape and dtype. shards : tuple[int, ...], optional Shard shape of the array. The default value of ``None`` results in no sharding at all. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec] | Literal["auto"], optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. The default value of ``"auto"`` instructs Zarr to use a default of [`zarr.codecs.ZstdCodec`][]. To create an array with no compressors, provide an empty iterable or the value ``None``. serializer : dict[str, JSON] | ArrayBytesCodec | Literal["auto"], optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. The default value of ``"auto"`` instructs Zarr to use a default codec based on the data type of the array. For most data types this default codec is [`zarr.codecs.BytesCodec`][]. For [`zarr.dtype.VariableLengthUTF8`][], the default codec is [`zarr.codecs.VlenUTF8Codec`][]. For [`zarr.dtype.VariableLengthBytes`][], the default codec is [`zarr.codecs.VlenBytesCodec`][]. fill_value : Any, optional Fill value for the array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If no ``order`` is provided, a default order will be used. This default can be changed by modifying the value of ``array.order`` in [`zarr.config`][zarr.config]. zarr_format : {2, 3}, optional The zarr format to use when saving. attributes : dict, optional Attributes for the array. chunk_key_encoding : ChunkKeyEncodingLike, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. dimension_names : Iterable[str], optional The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. config : ArrayConfigLike or None, default=None Configuration for this array. If ``None``, the default array runtime configuration will be used. This default is stored in the global configuration object. Returns ------- AsyncArray The AsyncArray. """ if zarr_format is None: zarr_format = _default_zarr_format() from zarr.codecs.sharding import ShardingCodec, ShardingCodecIndexLocation zdtype = parse_dtype(dtype, zarr_format=zarr_format) shape_parsed = parse_shapelike(shape) chunk_key_encoding_parsed = _parse_chunk_key_encoding( chunk_key_encoding, zarr_format=zarr_format ) if overwrite: if store_path.store.supports_deletes: await store_path.delete_dir() else: await ensure_no_existing_node(store_path, zarr_format=zarr_format) else: await ensure_no_existing_node(store_path, zarr_format=zarr_format) # Detect rectilinear (nested list) chunks or shards, e.g. [[10, 20, 30], [25, 25]] from zarr.core.chunk_grids import _is_rectilinear_chunks rectilinear_meta: RectilinearChunkGridMetadata | None = None rectilinear_shards = _is_rectilinear_chunks(shards) if _is_rectilinear_chunks(chunks): if zarr_format == 2: raise ValueError("Zarr format 2 does not support rectilinear chunk grids.") if shards is not None: raise ValueError( "Rectilinear chunks with sharding is not supported. " "Use rectilinear shards instead: " "chunks=(inner_size, ...), shards=[[shard_sizes], ...]" ) rectilinear_meta = RectilinearChunkGridMetadata( chunk_shapes=tuple(tuple(dim_edges) for dim_edges in chunks) ) # Use first chunk size per dim as placeholder for _auto_partition chunks_flat: tuple[int, ...] | Literal["auto"] = tuple(dim_edges[0] for dim_edges in chunks) else: # Normalize scalar int to per-dimension tuple (e.g. chunks=100000 for a 1D array) if isinstance(chunks, int): chunks = tuple(chunks for _ in shape_parsed) chunks_flat = cast("tuple[int, ...] | Literal['auto']", chunks) # Handle rectilinear shards: shards=[[60, 40, 20], [50, 50]] # means variable-sized shard boundaries with uniform inner chunks shards_for_partition: ShardsLike | None = shards if _is_rectilinear_chunks(shards): if zarr_format == 2: raise ValueError("Zarr format 2 does not support rectilinear chunk grids.") rectilinear_meta = RectilinearChunkGridMetadata( chunk_shapes=tuple(tuple(dim_edges) for dim_edges in shards) ) # Use first shard size per dim as placeholder for _auto_partition shards_for_partition = tuple(dim_edges[0] for dim_edges in shards) item_size = 1 if isinstance(zdtype, HasItemSize): item_size = zdtype.item_size shard_shape_parsed, chunk_shape_parsed = _auto_partition( array_shape=shape_parsed, shard_shape=shards_for_partition, chunk_shape=chunks_flat, item_size=item_size, ) chunks_out: tuple[int, ...] meta: ArrayV2Metadata | ArrayV3Metadata if zarr_format == 2: if shard_shape_parsed is not None: msg = ( "Zarr format 2 arrays can only be created with `shard_shape` set to `None`. " f"Got `shard_shape={shards}` instead." ) raise ValueError(msg) if serializer != "auto": raise ValueError("Zarr format 2 arrays do not support `serializer`.") filters_parsed, compressor_parsed = _parse_chunk_encoding_v2( compressor=compressors, filters=filters, dtype=zdtype ) if dimension_names is not None: raise ValueError("Zarr format 2 arrays do not support dimension names.") if order is None: order_parsed = zarr_config.get("array.order") else: order_parsed = order chunk_key_encoding_parsed = cast("V2ChunkKeyEncoding", chunk_key_encoding_parsed) meta = AsyncArray._create_metadata_v2( shape=shape_parsed, dtype=zdtype, chunks=chunk_shape_parsed, dimension_separator=chunk_key_encoding_parsed.separator, fill_value=fill_value, order=order_parsed, filters=filters_parsed, compressor=compressor_parsed, attributes=attributes, ) else: array_array, array_bytes, bytes_bytes = _parse_chunk_encoding_v3( compressors=compressors, filters=filters, serializer=serializer, dtype=zdtype, ) sub_codecs = cast("tuple[Codec, ...]", (*array_array, array_bytes, *bytes_bytes)) codecs_out: tuple[Codec, ...] if shard_shape_parsed is not None: index_location = None if isinstance(shards, dict): index_location = ShardingCodecIndexLocation(shards.get("index_location", None)) if index_location is None: index_location = ShardingCodecIndexLocation.end sharding_codec = ShardingCodec( chunk_shape=chunk_shape_parsed, codecs=sub_codecs, index_location=index_location ) # Use rectilinear grid for validation when shards are rectilinear if rectilinear_shards and rectilinear_meta is not None: validation_grid: ChunkGridMetadata = rectilinear_meta else: validation_grid = RegularChunkGridMetadata(chunk_shape=shard_shape_parsed) sharding_codec.validate( shape=chunk_shape_parsed, dtype=zdtype, chunk_grid=validation_grid, ) codecs_out = (sharding_codec,) chunks_out = shard_shape_parsed else: chunks_out = chunk_shape_parsed codecs_out = sub_codecs if order is not None: _warn_order_kwarg() grid: ChunkGridMetadata if rectilinear_meta is not None: grid = rectilinear_meta else: grid = RegularChunkGridMetadata(chunk_shape=chunks_out) meta = AsyncArray._create_metadata_v3( shape=shape_parsed, dtype=zdtype, fill_value=fill_value, chunk_grid=grid, chunk_key_encoding=chunk_key_encoding_parsed, codecs=codecs_out, dimension_names=dimension_names, attributes=attributes, ) arr = AsyncArray(metadata=meta, store_path=store_path, config=config) await arr._save_metadata(meta, ensure_parents=True) return arr async def create_array( store: StoreLike, *, name: str | None = None, shape: ShapeLike | None = None, dtype: ZDTypeLike | None = None, data: np.ndarray[Any, np.dtype[Any]] | None = None, chunks: ChunksLike | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", serializer: SerializerLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, zarr_format: ZarrFormat | None = 3, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, ) -> AnyAsyncArray: """Create an array. Parameters ---------- store : StoreLike StoreLike object to open. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. name : str or None, optional The name of the array within the store. If ``name`` is ``None``, the array will be located at the root of the store. shape : ShapeLike, optional Shape of the array. Must be ``None`` if ``data`` is provided. dtype : ZDTypeLike | None Data type of the array. Must be ``None`` if ``data`` is provided. data : np.ndarray, optional Array-like data to use for initializing the array. If this parameter is provided, the ``shape`` and ``dtype`` parameters must be ``None``. chunks : tuple[int, ...] | Sequence[Sequence[int]] | Literal["auto"], default="auto" Chunk shape of the array. If chunks is "auto", a chunk shape is guessed based on the shape of the array and the dtype. A nested list of per-dimension edge sizes creates a rectilinear grid. Rectilinear chunk grids are experimental and must be explicitly enabled with ``zarr.config.set({'array.rectilinear_chunks': True})`` while the feature is stabilizing. shards : tuple[int, ...], optional Shard shape of the array. The default value of ``None`` results in no sharding at all. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec], optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. For Zarr format 3, a "compressor" is a codec that takes a bytestream, and returns another bytestream. Multiple compressors my be provided for Zarr format 3. If no ``compressors`` are provided, a default set of compressors will be used. These defaults can be changed by modifying the value of ``array.v3_default_compressors`` in [`zarr.config`][zarr.config]. Use ``None`` to omit default compressors. For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may be provided for Zarr format 2. If no ``compressor`` is provided, a default compressor will be used. in [`zarr.config`][zarr.config]. Use ``None`` to omit the default compressor. serializer : dict[str, JSON] | ArrayBytesCodec, optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. If no ``serializer`` is provided, a default serializer will be used. These defaults can be changed by modifying the value of ``array.v3_default_serializer`` in [`zarr.config`][zarr.config]. fill_value : Any, optional Fill value for the array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If no ``order`` is provided, a default order will be used. This default can be changed by modifying the value of ``array.order`` in [`zarr.config`][zarr.config]. zarr_format : {2, 3}, optional The zarr format to use when saving. attributes : dict, optional Attributes for the array. chunk_key_encoding : ChunkKeyEncodingLike, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. dimension_names : Iterable[str], optional The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. storage_options : dict, optional If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. If ``True``, all existing paths in the store will be deleted. config : ArrayConfigLike, optional Runtime configuration for the array. write_data : bool If a pre-existing array-like object was provided to this function via the ``data`` parameter then ``write_data`` determines whether the values in that array-like object should be written to the Zarr array created by this function. If ``write_data`` is ``False``, then the array will be left empty. Returns ------- AsyncArray The array. Examples -------- >>> import zarr >>> store = zarr.storage.MemoryStore(mode='w') >>> async_arr = await zarr.api.asynchronous.create_array( >>> store=store, >>> shape=(100,100), >>> chunks=(10,10), >>> dtype='i4', >>> fill_value=0) """ data_parsed, shape_parsed, dtype_parsed = _parse_data_params( data=data, shape=shape, dtype=dtype ) if data_parsed is not None: return await from_array( store, data=data_parsed, write_data=write_data, name=name, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=zarr_format, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, storage_options=storage_options, overwrite=overwrite, config=config, ) else: mode: Literal["a"] = "a" store_path = await make_store_path( store, path=name, mode=mode, storage_options=storage_options ) return await init_array( store_path=store_path, shape=shape_parsed, dtype=dtype_parsed, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=zarr_format, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, overwrite=overwrite, config=config, ) def _parse_keep_array_attr( data: AnyArray | npt.ArrayLike, chunks: ChunksLike | Literal["auto", "keep"], shards: ShardsLike | None | Literal["keep"], filters: FiltersLike | Literal["keep"], compressors: CompressorsLike | Literal["keep"], serializer: SerializerLike | Literal["keep"], fill_value: Any | None, order: MemoryOrder | None, zarr_format: ZarrFormat | None, chunk_key_encoding: ChunkKeyEncodingLike | None, dimension_names: DimensionNamesLike, ) -> tuple[ ChunksLike | Literal["auto"], ShardsLike | None, FiltersLike, CompressorsLike, SerializerLike, Any | None, MemoryOrder | None, ZarrFormat, ChunkKeyEncodingLike | None, DimensionNamesLike, ]: if isinstance(data, Array): if chunks == "keep": if data._chunk_grid.is_regular: chunks = data.chunks else: chunks = data.write_chunk_sizes if shards == "keep": shards = data.shards if data._chunk_grid.is_regular else None if zarr_format is None: zarr_format = data.metadata.zarr_format if filters == "keep": if zarr_format == data.metadata.zarr_format: filters = data.filters or None else: filters = "auto" if compressors == "keep": if zarr_format == data.metadata.zarr_format: compressors = data.compressors or None else: compressors = "auto" if serializer == "keep": if zarr_format == 3 and data.metadata.zarr_format == 3: serializer = cast("SerializerLike", data.serializer) else: serializer = "auto" if fill_value is None: fill_value = data.fill_value if data.metadata.zarr_format == 2 and zarr_format == 3 and data.order == "F": # Can't set order="F" for v3 arrays warnings.warn( "The 'order' attribute of a Zarr format 2 array does not have a direct analogue in Zarr format 3. " "The existing order='F' of the source Zarr format 2 array will be ignored.", ZarrUserWarning, stacklevel=2, ) elif order is None and zarr_format == 2: order = data.order if chunk_key_encoding is None and zarr_format == data.metadata.zarr_format: if isinstance(data.metadata, ArrayV2Metadata): chunk_key_encoding = {"name": "v2", "separator": data.metadata.dimension_separator} elif isinstance(data.metadata, ArrayV3Metadata): chunk_key_encoding = data.metadata.chunk_key_encoding if dimension_names is None and data.metadata.zarr_format == 3: dimension_names = data.metadata.dimension_names else: if chunks == "keep": chunks = "auto" if shards == "keep": shards = None if zarr_format is None: zarr_format = 3 if filters == "keep": filters = "auto" if compressors == "keep": compressors = "auto" if serializer == "keep": serializer = "auto" # After resolving "keep" above, chunks is never "keep" at this point. chunks_out: ChunksLike | Literal["auto"] = chunks # type: ignore[assignment] return ( chunks_out, shards, filters, compressors, serializer, fill_value, order, zarr_format, chunk_key_encoding, dimension_names, ) def _parse_chunk_key_encoding( data: ChunkKeyEncodingLike | None, zarr_format: ZarrFormat ) -> ChunkKeyEncoding: """ Take an implicit specification of a chunk key encoding and parse it into a ChunkKeyEncoding object. """ if data is None: if zarr_format == 2: data = {"name": "v2", "configuration": {"separator": "."}} else: data = {"name": "default", "configuration": {"separator": "/"}} result = parse_chunk_key_encoding(data) if zarr_format == 2 and result.name != "v2": msg = ( "Invalid chunk key encoding. For Zarr format 2 arrays, the `name` field of the " f"chunk key encoding must be 'v2'. Got `name` = {result.name} instead." ) raise ValueError(msg) return result def default_filters_v3(dtype: ZDType[Any, Any]) -> tuple[ArrayArrayCodec, ...]: """ Given a data type, return the default filters for that data type. This is an empty tuple. No data types have default filters. """ return () def default_compressors_v3(dtype: ZDType[Any, Any]) -> tuple[BytesBytesCodec, ...]: """ Given a data type, return the default compressors for that data type. This is just a tuple containing ``ZstdCodec`` """ return (ZstdCodec(),) def default_serializer_v3(dtype: ZDType[Any, Any]) -> ArrayBytesCodec: """ Given a data type, return the default serializer for that data type. The default serializer for most data types is the ``BytesCodec``, which may or may not be parameterized with an endianness, depending on whether the data type has endianness. Variable length strings and variable length bytes have hard-coded serializers -- ``VLenUTF8Codec`` and ``VLenBytesCodec``, respectively. Structured data types with multi-byte fields use ``BytesCodec`` with little-endian encoding. """ serializer: ArrayBytesCodec = BytesCodec(endian=None) if isinstance(dtype, HasEndianness) or ( isinstance(dtype, Structured) and dtype.has_multi_byte_fields() ): serializer = BytesCodec(endian="little") elif isinstance(dtype, HasObjectCodec): if dtype.object_codec_id == "vlen-bytes": serializer = VLenBytesCodec() elif dtype.object_codec_id == "vlen-utf8": serializer = VLenUTF8Codec() else: msg = f"Data type {dtype} requires an unknown object codec: {dtype.object_codec_id!r}." raise ValueError(msg) return serializer def default_filters_v2(dtype: ZDType[Any, Any]) -> tuple[Numcodec] | None: """ Given a data type, return the default filters for that data type. For data types that require an object codec, namely variable length data types, this is a tuple containing the object codec. Otherwise it's ``None``. """ if isinstance(dtype, HasObjectCodec): if dtype.object_codec_id == "vlen-bytes": from numcodecs import VLenBytes return (VLenBytes(),) elif dtype.object_codec_id == "vlen-utf8": from numcodecs import VLenUTF8 return (VLenUTF8(),) else: msg = f"Data type {dtype} requires an unknown object codec: {dtype.object_codec_id!r}." raise ValueError(msg) return None def default_compressor_v2(dtype: ZDType[Any, Any]) -> Numcodec: """ Given a data type, return the default compressors for that data type. This is just the numcodecs ``Zstd`` codec. """ from numcodecs import Zstd return Zstd(level=0, checksum=False) # type: ignore[no-any-return] def _parse_chunk_encoding_v2( *, compressor: CompressorsLike, filters: FiltersLike, dtype: ZDType[TBaseDType, TBaseScalar], ) -> tuple[tuple[Numcodec, ...] | None, Numcodec | None]: """ Generate chunk encoding classes for Zarr format 2 arrays with optional defaults. """ _filters: tuple[Numcodec, ...] | None _compressor: Numcodec | None if compressor is None or compressor == (): _compressor = None elif compressor == "auto": _compressor = default_compressor_v2(dtype) elif isinstance(compressor, tuple | list) and len(compressor) == 1: _compressor = parse_compressor(compressor[0]) else: if isinstance(compressor, Iterable) and not isinstance(compressor, dict): msg = f"For Zarr format 2 arrays, the `compressor` must be a single codec. Got an iterable with type {type(compressor)} instead." raise TypeError(msg) _compressor = parse_compressor(compressor) if filters is None: _filters = None elif filters == "auto": _filters = default_filters_v2(dtype) else: if isinstance(filters, Iterable): for idx, f in enumerate(filters): if not _is_numcodec(f): msg = ( "For Zarr format 2 arrays, all elements of `filters` must be numcodecs codecs. " f"Element at index {idx} has type {type(f)}, which is not a numcodecs codec." ) raise TypeError(msg) _filters = parse_filters(filters) if isinstance(dtype, HasObjectCodec): # check the filters and the compressor for the object codec required for this data type if _filters is None: if _compressor is None: object_codec_id = None else: object_codec_id = get_object_codec_id((_compressor.get_config(),)) else: object_codec_id = get_object_codec_id( ( *[f.get_config() for f in _filters], _compressor.get_config() if _compressor is not None else None, ) ) if object_codec_id is None: if isinstance(dtype, VariableLengthUTF8): # type: ignore[unreachable] codec_name = "the numcodecs.VLenUTF8 codec" # type: ignore[unreachable] elif isinstance(dtype, VariableLengthBytes): # type: ignore[unreachable] codec_name = "the numcodecs.VLenBytes codec" # type: ignore[unreachable] else: codec_name = f"an unknown object codec with id {dtype.object_codec_id!r}" msg = ( f"Data type {dtype} requires {codec_name}, " "but no such codec was specified in the filters or compressor parameters for " "this array. " ) raise ValueError(msg) return _filters, _compressor def _parse_chunk_encoding_v3( *, compressors: CompressorsLike, filters: FiltersLike, serializer: SerializerLike, dtype: ZDType[TBaseDType, TBaseScalar], ) -> tuple[tuple[ArrayArrayCodec, ...], ArrayBytesCodec, tuple[BytesBytesCodec, ...]]: """ Generate chunk encoding classes for v3 arrays with optional defaults. """ if filters is None: out_array_array: tuple[ArrayArrayCodec, ...] = () elif filters == "auto": out_array_array = default_filters_v3(dtype) else: maybe_array_array: Iterable[Codec | dict[str, JSON]] if isinstance(filters, dict | Codec): maybe_array_array = (filters,) else: maybe_array_array = cast("Iterable[Codec | dict[str, JSON]]", filters) out_array_array = tuple(_parse_array_array_codec(c) for c in maybe_array_array) if serializer == "auto": out_array_bytes = default_serializer_v3(dtype) else: # TODO: ensure that the serializer is compatible with the ndarray produced by the # array-array codecs. For example, if a sequence of array-array codecs produces an # array with a single-byte data type, then the serializer should not specify endiannesss. out_array_bytes = _parse_array_bytes_codec(serializer) if compressors is None: out_bytes_bytes: tuple[BytesBytesCodec, ...] = () elif compressors == "auto": out_bytes_bytes = default_compressors_v3(dtype) else: maybe_bytes_bytes: Iterable[Codec | dict[str, JSON]] if isinstance(compressors, dict | Codec): maybe_bytes_bytes = (compressors,) else: maybe_bytes_bytes = cast("Iterable[Codec | dict[str, JSON]]", compressors) out_bytes_bytes = tuple(_parse_bytes_bytes_codec(c) for c in maybe_bytes_bytes) # TODO: ensure that the serializer is compatible with the ndarray produced by the # array-array codecs. For example, if a sequence of array-array codecs produces an # array with a single-byte data type, then the serializer should not specify endiannesss. # TODO: add checks to ensure that the right serializer is used for vlen data types return out_array_array, out_array_bytes, out_bytes_bytes def _parse_deprecated_compressor( compressor: CompressorLike | None, compressors: CompressorsLike, zarr_format: int = 3 ) -> CompressorsLike | None: if compressor != "auto": if compressors != "auto": raise ValueError("Cannot specify both `compressor` and `compressors`.") if zarr_format == 3: warn( "The `compressor` argument is deprecated. Use `compressors` instead.", category=ZarrUserWarning, stacklevel=2, ) if compressor is None: # "no compression" compressors = () else: compressors = (compressor,) elif zarr_format == 2 and compressor == compressors == "auto": compressors = ({"id": "blosc"},) return compressors def _parse_data_params( *, data: np.ndarray[Any, np.dtype[Any]] | None, shape: ShapeLike | None, dtype: ZDTypeLike | None, ) -> tuple[np.ndarray[Any, np.dtype[Any]] | None, ShapeLike, ZDTypeLike]: """ Ensure an array-like ``data`` parameter is consistent with the ``dtype`` and ``shape`` parameters. """ if data is None: if shape is None: msg = ( "The data parameter was set to None, but shape was not specified. " "Either provide a value for data, or specify shape." ) raise ValueError(msg) shape_out = shape if dtype is None: msg = ( "The data parameter was set to None, but dtype was not specified." "Either provide an array-like value for data, or specify dtype." ) raise ValueError(msg) dtype_out = dtype else: if shape is not None: msg = ( "The data parameter was used, but the shape parameter was also " "used. This is an error. Either use the data parameter, or the shape parameter, " "but not both." ) raise ValueError(msg) shape_out = data.shape if dtype is not None: msg = ( "The data parameter was used, but the dtype parameter was also " "used. This is an error. Either use the data parameter, or the dtype parameter, " "but not both." ) raise ValueError(msg) dtype_out = data.dtype return data, shape_out, dtype_out def _iter_chunk_coords( array: AnyArray | AnyAsyncArray, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, ) -> Iterator[tuple[int, ...]]: """ Create an iterator over the coordinates of chunks in chunk grid space. If the `origin` keyword is used, iteration will start at the chunk index specified by `origin`. The default behavior is to start at the origin of the grid coordinate space. If the `selection_shape` keyword is used, iteration will be bounded over a contiguous region ranging from `[origin, origin selection_shape]`, where the upper bound is exclusive as per python indexing conventions. Parameters ---------- array : Array | AsyncArray The array to iterate over. origin : Sequence[int] | None, default=None The origin of the selection in grid coordinates. selection_shape : Sequence[int] | None, default=None The shape of the selection in grid coordinates. Yields ------ chunk_coords: tuple[int, ...] The coordinates of each chunk in the selection. """ return _iter_grid(array._chunk_grid_shape, origin=origin, selection_shape=selection_shape) def _iter_shard_coords( array: AnyArray | AnyAsyncArray, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, ) -> Iterator[tuple[int, ...]]: """ Create an iterator over the coordinates of shards in shard grid space. If the `origin` keyword is used, iteration will start at the shard index specified by `origin`. The default behavior is to start at the origin of the grid coordinate space. If the `selection_shape` keyword is used, iteration will be bounded over a contiguous region ranging from `[origin, origin selection_shape]`, where the upper bound is exclusive as per python indexing conventions. If no shards are present this will iterate over the coordinates of chunks in chunk grid space instead. Parameters ---------- array : Array | AsyncArray The array to iterate over. origin : Sequence[int] | None, default=None The origin of the selection in grid coordinates. selection_shape : Sequence[int] | None, default=None The shape of the selection in grid coordinates. Yields ------ chunk_coords: tuple[int, ...] The coordinates of each shard in the selection or chunks if no shards are present. """ return _iter_grid(array._shard_grid_shape, origin=origin, selection_shape=selection_shape) def _iter_shard_keys( array: AnyArray | AnyAsyncArray, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, ) -> Iterator[str]: """ Iterate over the storage keys of each shard, relative to an optional origin, and optionally limited to a contiguous region in shard grid coordinates. This automatically falls back to chunks when no shards are present. Parameters ---------- array : Array | AsyncArray The array to iterate over. origin : Sequence[int] | None, default=None The origin of the selection in grid coordinates. selection_shape : Sequence[int] | None, default=None The shape of the selection in grid coordinates. Yields ------ key: str The storage key of each shard in the selection or chunk when no shards are present. """ # Iterate over the coordinates of chunks in chunk grid space. _iter = _iter_grid(array._shard_grid_shape, origin=origin, selection_shape=selection_shape) return (array.metadata.encode_chunk_key(k) for k in _iter) def _iter_shard_regions( array: AnyArray | AnyAsyncArray, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, ) -> Iterator[tuple[slice, ...]]: """ Iterate over the regions spanned by each shard. These are the smallest regions of the array that are safe to write concurrently. When no shards are present this will fall back to chunks. Parameters ---------- array : Array | AsyncArray The array to iterate over. origin : Sequence[int] | None, default=None The origin of the selection relative to the array's shard grid. selection_shape : Sequence[int] | None, default=None The shape of the selection in shard grid coordinates. Yields ------ region: tuple[slice, ...] A tuple of slice objects representing the region spanned by each shard in the selection or chunk when no shards are present. """ if array.shards is None: shard_shape = array.chunks else: shard_shape = array.shards return _iter_regions( array.shape, shard_shape, origin=origin, selection_shape=selection_shape, trim_excess=True ) def _iter_chunk_regions( array: AnyArray | AnyAsyncArray, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, ) -> Iterator[tuple[slice, ...]]: """ Iterate over the regions spanned by each shard. These are the smallest regions of the array that are efficient to read concurrently. Parameters ---------- array : Array | AsyncArray The array to iterate over. origin : Sequence[int] | None, default=None The origin of the selection in grid coordinates. selection_shape : Sequence[int] | None, default=None The shape of the selection in grid coordinates. Returns ------- region: tuple[slice, ...] A tuple of slice objects representing the region spanned by each shard in the selection. """ return array._chunk_grid.iter_chunk_regions(origin=origin, selection_shape=selection_shape) async def _nchunks_initialized( array: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], ) -> int: """ Calculate the number of chunks that have been initialized in storage. This value is calculated as the product of the number of initialized shards and the number of chunks per shard. For arrays that do not use sharding, the number of chunks per shard is effectively 1, and in that case the number of chunks initialized is the same as the number of stored objects associated with an array. Parameters ---------- array : AsyncArray The array to inspect. Returns ------- nchunks_initialized : int The number of chunks that have been initialized. """ if array.shards is None: chunks_per_shard = 1 else: chunks_per_shard = product( tuple(a // b for a, b in zip(array.shards, array.chunks, strict=True)) ) return (await _nshards_initialized(array)) * chunks_per_shard async def _nshards_initialized( array: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], ) -> int: """ Calculate the number of shards that have been initialized in storage. This is the number of shards that have been persisted to the storage backend. Parameters ---------- array : AsyncArray The array to inspect. Returns ------- nshards_initialized : int The number of shards that have been initialized. """ return len(await _shards_initialized(array)) async def _nbytes_stored( store_path: StorePath, ) -> int: """ Calculate the number of bytes stored for an array. Parameters ---------- store_path : StorePath The store path of the array. Returns ------- nbytes_stored : int The number of bytes stored. """ return await store_path.store.getsize_prefix(store_path.path) def _get_chunk_spec( metadata: ArrayMetadata, chunk_grid: ChunkGrid, chunk_coords: tuple[int, ...], array_config: ArrayConfig, prototype: BufferPrototype, ) -> ArraySpec: """Build an ArraySpec for a single chunk using the ChunkGrid.""" spec = chunk_grid[chunk_coords] if spec is None: raise IndexError(f"Chunk coordinates {chunk_coords} are out of bounds.") return ArraySpec( shape=spec.codec_shape, dtype=metadata.dtype, fill_value=metadata.fill_value, config=array_config, prototype=prototype, ) async def _get_selection( store_path: StorePath, metadata: ArrayMetadata, codec_pipeline: CodecPipeline, config: ArrayConfig, chunk_grid: ChunkGrid, indexer: Indexer, *, prototype: BufferPrototype, out: NDBuffer | None = None, fields: Fields | None = None, ) -> NDArrayLikeOrScalar: """ Get a selection from an array. Parameters ---------- store_path : StorePath The store path of the array. metadata : ArrayMetadata The array metadata. codec_pipeline : CodecPipeline The codec pipeline for encoding/decoding. config : ArrayConfig The array configuration. indexer : Indexer The indexer specifying the selection. prototype : BufferPrototype A buffer prototype to use for the retrieved data. out : NDBuffer | None, optional An output buffer to write the data to. fields : Fields | None, optional Fields to select from structured arrays. Returns ------- NDArrayLikeOrScalar The selected data. """ # Get dtype from metadata if metadata.zarr_format == 2: zdtype = metadata.dtype else: zdtype = metadata.data_type dtype = zdtype.to_native_dtype() # Determine memory order if metadata.zarr_format == 2: order = metadata.order else: order = config.order # check fields are sensible out_dtype = check_fields(fields, dtype) # setup output buffer if out is not None: if isinstance(out, NDBuffer): out_buffer = out else: raise TypeError(f"out argument needs to be an NDBuffer. Got {type(out)!r}") if out_buffer.shape != indexer.shape: raise ValueError( f"shape of out argument doesn't match. Expected {indexer.shape}, got {out.shape}" ) else: out_buffer = prototype.nd_buffer.empty( shape=indexer.shape, dtype=out_dtype, order=order, ) if product(indexer.shape) > 0: # need to use the order from the metadata for v2 _config = config if metadata.zarr_format == 2: _config = replace(_config, order=order) # reading chunks and decoding them indexed_chunks = list(indexer) results = await codec_pipeline.read( [ ( store_path / metadata.encode_chunk_key(chunk_coords), _get_chunk_spec(metadata, chunk_grid, chunk_coords, _config, prototype), chunk_selection, out_selection, is_complete_chunk, ) for chunk_coords, chunk_selection, out_selection, is_complete_chunk in indexed_chunks ], out_buffer, drop_axes=indexer.drop_axes, ) if _config.read_missing_chunks is False: missing_info = [] for i, result in enumerate(results): if result["status"] == "missing": coords = indexed_chunks[i][0] key = metadata.encode_chunk_key(coords) missing_info.append(f" chunk '{key}' (grid position {coords})") if missing_info: chunks_str = "\n".join(missing_info) raise ChunkNotFoundError( f"{len(missing_info)} chunk(s) not found in store '{store_path}'.\n" f"Set the 'array.read_missing_chunks' config to True to fill " f"missing chunks with the fill value.\n" f"Missing chunks:\n{chunks_str}" ) if isinstance(indexer, BasicIndexer) and indexer.shape == (): return out_buffer.as_scalar() return out_buffer.as_ndarray_like() async def _getitem( store_path: StorePath, metadata: ArrayMetadata, codec_pipeline: CodecPipeline, config: ArrayConfig, chunk_grid: ChunkGrid, selection: BasicSelection, *, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """ Retrieve a subset of the array's data based on the provided selection. Parameters ---------- store_path : StorePath The store path of the array. metadata : ArrayMetadata The array metadata. codec_pipeline : CodecPipeline The codec pipeline for encoding/decoding. config : ArrayConfig The array configuration. chunk_grid : ChunkGrid The chunk grid. selection : BasicSelection A selection object specifying the subset of data to retrieve. prototype : BufferPrototype, optional A buffer prototype to use for the retrieved data (default is None). Returns ------- NDArrayLikeOrScalar The retrieved subset of the array's data. """ if prototype is None: prototype = default_buffer_prototype() indexer = BasicIndexer( selection, shape=metadata.shape, chunk_grid=chunk_grid, ) return await _get_selection( store_path, metadata, codec_pipeline, config, chunk_grid, indexer, prototype=prototype ) async def _get_orthogonal_selection( store_path: StorePath, metadata: ArrayMetadata, codec_pipeline: CodecPipeline, config: ArrayConfig, chunk_grid: ChunkGrid, selection: OrthogonalSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """ Get an orthogonal selection from the array. Parameters ---------- store_path : StorePath The store path of the array. metadata : ArrayMetadata The array metadata. codec_pipeline : CodecPipeline The codec pipeline for encoding/decoding. config : ArrayConfig The array configuration. chunk_grid : ChunkGrid The chunk grid. selection : OrthogonalSelection The orthogonal selection specification. out : NDBuffer | None, optional An output buffer to write the data to. fields : Fields | None, optional Fields to select from structured arrays. prototype : BufferPrototype | None, optional A buffer prototype to use for the retrieved data. Returns ------- NDArrayLikeOrScalar The selected data. """ if prototype is None: prototype = default_buffer_prototype() indexer = OrthogonalIndexer(selection, metadata.shape, chunk_grid) return await _get_selection( store_path, metadata, codec_pipeline, config, chunk_grid, indexer=indexer, out=out, fields=fields, prototype=prototype, ) async def _get_mask_selection( store_path: StorePath, metadata: ArrayMetadata, codec_pipeline: CodecPipeline, config: ArrayConfig, chunk_grid: ChunkGrid, mask: MaskSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """ Get a mask selection from the array. Parameters ---------- store_path : StorePath The store path of the array. metadata : ArrayMetadata The array metadata. codec_pipeline : CodecPipeline The codec pipeline for encoding/decoding. config : ArrayConfig The array configuration. chunk_grid : ChunkGrid The chunk grid. mask : MaskSelection The boolean mask specifying the selection. out : NDBuffer | None, optional An output buffer to write the data to. fields : Fields | None, optional Fields to select from structured arrays. prototype : BufferPrototype | None, optional A buffer prototype to use for the retrieved data. Returns ------- NDArrayLikeOrScalar The selected data. """ if prototype is None: prototype = default_buffer_prototype() indexer = MaskIndexer(mask, metadata.shape, chunk_grid) return await _get_selection( store_path, metadata, codec_pipeline, config, chunk_grid, indexer=indexer, out=out, fields=fields, prototype=prototype, ) async def _get_coordinate_selection( store_path: StorePath, metadata: ArrayMetadata, codec_pipeline: CodecPipeline, config: ArrayConfig, chunk_grid: ChunkGrid, selection: CoordinateSelection, *, out: NDBuffer | None = None, fields: Fields | None = None, prototype: BufferPrototype | None = None, ) -> NDArrayLikeOrScalar: """ Get a coordinate selection from the array. Parameters ---------- store_path : StorePath The store path of the array. metadata : ArrayMetadata The array metadata. codec_pipeline : CodecPipeline The codec pipeline for encoding/decoding. config : ArrayConfig The array configuration. chunk_grid : ChunkGrid The chunk grid. selection : CoordinateSelection The coordinate selection specification. out : NDBuffer | None, optional An output buffer to write the data to. fields : Fields | None, optional Fields to select from structured arrays. prototype : BufferPrototype | None, optional A buffer prototype to use for the retrieved data. Returns ------- NDArrayLikeOrScalar The selected data. """ if prototype is None: prototype = default_buffer_prototype() indexer = CoordinateIndexer(selection, metadata.shape, chunk_grid) out_array = await _get_selection( store_path, metadata, codec_pipeline, config, chunk_grid, indexer=indexer, out=out, fields=fields, prototype=prototype, ) if hasattr(out_array, "shape"): # restore shape out_array = cast("NDArrayLikeOrScalar", np.array(out_array).reshape(indexer.sel_shape)) return out_array async def _set_selection( store_path: StorePath, metadata: ArrayMetadata, codec_pipeline: CodecPipeline, config: ArrayConfig, chunk_grid: ChunkGrid, indexer: Indexer, value: npt.ArrayLike, *, prototype: BufferPrototype, fields: Fields | None = None, ) -> None: """ Set a selection in an array. Parameters ---------- store_path : StorePath The store path of the array. metadata : ArrayMetadata The array metadata. codec_pipeline : CodecPipeline The codec pipeline for encoding/decoding. config : ArrayConfig The array configuration. chunk_grid : ChunkGrid The chunk grid. indexer : Indexer The indexer specifying the selection. value : npt.ArrayLike The values to write. prototype : BufferPrototype A buffer prototype to use. fields : Fields | None, optional Fields to select from structured arrays. """ # Get dtype from metadata if metadata.zarr_format == 2: zdtype = metadata.dtype else: zdtype = metadata.data_type dtype = zdtype.to_native_dtype() # check fields are sensible check_fields(fields, dtype) fields = check_no_multi_fields(fields) # check value shape if np.isscalar(value): array_like = prototype.buffer.create_zero_length().as_array_like() if isinstance(array_like, np._typing._SupportsArrayFunc): # TODO: need to handle array types that don't support __array_function__ # like PyTorch and JAX array_like_ = cast("np._typing._SupportsArrayFunc", array_like) value = np.asanyarray(value, dtype=dtype, like=array_like_) else: if not hasattr(value, "shape"): value = np.asarray(value, dtype) # assert ( # value.shape == indexer.shape # ), f"shape of value doesn't match indexer shape. Expected {indexer.shape}, got {value.shape}" if not hasattr(value, "dtype") or value.dtype.name != dtype.name: if hasattr(value, "astype"): # Handle things that are already NDArrayLike more efficiently value = value.astype(dtype=dtype, order="A") else: value = np.array(value, dtype=dtype, order="A") value = cast("NDArrayLike", value) # We accept any ndarray like object from the user and convert it # to an NDBuffer (or subclass). From this point onwards, we only pass # Buffer and NDBuffer between components. value_buffer = prototype.nd_buffer.from_ndarray_like(value) # Determine memory order if metadata.zarr_format == 2: order = metadata.order else: order = config.order # need to use the order from the metadata for v2 _config = config if metadata.zarr_format == 2: _config = replace(_config, order=order) # merging with existing data and encoding chunks await codec_pipeline.write( [ ( store_path / metadata.encode_chunk_key(chunk_coords), _get_chunk_spec(metadata, chunk_grid, chunk_coords, _config, prototype), chunk_selection, out_selection, is_complete_chunk, ) for chunk_coords, chunk_selection, out_selection, is_complete_chunk in indexer ], value_buffer, drop_axes=indexer.drop_axes, ) async def _setitem( store_path: StorePath, metadata: ArrayMetadata, codec_pipeline: CodecPipeline, config: ArrayConfig, chunk_grid: ChunkGrid, selection: BasicSelection, value: npt.ArrayLike, prototype: BufferPrototype | None = None, ) -> None: """ Set values in the array using basic indexing. Parameters ---------- store_path : StorePath The store path of the array. metadata : ArrayMetadata The array metadata. codec_pipeline : CodecPipeline The codec pipeline for encoding/decoding. config : ArrayConfig The array configuration. chunk_grid : ChunkGrid The chunk grid. selection : BasicSelection The selection defining the region of the array to set. value : npt.ArrayLike The values to be written into the selected region of the array. prototype : BufferPrototype or None, optional A prototype buffer that defines the structure and properties of the array chunks being modified. If None, the default buffer prototype is used. """ if prototype is None: prototype = default_buffer_prototype() indexer = BasicIndexer( selection, shape=metadata.shape, chunk_grid=chunk_grid, ) return await _set_selection( store_path, metadata, codec_pipeline, config, chunk_grid, indexer, value, prototype=prototype, ) async def _resize( array: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], new_shape: ShapeLike, delete_outside_chunks: bool = True, ) -> None: """ Resize an array to a new shape. Parameters ---------- array : AsyncArray The array to resize. new_shape : ShapeLike The desired new shape of the array. delete_outside_chunks : bool, optional If True (default), chunks that fall outside the new shape will be deleted. If False, the data in those chunks will be preserved. """ new_shape = parse_shapelike(new_shape) assert len(new_shape) == len(array.metadata.shape) new_metadata = array.metadata.update_shape(new_shape) new_chunk_grid = ChunkGrid.from_metadata(new_metadata) # ensure deletion is only run if array is shrinking as the delete_outside_chunks path is unbounded in memory only_growing = all(new >= old for new, old in zip(new_shape, array.metadata.shape, strict=True)) if delete_outside_chunks and not only_growing: # Remove all chunks outside of the new shape old_chunk_coords = set(array._chunk_grid.all_chunk_coords()) new_chunk_coords = set(new_chunk_grid.all_chunk_coords()) async def _delete_key(key: str) -> None: await (array.store_path / key).delete() await concurrent_map( [ (array.metadata.encode_chunk_key(chunk_coords),) for chunk_coords in old_chunk_coords.difference(new_chunk_coords) ], _delete_key, zarr_config.get("async.concurrency"), ) # Write new metadata await save_metadata(array.store_path, new_metadata) # Update metadata and chunk_grid (in place) object.__setattr__(array, "metadata", new_metadata) object.__setattr__(array, "_chunk_grid", new_chunk_grid) async def _append( array: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], data: npt.ArrayLike, axis: int = 0, ) -> tuple[int, ...]: """ Append data to an array along the specified axis. Parameters ---------- array : AsyncArray The array to append to. data : npt.ArrayLike Data to be appended. axis : int Axis along which to append. Returns ------- new_shape : tuple[int, ...] The new shape of the array after appending. Notes ----- The size of all dimensions other than `axis` must match between the array and `data`. """ # ensure data is array-like if not hasattr(data, "shape"): data = np.asanyarray(data) self_shape_preserved = tuple(s for i, s in enumerate(array.shape) if i != axis) data_shape_preserved = tuple(s for i, s in enumerate(data.shape) if i != axis) if self_shape_preserved != data_shape_preserved: raise ValueError( f"shape of data to append is not compatible with the array. " f"The shape of the data is ({data_shape_preserved})" f"and the shape of the array is ({self_shape_preserved})." "All dimensions must match except for the dimension being " "appended." ) # remember old shape old_shape = array.shape # determine new shape new_shape = tuple( array.shape[i] if i != axis else array.shape[i] + data.shape[i] for i in range(len(array.shape)) ) # resize await _resize(array, new_shape) # store data append_selection = tuple( slice(None) if i != axis else slice(old_shape[i], new_shape[i]) for i in range(len(array.shape)) ) await _setitem( array.store_path, array.metadata, array.codec_pipeline, array.config, array._chunk_grid, append_selection, data, ) return new_shape async def _update_attributes( array: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], new_attributes: dict[str, JSON], ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: """ Update the array's attributes. Parameters ---------- array : AsyncArray The array whose attributes to update. new_attributes : dict[str, JSON] A dictionary of new attributes to update or add to the array. Returns ------- AsyncArray The array with the updated attributes. """ array.metadata.attributes.update(new_attributes) # Write new metadata await save_metadata(array.store_path, array.metadata) return array async def _info_complete( array: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], ) -> Any: """ Return all the information for an array, including dynamic information like storage size. Parameters ---------- array : AsyncArray The array to get info for. Returns ------- ArrayInfo Complete information about the array including: - The count of chunks initialized - The sum of the bytes written """ return array._info( await _nshards_initialized(array), await array.store_path.store.getsize_prefix(array.store_path.path), ) zarr-python-3.2.1/src/zarr/core/array_spec.py000066400000000000000000000105721517635743000212310ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, fields from typing import TYPE_CHECKING, Any, Literal, Self, TypedDict, cast from zarr.core.common import ( MemoryOrder, parse_bool, parse_fill_value, parse_order, parse_shapelike, ) from zarr.core.config import config as zarr_config if TYPE_CHECKING: from typing import NotRequired from zarr.core.buffer import BufferPrototype from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType class ArrayConfigParams(TypedDict): """ A TypedDict model of the attributes of an ArrayConfig class, but with no required fields. This allows for partial construction of an ArrayConfig, with the assumption that the unset keys will be taken from a global configuration. """ order: NotRequired[MemoryOrder] write_empty_chunks: NotRequired[bool] read_missing_chunks: NotRequired[bool] @dataclass(frozen=True) class ArrayConfig: """ A model of the runtime configuration of an array. Parameters ---------- order : MemoryOrder The memory layout of the arrays returned when reading data from the store. write_empty_chunks : bool If True, empty chunks will be written to the store. read_missing_chunks : bool If True, missing chunks will be filled with the array's fill value on read. If False, reading missing chunks will raise a ``ChunkNotFoundError``. """ order: MemoryOrder write_empty_chunks: bool read_missing_chunks: bool def __init__( self, order: MemoryOrder, write_empty_chunks: bool, *, read_missing_chunks: bool = True ) -> None: order_parsed = parse_order(order) write_empty_chunks_parsed = parse_bool(write_empty_chunks) read_missing_chunks_parsed = parse_bool(read_missing_chunks) object.__setattr__(self, "order", order_parsed) object.__setattr__(self, "write_empty_chunks", write_empty_chunks_parsed) object.__setattr__(self, "read_missing_chunks", read_missing_chunks_parsed) @classmethod def from_dict(cls, data: ArrayConfigParams) -> Self: """ Create an ArrayConfig from a dict. The keys of that dict are a subset of the attributes of the ArrayConfig class. Any keys missing from that dict will be set to the the values in the ``array`` namespace of ``zarr.config``. """ kwargs_out: ArrayConfigParams = {} for f in fields(ArrayConfig): field_name = cast( "Literal['order', 'write_empty_chunks', 'read_missing_chunks']", f.name ) if field_name not in data: kwargs_out[field_name] = zarr_config.get(f"array.{field_name}") else: kwargs_out[field_name] = data[field_name] return cls(**kwargs_out) def to_dict(self) -> ArrayConfigParams: """ Serialize an instance of this class to a dict. """ return { "order": self.order, "write_empty_chunks": self.write_empty_chunks, "read_missing_chunks": self.read_missing_chunks, } ArrayConfigLike = ArrayConfig | ArrayConfigParams def parse_array_config(data: ArrayConfigLike | None) -> ArrayConfig: """ Convert various types of data to an ArrayConfig. """ if data is None: return ArrayConfig.from_dict({}) elif isinstance(data, ArrayConfig): return data else: return ArrayConfig.from_dict(data) @dataclass(frozen=True) class ArraySpec: shape: tuple[int, ...] dtype: ZDType[TBaseDType, TBaseScalar] fill_value: Any config: ArrayConfig prototype: BufferPrototype def __init__( self, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], fill_value: Any, config: ArrayConfig, prototype: BufferPrototype, ) -> None: shape_parsed = parse_shapelike(shape) fill_value_parsed = parse_fill_value(fill_value) object.__setattr__(self, "shape", shape_parsed) object.__setattr__(self, "dtype", dtype) object.__setattr__(self, "fill_value", fill_value_parsed) object.__setattr__(self, "config", config) object.__setattr__(self, "prototype", prototype) @property def ndim(self) -> int: return len(self.shape) @property def order(self) -> MemoryOrder: return self.config.order zarr-python-3.2.1/src/zarr/core/attributes.py000066400000000000000000000032271517635743000212660ustar00rootroot00000000000000from __future__ import annotations from collections.abc import MutableMapping from typing import TYPE_CHECKING from zarr.core.common import JSON if TYPE_CHECKING: from collections.abc import Iterator from zarr.core.group import Group from zarr.types import AnyArray class Attributes(MutableMapping[str, JSON]): def __init__(self, obj: AnyArray | Group) -> None: # key=".zattrs", read_only=False, cache=True, synchronizer=None self._obj = obj def __getitem__(self, key: str) -> JSON: return self._obj.metadata.attributes[key] def __setitem__(self, key: str, value: JSON) -> None: new_attrs = dict(self._obj.metadata.attributes) new_attrs[key] = value self._obj = self._obj.update_attributes(new_attrs) def __delitem__(self, key: str) -> None: new_attrs = dict(self._obj.metadata.attributes) del new_attrs[key] self.put(new_attrs) def __iter__(self) -> Iterator[str]: return iter(self._obj.metadata.attributes) def __len__(self) -> int: return len(self._obj.metadata.attributes) def put(self, d: dict[str, JSON]) -> None: """ Overwrite all attributes with the values from `d`. Equivalent to the following pseudo-code, but performed atomically. ```python attrs = {"a": 1, "b": 2} attrs.clear() attrs.update({"a": "3", "c": 4}) print(attrs) #> {'a': '3', 'c': 4} ``` """ self._obj.metadata.attributes.clear() self._obj = self._obj.update_attributes(d) def asdict(self) -> dict[str, JSON]: return dict(self._obj.metadata.attributes) zarr-python-3.2.1/src/zarr/core/buffer/000077500000000000000000000000001517635743000177735ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/core/buffer/__init__.py000066400000000000000000000006451517635743000221110ustar00rootroot00000000000000from zarr.core.buffer.core import ( ArrayLike, Buffer, BufferPrototype, NDArrayLike, NDArrayLikeOrScalar, NDBuffer, default_buffer_prototype, ) from zarr.core.buffer.cpu import numpy_buffer_prototype __all__ = [ "ArrayLike", "Buffer", "BufferPrototype", "NDArrayLike", "NDArrayLikeOrScalar", "NDBuffer", "default_buffer_prototype", "numpy_buffer_prototype", ] zarr-python-3.2.1/src/zarr/core/buffer/core.py000066400000000000000000000431111517635743000212750ustar00rootroot00000000000000from __future__ import annotations import sys from abc import ABC, abstractmethod from collections.abc import Iterable from typing import ( TYPE_CHECKING, Any, Literal, NamedTuple, Protocol, SupportsIndex, cast, runtime_checkable, ) import numpy as np import numpy.typing as npt if TYPE_CHECKING: from collections.abc import Iterable, Sequence from typing import Self from zarr.codecs.bytes import Endian from zarr.core.common import BytesLike # Everything here is imported into ``zarr.core.buffer`` namespace. __all__: list[str] = [] @runtime_checkable class ArrayLike(Protocol): """Protocol for the array-like type that underlie Buffer""" @property def dtype(self) -> np.dtype[Any]: ... @property def ndim(self) -> int: ... @property def size(self) -> int: ... def __getitem__(self, key: slice) -> Self: ... def __setitem__(self, key: slice, value: Any) -> None: ... @runtime_checkable class NDArrayLike(Protocol): """Protocol for the nd-array-like type that underlie NDBuffer""" @property def dtype(self) -> np.dtype[Any]: ... @property def ndim(self) -> int: ... @property def size(self) -> int: ... @property def shape(self) -> tuple[int, ...]: ... def __len__(self) -> int: ... def __getitem__(self, key: slice) -> Self: ... def __setitem__(self, key: slice, value: Any) -> None: ... def __array__(self) -> npt.NDArray[Any]: ... def reshape( self, shape: tuple[int, ...] | Literal[-1], *, order: Literal["A", "C", "F"] = ... ) -> Self: ... def view(self, dtype: npt.DTypeLike) -> Self: ... def astype( self, dtype: npt.DTypeLike, order: Literal["K", "A", "C", "F"] = ..., *, copy: bool = ..., ) -> Self: ... def fill(self, value: Any) -> None: ... def copy(self) -> Self: ... def transpose(self, axes: SupportsIndex | Sequence[SupportsIndex] | None) -> Self: ... def ravel(self, order: Literal["K", "A", "C", "F"] = ...) -> Self: ... def all(self) -> bool: ... def __eq__(self, other: object) -> Self: # type: ignore[override] """Element-wise equal Notes ----- Type checkers such as mypy complains because the return type isn't a bool like its supertype "object", which violates the Liskov substitution principle. This is true, but since NumPy's ndarray is defined as an element-wise equal, our hands are tied. """ ScalarType = int | float | complex | bytes | str | bool | np.generic NDArrayLikeOrScalar = ScalarType | NDArrayLike def check_item_key_is_1d_contiguous(key: Any) -> None: """Raises error if `key` isn't a 1d contiguous slice""" if not isinstance(key, slice): raise TypeError( f"Item key has incorrect type (expected slice, got {key.__class__.__name__})" ) if not (key.step is None or key.step == 1): raise ValueError("slice must be contiguous") class Buffer(ABC): """A flat contiguous memory block We use Buffer throughout Zarr to represent a contiguous block of memory. A Buffer is backed by an underlying array-like instance that represents the memory. The memory type is unspecified; can be regular host memory, CUDA device memory, or something else. The only requirement is that the array-like instance can be copied/converted to a regular Numpy array (host memory). Notes ----- This buffer is untyped, so all indexing and sizes are in bytes. Parameters ---------- array_like array-like object that must be 1-dim, contiguous, and byte dtype. """ def __init__(self, array_like: ArrayLike) -> None: if array_like.ndim != 1: raise ValueError("array_like: only 1-dim allowed") if array_like.dtype != np.dtype("B"): raise ValueError("array_like: only byte dtype allowed") self._data = array_like @classmethod @abstractmethod def create_zero_length(cls) -> Self: """Create an empty buffer with length zero Returns ------- New empty 0-length buffer """ if cls is Buffer: raise NotImplementedError("Cannot call abstract method on the abstract class 'Buffer'") return cls( cast("ArrayLike", None) ) # This line will never be reached, but it satisfies the type checker @classmethod def from_array_like(cls, array_like: ArrayLike) -> Self: """Create a new buffer of an array-like object Parameters ---------- array_like array-like object that must be 1-dim, contiguous, and byte dtype. Returns ------- New buffer representing `array_like` """ return cls(array_like) @classmethod @abstractmethod def from_buffer(cls, buffer: Buffer) -> Self: """Create a new buffer of an existing Buffer This is useful if you want to ensure that an existing buffer is of the correct subclass of Buffer. E.g., MemoryStore uses this to return a buffer instance of the subclass specified by its BufferPrototype argument. Typically, this only copies data if the data has to be moved between memory types, such as from host to device memory. Parameters ---------- buffer buffer object. Returns ------- A new buffer representing the content of the input buffer Notes ----- Subclasses of `Buffer` must override this method to implement more optimal conversions that avoid copies where possible """ if cls is Buffer: raise NotImplementedError("Cannot call abstract method on the abstract class 'Buffer'") return cls( cast("ArrayLike", None) ) # This line will never be reached, but it satisfies the type checker @classmethod @abstractmethod def from_bytes(cls, bytes_like: BytesLike) -> Self: """Create a new buffer of a bytes-like object (host memory) Parameters ---------- bytes_like bytes-like object Returns ------- New buffer representing `bytes_like` """ if cls is Buffer: raise NotImplementedError("Cannot call abstract method on the abstract class 'Buffer'") return cls( cast("ArrayLike", None) ) # This line will never be reached, but it satisfies the type checker def as_array_like(self) -> ArrayLike: """Returns the underlying array (host or device memory) of this buffer This will never copy data. Returns ------- The underlying 1d array such as a NumPy or CuPy array. """ return self._data @abstractmethod def as_numpy_array(self) -> npt.NDArray[Any]: """Returns the buffer as a NumPy array (host memory). Notes ----- Might have to copy data, consider using `.as_array_like()` instead. Returns ------- NumPy array of this buffer (might be a data copy) """ ... def as_buffer_like(self) -> BytesLike: """Returns the buffer as an object that implements the Python buffer protocol. Notes ----- Might have to copy data, since the implementation uses `.as_numpy_array()`. Returns ------- An object that implements the Python buffer protocol """ return memoryview(self.as_numpy_array()) def to_bytes(self) -> bytes: """Returns the buffer as `bytes` (host memory). Warnings -------- Will always copy data, only use this method for small buffers such as metadata buffers. If possible, use `.as_numpy_array()` or `.as_array_like()` instead. Returns ------- `bytes` of this buffer (data copy) """ return bytes(self.as_numpy_array()) def __getitem__(self, key: slice) -> Self: check_item_key_is_1d_contiguous(key) return self.__class__(self._data.__getitem__(key)) def __setitem__(self, key: slice, value: Any) -> None: check_item_key_is_1d_contiguous(key) self._data.__setitem__(key, value) def __len__(self) -> int: return self._data.size @abstractmethod def combine(self, others: Iterable[Buffer]) -> Self: """Concatenate many buffers""" ... def __add__(self, other: Buffer) -> Self: """Concatenate two buffers""" return self.combine([other]) def __eq__(self, other: object) -> bool: # Another Buffer class can override this to choose a more efficient path return isinstance(other, Buffer) and np.array_equal( self.as_numpy_array(), other.as_numpy_array() ) class NDBuffer: """An n-dimensional memory block We use NDBuffer throughout Zarr to represent a n-dimensional memory block. An NDBuffer is backed by an underlying ndarray-like instance that represents the memory. The memory type is unspecified; can be regular host memory, CUDA device memory, or something else. The only requirement is that the ndarray-like instance can be copied/converted to a regular Numpy array (host memory). Notes ----- The two buffer classes Buffer and NDBuffer are very similar. In fact, Buffer is a special case of NDBuffer where dim=1, stride=1, and dtype="B". However, in order to use Python's type system to differentiate between the contiguous Buffer and the n-dim (non-contiguous) NDBuffer, we keep the definition of the two classes separate. Parameters ---------- array : ndarray_like ndarray-like object that is convertible to a regular Numpy array. """ def __init__(self, array: NDArrayLike) -> None: self._data = array @classmethod @abstractmethod def create( cls, *, shape: Iterable[int], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C", fill_value: Any | None = None, ) -> Self: """Create a new buffer and its underlying ndarray-like object Parameters ---------- shape The shape of the buffer and its underlying ndarray-like object dtype The datatype of the buffer and its underlying ndarray-like object order Whether to store multi-dimensional data in row-major (C-style) or column-major (Fortran-style) order in memory. fill_value If not None, fill the new buffer with a scalar value. Returns ------- New buffer representing a new ndarray_like object Notes ----- A subclass can overwrite this method to create an ndarray-like object other then the default Numpy array. """ if cls is NDBuffer: raise NotImplementedError( "Cannot call abstract method on the abstract class 'NDBuffer'" ) return cls( cast("NDArrayLike", None) ) # This line will never be reached, but it satisfies the type checker @classmethod def empty( cls, shape: tuple[int, ...], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C" ) -> Self: """ Create an empty buffer with the given shape, dtype, and order. This method can be faster than ``NDBuffer.create`` because it doesn't have to initialize the memory used by the underlying ndarray-like object. Parameters ---------- shape The shape of the buffer and its underlying ndarray-like object dtype The datatype of the buffer and its underlying ndarray-like object order Whether to store multi-dimensional data in row-major (C-style) or column-major (Fortran-style) order in memory. Returns ------- buffer New buffer representing a new ndarray_like object with empty data. See Also -------- NDBuffer.create Create a new buffer with some initial fill value. """ # Implementations should override this method if they have a faster way # to allocate an empty buffer. return cls.create(shape=shape, dtype=dtype, order=order) @classmethod def from_ndarray_like(cls, ndarray_like: NDArrayLike) -> Self: """Create a new buffer of an ndarray-like object Parameters ---------- ndarray_like ndarray-like object Returns ------- New buffer representing `ndarray_like` """ return cls(ndarray_like) @classmethod @abstractmethod def from_numpy_array(cls, array_like: npt.ArrayLike) -> Self: """Create a new buffer of Numpy array-like object Parameters ---------- array_like Object that can be coerced into a Numpy array Returns ------- New buffer representing `array_like` """ if cls is NDBuffer: raise NotImplementedError( "Cannot call abstract method on the abstract class 'NDBuffer'" ) return cls( cast("NDArrayLike", None) ) # This line will never be reached, but it satisfies the type checker def as_ndarray_like(self) -> NDArrayLike: """Returns the underlying array (host or device memory) of this buffer This will never copy data. Returns ------- The underlying array such as a NumPy or CuPy array. """ return self._data @abstractmethod def as_numpy_array(self) -> npt.NDArray[Any]: """Returns the buffer as a NumPy array (host memory). Warnings -------- Might have to copy data, consider using `.as_ndarray_like()` instead. Returns ------- NumPy array of this buffer (might be a data copy) """ ... def as_scalar(self) -> ScalarType: """Returns the buffer as a scalar value""" if self._data.size != 1: raise ValueError("Buffer does not contain a single scalar value") return cast("ScalarType", self.as_numpy_array()[()]) @property def dtype(self) -> np.dtype[Any]: return self._data.dtype @property def shape(self) -> tuple[int, ...]: return self._data.shape @property def byteorder(self) -> Endian: from zarr.codecs.bytes import Endian if self.dtype.byteorder == "<": return Endian.little elif self.dtype.byteorder == ">": return Endian.big else: return Endian(sys.byteorder) def reshape(self, newshape: tuple[int, ...] | Literal[-1]) -> Self: return self.__class__(self._data.reshape(newshape)) def squeeze(self, axis: tuple[int, ...]) -> Self: newshape = tuple(a for i, a in enumerate(self.shape) if i not in axis) return self.__class__(self._data.reshape(newshape)) def astype(self, dtype: npt.DTypeLike, order: Literal["K", "A", "C", "F"] = "K") -> Self: return self.__class__(self._data.astype(dtype=dtype, order=order)) @abstractmethod def __getitem__(self, key: Any) -> Self: ... @abstractmethod def __setitem__(self, key: Any, value: Any) -> None: ... def __len__(self) -> int: return self._data.__len__() def __repr__(self) -> str: return f"" def all_equal(self, other: Any, equal_nan: bool = True) -> bool: """Compare to `other` using np.array_equal.""" if other is None: # Handle None fill_value for Zarr V2 return False # Handle positive and negative zero by comparing bit patterns: if ( np.asarray(other).dtype.kind == "f" and other == 0.0 and self._data.dtype.kind not in ("U", "S", "T", "O", "V") ): _data, other = np.broadcast_arrays(self._data, np.asarray(other, self._data.dtype)) void_dtype = f"V{_data.dtype.itemsize}" return np.array_equal(_data.view(void_dtype), other.view(void_dtype)) # use array_equal to obtain equal_nan=True functionality # Since fill-value is a scalar, isn't there a faster path than allocating a new array for fill value # every single time we have to write data? _data, other = np.broadcast_arrays(self._data, other) return np.array_equal( _data, other, equal_nan=equal_nan if self._data.dtype.kind not in ("U", "S", "T", "O", "V") else False, ) def fill(self, value: Any) -> None: self._data.fill(value) def copy(self) -> Self: return self.__class__(self._data.copy()) def transpose(self, axes: SupportsIndex | Sequence[SupportsIndex] | None) -> Self: return self.__class__(self._data.transpose(axes)) class BufferPrototype(NamedTuple): """Prototype of the Buffer and NDBuffer class The protocol must be pickable. Attributes ---------- buffer The Buffer class to use when Zarr needs to create new Buffer. nd_buffer The NDBuffer class to use when Zarr needs to create new NDBuffer. """ buffer: type[Buffer] nd_buffer: type[NDBuffer] # The default buffer prototype used throughout the Zarr codebase. def default_buffer_prototype() -> BufferPrototype: from zarr.registry import ( get_buffer_class, get_ndbuffer_class, ) return BufferPrototype(buffer=get_buffer_class(), nd_buffer=get_ndbuffer_class()) zarr-python-3.2.1/src/zarr/core/buffer/cpu.py000066400000000000000000000170121517635743000211350ustar00rootroot00000000000000from __future__ import annotations from typing import ( TYPE_CHECKING, Any, Literal, ) import numpy as np import numpy.typing as npt from zarr.core.buffer import core from zarr.registry import ( register_buffer, register_ndbuffer, ) if TYPE_CHECKING: from collections.abc import Callable, Iterable from typing import Self from zarr.core.buffer.core import ArrayLike, NDArrayLike from zarr.core.common import BytesLike class Buffer(core.Buffer): """A flat contiguous memory block We use Buffer throughout Zarr to represent a contiguous block of memory. A Buffer is backed by an underlying array-like instance that represents the memory. The memory type is unspecified; can be regular host memory, CUDA device memory, or something else. The only requirement is that the array-like instance can be copied/converted to a regular Numpy array (host memory). Notes ----- This buffer is untyped, so all indexing and sizes are in bytes. Parameters ---------- array_like array-like object that must be 1-dim, contiguous, and byte dtype. """ def __init__(self, array_like: ArrayLike) -> None: super().__init__(array_like) @classmethod def create_zero_length(cls) -> Self: return cls(np.array([], dtype="B")) @classmethod def from_buffer(cls, buffer: core.Buffer) -> Self: """Create a new buffer of an existing Buffer This is useful if you want to ensure that an existing buffer is of the correct subclass of Buffer. E.g., MemoryStore uses this to return a buffer instance of the subclass specified by its BufferPrototype argument. Typically, this only copies data if the data has to be moved between memory types, such as from host to device memory. Parameters ---------- buffer buffer object. Returns ------- A new buffer representing the content of the input buffer Notes ----- Subclasses of `Buffer` must override this method to implement more optimal conversions that avoid copies where possible """ return cls.from_array_like(buffer.as_numpy_array()) @classmethod def from_bytes(cls, bytes_like: BytesLike) -> Self: """Create a new buffer of a bytes-like object (host memory) Parameters ---------- bytes_like bytes-like object Returns ------- New buffer representing `bytes_like` """ return cls.from_array_like(np.frombuffer(bytes_like, dtype="B")) def as_numpy_array(self) -> npt.NDArray[Any]: """Returns the buffer as a NumPy array (host memory). Notes ----- Might have to copy data, consider using `.as_array_like()` instead. Returns ------- NumPy array of this buffer (might be a data copy) """ return np.asanyarray(self._data) def combine(self, others: Iterable[core.Buffer]) -> Self: data = [np.asanyarray(self._data)] for buf in others: other_array = buf.as_array_like() assert other_array.dtype == np.dtype("B") data.append(np.asanyarray(other_array)) return self.__class__(np.concatenate(data)) class NDBuffer(core.NDBuffer): """An n-dimensional memory block We use NDBuffer throughout Zarr to represent a n-dimensional memory block. An NDBuffer is backed by an underlying ndarray-like instance that represents the memory. The memory type is unspecified; can be regular host memory, CUDA device memory, or something else. The only requirement is that the ndarray-like instance can be copied/converted to a regular Numpy array (host memory). Notes ----- The two buffer classes Buffer and NDBuffer are very similar. In fact, Buffer is a special case of NDBuffer where dim=1, stride=1, and dtype="B". However, in order to use Python's type system to differentiate between the contiguous Buffer and the n-dim (non-contiguous) NDBuffer, we keep the definition of the two classes separate. Parameters ---------- array ndarray-like object that is convertible to a regular Numpy array. """ def __init__(self, array: NDArrayLike) -> None: super().__init__(array) @classmethod def create( cls, *, shape: Iterable[int], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C", fill_value: Any | None = None, ) -> Self: # np.zeros is much faster than np.full, and therefore using it when possible is better. if fill_value is None or (isinstance(fill_value, int) and fill_value == 0): return cls(np.zeros(shape=tuple(shape), dtype=dtype, order=order)) else: return cls(np.full(shape=tuple(shape), fill_value=fill_value, dtype=dtype, order=order)) @classmethod def empty( cls, shape: tuple[int, ...], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C" ) -> Self: return cls(np.empty(shape=shape, dtype=dtype, order=order)) @classmethod def from_numpy_array(cls, array_like: npt.ArrayLike) -> Self: return cls.from_ndarray_like(np.asanyarray(array_like)) def as_numpy_array(self) -> npt.NDArray[Any]: """Returns the buffer as a NumPy array (host memory). Warnings -------- Might have to copy data, consider using `.as_ndarray_like()` instead. Returns ------- NumPy array of this buffer (might be a data copy) """ return np.asanyarray(self._data) def __getitem__(self, key: Any) -> Self: return self.__class__(np.asanyarray(self._data.__getitem__(key))) def __setitem__(self, key: Any, value: Any) -> None: if isinstance(value, NDBuffer): value = value._data self._data.__setitem__(key, value) def as_numpy_array_wrapper( func: Callable[[npt.NDArray[Any]], bytes], buf: core.Buffer, prototype: core.BufferPrototype ) -> core.Buffer: """Converts the input of `func` to a numpy array and the output back to `Buffer`. This function is useful when calling a `func` that only support host memory such as `GZip.decode` and `Blosc.decode`. In this case, use this wrapper to convert the input `buf` to a Numpy array and convert the result back into a `Buffer`. Parameters ---------- func The callable that will be called with the converted `buf` as input. `func` must return bytes, which will be converted into a `Buffer` before returned. buf The buffer that will be converted to a Numpy array before given as input to `func`. prototype The prototype of the output buffer. Returns ------- The result of `func` converted to a `Buffer` """ return prototype.buffer.from_bytes(func(buf.as_numpy_array())) # CPU buffer prototype using numpy arrays buffer_prototype = core.BufferPrototype(buffer=Buffer, nd_buffer=NDBuffer) # default_buffer_prototype = buffer_prototype # The numpy prototype used for E.g. when reading the shard index def numpy_buffer_prototype() -> core.BufferPrototype: return core.BufferPrototype(buffer=Buffer, nd_buffer=NDBuffer) register_buffer(Buffer, qualname="zarr.buffer.cpu.Buffer") register_ndbuffer(NDBuffer, qualname="zarr.buffer.cpu.NDBuffer") # backwards compatibility register_buffer(Buffer, qualname="zarr.core.buffer.cpu.Buffer") register_ndbuffer(NDBuffer, qualname="zarr.core.buffer.cpu.NDBuffer") zarr-python-3.2.1/src/zarr/core/buffer/gpu.py000066400000000000000000000171411517635743000211440ustar00rootroot00000000000000from __future__ import annotations import warnings from typing import ( TYPE_CHECKING, Any, Literal, cast, ) import numpy as np import numpy.typing as npt from zarr.core.buffer import core from zarr.core.buffer.core import ArrayLike, BufferPrototype, NDArrayLike from zarr.errors import ZarrUserWarning from zarr.registry import ( register_buffer, register_ndbuffer, ) if TYPE_CHECKING: from collections.abc import Iterable from typing import Self from zarr.core.common import BytesLike try: import cupy as cp except ImportError: cp = None class Buffer(core.Buffer): """A flat contiguous memory block on the GPU We use Buffer throughout Zarr to represent a contiguous block of memory. A Buffer is backed by an underlying array-like instance that represents the memory. The memory type is unspecified; can be regular host memory, CUDA device memory, or something else. The only requirement is that the array-like instance can be copied/converted to a regular Numpy array (host memory). Notes ----- This buffer is untyped, so all indexing and sizes are in bytes. Parameters ---------- array_like array-like object that must be 1-dim, contiguous, and byte dtype. """ def __init__(self, array_like: ArrayLike) -> None: if cp is None: raise ImportError( "Cannot use zarr.buffer.gpu.Buffer without cupy. Please install cupy." ) if array_like.ndim != 1: raise ValueError("array_like: only 1-dim allowed") if array_like.dtype != np.dtype("B"): raise ValueError("array_like: only byte dtype allowed") if not hasattr(array_like, "__cuda_array_interface__"): # Slow copy based path for arrays that don't support the __cuda_array_interface__ # TODO: Add a fast zero-copy path for arrays that support the dlpack protocol msg = ( "Creating a zarr.buffer.gpu.Buffer with an array that does not support the " "__cuda_array_interface__ for zero-copy transfers, " "falling back to slow copy based path" ) warnings.warn( msg, category=ZarrUserWarning, stacklevel=2, ) self._data = cp.asarray(array_like) @classmethod def create_zero_length(cls) -> Self: """Create an empty buffer with length zero Returns ------- New empty 0-length buffer """ return cls(cp.array([], dtype="B")) @classmethod def from_buffer(cls, buffer: core.Buffer) -> Self: """Create a GPU Buffer given an arbitrary Buffer This will try to be zero-copy if `buffer` is already on the GPU and will trigger a copy if not. Returns ------- New GPU Buffer constructed from `buffer` """ return cls(buffer.as_array_like()) @classmethod def from_bytes(cls, bytes_like: BytesLike) -> Self: return cls.from_array_like(cp.frombuffer(bytes_like, dtype="B")) def as_numpy_array(self) -> npt.NDArray[Any]: return cast("npt.NDArray[Any]", cp.asnumpy(self._data)) def combine(self, others: Iterable[core.Buffer]) -> Self: data = [cp.asanyarray(self._data)] for other in others: other_array = other.as_array_like() assert other_array.dtype == np.dtype("B") gpu_other = Buffer(other_array) gpu_other_array = gpu_other.as_array_like() data.append(cp.asanyarray(gpu_other_array)) return self.__class__(cp.concatenate(data)) class NDBuffer(core.NDBuffer): """A n-dimensional memory block on the GPU We use NDBuffer throughout Zarr to represent a n-dimensional memory block. An NDBuffer is backed by an underlying ndarray-like instance that represents the memory. The memory type is unspecified; can be regular host memory, CUDA device memory, or something else. The only requirement is that the ndarray-like instance can be copied/converted to a regular Numpy array (host memory). Notes ----- The two buffer classes Buffer and NDBuffer are very similar. In fact, Buffer is a special case of NDBuffer where dim=1, stride=1, and dtype="B". However, in order to use Python's type system to differentiate between the contiguous Buffer and the n-dim (non-contiguous) NDBuffer, we keep the definition of the two classes separate. Parameters ---------- array ndarray-like object that is convertible to a regular Numpy array. """ def __init__(self, array: NDArrayLike) -> None: if cp is None: raise ImportError( "Cannot use zarr.buffer.gpu.NDBuffer without cupy. Please install cupy." ) # assert array.ndim > 0 assert array.dtype != object self._data = array if not hasattr(array, "__cuda_array_interface__"): # Slow copy based path for arrays that don't support the __cuda_array_interface__ # TODO: Add a fast zero-copy path for arrays that support the dlpack protocol msg = ( "Creating a zarr.buffer.gpu.NDBuffer with an array that does not support the " "__cuda_array_interface__ for zero-copy transfers, " "falling back to slow copy based path" ) warnings.warn( msg, stacklevel=2, ) self._data = cp.asarray(array) @classmethod def create( cls, *, shape: Iterable[int], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C", fill_value: Any | None = None, ) -> Self: ret = cls(cp.empty(shape=tuple(shape), dtype=dtype, order=order)) if fill_value is not None: ret.fill(fill_value) return ret @classmethod def empty( cls, shape: tuple[int, ...], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C" ) -> Self: return cls(cp.empty(shape=shape, dtype=dtype, order=order)) @classmethod def from_numpy_array(cls, array_like: npt.ArrayLike) -> Self: """Create a new buffer of Numpy array-like object Parameters ---------- array_like Object that can be coerced into a Numpy array Returns ------- New buffer representing `array_like` """ return cls(cp.asarray(array_like)) def as_numpy_array(self) -> npt.NDArray[Any]: """Returns the buffer as a NumPy array (host memory). Warnings -------- Might have to copy data, consider using `.as_ndarray_like()` instead. Returns ------- NumPy array of this buffer (might be a data copy) """ return cast("npt.NDArray[Any]", cp.asnumpy(self._data)) def __getitem__(self, key: Any) -> Self: return self.__class__(self._data.__getitem__(key)) def __setitem__(self, key: Any, value: Any) -> None: if isinstance(value, NDBuffer): value = value._data elif isinstance(value, core.NDBuffer): gpu_value = NDBuffer(value.as_ndarray_like()) value = gpu_value._data self._data.__setitem__(key, value) buffer_prototype = BufferPrototype(buffer=Buffer, nd_buffer=NDBuffer) register_buffer(Buffer, qualname="zarr.buffer.gpu.Buffer") register_ndbuffer(NDBuffer, qualname="zarr.buffer.gpu.NDBuffer") # backwards compatibility register_buffer(Buffer, qualname="zarr.core.buffer.gpu.Buffer") register_ndbuffer(NDBuffer, qualname="zarr.core.buffer.gpu.NDBuffer") zarr-python-3.2.1/src/zarr/core/chunk_grids.py000066400000000000000000000733531517635743000214070ustar00rootroot00000000000000from __future__ import annotations import bisect import itertools import math import numbers import operator import warnings from dataclasses import dataclass, field from functools import reduce from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeGuard, cast, runtime_checkable import numpy as np import numpy.typing as npt import zarr from zarr.core.common import ( ShapeLike, ceildiv, parse_shapelike, ) from zarr.errors import ZarrUserWarning if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Sequence from zarr.core.array import ShardsLike from zarr.core.metadata import ArrayMetadata @dataclass(frozen=True) class FixedDimension: """Uniform chunk size. Boundary chunks contain less data but are encoded at full size by the codec pipeline.""" size: int # chunk edge length (>= 0) extent: int # array dimension length nchunks: int = field(init=False, repr=False) ngridcells: int = field(init=False, repr=False) def __post_init__(self) -> None: if self.size < 0: raise ValueError(f"FixedDimension size must be >= 0, got {self.size}") if self.extent < 0: raise ValueError(f"FixedDimension extent must be >= 0, got {self.extent}") if self.size == 0: n = 0 else: n = ceildiv(self.extent, self.size) object.__setattr__(self, "nchunks", n) object.__setattr__(self, "ngridcells", n) def index_to_chunk(self, idx: int) -> int: if idx < 0: raise IndexError(f"Negative index {idx} is not allowed") if idx >= self.extent: raise IndexError(f"Index {idx} is out of bounds for extent {self.extent}") if self.size == 0: return 0 return idx // self.size def chunk_offset(self, chunk_ix: int) -> int: """Byte-aligned start position of chunk *chunk_ix* in array coordinates. Does not validate *chunk_ix* — callers must ensure it is in ``[0, nchunks)``. Use ``ChunkGrid.__getitem__`` for safe access. """ return chunk_ix * self.size def chunk_size(self, chunk_ix: int) -> int: """Buffer size for codec processing — always uniform. Does not validate *chunk_ix* — callers must ensure it is in ``[0, nchunks)``. Use ``ChunkGrid.__getitem__`` for safe access. """ return self.size def data_size(self, chunk_ix: int) -> int: """Valid data region within the buffer — clipped at extent. Does not validate *chunk_ix* — callers must ensure it is in ``[0, nchunks)``. Use ``ChunkGrid.__getitem__`` for safe access. """ if self.size == 0: return 0 return max(0, min(self.size, self.extent - chunk_ix * self.size)) @property def _unique_edge_lengths(self) -> Iterable[int]: """Distinct chunk edge lengths for this dimension. Used by shard validation to check that every unique edge length is divisible by the inner chunk size. O(1) for fixed dimensions since there is only one edge length. """ return (self.size,) def indices_to_chunks(self, indices: npt.NDArray[np.intp]) -> npt.NDArray[np.intp]: if self.size == 0: return np.zeros_like(indices) return indices // self.size def with_extent(self, new_extent: int) -> FixedDimension: """Re-bind to *new_extent* without modifying edges. Used when constructing a grid from existing metadata where edges are already correct. Raises on ``VaryingDimension`` if edges don't cover the new extent. """ return FixedDimension(size=self.size, extent=new_extent) def resize(self, new_extent: int) -> FixedDimension: """Adapt for a user-initiated array resize, growing edges if needed. For ``FixedDimension`` this is identical to ``with_extent`` since regular grids don't store explicit edges. """ return FixedDimension(size=self.size, extent=new_extent) @property def _size_repr(self) -> str: return str(self.size) @dataclass(frozen=True) class VaryingDimension: """Explicit per-chunk sizes. The last chunk may extend past the array extent (``extent < sum(edges)``), in which case ``data_size`` clips to the valid region while ``chunk_size`` returns the full edge length for codec processing. This underflow is allowed to match how regular grids handle boundary chunks, and to support shrinking an array without rewriting chunk edges (the spec allows trailing edges beyond the extent).""" edges: tuple[int, ...] # per-chunk edge lengths (all > 0) cumulative: tuple[int, ...] # prefix sums for O(log n) lookup extent: int # array dimension length (may be < sum(edges) after resize) nchunks: int = field(init=False, repr=False) # cached at construction ngridcells: int = field(init=False, repr=False) # cached at construction # TODO(perf): for long dimensions (O(million chunks)): # - with_extent/resize recompute cumulative sums and nchunks from scratch; # add a fast path that reuses the existing cumulative tuple. # - Consider storing cumulative as ndarray so bisect calls can use # np.searchsorted. Scalar lookups (chunk_offset, index_to_chunk) # would need benchmarking to confirm no regression. def __init__(self, edges: Sequence[int], extent: int) -> None: edges_tuple = tuple(edges) if not edges_tuple: raise ValueError("VaryingDimension edges must not be empty") if any(e <= 0 for e in edges_tuple): raise ValueError(f"All edge lengths must be > 0, got {edges_tuple}") cumulative = tuple(itertools.accumulate(edges_tuple)) if extent < 0: raise ValueError(f"VaryingDimension extent must be >= 0, got {extent}") if extent > cumulative[-1]: raise ValueError( f"VaryingDimension extent {extent} exceeds sum of edges {cumulative[-1]}" ) object.__setattr__(self, "edges", edges_tuple) object.__setattr__(self, "cumulative", cumulative) object.__setattr__(self, "extent", extent) # Cache nchunks: number of chunks that overlap [0, extent) if extent == 0: n = 0 else: n = bisect.bisect_left(cumulative, extent) + 1 object.__setattr__(self, "nchunks", n) object.__setattr__(self, "ngridcells", len(edges_tuple)) def index_to_chunk(self, idx: int) -> int: if idx < 0 or idx >= self.extent: raise IndexError(f"Index {idx} out of bounds for dimension with extent {self.extent}") return bisect.bisect_right(self.cumulative, idx) def chunk_offset(self, chunk_ix: int) -> int: """Start position of chunk *chunk_ix* in array coordinates. Does not validate *chunk_ix* — callers must ensure it is in ``[0, ngridcells)``. Use ``ChunkGrid.__getitem__`` for safe access. """ return self.cumulative[chunk_ix - 1] if chunk_ix > 0 else 0 def chunk_size(self, chunk_ix: int) -> int: """Buffer size for codec processing. Does not validate *chunk_ix* — callers must ensure it is in ``[0, ngridcells)``. Use ``ChunkGrid.__getitem__`` for safe access. """ return self.edges[chunk_ix] def data_size(self, chunk_ix: int) -> int: """Valid data region within the buffer — clipped at extent. Does not validate *chunk_ix* — callers must ensure it is in ``[0, ngridcells)``. Use ``ChunkGrid.__getitem__`` for safe access. """ offset = self.cumulative[chunk_ix - 1] if chunk_ix > 0 else 0 return max(0, min(self.edges[chunk_ix], self.extent - offset)) @property def _unique_edge_lengths(self) -> Iterable[int]: """Distinct chunk edge lengths for this dimension (lazily deduplicated). Used by shard validation to check that every unique edge length is divisible by the inner chunk size. Lazy deduplication avoids materializing all edges for dimensions with many repeated sizes. """ seen: set[int] = set() for e in self.edges: if e not in seen: seen.add(e) yield e def indices_to_chunks(self, indices: npt.NDArray[np.intp]) -> npt.NDArray[np.intp]: return np.searchsorted(self.cumulative, indices, side="right") def with_extent(self, new_extent: int) -> VaryingDimension: """Re-bind to *new_extent* without modifying edges. Used when constructing a grid from existing metadata where edges are already correct. Raises if the existing edges don't cover *new_extent*. """ edge_sum = self.cumulative[-1] if edge_sum < new_extent: raise ValueError( f"VaryingDimension edge sum {edge_sum} is less than new extent {new_extent}" ) return VaryingDimension(self.edges, extent=new_extent) def resize(self, new_extent: int) -> VaryingDimension: """Adapt for a user-initiated array resize, growing edges if needed. Unlike ``with_extent``, this never fails — if *new_extent* exceeds the current edge sum, a new chunk is appended to cover the gap. Shrinking preserves all edges (the spec allows trailing edges beyond the array extent). """ if new_extent == self.extent: return self elif new_extent > self.cumulative[-1]: expanded_edges = list(self.edges) + [new_extent - self.cumulative[-1]] return VaryingDimension(expanded_edges, extent=new_extent) else: return VaryingDimension(self.edges, extent=new_extent) @property def _size_repr(self) -> str: return repr(tuple(self.edges)) @runtime_checkable class DimensionGrid(Protocol): """Structural interface shared by FixedDimension and VaryingDimension.""" @property def nchunks(self) -> int: ... @property def ngridcells(self) -> int: ... @property def extent(self) -> int: ... def index_to_chunk(self, idx: int) -> int: ... def chunk_offset(self, chunk_ix: int) -> int: ... def chunk_size(self, chunk_ix: int) -> int: ... def data_size(self, chunk_ix: int) -> int: ... def indices_to_chunks(self, indices: npt.NDArray[np.intp]) -> npt.NDArray[np.intp]: ... @property def _unique_edge_lengths(self) -> Iterable[int]: ... def with_extent(self, new_extent: int) -> DimensionGrid: ... def resize(self, new_extent: int) -> DimensionGrid: ... @property def _size_repr(self) -> str: ... @dataclass(frozen=True) class ChunkSpec: """Specification of a single chunk's location and size. ``slices`` gives the valid data region in array coordinates. ``codec_shape`` gives the buffer shape for codec processing. For interior chunks these are equal. For boundary chunks of a regular grid, ``codec_shape`` is the full declared chunk size while ``shape`` is clipped. For rectilinear grids, ``shape == codec_shape`` unless the last chunk extends past the array extent. """ slices: tuple[slice, ...] codec_shape: tuple[int, ...] @property def shape(self) -> tuple[int, ...]: return tuple(s.stop - s.start for s in self.slices) @property def is_boundary(self) -> bool: return self.shape != self.codec_shape # A single dimension's rectilinear chunk spec: bare int (uniform shorthand), # list of ints (explicit edges), or mixed RLE (e.g. [[10, 3], 5]). def _is_rectilinear_chunks(chunks: Any) -> TypeGuard[Sequence[Sequence[int]]]: """Check if chunks is a nested sequence (e.g. [[10, 20], [5, 5]]). Returns True for inputs like [[10, 20], [5, 5]] or [(10, 20), (5, 5)]. Returns False for flat sequences like (10, 10) or [10, 10]. """ if isinstance(chunks, (str, int, ChunkGrid)): return False if not hasattr(chunks, "__iter__"): return False try: first_elem = next(iter(chunks), None) if first_elem is None: return False return hasattr(first_elem, "__iter__") and not isinstance(first_elem, (str, bytes, int)) except (TypeError, StopIteration): return False @dataclass(frozen=True) class ChunkGrid: """ Unified chunk grid supporting both regular and rectilinear chunking. A chunk grid is a concrete arrangement of chunks for a specific array. It stores the extent (array dimension length) per dimension, enabling ``grid[coords]`` to return a ``ChunkSpec`` without external parameters. Internally represents each dimension as either FixedDimension (uniform chunks) or VaryingDimension (per-chunk edge lengths with prefix sums). """ _dimensions: tuple[DimensionGrid, ...] _is_regular: bool def __init__(self, *, dimensions: tuple[DimensionGrid, ...]) -> None: object.__setattr__(self, "_dimensions", dimensions) object.__setattr__( self, "_is_regular", all(isinstance(d, FixedDimension) for d in dimensions) ) def __repr__(self) -> str: sizes = ", ".join(d._size_repr for d in self._dimensions) shape = tuple(d.extent for d in self._dimensions) return f"ChunkGrid(chunk_sizes=({sizes}), array_shape={shape})" @classmethod def from_metadata(cls, metadata: ArrayMetadata) -> ChunkGrid: """Construct a ChunkGrid from array metadata. For v2 metadata, builds from shape and chunks. For v3 metadata, dispatches on the chunk grid type. """ from zarr.core.metadata import ArrayV2Metadata from zarr.core.metadata.v3 import RectilinearChunkGridMetadata, RegularChunkGridMetadata if isinstance(metadata, ArrayV2Metadata): return cls.from_sizes(metadata.shape, tuple(metadata.chunks)) chunk_grid_meta = metadata.chunk_grid if isinstance(chunk_grid_meta, RegularChunkGridMetadata): return cls.from_sizes(metadata.shape, tuple(chunk_grid_meta.chunk_shape)) elif isinstance(chunk_grid_meta, RectilinearChunkGridMetadata): return cls.from_sizes(metadata.shape, chunk_grid_meta.chunk_shapes) else: raise TypeError(f"Unknown chunk grid metadata type: {type(chunk_grid_meta)}") @classmethod def from_sizes( cls, array_shape: ShapeLike, chunk_sizes: Sequence[int | Sequence[int]], ) -> ChunkGrid: """Create a ChunkGrid from per-dimension chunk size specifications. Parameters ---------- array_shape The array shape (one extent per dimension). chunk_sizes Per-dimension chunk sizes. Each element is either: - An ``int`` — regular (fixed) chunk size for that dimension. - A ``Sequence[int]`` — explicit per-chunk edge lengths. If all edges are identical and cover the extent, the dimension is stored as ``FixedDimension``; otherwise as ``VaryingDimension``. """ extents = parse_shapelike(array_shape) if len(extents) != len(chunk_sizes): raise ValueError( f"array_shape has {len(extents)} dimensions but chunk_sizes " f"has {len(chunk_sizes)} dimensions" ) dims: list[DimensionGrid] = [] for dim_spec, extent in zip(chunk_sizes, extents, strict=True): if isinstance(dim_spec, int): dims.append(FixedDimension(size=dim_spec, extent=extent)) else: edges_list = list(dim_spec) if not edges_list: raise ValueError("Each dimension must have at least one chunk") edge_sum = sum(edges_list) if ( edges_list[0] > 0 and all(e == edges_list[0] for e in edges_list) and (extent == edge_sum or len(edges_list) == ceildiv(extent, edges_list[0])) ): dims.append(FixedDimension(size=edges_list[0], extent=extent)) else: dims.append(VaryingDimension(edges_list, extent=extent)) return cls(dimensions=tuple(dims)) # -- Properties -- @property def ndim(self) -> int: return len(self._dimensions) @property def is_regular(self) -> bool: return self._is_regular @property def grid_shape(self) -> tuple[int, ...]: """Number of chunks per dimension.""" return tuple(d.nchunks for d in self._dimensions) @property def chunk_shape(self) -> tuple[int, ...]: """Return the uniform chunk shape. Raises if grid is not regular.""" if not self.is_regular: raise ValueError( "chunk_shape is only available for regular chunk grids. " "Use grid[coords] for per-chunk sizes." ) return tuple(d.size for d in self._dimensions if isinstance(d, FixedDimension)) @property def chunk_sizes(self) -> tuple[tuple[int, ...], ...]: """Per-dimension chunk sizes, including the final boundary chunk. Returns the actual data size of each chunk (clipped at the array extent), matching the dask ``Array.chunks`` convention. Works for both regular and rectilinear grids. Returns ------- tuple[tuple[int, ...], ...] One inner tuple per dimension, each containing the data size of every chunk along that dimension. """ return tuple(tuple(d.data_size(i) for i in range(d.nchunks)) for d in self._dimensions) # -- Collection interface -- def __getitem__(self, coords: int | tuple[int, ...]) -> ChunkSpec | None: """Return the ChunkSpec for a chunk at the given grid position, or None if OOB.""" if isinstance(coords, int): coords = (coords,) if len(coords) != self.ndim: raise ValueError( f"Expected {self.ndim} coordinate(s) for a {self.ndim}-d chunk grid, " f"got {len(coords)}." ) slices: list[slice] = [] codec_shape: list[int] = [] for dim, ix in zip(self._dimensions, coords, strict=True): if ix < 0 or ix >= dim.nchunks: return None offset = dim.chunk_offset(ix) slices.append(slice(offset, offset + dim.data_size(ix), 1)) codec_shape.append(dim.chunk_size(ix)) return ChunkSpec(tuple(slices), tuple(codec_shape)) def __iter__(self) -> Iterator[ChunkSpec]: """Iterate all chunks, yielding ChunkSpec for each.""" for coords in itertools.product(*(range(d.nchunks) for d in self._dimensions)): spec = self[coords] if spec is not None: yield spec def all_chunk_coords( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, ) -> Iterator[tuple[int, ...]]: """Iterate over chunk coordinates, optionally restricted to a subregion. Parameters ---------- origin : Sequence[int] | None The first chunk coordinate to return. Defaults to the grid origin. selection_shape : Sequence[int] | None The number of chunks per dimension to iterate. Defaults to the remaining extent from origin. """ if origin is None: origin_parsed = (0,) * self.ndim else: origin_parsed = tuple(origin) if selection_shape is None: selection_shape_parsed = tuple( g - o for o, g in zip(origin_parsed, self.grid_shape, strict=True) ) else: selection_shape_parsed = tuple(selection_shape) ranges = tuple( range(o, o + s) for o, s in zip(origin_parsed, selection_shape_parsed, strict=True) ) return itertools.product(*ranges) def iter_chunk_regions( self, *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, ) -> Iterator[tuple[slice, ...]]: """Iterate over the data regions (slices) spanned by each chunk. Parameters ---------- origin : Sequence[int] | None The first chunk coordinate to return. Defaults to the grid origin. selection_shape : Sequence[int] | None The number of chunks per dimension to iterate. Defaults to the remaining extent from origin. """ for coords in self.all_chunk_coords(origin=origin, selection_shape=selection_shape): spec = self[coords] if spec is not None: yield spec.slices def get_nchunks(self) -> int: return reduce(operator.mul, (d.nchunks for d in self._dimensions), 1) # -- Resize -- def update_shape(self, new_shape: tuple[int, ...]) -> ChunkGrid: """Return a new ChunkGrid adjusted for *new_shape*. For regular (FixedDimension) axes the extent is simply re-bound. For varying (VaryingDimension) axes: * **grow**: a new chunk whose size equals the growth is appended. * **shrink**: trailing chunks that lie entirely beyond *new_shape* are dropped; the last retained chunk is the one whose cumulative offset first reaches or exceeds the new extent. * **no change**: the dimension is kept as-is. Raises ------ ValueError If *new_shape* has the wrong number of dimensions. """ if len(new_shape) != self.ndim: raise ValueError( f"new_shape has {len(new_shape)} dimensions but " f"chunk grid has {self.ndim} dimensions" ) dims = tuple( dim.resize(new_extent) for dim, new_extent in zip(self._dimensions, new_shape, strict=True) ) return ChunkGrid(dimensions=dims) def _guess_chunks( shape: tuple[int, ...] | int, typesize: int, *, increment_bytes: int = 256 * 1024, min_bytes: int = 128 * 1024, max_bytes: int = 64 * 1024 * 1024, ) -> tuple[int, ...]: """ Iteratively guess an appropriate chunk layout for an array, given its shape and the size of each element in bytes, and size constraints expressed in bytes. This logic is adapted from h5py. Parameters ---------- shape : tuple[int, ...] The chunk shape. typesize : int The size, in bytes, of each element of the chunk. increment_bytes : int = 256 * 1024 The number of bytes used to increment or decrement the target chunk size in bytes. min_bytes : int = 128 * 1024 The soft lower bound on the final chunk size in bytes. max_bytes : int = 64 * 1024 * 1024 The hard upper bound on the final chunk size in bytes. Returns ------- tuple[int, ...] """ if min_bytes >= max_bytes: raise ValueError(f"Cannot have more min_bytes ({min_bytes}) than max_bytes ({max_bytes})") if isinstance(shape, int): shape = (shape,) if typesize == 0: return shape ndims = len(shape) # require chunks to have non-zero length for all dimensions chunks = np.maximum(np.array(shape, dtype="=f8"), 1) # Determine the optimal chunk size in bytes using a PyTables expression. # This is kept as a float. dset_size = np.prod(chunks) * typesize target_size = increment_bytes * (2 ** np.log10(dset_size / (1024.0 * 1024))) if target_size > max_bytes: target_size = max_bytes elif target_size < min_bytes: target_size = min_bytes idx = 0 while True: # Repeatedly loop over the axes, dividing them by 2. Stop when: # 1a. We're smaller than the target chunk size, OR # 1b. We're within 50% of the target chunk size, AND # 2. The chunk is smaller than the maximum chunk size chunk_bytes = np.prod(chunks) * typesize if ( chunk_bytes < target_size or abs(chunk_bytes - target_size) / target_size < 0.5 ) and chunk_bytes < max_bytes: break if np.prod(chunks) == 1: break # Element size larger than max_bytes chunks[idx % ndims] = math.ceil(chunks[idx % ndims] / 2.0) idx += 1 return tuple(int(x) for x in chunks) def normalize_chunks(chunks: Any, shape: tuple[int, ...], typesize: int) -> tuple[int, ...]: """Convenience function to normalize the `chunks` argument for an array with the given `shape`.""" # N.B., expect shape already normalized # handle auto-chunking if chunks is None or chunks is True: return _guess_chunks(shape, typesize) # handle no chunking if chunks is False: return shape # handle 1D convenience form if isinstance(chunks, numbers.Integral): chunks = tuple(int(chunks) for _ in shape) # handle dask-style chunks (iterable of iterables) if all(isinstance(c, (tuple, list)) for c in chunks): for i, c in enumerate(chunks): if any(x != y for x, y in itertools.pairwise(c[:-1])) or (len(c) > 1 and c[-1] > c[0]): raise ValueError( f"Irregular chunk sizes in dimension {i}: {tuple(c)}. " "Only uniform chunks (with an optional smaller final chunk) are supported." ) chunks = tuple(c[0] for c in chunks) # handle bad dimensionality if len(chunks) > len(shape): raise ValueError("too many dimensions in chunks") # handle underspecified chunks if len(chunks) < len(shape): # assume chunks across remaining dimensions chunks += shape[len(chunks) :] # handle None or -1 in chunks if -1 in chunks or None in chunks: chunks = tuple( s if c == -1 or c is None else int(c) for s, c in zip(shape, chunks, strict=False) ) if not all(isinstance(c, numbers.Integral) for c in chunks): raise TypeError("non integer value in chunks") return tuple(int(c) for c in chunks) def _guess_num_chunks_per_axis_shard( chunk_shape: tuple[int, ...], item_size: int, max_bytes: int, array_shape: tuple[int, ...] ) -> int: """Generate the number of chunks per axis to hit a target max byte size for a shard. For example, for a (2,2,2) chunk size and item size 4, maximum bytes of 256 would return 2. In other words the shard would be a (2,2,2) grid of (2,2,2) chunks i.e., prod(chunk_shape) * (returned_val * len(chunk_shape)) * item_size = 256 bytes. Parameters ---------- chunk_shape The shape of the (inner) chunks. item_size The item size of the data i.e., 2 for uint16. max_bytes The maximum number of bytes per shard to allow. array_shape The shape of the underlying array. Returns ------- The number of chunks per axis. """ bytes_per_chunk = np.prod(chunk_shape) * item_size if max_bytes < bytes_per_chunk: return 1 num_axes = len(chunk_shape) chunks_per_shard = 1 # First check for byte size, second check to make sure we don't go bigger than the array shape while (bytes_per_chunk * ((chunks_per_shard + 1) ** num_axes)) <= max_bytes and all( c * (chunks_per_shard + 1) <= a for c, a in zip(chunk_shape, array_shape, strict=True) ): chunks_per_shard += 1 return chunks_per_shard def _auto_partition( *, array_shape: tuple[int, ...], chunk_shape: tuple[int, ...] | Literal["auto"], shard_shape: ShardsLike | None, item_size: int, ) -> tuple[tuple[int, ...] | None, tuple[int, ...]]: """ Automatically determine the shard shape and chunk shape for an array, given the shape and dtype of the array. If `shard_shape` is `None` and the chunk_shape is "auto", the chunks will be set heuristically based on the dtype and shape of the array. If `shard_shape` is "auto", then the shard shape will be set heuristically from the dtype and shape of the array; if the `chunk_shape` is also "auto", then the chunks will be set heuristically as well, given the dtype and shard shape. Otherwise, the chunks will be returned as-is. """ if shard_shape is None: _shards_out: None | tuple[int, ...] = None if chunk_shape == "auto": _chunks_out = _guess_chunks(array_shape, item_size) else: _chunks_out = chunk_shape else: if chunk_shape == "auto": # aim for a 1MiB chunk _chunks_out = _guess_chunks(array_shape, item_size, max_bytes=1048576) else: _chunks_out = chunk_shape if shard_shape == "auto": warnings.warn( "Automatic shard shape inference is experimental and may change without notice.", ZarrUserWarning, stacklevel=2, ) _shards_out = () target_shard_size_bytes = zarr.config.get("array.target_shard_size_bytes", None) num_chunks_per_shard_axis = ( _guess_num_chunks_per_axis_shard( chunk_shape=_chunks_out, item_size=item_size, max_bytes=target_shard_size_bytes, array_shape=array_shape, ) if (has_auto_shard := (target_shard_size_bytes is not None)) else 2 ) for a_shape, c_shape in zip(array_shape, _chunks_out, strict=True): # The previous heuristic was `a_shape // c_shape > 8` and now, with target_shard_size_bytes, we only check that the shard size is less than the array size. can_shard_axis = a_shape // c_shape > 8 if not has_auto_shard else True if can_shard_axis: _shards_out += (c_shape * num_chunks_per_shard_axis,) else: _shards_out += (c_shape,) elif isinstance(shard_shape, dict): _shards_out = tuple(shard_shape["shape"]) else: _shards_out = cast("tuple[int, ...]", shard_shape) return _shards_out, _chunks_out zarr-python-3.2.1/src/zarr/core/chunk_key_encodings.py000066400000000000000000000104121517635743000231030ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypedDict, cast if TYPE_CHECKING: from typing import NotRequired, Self from zarr.abc.metadata import Metadata from zarr.core.common import ( JSON, NamedConfig, parse_named_configuration, ) from zarr.registry import get_chunk_key_encoding_class, register_chunk_key_encoding SeparatorLiteral = Literal[".", "/"] def parse_separator(data: JSON) -> SeparatorLiteral: if data not in (".", "/"): raise ValueError(f"Expected an '.' or '/' separator. Got {data} instead.") return cast("SeparatorLiteral", data) class ChunkKeyEncodingParams(TypedDict): name: Literal["v2", "default"] separator: NotRequired[SeparatorLiteral] @dataclass(frozen=True) class ChunkKeyEncoding(ABC, Metadata): """ Defines how chunk coordinates are mapped to store keys. Subclasses must define a class variable `name` and implement `encode_chunk_key`. """ name: ClassVar[str] @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: _, config_parsed = parse_named_configuration(data, require_configuration=False) return cls(**config_parsed or {}) def to_dict(self) -> dict[str, JSON]: return {"name": self.name, "configuration": super().to_dict()} def decode_chunk_key(self, chunk_key: str) -> tuple[int, ...]: """ Optional: decode a chunk key string into chunk coordinates. Not required for normal operation; override if needed for testing or debugging. """ raise NotImplementedError(f"{self.__class__.__name__} does not implement decode_chunk_key.") @abstractmethod def encode_chunk_key(self, chunk_coords: tuple[int, ...]) -> str: """ Encode chunk coordinates into a chunk key string. Must be implemented by subclasses. """ type ChunkKeyEncodingLike = ( dict[str, JSON] | ChunkKeyEncodingParams | ChunkKeyEncoding | NamedConfig[str, Any] ) @dataclass(frozen=True) class DefaultChunkKeyEncoding(ChunkKeyEncoding): name: ClassVar[Literal["default"]] = "default" separator: SeparatorLiteral = "/" def __post_init__(self) -> None: separator_parsed = parse_separator(self.separator) object.__setattr__(self, "separator", separator_parsed) def decode_chunk_key(self, chunk_key: str) -> tuple[int, ...]: if chunk_key == "c": return () return tuple(map(int, chunk_key[1:].split(self.separator))) def encode_chunk_key(self, chunk_coords: tuple[int, ...]) -> str: return self.separator.join(map(str, ("c",) + chunk_coords)) @dataclass(frozen=True) class V2ChunkKeyEncoding(ChunkKeyEncoding): name: ClassVar[Literal["v2"]] = "v2" separator: SeparatorLiteral = "." def __post_init__(self) -> None: separator_parsed = parse_separator(self.separator) object.__setattr__(self, "separator", separator_parsed) def decode_chunk_key(self, chunk_key: str) -> tuple[int, ...]: return tuple(map(int, chunk_key.split(self.separator))) def encode_chunk_key(self, chunk_coords: tuple[int, ...]) -> str: chunk_identifier = self.separator.join(map(str, chunk_coords)) return "0" if chunk_identifier == "" else chunk_identifier def parse_chunk_key_encoding(data: ChunkKeyEncodingLike) -> ChunkKeyEncoding: """ Take an implicit specification of a chunk key encoding and parse it into a ChunkKeyEncoding object. """ if isinstance(data, ChunkKeyEncoding): return data # handle ChunkKeyEncodingParams if "name" in data and "separator" in data: data = {"name": data["name"], "configuration": {"separator": data["separator"]}} # type: ignore[typeddict-item] # Now must be a named config data = cast("dict[str, JSON]", data) name_parsed, _ = parse_named_configuration(data, require_configuration=False) try: chunk_key_encoding = get_chunk_key_encoding_class(name_parsed).from_dict(data) except KeyError as e: raise ValueError(f"Unknown chunk key encoding: {e.args[0]!r}") from e return chunk_key_encoding register_chunk_key_encoding("default", DefaultChunkKeyEncoding) register_chunk_key_encoding("v2", V2ChunkKeyEncoding) zarr-python-3.2.1/src/zarr/core/codec_pipeline.py000066400000000000000000000645451517635743000220540ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from itertools import islice, pairwise from typing import TYPE_CHECKING, Any from warnings import warn from zarr.abc.codec import ( ArrayArrayCodec, ArrayBytesCodec, ArrayBytesCodecPartialDecodeMixin, ArrayBytesCodecPartialEncodeMixin, BytesBytesCodec, Codec, CodecPipeline, GetResult, SupportsSyncCodec, ) from zarr.core.common import concurrent_map from zarr.core.config import config from zarr.core.indexing import SelectorTuple, is_scalar from zarr.errors import ZarrUserWarning from zarr.registry import register_pipeline if TYPE_CHECKING: from collections.abc import Iterable, Iterator from typing import Self from zarr.abc.store import ByteGetter, ByteSetter from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer, BufferPrototype, NDBuffer from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType from zarr.core.metadata.v3 import ChunkGridMetadata def _unzip2[T, U](iterable: Iterable[tuple[T, U]]) -> tuple[list[T], list[U]]: out0: list[T] = [] out1: list[U] = [] for item0, item1 in iterable: out0.append(item0) out1.append(item1) return (out0, out1) def batched[T](iterable: Iterable[T], n: int) -> Iterable[tuple[T, ...]]: if n < 1: raise ValueError("n must be at least one") it = iter(iterable) while batch := tuple(islice(it, n)): yield batch def resolve_batched(codec: Codec, chunk_specs: Iterable[ArraySpec]) -> Iterable[ArraySpec]: return [codec.resolve_metadata(chunk_spec) for chunk_spec in chunk_specs] def fill_value_or_default(chunk_spec: ArraySpec) -> Any: fill_value = chunk_spec.fill_value if fill_value is None: # Zarr V2 allowed `fill_value` to be null in the metadata. # Zarr V3 requires it to be set. This has already been # validated when decoding the metadata, but we support reading # Zarr V2 data and need to support the case where fill_value # is None. return chunk_spec.dtype.default_scalar() else: return fill_value @dataclass(slots=True, kw_only=True) class ChunkTransform: """A synchronous codec chain bound to an ArraySpec. Provides `encode` and `decode` for pure-compute codec operations (no IO, no threading, no batching). All codecs must implement `SupportsSyncCodec`. Construction will raise `TypeError` if any codec does not. """ codecs: tuple[Codec, ...] array_spec: ArraySpec # (sync codec, input_spec) pairs in pipeline order. _aa_codecs: tuple[tuple[SupportsSyncCodec[NDBuffer, NDBuffer], ArraySpec], ...] = field( init=False, repr=False, compare=False ) _ab_codec: SupportsSyncCodec[NDBuffer, Buffer] = field(init=False, repr=False, compare=False) _ab_spec: ArraySpec = field(init=False, repr=False, compare=False) _bb_codecs: tuple[SupportsSyncCodec[Buffer, Buffer], ...] = field( init=False, repr=False, compare=False ) def __post_init__(self) -> None: non_sync = [c for c in self.codecs if not isinstance(c, SupportsSyncCodec)] if non_sync: names = ", ".join(type(c).__name__ for c in non_sync) raise TypeError( f"All codecs must implement SupportsSyncCodec. The following do not: {names}" ) aa, ab, bb = codecs_from_list(list(self.codecs)) aa_codecs: list[tuple[SupportsSyncCodec[NDBuffer, NDBuffer], ArraySpec]] = [] spec = self.array_spec for aa_codec in aa: assert isinstance(aa_codec, SupportsSyncCodec) aa_codecs.append((aa_codec, spec)) spec = aa_codec.resolve_metadata(spec) self._aa_codecs = tuple(aa_codecs) assert isinstance(ab, SupportsSyncCodec) self._ab_codec = ab self._ab_spec = spec bb_sync: list[SupportsSyncCodec[Buffer, Buffer]] = [] for bb_codec in bb: assert isinstance(bb_codec, SupportsSyncCodec) bb_sync.append(bb_codec) self._bb_codecs = tuple(bb_sync) def decode( self, chunk_bytes: Buffer, ) -> NDBuffer: """Decode a single chunk through the full codec chain, synchronously. Pure compute -- no IO. """ data: Buffer = chunk_bytes for bb_codec in reversed(self._bb_codecs): data = bb_codec._decode_sync(data, self._ab_spec) chunk_array: NDBuffer = self._ab_codec._decode_sync(data, self._ab_spec) for aa_codec, spec in reversed(self._aa_codecs): chunk_array = aa_codec._decode_sync(chunk_array, spec) return chunk_array def encode( self, chunk_array: NDBuffer, ) -> Buffer | None: """Encode a single chunk through the full codec chain, synchronously. Pure compute -- no IO. """ aa_data: NDBuffer = chunk_array for aa_codec, spec in self._aa_codecs: aa_result = aa_codec._encode_sync(aa_data, spec) if aa_result is None: return None aa_data = aa_result ab_result = self._ab_codec._encode_sync(aa_data, self._ab_spec) if ab_result is None: return None bb_data: Buffer = ab_result for bb_codec in self._bb_codecs: bb_result = bb_codec._encode_sync(bb_data, self._ab_spec) if bb_result is None: return None bb_data = bb_result return bb_data def compute_encoded_size(self, byte_length: int, array_spec: ArraySpec) -> int: for codec in self.codecs: byte_length = codec.compute_encoded_size(byte_length, array_spec) array_spec = codec.resolve_metadata(array_spec) return byte_length @dataclass(frozen=True) class BatchedCodecPipeline(CodecPipeline): """Default codec pipeline. This batched codec pipeline divides the chunk batches into batches of a configurable batch size ("mini-batch"). Fetching, decoding, encoding and storing are performed in lock step for each mini-batch. Multiple mini-batches are processing concurrently. """ array_array_codecs: tuple[ArrayArrayCodec, ...] array_bytes_codec: ArrayBytesCodec bytes_bytes_codecs: tuple[BytesBytesCodec, ...] batch_size: int def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: # Each codec must be evolved against the spec it will actually see # at run-time, not the original array spec. Earlier array->array # codecs may transform the dtype (e.g. cast_value), so the spec # threaded into later codecs (the array->bytes serializer and any # bytes->bytes filters) must reflect those transformations. evolved: list[Codec] = [] spec = array_spec for codec in self: evolved_codec = codec.evolve_from_array_spec(array_spec=spec) evolved.append(evolved_codec) spec = evolved_codec.resolve_metadata(spec) return type(self).from_codecs(evolved) @classmethod def from_codecs(cls, codecs: Iterable[Codec], *, batch_size: int | None = None) -> Self: array_array_codecs, array_bytes_codec, bytes_bytes_codecs = codecs_from_list(codecs) return cls( array_array_codecs=array_array_codecs, array_bytes_codec=array_bytes_codec, bytes_bytes_codecs=bytes_bytes_codecs, batch_size=batch_size or config.get("codec_pipeline.batch_size"), ) @property def supports_partial_decode(self) -> bool: """Determines whether the codec pipeline supports partial decoding. Currently, only codec pipelines with a single ArrayBytesCodec that supports partial decoding can support partial decoding. This limitation is due to the fact that ArrayArrayCodecs can change the slice selection leading to non-contiguous slices and BytesBytesCodecs can change the chunk bytes in a way that slice selections cannot be attributed to byte ranges anymore which renders partial decoding infeasible. This limitation may softened in the future.""" return (len(self.array_array_codecs) + len(self.bytes_bytes_codecs)) == 0 and isinstance( self.array_bytes_codec, ArrayBytesCodecPartialDecodeMixin ) @property def supports_partial_encode(self) -> bool: """Determines whether the codec pipeline supports partial encoding. Currently, only codec pipelines with a single ArrayBytesCodec that supports partial encoding can support partial encoding. This limitation is due to the fact that ArrayArrayCodecs can change the slice selection leading to non-contiguous slices and BytesBytesCodecs can change the chunk bytes in a way that slice selections cannot be attributed to byte ranges anymore which renders partial encoding infeasible. This limitation may softened in the future.""" return (len(self.array_array_codecs) + len(self.bytes_bytes_codecs)) == 0 and isinstance( self.array_bytes_codec, ArrayBytesCodecPartialEncodeMixin ) def __iter__(self) -> Iterator[Codec]: yield from self.array_array_codecs yield self.array_bytes_codec yield from self.bytes_bytes_codecs def validate( self, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunk_grid: ChunkGridMetadata, ) -> None: for codec in self: codec.validate(shape=shape, dtype=dtype, chunk_grid=chunk_grid) def compute_encoded_size(self, byte_length: int, array_spec: ArraySpec) -> int: for codec in self: byte_length = codec.compute_encoded_size(byte_length, array_spec) array_spec = codec.resolve_metadata(array_spec) return byte_length def _codecs_with_resolved_metadata_batched( self, chunk_specs: Iterable[ArraySpec] ) -> tuple[ list[tuple[ArrayArrayCodec, list[ArraySpec]]], tuple[ArrayBytesCodec, list[ArraySpec]], list[tuple[BytesBytesCodec, list[ArraySpec]]], ]: aa_codecs_with_spec: list[tuple[ArrayArrayCodec, list[ArraySpec]]] = [] chunk_specs = list(chunk_specs) for aa_codec in self.array_array_codecs: aa_codecs_with_spec.append((aa_codec, chunk_specs)) chunk_specs = [aa_codec.resolve_metadata(chunk_spec) for chunk_spec in chunk_specs] ab_codec_with_spec = (self.array_bytes_codec, chunk_specs) chunk_specs = [ self.array_bytes_codec.resolve_metadata(chunk_spec) for chunk_spec in chunk_specs ] bb_codecs_with_spec: list[tuple[BytesBytesCodec, list[ArraySpec]]] = [] for bb_codec in self.bytes_bytes_codecs: bb_codecs_with_spec.append((bb_codec, chunk_specs)) chunk_specs = [bb_codec.resolve_metadata(chunk_spec) for chunk_spec in chunk_specs] return (aa_codecs_with_spec, ab_codec_with_spec, bb_codecs_with_spec) async def decode_batch( self, chunk_bytes_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], ) -> Iterable[NDBuffer | None]: chunk_bytes_batch: Iterable[Buffer | None] chunk_bytes_batch, chunk_specs = _unzip2(chunk_bytes_and_specs) ( aa_codecs_with_spec, ab_codec_with_spec, bb_codecs_with_spec, ) = self._codecs_with_resolved_metadata_batched(chunk_specs) for bb_codec, chunk_spec_batch in bb_codecs_with_spec[::-1]: chunk_bytes_batch = await bb_codec.decode( zip(chunk_bytes_batch, chunk_spec_batch, strict=False) ) ab_codec, chunk_spec_batch = ab_codec_with_spec chunk_array_batch = await ab_codec.decode( zip(chunk_bytes_batch, chunk_spec_batch, strict=False) ) for aa_codec, chunk_spec_batch in aa_codecs_with_spec[::-1]: chunk_array_batch = await aa_codec.decode( zip(chunk_array_batch, chunk_spec_batch, strict=False) ) return chunk_array_batch async def decode_partial_batch( self, batch_info: Iterable[tuple[ByteGetter, SelectorTuple, ArraySpec]], ) -> Iterable[NDBuffer | None]: assert self.supports_partial_decode assert isinstance(self.array_bytes_codec, ArrayBytesCodecPartialDecodeMixin) return await self.array_bytes_codec.decode_partial(batch_info) async def encode_batch( self, chunk_arrays_and_specs: Iterable[tuple[NDBuffer | None, ArraySpec]], ) -> Iterable[Buffer | None]: chunk_array_batch: Iterable[NDBuffer | None] chunk_specs: Iterable[ArraySpec] chunk_array_batch, chunk_specs = _unzip2(chunk_arrays_and_specs) for aa_codec in self.array_array_codecs: chunk_array_batch = await aa_codec.encode( zip(chunk_array_batch, chunk_specs, strict=False) ) chunk_specs = resolve_batched(aa_codec, chunk_specs) chunk_bytes_batch = await self.array_bytes_codec.encode( zip(chunk_array_batch, chunk_specs, strict=False) ) chunk_specs = resolve_batched(self.array_bytes_codec, chunk_specs) for bb_codec in self.bytes_bytes_codecs: chunk_bytes_batch = await bb_codec.encode( zip(chunk_bytes_batch, chunk_specs, strict=False) ) chunk_specs = resolve_batched(bb_codec, chunk_specs) return chunk_bytes_batch async def encode_partial_batch( self, batch_info: Iterable[tuple[ByteSetter, NDBuffer, SelectorTuple, ArraySpec]], ) -> None: assert self.supports_partial_encode assert isinstance(self.array_bytes_codec, ArrayBytesCodecPartialEncodeMixin) await self.array_bytes_codec.encode_partial(batch_info) async def read_batch( self, batch_info: Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]], out: NDBuffer, drop_axes: tuple[int, ...] = (), ) -> tuple[GetResult, ...]: results: list[GetResult] = [] if self.supports_partial_decode: batch_info_list = list(batch_info) chunk_array_batch = await self.decode_partial_batch( [ (byte_getter, chunk_selection, chunk_spec) for byte_getter, chunk_spec, chunk_selection, *_ in batch_info_list ] ) for chunk_array, (_, chunk_spec, _, out_selection, _) in zip( chunk_array_batch, batch_info_list, strict=False ): if chunk_array is not None: if drop_axes: chunk_array = chunk_array.squeeze(axis=drop_axes) out[out_selection] = chunk_array results.append(GetResult(status="present")) else: out[out_selection] = fill_value_or_default(chunk_spec) results.append(GetResult(status="missing")) else: batch_info_list = list(batch_info) chunk_bytes_batch = await concurrent_map( [ (byte_getter, array_spec.prototype) for byte_getter, array_spec, *_ in batch_info_list ], lambda byte_getter, prototype: byte_getter.get(prototype), config.get("async.concurrency"), ) chunk_array_batch = await self.decode_batch( [ (chunk_bytes, chunk_spec) for chunk_bytes, (_, chunk_spec, *_) in zip( chunk_bytes_batch, batch_info_list, strict=False ) ], ) for chunk_array, (_, chunk_spec, chunk_selection, out_selection, _) in zip( chunk_array_batch, batch_info_list, strict=False ): if chunk_array is not None: tmp = chunk_array[chunk_selection] if drop_axes: tmp = tmp.squeeze(axis=drop_axes) out[out_selection] = tmp results.append(GetResult(status="present")) else: out[out_selection] = fill_value_or_default(chunk_spec) results.append(GetResult(status="missing")) return tuple(results) def _merge_chunk_array( self, existing_chunk_array: NDBuffer | None, value: NDBuffer, out_selection: SelectorTuple, chunk_spec: ArraySpec, chunk_selection: SelectorTuple, is_complete_chunk: bool, drop_axes: tuple[int, ...], ) -> NDBuffer: if ( is_complete_chunk and value.shape == chunk_spec.shape # Guard that this is not a partial chunk at the end with is_complete_chunk=True and value[out_selection].shape == chunk_spec.shape ): return value if existing_chunk_array is None: chunk_array = chunk_spec.prototype.nd_buffer.create( shape=chunk_spec.shape, dtype=chunk_spec.dtype.to_native_dtype(), order=chunk_spec.order, fill_value=fill_value_or_default(chunk_spec), ) else: chunk_array = existing_chunk_array.copy() # make a writable copy if chunk_selection == () or is_scalar( value.as_ndarray_like(), chunk_spec.dtype.to_native_dtype() ): chunk_value = value else: chunk_value = value[out_selection] # handle missing singleton dimensions if drop_axes: item = tuple( None # equivalent to np.newaxis if idx in drop_axes else slice(None) for idx in range(chunk_spec.ndim) ) chunk_value = chunk_value[item] chunk_array[chunk_selection] = chunk_value return chunk_array async def write_batch( self, batch_info: Iterable[tuple[ByteSetter, ArraySpec, SelectorTuple, SelectorTuple, bool]], value: NDBuffer, drop_axes: tuple[int, ...] = (), ) -> None: if self.supports_partial_encode: # Pass scalar values as is if len(value.shape) == 0: await self.encode_partial_batch( [ (byte_setter, value, chunk_selection, chunk_spec) for byte_setter, chunk_spec, chunk_selection, out_selection, _ in batch_info ], ) else: await self.encode_partial_batch( [ (byte_setter, value[out_selection], chunk_selection, chunk_spec) for byte_setter, chunk_spec, chunk_selection, out_selection, _ in batch_info ], ) else: # Read existing bytes if not total slice async def _read_key( byte_setter: ByteSetter | None, prototype: BufferPrototype ) -> Buffer | None: if byte_setter is None: return None return await byte_setter.get(prototype=prototype) chunk_bytes_batch: Iterable[Buffer | None] chunk_bytes_batch = await concurrent_map( [ ( None if is_complete_chunk else byte_setter, chunk_spec.prototype, ) for byte_setter, chunk_spec, chunk_selection, _, is_complete_chunk in batch_info ], _read_key, config.get("async.concurrency"), ) chunk_array_decoded = await self.decode_batch( [ (chunk_bytes, chunk_spec) for chunk_bytes, (_, chunk_spec, *_) in zip( chunk_bytes_batch, batch_info, strict=False ) ], ) chunk_array_merged = [ self._merge_chunk_array( chunk_array, value, out_selection, chunk_spec, chunk_selection, is_complete_chunk, drop_axes, ) for chunk_array, ( _, chunk_spec, chunk_selection, out_selection, is_complete_chunk, ) in zip(chunk_array_decoded, batch_info, strict=False) ] chunk_array_batch: list[NDBuffer | None] = [] for chunk_array, (_, chunk_spec, *_) in zip( chunk_array_merged, batch_info, strict=False ): if chunk_array is None: chunk_array_batch.append(None) # type: ignore[unreachable] else: if not chunk_spec.config.write_empty_chunks and chunk_array.all_equal( fill_value_or_default(chunk_spec) ): chunk_array_batch.append(None) else: chunk_array_batch.append(chunk_array) chunk_bytes_batch = await self.encode_batch( [ (chunk_array, chunk_spec) for chunk_array, (_, chunk_spec, *_) in zip( chunk_array_batch, batch_info, strict=False ) ], ) async def _write_key(byte_setter: ByteSetter, chunk_bytes: Buffer | None) -> None: if chunk_bytes is None: await byte_setter.delete() else: await byte_setter.set(chunk_bytes) await concurrent_map( [ (byte_setter, chunk_bytes) for chunk_bytes, (byte_setter, *_) in zip( chunk_bytes_batch, batch_info, strict=False ) ], _write_key, config.get("async.concurrency"), ) async def decode( self, chunk_bytes_and_specs: Iterable[tuple[Buffer | None, ArraySpec]], ) -> Iterable[NDBuffer | None]: output: list[NDBuffer | None] = [] for batch_info in batched(chunk_bytes_and_specs, self.batch_size): output.extend(await self.decode_batch(batch_info)) return output async def encode( self, chunk_arrays_and_specs: Iterable[tuple[NDBuffer | None, ArraySpec]], ) -> Iterable[Buffer | None]: output: list[Buffer | None] = [] for single_batch_info in batched(chunk_arrays_and_specs, self.batch_size): output.extend(await self.encode_batch(single_batch_info)) return output async def read( self, batch_info: Iterable[tuple[ByteGetter, ArraySpec, SelectorTuple, SelectorTuple, bool]], out: NDBuffer, drop_axes: tuple[int, ...] = (), ) -> tuple[GetResult, ...]: batch_results = await concurrent_map( [ (single_batch_info, out, drop_axes) for single_batch_info in batched(batch_info, self.batch_size) ], self.read_batch, config.get("async.concurrency"), ) results: list[GetResult] = [] for batch in batch_results: results.extend(batch) return tuple(results) async def write( self, batch_info: Iterable[tuple[ByteSetter, ArraySpec, SelectorTuple, SelectorTuple, bool]], value: NDBuffer, drop_axes: tuple[int, ...] = (), ) -> None: await concurrent_map( [ (single_batch_info, value, drop_axes) for single_batch_info in batched(batch_info, self.batch_size) ], self.write_batch, config.get("async.concurrency"), ) def codecs_from_list( codecs: Iterable[Codec], ) -> tuple[tuple[ArrayArrayCodec, ...], ArrayBytesCodec, tuple[BytesBytesCodec, ...]]: from zarr.codecs.sharding import ShardingCodec array_array: tuple[ArrayArrayCodec, ...] = () array_bytes_maybe: ArrayBytesCodec | None = None bytes_bytes: tuple[BytesBytesCodec, ...] = () if any(isinstance(codec, ShardingCodec) for codec in codecs) and len(tuple(codecs)) > 1: warn( "Combining a `sharding_indexed` codec disables partial reads and " "writes, which may lead to inefficient performance.", category=ZarrUserWarning, stacklevel=3, ) for prev_codec, cur_codec in pairwise((None, *codecs)): if isinstance(cur_codec, ArrayArrayCodec): if isinstance(prev_codec, ArrayBytesCodec | BytesBytesCodec): msg = ( f"Invalid codec order. ArrayArrayCodec {cur_codec}" "must be preceded by another ArrayArrayCodec. " f"Got {type(prev_codec)} instead." ) raise TypeError(msg) array_array += (cur_codec,) elif isinstance(cur_codec, ArrayBytesCodec): if isinstance(prev_codec, BytesBytesCodec): msg = ( f"Invalid codec order. ArrayBytes codec {cur_codec}" f" must be preceded by an ArrayArrayCodec. Got {type(prev_codec)} instead." ) raise TypeError(msg) if array_bytes_maybe is not None: msg = ( f"Got two instances of ArrayBytesCodec: {array_bytes_maybe} and {cur_codec}. " "Only one array-to-bytes codec is allowed." ) raise ValueError(msg) array_bytes_maybe = cur_codec elif isinstance(cur_codec, BytesBytesCodec): if isinstance(prev_codec, ArrayArrayCodec): msg = ( f"Invalid codec order. BytesBytesCodec {cur_codec}" "must be preceded by either another BytesBytesCodec, or an ArrayBytesCodec. " f"Got {type(prev_codec)} instead." ) bytes_bytes += (cur_codec,) else: raise TypeError if array_bytes_maybe is None: raise ValueError("Required ArrayBytesCodec was not found.") else: return array_array, array_bytes_maybe, bytes_bytes register_pipeline(BatchedCodecPipeline) zarr-python-3.2.1/src/zarr/core/common.py000066400000000000000000000253121517635743000203670ustar00rootroot00000000000000from __future__ import annotations import asyncio import functools import math import operator import warnings from collections.abc import Iterable, Mapping, Sequence from enum import Enum from itertools import starmap from typing import ( TYPE_CHECKING, Any, Final, Literal, NotRequired, TypedDict, cast, overload, ) import numpy as np from typing_extensions import ReadOnly from zarr.core.config import config as zarr_config from zarr.errors import ZarrRuntimeWarning if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Iterator ZARR_JSON = "zarr.json" ZARRAY_JSON = ".zarray" ZGROUP_JSON = ".zgroup" ZATTRS_JSON = ".zattrs" ZMETADATA_V2_JSON = ".zmetadata" BytesLike = bytes | bytearray | memoryview ShapeLike = Iterable[int | np.integer[Any]] | int | np.integer[Any] ChunksLike = ShapeLike | Sequence[Sequence[int]] | None # For backwards compatibility ChunkCoords = tuple[int, ...] ZarrFormat = Literal[2, 3] NodeType = Literal["array", "group"] JSON = str | int | float | bool | Mapping[str, "JSON"] | Sequence["JSON"] | None MemoryOrder = Literal["C", "F"] AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"] ANY_ACCESS_MODE: Final = "r", "r+", "a", "w", "w-" DimensionNamesLike = Iterable[str | None] | None DimensionNames = DimensionNamesLike # for backwards compatibility class NamedConfig[TName: str, TConfig: Mapping[str, object]](TypedDict): """ A typed dictionary representing an object with a name and configuration, where the configuration is an optional mapping of string keys to values, e.g. another typed dictionary or a JSON object. This class is generic with two type parameters: the type of the name (``TName``) and the type of the configuration (``TConfig``). """ name: ReadOnly[TName] """The name of the object.""" configuration: NotRequired[ReadOnly[TConfig]] """The configuration of the object. Not required.""" class NamedRequiredConfig[TName: str, TConfig: Mapping[str, object]](TypedDict): """ A typed dictionary representing an object with a name and configuration, where the configuration is a mapping of string keys to values, e.g. another typed dictionary or a JSON object. This class is generic with two type parameters: the type of the name (``TName``) and the type of the configuration (``TConfig``). """ name: ReadOnly[TName] """The name of the object.""" configuration: ReadOnly[TConfig] """The configuration of the object.""" def product(tup: tuple[int, ...]) -> int: return functools.reduce(operator.mul, tup, 1) def ceildiv(a: float, b: float) -> int: if a == 0: return 0 return math.ceil(a / b) async def concurrent_map[T: tuple[Any, ...], V]( items: Iterable[T], func: Callable[..., Awaitable[V]], limit: int | None = None, ) -> list[V]: if limit is None: return await asyncio.gather(*list(starmap(func, items))) else: sem = asyncio.Semaphore(limit) async def run(item: tuple[Any]) -> V: async with sem: return await func(*item) return await asyncio.gather(*[asyncio.ensure_future(run(item)) for item in items]) def enum_names[E: Enum](enum: type[E]) -> Iterator[str]: for item in enum: yield item.name def parse_enum[E: Enum](data: object, cls: type[E]) -> E: if isinstance(data, cls): return data if not isinstance(data, str): raise TypeError(f"Expected str, got {type(data)}") if data in enum_names(cls): return cls(data) raise ValueError(f"Value must be one of {list(enum_names(cls))!r}. Got {data} instead.") def parse_name(data: JSON, expected: str | None = None) -> str: if isinstance(data, str): if expected is None or data == expected: return data raise ValueError(f"Expected '{expected}'. Got {data} instead.") else: raise TypeError(f"Expected a string, got an instance of {type(data)}.") def parse_configuration(data: JSON) -> JSON: if not isinstance(data, dict): raise TypeError(f"Expected dict, got {type(data)}") return data @overload def parse_named_configuration( data: JSON | NamedConfig[str, Any], expected_name: str | None = None ) -> tuple[str, dict[str, JSON]]: ... @overload def parse_named_configuration( data: JSON | NamedConfig[str, Any], expected_name: str | None = None, *, require_configuration: bool = True, ) -> tuple[str, dict[str, JSON] | None]: ... def parse_named_configuration( data: JSON | NamedConfig[str, Any], expected_name: str | None = None, *, require_configuration: bool = True, ) -> tuple[str, JSON | None]: if not isinstance(data, dict): raise TypeError(f"Expected dict, got {type(data)}") if "name" not in data: raise ValueError(f"Named configuration does not have a 'name' key. Got {data}.") name_parsed = parse_name(data["name"], expected_name) if "configuration" in data: configuration_parsed = parse_configuration(data["configuration"]) elif require_configuration: raise ValueError(f"Named configuration does not have a 'configuration' key. Got {data}.") else: configuration_parsed = None return name_parsed, configuration_parsed def parse_shapelike(data: ShapeLike) -> tuple[int, ...]: """ Parse a shape-like input into an explicit shape. """ if isinstance(data, int | np.integer): if data < 0: raise ValueError(f"Expected a non-negative integer. Got {data} instead") return (int(data),) try: data_tuple = tuple(data) except TypeError as e: msg = f"Expected an integer or an iterable of integers. Got {data} instead." raise TypeError(msg) from e if not all(isinstance(v, int | np.integer) for v in data_tuple): msg = f"Expected an iterable of integers. Got {data} instead." raise TypeError(msg) if not all(v > -1 for v in data_tuple): msg = f"Expected all values to be non-negative. Got {data} instead." raise ValueError(msg) # cast NumPy scalars to plain python ints return tuple(int(x) for x in data_tuple) def parse_fill_value(data: Any) -> Any: # todo: real validation return data def parse_order(data: Any) -> Literal["C", "F"]: if data in ("C", "F"): return cast("Literal['C', 'F']", data) raise ValueError(f"Expected one of ('C', 'F'), got {data} instead.") def parse_bool(data: Any) -> bool: if isinstance(data, bool): return data raise ValueError(f"Expected bool, got {data} instead.") def _warn_write_empty_chunks_kwarg() -> None: # TODO: link to docs page on array configuration in this message msg = ( "The `write_empty_chunks` keyword argument is deprecated and will be removed in future versions. " "To control whether empty chunks are written to storage, either use the `config` keyword " "argument, as in `config={'write_empty_chunks': True}`," "or change the global 'array.write_empty_chunks' configuration variable." ) warnings.warn(msg, ZarrRuntimeWarning, stacklevel=2) def _warn_order_kwarg() -> None: # TODO: link to docs page on array configuration in this message msg = ( "The `order` keyword argument has no effect for Zarr format 3 arrays. " "To control the memory layout of the array, either use the `config` keyword " "argument, as in `config={'order': 'C'}`," "or change the global 'array.order' configuration variable." ) warnings.warn(msg, ZarrRuntimeWarning, stacklevel=2) def _default_zarr_format() -> ZarrFormat: """Return the default zarr_format.""" return cast("ZarrFormat", int(zarr_config.get("default_zarr_format", 3))) def expand_rle(data: Sequence[int | list[int]]) -> list[int]: """Expand a mixed array of bare integers and RLE pairs. Per the rectilinear chunk grid spec, each element can be: - a bare integer (an explicit edge length) - a two-element array ``[value, count]`` (run-length encoded) """ result: list[int] = [] for item in data: if isinstance(item, (int, float)) and not isinstance(item, bool): val = int(item) if val < 1: raise ValueError(f"Chunk edge length must be >= 1, got {val}") result.append(val) elif isinstance(item, list) and len(item) == 2: size, count = int(item[0]), int(item[1]) if size < 1: raise ValueError(f"Chunk edge length must be >= 1, got {size}") if count < 1: raise ValueError(f"RLE repeat count must be >= 1, got {count}") result.extend([size] * count) else: raise ValueError(f"RLE entries must be an integer or [size, count], got {item}") return result def compress_rle(sizes: Sequence[int]) -> list[int | list[int]]: """Compress chunk sizes to mixed RLE format per the rectilinear spec. Runs of length > 1 are emitted as ``[value, count]`` pairs; runs of length 1 are emitted as bare integers:: [10, 10, 10, 5] -> [[10, 3], 5] """ if not sizes: return [] result: list[int | list[int]] = [] current = sizes[0] count = 1 for s in sizes[1:]: if s == current: count += 1 else: result.append([current, count] if count > 1 else current) current = s count = 1 result.append([current, count] if count > 1 else current) return result def validate_rectilinear_kind(kind: str | None) -> None: """Validate the ``kind`` field of a rectilinear chunk grid configuration. The rectilinear spec requires ``kind: "inline"``. """ if kind is None: raise ValueError( "Rectilinear chunk grid configuration requires a 'kind' field. " "Only 'inline' is currently supported." ) if kind != "inline": raise ValueError( f"Unsupported rectilinear chunk grid kind: {kind!r}. " "Only 'inline' is currently supported." ) def validate_rectilinear_edges( chunk_shapes: Sequence[int | Sequence[int]], array_shape: Sequence[int] ) -> None: """Validate that rectilinear chunk edges cover the array extent per dimension. Bare-int dimensions (regular step) always cover any extent, so they are skipped. Explicit edge lists must sum to at least the array extent. """ for i, (dim_spec, extent) in enumerate(zip(chunk_shapes, array_shape, strict=True)): if isinstance(dim_spec, int): continue edge_sum = sum(dim_spec) if edge_sum < extent: raise ValueError( f"Rectilinear chunk edges for dimension {i} sum to {edge_sum} " f"but array shape extent is {extent} (edge sum must be >= extent)" ) zarr-python-3.2.1/src/zarr/core/config.py000066400000000000000000000143451517635743000203500ustar00rootroot00000000000000""" The config module is responsible for managing the configuration of zarr and is based on the Donfig python library. For selecting custom implementations of codecs, pipelines, buffers and ndbuffers, first register the implementations in the registry and then select them in the config. Example: An implementation of the bytes codec in a class ``your.module.NewBytesCodec`` requires the value of ``codecs.bytes`` to be ``your.module.NewBytesCodec``. Donfig can be configured programmatically, by environment variables, or from YAML files in standard locations. ```python from your.module import NewBytesCodec from zarr.core.config import register_codec, config register_codec("bytes", NewBytesCodec) config.set({"codecs.bytes": "your.module.NewBytesCodec"}) ``` Instead of setting the value programmatically with ``config.set``, you can also set the value with an environment variable. The environment variable ``ZARR_CODECS__BYTES`` can be set to ``your.module.NewBytesCodec``. The double underscore ``__`` is used to indicate nested access. ```bash export ZARR_CODECS__BYTES="your.module.NewBytesCodec" ``` For more information, see the Donfig documentation at https://github.com/pytroll/donfig. """ from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal, cast from donfig import Config as DConfig if TYPE_CHECKING: from donfig.config_obj import ConfigSet class BadConfigError(ValueError): _msg = "bad Config: %r" class Config(DConfig): # type: ignore[misc] """The Config will collect configuration from config files and environment variables Example environment variables: Grabs environment variables of the form "ZARR_FOO__BAR_BAZ=123" and turns these into config variables of the form ``{"foo": {"bar-baz": 123}}`` It transforms the key and value in the following way: - Lower-cases the key text - Treats ``__`` (double-underscore) as nested access - Calls ``ast.literal_eval`` on the value """ def reset(self) -> None: self.clear() self.refresh() def enable_gpu(self) -> ConfigSet: """ Configure Zarr to use GPUs where possible. """ return self.set( {"buffer": "zarr.buffer.gpu.Buffer", "ndbuffer": "zarr.buffer.gpu.NDBuffer"} ) # these keys were removed from the config as part of the 3.1.0 release. # these deprecations should be removed in 3.1.1 or thereabouts. deprecations = { "array.v2_default_compressor.numeric": None, "array.v2_default_compressor.string": None, "array.v2_default_compressor.bytes": None, "array.v2_default_filters.string": None, "array.v2_default_filters.bytes": None, "array.v3_default_filters.numeric": None, "array.v3_default_filters.raw": None, "array.v3_default_filters.bytes": None, "array.v3_default_serializer.numeric": None, "array.v3_default_serializer.string": None, "array.v3_default_serializer.bytes": None, "array.v3_default_compressors.string": None, "array.v3_default_compressors.bytes": None, "array.v3_default_compressors": None, } # The default configuration for zarr config = Config( "zarr", defaults=[ { "default_zarr_format": 3, "array": { "order": "C", "write_empty_chunks": False, "read_missing_chunks": True, "target_shard_size_bytes": None, "rectilinear_chunks": False, }, "async": {"concurrency": 10, "timeout": None}, "threading": {"max_workers": None}, "json_indent": 2, "codec_pipeline": { "path": "zarr.core.codec_pipeline.BatchedCodecPipeline", "batch_size": 1, }, "codecs": { "blosc": "zarr.codecs.blosc.BloscCodec", "gzip": "zarr.codecs.gzip.GzipCodec", "zstd": "zarr.codecs.zstd.ZstdCodec", "bytes": "zarr.codecs.bytes.BytesCodec", "endian": "zarr.codecs.bytes.BytesCodec", # compatibility with earlier versions of ZEP1 "crc32c": "zarr.codecs.crc32c_.Crc32cCodec", "sharding_indexed": "zarr.codecs.sharding.ShardingCodec", "transpose": "zarr.codecs.transpose.TransposeCodec", "vlen-utf8": "zarr.codecs.vlen_utf8.VLenUTF8Codec", "vlen-bytes": "zarr.codecs.vlen_utf8.VLenBytesCodec", "numcodecs.bz2": "zarr.codecs.numcodecs.BZ2", "numcodecs.crc32": "zarr.codecs.numcodecs.CRC32", "numcodecs.crc32c": "zarr.codecs.numcodecs.CRC32C", "numcodecs.lz4": "zarr.codecs.numcodecs.LZ4", "numcodecs.lzma": "zarr.codecs.numcodecs.LZMA", "numcodecs.zfpy": "zarr.codecs.numcodecs.ZFPY", "numcodecs.adler32": "zarr.codecs.numcodecs.Adler32", "numcodecs.astype": "zarr.codecs.numcodecs.AsType", "numcodecs.bitround": "zarr.codecs.numcodecs.BitRound", "numcodecs.blosc": "zarr.codecs.numcodecs.Blosc", "numcodecs.delta": "zarr.codecs.numcodecs.Delta", "numcodecs.fixedscaleoffset": "zarr.codecs.numcodecs.FixedScaleOffset", "numcodecs.fletcher32": "zarr.codecs.numcodecs.Fletcher32", "numcodecs.gzip": "zarr.codecs.numcodecs.GZip", "numcodecs.jenkins_lookup3": "zarr.codecs.numcodecs.JenkinsLookup3", "numcodecs.pcodec": "zarr.codecs.numcodecs.PCodec", "numcodecs.packbits": "zarr.codecs.numcodecs.PackBits", "numcodecs.shuffle": "zarr.codecs.numcodecs.Shuffle", "numcodecs.quantize": "zarr.codecs.numcodecs.Quantize", "numcodecs.zlib": "zarr.codecs.numcodecs.Zlib", "numcodecs.zstd": "zarr.codecs.numcodecs.Zstd", }, "buffer": "zarr.buffer.cpu.Buffer", "ndbuffer": "zarr.buffer.cpu.NDBuffer", } ], deprecations=deprecations, ) def parse_indexing_order(data: Any) -> Literal["C", "F"]: if data in ("C", "F"): return cast("Literal['C', 'F']", data) msg = f"Expected one of ('C', 'F'), got {data} instead." raise ValueError(msg) zarr-python-3.2.1/src/zarr/core/dtype/000077500000000000000000000000001517635743000176475ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/core/dtype/__init__.py000066400000000000000000000217211517635743000217630ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Sequence from typing import TYPE_CHECKING, Final from zarr.core.dtype.common import ( DataTypeValidationError, DTypeJSON, ) from zarr.core.dtype.npy.bool import Bool from zarr.core.dtype.npy.bytes import ( NullTerminatedBytes, NullterminatedBytesJSON_V2, NullTerminatedBytesJSON_V3, RawBytes, RawBytesJSON_V2, RawBytesJSON_V3, VariableLengthBytes, VariableLengthBytesJSON_V2, ) from zarr.core.dtype.npy.complex import Complex64, Complex128 from zarr.core.dtype.npy.float import Float16, Float32, Float64 from zarr.core.dtype.npy.int import Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 from zarr.core.dtype.npy.structured import ( Struct, StructJSON_V3, Structured, StructuredJSON_V2, StructuredJSON_V3, ) from zarr.core.dtype.npy.time import ( DateTime64, DateTime64JSON_V2, DateTime64JSON_V3, TimeDelta64, TimeDelta64JSON_V2, TimeDelta64JSON_V3, ) if TYPE_CHECKING: from zarr.core.common import ZarrFormat from collections.abc import Mapping import numpy as np import numpy.typing as npt from zarr.core.common import JSON from zarr.core.dtype.npy.string import ( FixedLengthUTF32, FixedLengthUTF32JSON_V2, FixedLengthUTF32JSON_V3, VariableLengthUTF8, VariableLengthUTF8JSON_V2, ) from zarr.core.dtype.registry import DataTypeRegistry from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType __all__ = [ "Bool", "Complex64", "Complex128", "DataTypeRegistry", "DataTypeValidationError", "DateTime64", "DateTime64JSON_V2", "DateTime64JSON_V3", "FixedLengthUTF32", "FixedLengthUTF32JSON_V2", "FixedLengthUTF32JSON_V3", "Float16", "Float32", "Float64", "Int8", "Int16", "Int32", "Int64", "NullTerminatedBytes", "NullTerminatedBytesJSON_V3", "NullterminatedBytesJSON_V2", "RawBytes", "RawBytesJSON_V2", "RawBytesJSON_V3", "Struct", "StructJSON_V3", "Structured", "StructuredJSON_V2", "StructuredJSON_V3", "TBaseDType", "TBaseScalar", "TimeDelta64", "TimeDelta64JSON_V2", "TimeDelta64JSON_V3", "UInt8", "UInt16", "UInt32", "UInt64", "VariableLengthBytes", "VariableLengthBytesJSON_V2", "VariableLengthUTF8", "VariableLengthUTF8JSON_V2", "ZDType", "data_type_registry", "parse_data_type", "parse_dtype", ] data_type_registry = DataTypeRegistry() IntegerDType = Int8 | Int16 | Int32 | Int64 | UInt8 | UInt16 | UInt32 | UInt64 INTEGER_DTYPE: Final = Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 FloatDType = Float16 | Float32 | Float64 FLOAT_DTYPE: Final = Float16, Float32, Float64 ComplexFloatDType = Complex64 | Complex128 COMPLEX_FLOAT_DTYPE: Final = Complex64, Complex128 StringDType = FixedLengthUTF32 | VariableLengthUTF8 STRING_DTYPE: Final = FixedLengthUTF32, VariableLengthUTF8 TimeDType = DateTime64 | TimeDelta64 TIME_DTYPE: Final = DateTime64, TimeDelta64 BytesDType = RawBytes | NullTerminatedBytes | VariableLengthBytes BYTES_DTYPE: Final = RawBytes, NullTerminatedBytes, VariableLengthBytes AnyDType = ( Bool | IntegerDType | FloatDType | ComplexFloatDType | StringDType | BytesDType | Struct | TimeDType | VariableLengthBytes ) # mypy has trouble inferring the type of variablelengthstring dtype, because its class definition # depends on the installed numpy version. That's why the type: ignore statement is needed here. ANY_DTYPE: Final = ( Bool, *INTEGER_DTYPE, *FLOAT_DTYPE, *COMPLEX_FLOAT_DTYPE, *STRING_DTYPE, *BYTES_DTYPE, Struct, *TIME_DTYPE, VariableLengthBytes, ) # These are aliases for variable-length UTF-8 strings # We handle them when a user requests a data type instead of using NumPy's dtype inferece because # the default NumPy behavior -- to inspect the user-provided array data and choose # an appropriately sized U dtype -- is unworkable for Zarr. VLEN_UTF8_ALIAS: Final = ("str", str, "string") # This type models inputs that can be coerced to a ZDType type ZDTypeLike = npt.DTypeLike | ZDType[TBaseDType, TBaseScalar] | Mapping[str, JSON] | str for dtype in ANY_DTYPE: # mypy does not know that all the elements of ANY_DTYPE are subclasses of ZDType data_type_registry.register(dtype._zarr_v3_name, dtype) # type: ignore[arg-type] # TODO: find a better name for this function def get_data_type_from_native_dtype(dtype: npt.DTypeLike) -> ZDType[TBaseDType, TBaseScalar]: """ Get a data type wrapper (an instance of ``ZDType``) from a native data type, e.g. a numpy dtype. """ if not isinstance(dtype, np.dtype): na_dtype: np.dtype[np.generic] if isinstance(dtype, list): # this is a valid _VoidDTypeLike check na_dtype = np.dtype([tuple(d) for d in dtype]) else: na_dtype = np.dtype(dtype) else: na_dtype = dtype return data_type_registry.match_dtype(dtype=na_dtype) def get_data_type_from_json( dtype_spec: DTypeJSON, *, zarr_format: ZarrFormat ) -> ZDType[TBaseDType, TBaseScalar]: """ Given a JSON representation of a data type and a Zarr format version, attempt to create a ZDType instance from the registered ZDType classes. """ return data_type_registry.match_json(dtype_spec, zarr_format=zarr_format) def parse_data_type( dtype_spec: ZDTypeLike, *, zarr_format: ZarrFormat, ) -> ZDType[TBaseDType, TBaseScalar]: """ Interpret the input as a ZDType. This function wraps ``parse_dtype``. The only difference is the function name. This function may be deprecated in a future version of Zarr Python in favor of ``parse_dtype``. Parameters ---------- dtype_spec : ZDTypeLike The input to be interpreted as a ZDType. This could be a ZDType, which will be returned directly, or a JSON representation of a ZDType, or a native dtype, or a python object that can be converted into a native dtype. zarr_format : ZarrFormat The Zarr format version. This parameter is required because this function will attempt to parse the JSON representation of a data type, and the JSON representation of data types varies between Zarr 2 and Zarr 3. Returns ------- ZDType[TBaseDType, TBaseScalar] The ZDType corresponding to the input. Examples -------- ```python from zarr.dtype import parse_data_type import numpy as np parse_data_type("int32", zarr_format=2) # Int32(endianness='little') parse_data_type(np.dtype('S10'), zarr_format=2) # NullTerminatedBytes(length=10) parse_data_type({"name": "numpy.datetime64", "configuration": {"unit": "s", "scale_factor": 10}}, zarr_format=3) # DateTime64(endianness='little', scale_factor=10, unit='s') ``` """ return parse_dtype(dtype_spec, zarr_format=zarr_format) def parse_dtype( dtype_spec: ZDTypeLike, *, zarr_format: ZarrFormat, ) -> ZDType[TBaseDType, TBaseScalar]: """ Convert the input as a ZDType. Parameters ---------- dtype_spec : ZDTypeLike The input to be converted to a ZDType. This could be a ZDType, which will be returned directly, or a JSON representation of a ZDType, or a numpy dtype, or a python object that can be converted into a native dtype. zarr_format : ZarrFormat The Zarr format version. This parameter is required because this function will attempt to parse the JSON representation of a data type, and the JSON representation of data types varies between Zarr 2 and Zarr 3. Returns ------- ZDType[TBaseDType, TBaseScalar] The ZDType corresponding to the input. Examples -------- ```python from zarr.dtype import parse_dtype import numpy as np parse_dtype("int32", zarr_format=2) # Int32(endianness='little') parse_dtype(np.dtype('S10'), zarr_format=2) # NullTerminatedBytes(length=10) parse_dtype({"name": "numpy.datetime64", "configuration": {"unit": "s", "scale_factor": 10}}, zarr_format=3) # DateTime64(endianness='little', scale_factor=10, unit='s') ``` """ if isinstance(dtype_spec, ZDType): return dtype_spec # First attempt to interpret the input as JSON if isinstance(dtype_spec, Mapping | str | Sequence): try: return get_data_type_from_json(dtype_spec, zarr_format=zarr_format) # type: ignore[arg-type] except ValueError: # no data type matched this JSON-like input pass if dtype_spec in VLEN_UTF8_ALIAS: # If the dtype request is one of the aliases for variable-length UTF-8 strings, # return that dtype. return VariableLengthUTF8() # type: ignore[return-value] # otherwise, we have either a numpy dtype string, or a zarr v3 dtype string, and in either case # we can create a native dtype from it, and do the dtype inference from that return get_data_type_from_native_dtype(dtype_spec) # type: ignore[arg-type] zarr-python-3.2.1/src/zarr/core/dtype/common.py000066400000000000000000000204211517635743000215100ustar00rootroot00000000000000from __future__ import annotations import warnings from collections.abc import Mapping, Sequence from dataclasses import dataclass from typing import ( ClassVar, Final, Literal, TypedDict, TypeGuard, ) 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" SpecialFloatStrings = Literal["NaN", "Infinity", "-Infinity"] SPECIAL_FLOAT_STRINGS: Final = ("NaN", "Infinity", "-Infinity") JSONFloatV2 = float | SpecialFloatStrings JSONFloatV3 = float | SpecialFloatStrings | str ObjectCodecID = Literal["vlen-utf8", "vlen-bytes", "vlen-array", "pickle", "json2", "msgpack2"] # These are the ids of the known object codecs for zarr v2. OBJECT_CODEC_IDS: Final = ("vlen-utf8", "vlen-bytes", "vlen-array", "pickle", "json2", "msgpack2") # This is a wider type than our standard JSON type because we need # to work with typeddict objects which are assignable to Mapping[str, object] DTypeJSON = str | int | float | Sequence["DTypeJSON"] | None | Mapping[str, object] # The DTypeJSON_V2 type exists because ZDType.from_json takes a single argument, which must contain # all the information necessary to decode the data type. Zarr v2 supports multiple distinct # data types that all used the "|O" data type identifier. These data types can only be # discriminated on the basis of their "object codec", i.e. a special data type specific # compressor or filter. So to figure out what data type a zarr v2 array has, we need the # data type identifier from metadata, as well as an object codec id if the data type identifier # is "|O". # So we will pack the name of the dtype alongside the name of the object codec id, if applicable, # in a single dict, and pass that to the data type inference logic. # These type variables have a very wide bound because the individual zdtype # classes can perform a very specific type check. # This is the JSON representation of a structured dtype in zarr v2 StructuredName_V2 = Sequence["str | StructuredName_V2"] # This models the type of the name a dtype might have in zarr v2 array metadata DTypeName_V2 = StructuredName_V2 | str class DTypeConfig_V2[TDTypeNameV2: DTypeName_V2, TObjectCodecID: None | str](TypedDict): name: ReadOnly[TDTypeNameV2] object_codec_id: ReadOnly[TObjectCodecID] DTypeSpec_V2 = DTypeConfig_V2[DTypeName_V2, None | str] def check_structured_dtype_v2_inner(data: object) -> TypeGuard[StructuredName_V2]: """ A type guard for the inner elements of a structured dtype. This is a recursive check because the type is itself recursive. This check ensures that all the elements are 2-element sequences beginning with a string and ending with either another string or another 2-element sequence beginning with a string and ending with another instance of that type. """ if isinstance(data, (str, Mapping)): return False if not isinstance(data, Sequence): return False if len(data) != 2: return False if not (isinstance(data[0], str)): return False if isinstance(data[-1], str): return True elif isinstance(data[-1], Sequence): return check_structured_dtype_v2_inner(data[-1]) return False def check_structured_dtype_name_v2(data: Sequence[object]) -> TypeGuard[StructuredName_V2]: """ Check that all the elements of a sequence are valid zarr v2 structured dtype identifiers """ return all(check_structured_dtype_v2_inner(d) for d in data) def check_dtype_name_v2(data: object) -> TypeGuard[DTypeName_V2]: """ Type guard for narrowing the type of a python object to a valid zarr v2 dtype name. """ if isinstance(data, str): return True elif isinstance(data, Sequence): return check_structured_dtype_name_v2(data) return False def check_dtype_spec_v2(data: object) -> TypeGuard[DTypeSpec_V2]: """ Type guard for narrowing a python object to an instance of DTypeSpec_V2 """ if not isinstance(data, Mapping): return False if set(data.keys()) != {"name", "object_codec_id"}: return False if not check_dtype_name_v2(data["name"]): return False return isinstance(data["object_codec_id"], str | None) # By comparison, The JSON representation of a dtype in zarr v3 is much simpler. # It's either a string, or a structured dict DTypeSpec_V3 = str | NamedConfig[str, Mapping[str, object]] def check_dtype_spec_v3(data: object) -> TypeGuard[DTypeSpec_V3]: """ Type guard for narrowing the type of a python object to an instance of DTypeSpec_V3, i.e either a string or a dict with a "name" field that's a string and a "configuration" field that's a mapping with string keys. """ if isinstance(data, str) or ( # noqa: SIM103 isinstance(data, Mapping) and set(data.keys()) == {"name", "configuration"} and isinstance(data["configuration"], Mapping) and all(isinstance(k, str) for k in data["configuration"]) ): return True return False def unpack_dtype_json(data: DTypeSpec_V2 | DTypeSpec_V3) -> DTypeJSON: """ Return the array metadata form of the dtype JSON representation. For the Zarr V3 form of dtype metadata, this is a no-op. For the Zarr V2 form of dtype metadata, this unpacks the dtype name. """ if isinstance(data, Mapping) and set(data.keys()) == {"name", "object_codec_id"}: return data["name"] return data class DataTypeValidationError(ValueError): ... class ScalarTypeValidationError(ValueError): ... @dataclass(frozen=True, kw_only=True) class HasLength: """ A mix-in class for data types with a length attribute, such as fixed-size collections of unicode strings, or bytes. Attributes ---------- length : int The length of the scalars belonging to this data type. Note that this class does not assign a unit to the length. Child classes may assign units. """ length: int @dataclass(frozen=True, kw_only=True) class HasEndianness: """ A mix-in class for data types with an endianness attribute """ endianness: EndiannessStr = "little" @dataclass(frozen=True, kw_only=True) class HasItemSize: """ A mix-in class for data types with an item size attribute. This mix-in bears a property ``item_size``, which denotes the size of each element of the data type, in bytes. """ @property def item_size(self) -> int: raise NotImplementedError @dataclass(frozen=True, kw_only=True) class HasObjectCodec: """ A mix-in class for data types that require an object codec id. This class bears the property ``object_codec_id``, which is the string name of an object codec that is required to encode and decode the data type. In zarr-python 2.x certain data types like variable-length strings or variable-length arrays used the catch-all numpy "object" data type for their in-memory representation. But these data types cannot be stored as numpy object data types, because the object data type does not define a fixed memory layout. So these data types required a special codec, called an "object codec", that effectively defined a compact representation for the data type, which was used to encode and decode the data type. Zarr-python 2.x would not allow the creation of arrays with the "object" data type if an object codec was not specified, and thus the name of the object codec is effectively part of the data type model. """ object_codec_id: ClassVar[str] def v3_unstable_dtype_warning(dtype: object) -> None: """ Emit this warning when a data type does not have a stable zarr v3 spec """ msg = ( f"The data type ({dtype}) does not have a Zarr V3 specification. " "That means that the representation of arrays saved with this data type may change without " "warning in a future version of Zarr Python. " "Arrays stored with this data type may be unreadable by other Zarr libraries. " "Use this data type at your own risk! " "Check https://github.com/zarr-developers/zarr-extensions/tree/main/data-types for the " "status of data type specifications for Zarr V3." ) warnings.warn(msg, category=UnstableSpecificationWarning, stacklevel=2) zarr-python-3.2.1/src/zarr/core/dtype/npy/000077500000000000000000000000001517635743000204555ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/core/dtype/npy/__init__.py000066400000000000000000000000001517635743000225540ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/core/dtype/npy/bool.py000066400000000000000000000213141517635743000217630ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar, Literal, Self, TypeGuard, overload import numpy as np from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasItemSize, check_dtype_spec_v2, ) from zarr.core.dtype.wrapper import TBaseDType, ZDType if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat @dataclass(frozen=True, kw_only=True, slots=True) class Bool(ZDType[np.dtypes.BoolDType, np.bool_], HasItemSize): """ A Zarr data type for arrays containing booleans. Wraps the [`np.dtypes.BoolDType`][numpy.dtypes.BoolDType] data type. Scalars for this data type are instances of [`np.bool_`][numpy.bool_]. Attributes ---------- _zarr_v3_name : Literal["bool"] = "bool" The Zarr v3 name of the dtype. _zarr_v2_name : ``Literal["|b1"]`` = ``"|b1"`` The Zarr v2 name of the dtype, which is also a string representation of the boolean dtype used by NumPy. dtype_cls : ClassVar[type[np.dtypes.BoolDType]] = np.dtypes.BoolDType The NumPy dtype class. References ---------- This class implements the boolean data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding)and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ _zarr_v3_name: ClassVar[Literal["bool"]] = "bool" _zarr_v2_name: ClassVar[Literal["|b1"]] = "|b1" dtype_cls = np.dtypes.BoolDType @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of Bool from an instance of np.dtypes.BoolDType. Parameters ---------- dtype : TBaseDType The NumPy boolean dtype instance to convert. Returns ------- Bool An instance of Bool. Raises ------ DataTypeValidationError If the provided dtype is not compatible with this ZDType. """ if cls._check_native_dtype(dtype): return cls() raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self: Self) -> np.dtypes.BoolDType: """ Create a NumPy boolean dtype instance from this ZDType. Returns ------- np.dtypes.BoolDType The NumPy boolean dtype. """ return self.dtype_cls() @classmethod def _check_json_v2( cls, data: DTypeJSON, ) -> TypeGuard[DTypeConfig_V2[Literal["|b1"], None]]: """ Check that the input is a valid JSON representation of a Bool. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- ``TypeGuard[DTypeConfig_V2[Literal["|b1"], None]]`` True if the input is a valid JSON representation, False otherwise. """ return ( check_dtype_spec_v2(data) and data["name"] == cls._zarr_v2_name and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[Literal["bool"]]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- bool True if the input is a valid JSON representation, False otherwise. """ return data == cls._zarr_v3_name @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of Bool from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Bool An instance of Bool. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v2(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v2_name!r}" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls: type[Self], data: DTypeJSON) -> Self: """ Create an instance of Bool from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Bool An instance of Bool. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal["|b1"], None]: ... @overload def to_json(self, zarr_format: Literal[3]) -> Literal["bool"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal["|b1"], None] | Literal["bool"]: """ Serialize this Bool instance to JSON. Parameters ---------- zarr_format : ZarrFormat The Zarr format version (2 or 3). Returns ------- ``DTypeConfig_V2[Literal["|b1"], None] | Literal["bool"]`` The JSON representation of the Bool instance. Raises ------ ValueError If the zarr_format is not 2 or 3. """ if zarr_format == 2: return {"name": self._zarr_v2_name, "object_codec_id": None} elif zarr_format == 3: return self._zarr_v3_name raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> bool: """ Check if the input can be cast to a boolean scalar. Parameters ---------- data : object The data to check. Returns ------- bool True if the input can be cast to a boolean scalar, False otherwise. """ return True def cast_scalar(self, data: object) -> np.bool_: """ Cast the input to a numpy boolean scalar. Parameters ---------- data : object The data to cast. Returns ------- bool : np.bool_ The numpy boolean scalar. Raises ------ TypeError If the input cannot be converted to a numpy boolean. """ if self._check_scalar(data): return np.bool_(data) msg = ( # pragma: no cover f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) # pragma: no cover def default_scalar(self) -> np.bool_: """ Get the default value for the boolean dtype. Returns ------- bool : np.bool_ The default value. """ return np.False_ def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> bool: """ Convert a scalar to a python bool. Parameters ---------- data : object The value to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- bool The JSON-serializable format. """ return bool(data) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.bool_: """ Read a JSON-serializable value as a numpy boolean scalar. Parameters ---------- data : JSON The JSON-serializable value. zarr_format : ZarrFormat The zarr format version. Returns ------- bool : np.bool_ The numpy boolean scalar. Raises ------ TypeError If the input is not a valid boolean type. """ if self._check_scalar(data): return np.bool_(data) raise TypeError(f"Invalid type: {data}. Expected a boolean.") # pragma: no cover @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 1 zarr-python-3.2.1/src/zarr/core/dtype/npy/bytes.py000066400000000000000000001160531517635743000221630ustar00rootroot00000000000000from __future__ import annotations import base64 import re from dataclasses import dataclass from typing import ClassVar, Literal, Self, TypedDict, TypeGuard, cast, overload import numpy as np from zarr.core.common import JSON, NamedConfig, ZarrFormat from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasItemSize, HasLength, HasObjectCodec, check_dtype_spec_v2, v3_unstable_dtype_warning, ) from zarr.core.dtype.npy.common import check_json_str from zarr.core.dtype.wrapper import TBaseDType, ZDType BytesLike = np.bytes_ | str | bytes | int class FixedLengthBytesConfig(TypedDict): """ A configuration for a data type that takes a ``length_bytes`` parameter. Attributes ---------- length_bytes : int The length in bytes of the data associated with this configuration. Examples -------- ```python { "length_bytes": 12 } ``` """ length_bytes: int class NullterminatedBytesJSON_V2(DTypeConfig_V2[str, None]): """ A wrapper around the JSON representation of the ``NullTerminatedBytes`` data type in Zarr V2. The ``name`` field of this class contains the value that would appear under the ``dtype`` field in Zarr V2 array metadata. References ---------- The structure of the ``name`` field is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). Examples -------- ```python { "name": "|S10", "object_codec_id": None } ``` """ class NullTerminatedBytesJSON_V3( NamedConfig[Literal["null_terminated_bytes"], FixedLengthBytesConfig] ): """ The JSON representation of the ``NullTerminatedBytes`` data type in Zarr V3. References ---------- This representation is not currently defined in an external specification. Examples -------- ```python { "name": "null_terminated_bytes", "configuration": { "length_bytes": 12 } } ``` """ class RawBytesJSON_V2(DTypeConfig_V2[str, None]): """ A wrapper around the JSON representation of the ``RawBytes`` data type in Zarr V2. The ``name`` field of this class contains the value that would appear under the ``dtype`` field in Zarr V2 array metadata. References ---------- The structure of the ``name`` field is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). Examples -------- ```python { "name": "|V10", "object_codec_id": None } ``` """ class RawBytesJSON_V3(NamedConfig[Literal["raw_bytes"], FixedLengthBytesConfig]): """ The JSON representation of the ``RawBytes`` data type in Zarr V3. References ---------- This representation is not currently defined in an external specification. Examples -------- ```python { "name": "raw_bytes", "configuration": { "length_bytes": 12 } } ``` """ class VariableLengthBytesJSON_V2(DTypeConfig_V2[Literal["|O"], Literal["vlen-bytes"]]): """ A wrapper around the JSON representation of the ``VariableLengthBytes`` data type in Zarr V2. The ``name`` field of this class contains the value that would appear under the ``dtype`` field in Zarr V2 array metadata. The ``object_codec_id`` field is always ``"vlen-bytes"`` References ---------- The structure of the ``name`` field is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). Examples -------- ```python { "name": "|O", "object_codec_id": "vlen-bytes" } ``` """ @dataclass(frozen=True, kw_only=True) class NullTerminatedBytes(ZDType[np.dtypes.BytesDType[int], np.bytes_], HasLength, HasItemSize): """ A Zarr data type for arrays containing fixed-length null-terminated byte sequences. Wraps the [`np.dtypes.BytesDType`][numpy.dtypes.BytesDType] data type. Scalars for this data type are instances of [`np.bytes_`][numpy.bytes_]. This data type is parametrized by an integral length which specifies size in bytes of each scalar. Because this data type uses null-terminated semantics, indexing into NumPy arrays with this data type may return fewer than ``length`` bytes. Attributes ---------- dtype_cls: ClassVar[type[np.dtypes.BytesDType[int]]] = np.dtypes.BytesDType The NumPy data type wrapped by this ZDType. _zarr_v3_name : ClassVar[Literal["null_terminated_bytes"]] length : int The length of the bytes. Notes ----- This data type is designed for compatibility with NumPy arrays that use the NumPy ``bytes`` data type. It may not be desirable for usage outside of that context. If compatibility with the NumPy ``bytes`` data type is not essential, consider using the ``RawBytes`` or ``VariableLengthBytes`` data types instead. """ dtype_cls = np.dtypes.BytesDType _zarr_v3_name: ClassVar[Literal["null_terminated_bytes"]] = "null_terminated_bytes" def __post_init__(self) -> None: """ We don't allow instances of this class with length less than 1 because there is no way such a data type can contain actual data. """ if self.length < 1: raise ValueError(f"length must be >= 1, got {self.length}.") @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of NullTerminatedBytes from an instance of np.dtypes.BytesDType. This method checks if the provided data type is an instance of np.dtypes.BytesDType. If so, it returns a new instance of NullTerminatedBytes with a length equal to the length of input data type. Parameters ---------- dtype : TBaseDType The native dtype to convert. Returns ------- NullTerminatedBytes An instance of NullTerminatedBytes with the specified length. Raises ------ DataTypeValidationError If the dtype is not compatible with NullTerminatedBytes. """ if cls._check_native_dtype(dtype): return cls(length=dtype.itemsize) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.BytesDType[int]: """ Create a NumPy bytes dtype from this NullTerminatedBytes ZDType. Returns ------- np.dtypes.BytesDType[int] A NumPy data type object representing null-terminated bytes with a specified length. """ return self.dtype_cls(self.length) @classmethod def _check_json_v2(cls, data: DTypeJSON) -> TypeGuard[NullterminatedBytesJSON_V2]: """ Check that the input is a valid JSON representation of NullTerminatedBytes in Zarr V2. The input data must be a mapping that contains a "name" key that matches the pattern "|S" and an "object_codec_id" key that is None. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- bool True if the input data is a valid representation, False otherwise. """ return ( check_dtype_spec_v2(data) and isinstance(data["name"], str) and re.match(r"^\|S\d+$", data["name"]) is not None and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[NullTerminatedBytesJSON_V3]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[NullTerminatedBytesJSON_V3] True if the input is a valid representation of this class in Zarr V3, False otherwise. """ return ( isinstance(data, dict) and set(data.keys()) == {"name", "configuration"} and data["name"] == cls._zarr_v3_name and isinstance(data["configuration"], dict) and "length_bytes" in data["configuration"] and isinstance(data["configuration"]["length_bytes"], int) ) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this class from Zarr V2-flavored JSON. This method checks if the input data is a valid representation of this class in Zarr V2. If so, it returns a new instance of this class with a ``length`` as specified in the input data. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data is not a valid representation of this class. """ if cls._check_json_v2(data): name = data["name"] return cls(length=int(name[2:])) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a string like '|S1', '|S2', etc" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this class from Zarr V3-flavored JSON. This method checks if the input data is a valid representation of this class in Zarr V3. If so, it returns a new instance of this class with a ``length`` as specified in the input data. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data is not a valid representation of this class. """ if cls._check_json_v3(data): return cls(length=data["configuration"]["length_bytes"]) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> NullterminatedBytesJSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> NullTerminatedBytesJSON_V3: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[str, None] | NullTerminatedBytesJSON_V3: """ Generate a JSON representation of this data type. Parameters ---------- zarr_format : ZarrFormat The zarr format version. Returns ------- NullterminatedBytesJSON_V2 | NullTerminatedBytesJSON_V3 The JSON-serializable representation of the data type """ if zarr_format == 2: return {"name": self.to_native_dtype().str, "object_codec_id": None} elif zarr_format == 3: v3_unstable_dtype_warning(self) return { "name": self._zarr_v3_name, "configuration": {"length_bytes": self.length}, } raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[BytesLike]: """ Check if the provided data is of type BytesLike. This method is used to verify if the input data can be considered as a scalar of bytes-like type, which includes NumPy bytes, strings, bytes, and integers. Parameters ---------- data : object The data to check. Returns ------- TypeGuard[BytesLike] True if the data is bytes-like, False otherwise. """ return isinstance(data, BytesLike) def _cast_scalar_unchecked(self, data: BytesLike) -> np.bytes_: """ Cast the provided scalar data to [`np.bytes_`][numpy.bytes_], truncating if necessary. Parameters ---------- data : BytesLike The data to cast. Returns ------- bytes : [`np.bytes_`][numpy.bytes_] The casted data as a NumPy bytes scalar. Notes ----- This method does not perform any type checking. The input data must be bytes-like. """ if isinstance(data, int): return self.to_native_dtype().type(str(data)[: self.length]) else: return self.to_native_dtype().type(data[: self.length]) def cast_scalar(self, data: object) -> np.bytes_: """ Attempt to cast a given object to a NumPy bytes scalar. This method first checks if the provided data is a valid scalar that can be converted to a NumPy bytes scalar. If the check succeeds, the unchecked casting operation is performed. If the data is not valid, a TypeError is raised. Parameters ---------- data : object The data to be cast to a NumPy bytes scalar. Returns ------- bytes : [`np.bytes_`][numpy.bytes_] The data cast as a NumPy bytes scalar. Raises ------ TypeError If the data cannot be converted to a NumPy bytes scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> np.bytes_: """ Return a default scalar value, which for this data type is an empty byte string. Returns ------- bytes : [`np.bytes_`][numpy.bytes_] The default scalar value. """ return np.bytes_(b"") def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> str: """ Convert a scalar to a JSON-serializable string representation. This method encodes the given scalar as a NumPy bytes scalar and then encodes the bytes as a base64-encoded string. Parameters ---------- data : object The scalar to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- str A string representation of the scalar. """ as_bytes = self.cast_scalar(data) return base64.standard_b64encode(as_bytes).decode("ascii") def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.bytes_: """ Read a JSON-serializable value as [`np.bytes_`][numpy.bytes_]. Parameters ---------- data : JSON The JSON-serializable base64-encoded string. zarr_format : ZarrFormat The zarr format version. Returns ------- bytes : [`np.bytes_`][numpy.bytes_] The NumPy bytes scalar obtained from decoding the base64 string. Raises ------ TypeError If the input data is not a base64-encoded string. """ if check_json_str(data): return self.to_native_dtype().type(base64.standard_b64decode(data.encode("ascii"))) raise TypeError( f"Invalid type: {data}. Expected a base64-encoded string." ) # pragma: no cover @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return self.length @dataclass(frozen=True, kw_only=True) class RawBytes(ZDType[np.dtypes.VoidDType[int], np.void], HasLength, HasItemSize): """ A Zarr data type for arrays containing fixed-length sequences of raw bytes. Wraps the NumPy ``void`` data type. Scalars for this data type are instances of [`np.void`][numpy.void]. This data type is parametrized by an integral length which specifies size in bytes of each scalar belonging to this data type. Attributes ---------- dtype_cls: ClassVar[type[np.dtypes.VoidDType[int]]] = np.dtypes.VoidDtype The NumPy data type wrapped by this ZDType. _zarr_v3_name : ClassVar[Literal["raw_bytes"]] length : int The length of the bytes. Notes ----- Although the NumPy "Void" data type is used to create "structured" data types in NumPy, this class does not support structured data types. See the ``Structured`` data type for this functionality. """ # np.dtypes.VoidDType is specified in an odd way in NumPy # it cannot be used to create instances of the dtype # so we have to tell mypy to ignore this here dtype_cls = np.dtypes.VoidDType # type: ignore[assignment] _zarr_v3_name: ClassVar[Literal["raw_bytes"]] = "raw_bytes" def __post_init__(self) -> None: """ We don't allow instances of this class with length less than 1 because there is no way such a data type can contain actual data. """ if self.length < 1: raise ValueError(f"length must be >= 1, got {self.length}.") @classmethod def _check_native_dtype( cls: type[Self], dtype: TBaseDType ) -> TypeGuard[np.dtypes.VoidDType[int]]: """ Check that the input is a NumPy void dtype with no fields. Numpy void dtype comes in two forms: * If the ``fields`` attribute is ``None``, then the dtype represents N raw bytes. * If the ``fields`` attribute is not ``None``, then the dtype represents a structured dtype, In this check we ensure that ``fields`` is ``None``. Parameters ---------- dtype : TDBaseDType The dtype to check. Returns ------- Bool True if the dtype is an instance of np.dtypes.VoidDType with no fields, False otherwise. """ return cls.dtype_cls is type(dtype) and dtype.fields is None @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of RawBytes from an instance of np.dtypes.VoidDType. This method checks if the provided data type is compatible with RawBytes. The input must be an instance of np.dtypes.VoidDType, and have no fields. If the input is compatible, this method returns an instance of RawBytes with the specified length. Parameters ---------- dtype : TBaseDType The native dtype to convert. Returns ------- RawBytes An instance of RawBytes with the specified length. Raises ------ DataTypeValidationError If the dtype is not compatible with RawBytes. """ if cls._check_native_dtype(dtype): return cls(length=dtype.itemsize) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.VoidDType[int]: """ Create a NumPy void dtype from this RawBytes ZDType. Returns ------- np.dtypes.VoidDType[int] A NumPy data type object representing raw bytes with a specified length. """ # Numpy does not allow creating a void type # by invoking np.dtypes.VoidDType directly return cast("np.dtypes.VoidDType[int]", np.dtype(f"V{self.length}")) @classmethod def _check_json_v2(cls, data: DTypeJSON) -> TypeGuard[RawBytesJSON_V2]: """ Check that the input is a valid representation of this class in Zarr V2. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- True if the input is a valid representation of this class in Zarr V3, False otherwise. """ return ( check_dtype_spec_v2(data) and isinstance(data["name"], str) and re.match(r"^\|V\d+$", data["name"]) is not None and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[RawBytesJSON_V3]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[RawBytesJSON_V3] True if the input is a valid representation of this class in Zarr V3, False otherwise. """ return ( isinstance(data, dict) and set(data.keys()) == {"name", "configuration"} and data["name"] == cls._zarr_v3_name and isinstance(data["configuration"], dict) and set(data["configuration"].keys()) == {"length_bytes"} and isinstance(data["configuration"]["length_bytes"], int) ) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of RawBytes from Zarr V2-flavored JSON. This method checks if the input data is a valid representation of RawBytes in Zarr V2. If so, it returns a new instance of RawBytes with a ``length`` as specified in the input data. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data is not a valid representation of this class. """ if cls._check_json_v2(data): name = data["name"] return cls(length=int(name[2:])) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a string like '|V1', '|V2', etc" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of RawBytes from Zarr V3-flavored JSON. This method checks if the input data is a valid representation of RawBytes in Zarr V3. If so, it returns a new instance of RawBytes with a ``length`` as specified in the input data. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- RawBytes An instance of RawBytes. Raises ------ DataTypeValidationError If the input data is not a valid representation of this class. """ if cls._check_json_v3(data): return cls(length=data["configuration"]["length_bytes"]) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> RawBytesJSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> RawBytesJSON_V3: ... def to_json(self, zarr_format: ZarrFormat) -> RawBytesJSON_V2 | RawBytesJSON_V3: """ Generate a JSON representation of this data type. Parameters ---------- zarr_format : ZarrFormat The zarr format version. Returns ------- RawBytesJSON_V2 | RawBytesJSON_V3 The JSON-serializable representation of the data type. """ if zarr_format == 2: return {"name": self.to_native_dtype().str, "object_codec_id": None} elif zarr_format == 3: v3_unstable_dtype_warning(self) return {"name": self._zarr_v3_name, "configuration": {"length_bytes": self.length}} raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[np.bytes_ | str | bytes | np.void]: """ Check if the provided data can be cast to np.void. This method is used to verify if the input data can be considered as a scalar of bytes-like type, which includes np.bytes_, np.void, strings, and bytes objects. Parameters ---------- data : object The data to check. Returns ------- TypeGuard[np.bytes_ | str | bytes | np.void] True if the data is void-scalar-like, False otherwise. """ return isinstance(data, np.bytes_ | str | bytes | np.void) def _cast_scalar_unchecked(self, data: object) -> np.void: """ Cast the provided scalar data to np.void. Parameters ---------- data : BytesLike The data to cast. Returns ------- np.void The casted data as a NumPy void scalar. Notes ----- This method does not perform any type checking. The input data must be castable to np.void. """ native_dtype = self.to_native_dtype() # Without the second argument, NumPy will return a void scalar for dtype V1. # The second argument ensures that, if native_dtype is something like V10, # the result will actually be a V10 scalar. return native_dtype.type(data, native_dtype) def cast_scalar(self, data: object) -> np.void: """ Attempt to cast a given object to a NumPy void scalar. This method first checks if the provided data is a valid scalar that can be converted to a NumPy void scalar. If the check succeeds, the unchecked casting operation is performed. If the data is not valid, a TypeError is raised. Parameters ---------- data : object The data to be cast to a NumPy void scalar. Returns ------- np.void The data cast as a NumPy void scalar. Raises ------ TypeError If the data cannot be converted to a NumPy void scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> np.void: """ Return the default scalar value for this data type. The default scalar is a NumPy void scalar of the same length as the data type, filled with zero bytes. Returns ------- np.void The default scalar value. """ return self.to_native_dtype().type(("\x00" * self.length).encode("ascii")) def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> str: """ Convert a scalar to a JSON-serializable string representation. This method converts the given scalar to bytes and then encodes the bytes as a base64-encoded string. Parameters ---------- data : object The scalar to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- str A string representation of the scalar. """ as_bytes = self.cast_scalar(data) return base64.standard_b64encode(as_bytes.tobytes()).decode("ascii") def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.void: """ Read a JSON-serializable value as an np.void. Parameters ---------- data : JSON The JSON-serializable value. zarr_format : ZarrFormat The zarr format version. Returns ------- np.void The NumPy void scalar. Raises ------ TypeError If the data is not a string, or if the string is not a valid base64 encoding. """ if check_json_str(data): return self.to_native_dtype().type(base64.standard_b64decode(data)) raise TypeError(f"Invalid type: {data}. Expected a string.") # pragma: no cover @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return self.length @dataclass(frozen=True, kw_only=True) class VariableLengthBytes(ZDType[np.dtypes.ObjectDType, bytes], HasObjectCodec): """ A Zarr data type for arrays containing variable-length sequences of bytes. Wraps the NumPy "object" data type. Scalars for this data type are instances of ``bytes``. Attributes ---------- dtype_cls: ClassVar[type[np.dtypes.ObjectDType]] = np.dtypes.ObjectDType The NumPy data type wrapped by this ZDType. _zarr_v3_name: ClassVar[Literal["variable_length_bytes"]] = "variable_length_bytes" The name of this data type in Zarr V3. object_codec_id: ClassVar[Literal["vlen-bytes"]] = "vlen-bytes" The object codec ID for this data type. Notes ----- Because this data type uses the NumPy "object" data type, it does not guarantee a compact memory representation of array data. Therefore a "vlen-bytes" codec is needed to ensure that the array data can be persisted to storage. """ dtype_cls = np.dtypes.ObjectDType _zarr_v3_name: ClassVar[Literal["variable_length_bytes"]] = "variable_length_bytes" object_codec_id: ClassVar[Literal["vlen-bytes"]] = "vlen-bytes" @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of VariableLengthBytes from an instance of np.dtypes.ObjectDType. This method checks if the provided data type is an instance of np.dtypes.ObjectDType. If so, it returns an instance of VariableLengthBytes. Parameters ---------- dtype : TBaseDType The native dtype to convert. Returns ------- VariableLengthBytes An instance of VariableLengthBytes. Raises ------ DataTypeValidationError If the dtype is not compatible with VariableLengthBytes. """ if cls._check_native_dtype(dtype): return cls() raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.ObjectDType: """ Create a NumPy object dtype from this VariableLengthBytes ZDType. Returns ------- np.dtypes.ObjectDType A NumPy data type object representing variable-length bytes. """ return self.dtype_cls() @classmethod def _check_json_v2( cls, data: DTypeJSON, ) -> TypeGuard[VariableLengthBytesJSON_V2]: """ Check that the input is a valid JSON representation of a NumPy O dtype, and that the object codec id is appropriate for variable-length bytes strings. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- True if the input is a valid representation of this class in Zarr V2, False otherwise. """ # Check that the input is a valid JSON representation of a Zarr v2 data type spec. if not check_dtype_spec_v2(data): return False # Check that the object codec id is appropriate for variable-length bytes strings. if data["name"] != "|O": return False return data["object_codec_id"] == cls.object_codec_id @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[Literal["variable_length_bytes"]]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[Literal["variable_length_bytes"]] True if the input is a valid representation of this class in Zarr V3, False otherwise. """ return data in (cls._zarr_v3_name, "bytes") @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this VariableLengthBytes from Zarr V2-flavored JSON. This method checks if the input data is a valid representation of this class in Zarr V2. If so, it returns a new instance this class. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data is not a valid representation of this class class. """ if cls._check_json_v2(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string '|O' and an object_codec_id of {cls.object_codec_id}" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of VariableLengthBytes from Zarr V3-flavored JSON. This method checks if the input data is a valid representation of VariableLengthBytes in Zarr V3. If so, it returns a new instance of VariableLengthBytes. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- VariableLengthBytes An instance of VariableLengthBytes. Raises ------ DataTypeValidationError If the input data is not a valid representation of this class. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> VariableLengthBytesJSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> Literal["variable_length_bytes"]: ... def to_json( self, zarr_format: ZarrFormat ) -> VariableLengthBytesJSON_V2 | Literal["variable_length_bytes"]: """ Convert the variable-length bytes data type to a JSON-serializable form. Parameters ---------- zarr_format : ZarrFormat The zarr format version. Accepted values are 2 and 3. Returns ------- ``DTypeConfig_V2[Literal["|O"], Literal["vlen-bytes"]] | Literal["variable_length_bytes"]`` The JSON-serializable representation of the variable-length bytes data type. For zarr_format 2, returns a dictionary with "name" and "object_codec_id". For zarr_format 3, returns a string identifier "variable_length_bytes". Raises ------ ValueError If zarr_format is not 2 or 3. """ if zarr_format == 2: return {"name": "|O", "object_codec_id": self.object_codec_id} elif zarr_format == 3: v3_unstable_dtype_warning(self) return self._zarr_v3_name raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def default_scalar(self) -> bytes: """ Return the default scalar value for the variable-length bytes data type. Returns ------- bytes The default scalar value, which is an empty byte string. """ return b"" def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> str: """ Convert a scalar to a JSON-serializable string representation. This method encodes the given scalar as bytes and then encodes the bytes as a base64-encoded string. Parameters ---------- data : object The scalar to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- str A string representation of the scalar. """ return base64.standard_b64encode(data).decode("ascii") # type: ignore[arg-type] def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> bytes: """ Decode a base64-encoded JSON string to bytes. Parameters ---------- data : JSON The JSON-serializable base64-encoded string. zarr_format : ZarrFormat The zarr format version. Returns ------- bytes The decoded bytes from the base64 string. Raises ------ TypeError If the input data is not a base64-encoded string. """ if check_json_str(data): return base64.standard_b64decode(data.encode("ascii")) raise TypeError(f"Invalid type: {data}. Expected a string.") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[BytesLike]: """ Check if the provided data is of type BytesLike. This method is used to verify if the input data can be considered as a scalar of bytes-like type, which includes NumPy bytes, strings, bytes, and integers. Parameters ---------- data : object The data to check. Returns ------- TypeGuard[BytesLike] True if the data is bytes-like, False otherwise. """ return isinstance(data, BytesLike) def _cast_scalar_unchecked(self, data: BytesLike) -> bytes: """ Cast the provided scalar data to bytes. Parameters ---------- data : BytesLike The data to cast. Returns ------- bytes The casted data as bytes. Notes ----- This method does not perform any type checking. The input data must be bytes-like. """ if isinstance(data, str): return bytes(data, encoding="utf-8") return bytes(data) def cast_scalar(self, data: object) -> bytes: """ Attempt to cast a given object to a bytes scalar. This method first checks if the provided data is a valid scalar that can be converted to a bytes scalar. If the check succeeds, the unchecked casting operation is performed. If the data is not valid, a TypeError is raised. Parameters ---------- data : object The data to be cast to a bytes scalar. Returns ------- bytes The data cast as a bytes scalar. Raises ------ TypeError If the data cannot be converted to a bytes scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) zarr-python-3.2.1/src/zarr/core/dtype/npy/common.py000066400000000000000000000327541517635743000223320ustar00rootroot00000000000000from __future__ import annotations import base64 import struct import sys from collections.abc import Sequence from typing import ( TYPE_CHECKING, Any, Final, Literal, NewType, SupportsComplex, SupportsFloat, SupportsIndex, SupportsInt, TypeGuard, ) import numpy as np from zarr.core.dtype.common import ( ENDIANNESS_STR, SPECIAL_FLOAT_STRINGS, EndiannessStr, JSONFloatV2, JSONFloatV3, ) if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat 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", ) IntishFloat = NewType("IntishFloat", float) """A type for floats that represent integers, like 1.0 (but not 1.1).""" IntishStr = NewType("IntishStr", str) """A type for strings that represent integers, like "0" or "42".""" FloatishStr = NewType("FloatishStr", str) """A type for strings that represent floats, like "3.14" or "-2.5".""" NumpyEndiannessStr = Literal[">", "<", "="] NUMPY_ENDIANNESS_STR: Final = ">", "<", "=" def endianness_from_numpy_str(endianness: NumpyEndiannessStr) -> EndiannessStr: """ Convert a numpy endianness string literal to a human-readable literal value. Parameters ---------- endianness : Literal[">", "<", "="] The numpy string representation of the endianness. Returns ------- Endianness The human-readable representation of the endianness. Raises ------ ValueError If the endianness is invalid. """ match endianness: case "=": # Use the local system endianness return sys.byteorder case "<": return "little" case ">": return "big" raise ValueError(f"Invalid endianness: {endianness!r}. Expected one of {NUMPY_ENDIANNESS_STR}") def endianness_to_numpy_str(endianness: EndiannessStr) -> NumpyEndiannessStr: """ Convert an endianness literal to its numpy string representation. Parameters ---------- endianness : Endianness The endianness to convert. Returns ------- Literal[">", "<"] The numpy string representation of the endianness. Raises ------ ValueError If the endianness is invalid. """ match endianness: case "little": return "<" case "big": return ">" raise ValueError( f"Invalid endianness: {endianness!r}. Expected one of {ENDIANNESS_STR} or None" ) def get_endianness_from_numpy_dtype(dtype: np.dtype[np.generic]) -> EndiannessStr: """ Gets the endianness from a numpy dtype that has an endianness. This function will raise a ValueError if the numpy data type does not have a concrete endianness. """ endianness = dtype.byteorder if dtype.byteorder in NUMPY_ENDIANNESS_STR: return endianness_from_numpy_str(endianness) # type: ignore [arg-type] raise ValueError(f"The dtype {dtype} has an unsupported endianness: {endianness}") def float_from_json_v2(data: JSONFloatV2) -> float: """ Convert a JSON float to a float (Zarr v2). Parameters ---------- data : JSONFloat The JSON float to convert. Returns ------- float The float value. """ match data: case "NaN": return float("nan") case "Infinity": return float("inf") case "-Infinity": return float("-inf") case _: return float(data) def float_from_json_v3(data: JSONFloatV3) -> float: """ Convert a JSON float to a float (v3). Parameters ---------- data : JSONFloat The JSON float to convert. Returns ------- float The float value. Notes ----- Zarr V3 allows floats to be stored as hex strings. To quote the spec: "...for float32, "NaN" is equivalent to "0x7fc00000". This representation is the only way to specify a NaN value other than the specific NaN value denoted by "NaN"." """ if isinstance(data, str): if data in SPECIAL_FLOAT_STRINGS: return float_from_json_v2(data) # type: ignore[arg-type] if not data.startswith("0x"): msg = ( f"Invalid float value: {data!r}. Expected a string starting with the hex prefix" " '0x', or one of 'NaN', 'Infinity', or '-Infinity'." ) raise ValueError(msg) if len(data[2:]) == 4: dtype_code = ">e" elif len(data[2:]) == 8: dtype_code = ">f" elif len(data[2:]) == 16: dtype_code = ">d" else: msg = ( f"Invalid hexadecimal float value: {data!r}. " "Expected the '0x' prefix to be followed by 4, 8, or 16 numeral characters" ) raise ValueError(msg) return float(struct.unpack(dtype_code, bytes.fromhex(data[2:]))[0]) return float_from_json_v2(data) def bytes_from_json(data: str, *, zarr_format: ZarrFormat) -> bytes: """ Convert a JSON string to bytes Parameters ---------- data : str The JSON string to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- bytes The bytes. """ if zarr_format == 2: return base64.b64decode(data.encode("ascii")) # TODO: differentiate these as needed. This is a spec question. if zarr_format == 3: return base64.b64decode(data.encode("ascii")) raise ValueError(f"Invalid zarr format: {zarr_format}. Expected 2 or 3.") # pragma: no cover def bytes_to_json(data: bytes, zarr_format: ZarrFormat) -> str: """ Convert bytes to JSON. Parameters ---------- data : bytes The bytes to store. zarr_format : ZarrFormat The zarr format version. Returns ------- str The bytes encoded as ascii using the base64 alphabet. """ # TODO: decide if we are going to make this implementation zarr format-specific return base64.b64encode(data).decode("ascii") def float_to_json_v2(data: float | np.floating[Any]) -> JSONFloatV2: """ Convert a float to JSON (v2). Parameters ---------- data : float or np.floating The float value to convert. Returns ------- JSONFloat The JSON representation of the float. """ if np.isnan(data): return "NaN" elif np.isinf(data): return "Infinity" if data > 0 else "-Infinity" return float(data) def float_to_json_v3(data: float | np.floating[Any]) -> JSONFloatV3: """ Convert a float to JSON (v3). Parameters ---------- data : float or np.floating The float value to convert. Returns ------- JSONFloat The JSON representation of the float. """ # v3 can in principle handle distinct NaN values, but numpy does not represent these explicitly # so we just reuse the v2 routine here return float_to_json_v2(data) def complex_float_to_json_v3( data: complex | np.complexfloating[Any, Any], ) -> tuple[JSONFloatV3, JSONFloatV3]: """ Convert a complex number to JSON as defined by the Zarr V3 spec. Parameters ---------- data : complex or np.complexfloating The complex value to convert. Returns ------- tuple[JSONFloat, JSONFloat] The JSON representation of the complex number. """ return float_to_json_v3(data.real), float_to_json_v3(data.imag) def complex_float_to_json_v2( data: complex | np.complexfloating[Any, Any], ) -> tuple[JSONFloatV2, JSONFloatV2]: """ Convert a complex number to JSON as defined by the Zarr V2 spec. Parameters ---------- data : complex | np.complexfloating The complex value to convert. Returns ------- tuple[JSONFloat, JSONFloat] The JSON representation of the complex number. """ return float_to_json_v2(data.real), float_to_json_v2(data.imag) def complex_float_from_json_v2(data: tuple[JSONFloatV2, JSONFloatV2]) -> complex: """ Convert a JSON complex float to a complex number (v2). Parameters ---------- data : tuple[JSONFloat, JSONFloat] The JSON complex float to convert. Returns ------- np.complexfloating The complex number. """ return complex(float_from_json_v2(data[0]), float_from_json_v2(data[1])) def complex_float_from_json_v3(data: tuple[JSONFloatV3, JSONFloatV3]) -> complex: """ Convert a JSON complex float to a complex number (v3). Parameters ---------- data : tuple[JSONFloat, JSONFloat] The JSON complex float to convert. Returns ------- np.complexfloating The complex number. """ return complex(float_from_json_v3(data[0]), float_from_json_v3(data[1])) def check_json_float_v2(data: JSON) -> TypeGuard[JSONFloatV2]: """ Check if a JSON value represents a float (v2). Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is a float, False otherwise. """ return data in ("NaN", "Infinity", "-Infinity") or isinstance(data, float | int) def check_json_float_v3(data: JSON) -> TypeGuard[JSONFloatV3]: """ Check if a JSON value represents a float (v3). Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is a float, False otherwise. """ return check_json_float_v2(data) or (isinstance(data, str) and data.startswith("0x")) def check_json_complex_float_v2(data: JSON) -> TypeGuard[tuple[JSONFloatV2, JSONFloatV2]]: """ Check if a JSON value represents a complex float, as per the behavior of zarr-python 2.x Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is a complex float, False otherwise. """ return ( not isinstance(data, str) and isinstance(data, Sequence) and len(data) == 2 and check_json_float_v2(data[0]) and check_json_float_v2(data[1]) ) def check_json_complex_float_v3(data: JSON) -> TypeGuard[tuple[JSONFloatV3, JSONFloatV3]]: """ Check if a JSON value represents a complex float, as per the zarr v3 spec Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is a complex float, False otherwise. """ return ( not isinstance(data, str) and isinstance(data, Sequence) and len(data) == 2 and check_json_float_v3(data[0]) and check_json_float_v3(data[1]) ) def check_json_int(data: JSON) -> TypeGuard[int]: """ Check if a JSON value is an integer. Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is an integer, False otherwise. """ return bool(isinstance(data, int)) def check_json_intish_float(data: JSON) -> TypeGuard[IntishFloat]: """ Check if a JSON value is an "intish float", i.e. a float that represents an integer, like 0.0. Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is an intish float, False otherwise. """ return isinstance(data, float) and data.is_integer() def check_json_intish_str(data: JSON) -> TypeGuard[IntishStr]: """ Check if a JSON value is a string that represents an integer, like "0", "42", or "-5". Parameters ---------- data : JSON The JSON value to check. Returns ------- bool True if the data is a string representing an integer, False otherwise. """ if not isinstance(data, str): return False try: int(data) except ValueError: return False else: return True def check_json_floatish_str(data: JSON) -> TypeGuard[FloatishStr]: """ Check if a JSON value is a string that represents a float, like "3.14", "-2.5", or "0.0". Note: This function is intended to be used AFTER check_json_float_v2/v3, so it only handles regular string representations that those functions don't cover. Parameters ---------- data : JSON The JSON value to check. Returns ------- bool True if the data is a string representing a regular float, False otherwise. """ if not isinstance(data, str): return False try: float(data) except ValueError: return False else: return True def check_json_str(data: JSON) -> TypeGuard[str]: """ Check if a JSON value is a string. Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is a string, False otherwise. """ return bool(isinstance(data, str)) def check_json_bool(data: JSON) -> TypeGuard[bool]: """ Check if a JSON value is a boolean. Parameters ---------- data : JSON The JSON value to check. Returns ------- Bool True if the data is a boolean, False otherwise. """ return isinstance(data, bool) zarr-python-3.2.1/src/zarr/core/dtype/npy/complex.py000066400000000000000000000311361517635743000225020ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import ( TYPE_CHECKING, ClassVar, Literal, Self, TypeGuard, overload, ) import numpy as np from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasEndianness, HasItemSize, check_dtype_spec_v2, ) from zarr.core.dtype.npy.common import ( ComplexLike, check_json_complex_float_v2, check_json_complex_float_v3, complex_float_from_json_v2, complex_float_from_json_v3, complex_float_to_json_v2, complex_float_to_json_v3, endianness_to_numpy_str, get_endianness_from_numpy_dtype, ) from zarr.core.dtype.wrapper import TBaseDType, ZDType if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat @dataclass(frozen=True) class BaseComplex[ DType: np.dtypes.Complex64DType | np.dtypes.Complex128DType, Scalar: np.complex64 | np.complex128, ](ZDType[DType, Scalar], HasEndianness, HasItemSize): """ A base class for Zarr data types that wrap NumPy complex float data types. """ # This attribute holds the possible zarr v2 JSON names for the data type _zarr_v2_names: ClassVar[tuple[str, ...]] @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of this data type from a NumPy complex dtype. Parameters ---------- dtype : TBaseDType The native dtype to convert. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the dtype is not compatible with this data type. """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> DType: """ Convert this class to a NumPy complex dtype with the appropriate byte order. Returns ------- DType A NumPy data type object representing the complex data type with the specified byte order. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) # type: ignore[no-any-return,call-overload] @classmethod def _check_json_v2(cls, data: DTypeJSON) -> TypeGuard[DTypeConfig_V2[str, None]]: """ Check that the input is a valid JSON representation of this data type. The input data must be a mapping that contains a "name" key that is one of the strings from cls._zarr_v2_names and an "object_codec_id" key that is None. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- bool True if the input is a valid JSON representation, False otherwise. """ return ( check_dtype_spec_v2(data) and data["name"] in cls._zarr_v2_names and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[str]: """ Check that the input is a valid JSON representation of this data type in Zarr V3. This method verifies that the provided data matches the expected Zarr V3 representation, which is the string specified by the class-level attribute _zarr_v3_name. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[str] True if the input is a valid representation of this class in Zarr V3, False otherwise. """ return data == cls._zarr_v3_name @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this class from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this class. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v2(data): # Going via numpy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected one of the strings {cls._zarr_v2_names}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this class from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected {cls._zarr_v3_name}." raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[str, None]: ... @overload def to_json(self, zarr_format: Literal[3]) -> str: ... def to_json(self, zarr_format: ZarrFormat) -> DTypeConfig_V2[str, None] | str: """ Serialize this object to a JSON-serializable representation. Parameters ---------- zarr_format : ZarrFormat The Zarr format version. Supported values are 2 and 3. Returns ------- DTypeConfig_V2[str, None] | str If ``zarr_format`` is 2, a dictionary with ``"name"`` and ``"object_codec_id"`` keys is returned. If ``zarr_format`` is 3, a string representation of the complex data type is returned. Raises ------ ValueError If `zarr_format` is not 2 or 3. """ if zarr_format == 2: return {"name": self.to_native_dtype().str, "object_codec_id": None} elif zarr_format == 3: return self._zarr_v3_name raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[ComplexLike]: """ Check that the input is a scalar complex value. Parameters ---------- data : object The value to check. Returns ------- TypeGuard[ComplexLike] True if the input is a scalar complex value, False otherwise. """ return isinstance(data, ComplexLike) def _cast_scalar_unchecked(self, data: ComplexLike) -> Scalar: """ Cast the provided scalar data to the native scalar type of this class. Parameters ---------- data : ComplexLike The data to cast. Returns ------- Scalar The casted data as a numpy complex scalar. Notes ----- This method does not perform any type checking. The input data must be a scalar complex value. """ return self.to_native_dtype().type(data) # type: ignore[return-value] def cast_scalar(self, data: object) -> Scalar: """ Attempt to cast a given object to a numpy complex scalar. Parameters ---------- data : object The data to be cast to a numpy complex scalar. Returns ------- Scalar The data cast as a numpy complex scalar. Raises ------ TypeError If the data cannot be converted to a numpy complex scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> Scalar: """ Get the default value, which is 0 cast to this dtype Returns ------- Int scalar The default value. """ return self._cast_scalar_unchecked(0) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar: """ Read a JSON-serializable value as a numpy float. Parameters ---------- data : JSON The JSON-serializable value. zarr_format : ZarrFormat The zarr format version. Returns ------- Scalar The numpy float. """ if zarr_format == 2: if check_json_complex_float_v2(data): return self._cast_scalar_unchecked(complex_float_from_json_v2(data)) raise TypeError( f"Invalid type: {data}. Expected a float or a special string encoding of a float." ) elif zarr_format == 3: if check_json_complex_float_v3(data): return self._cast_scalar_unchecked(complex_float_from_json_v3(data)) raise TypeError( f"Invalid type: {data}. Expected a float or a special string encoding of a float." ) raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> JSON: """ Convert an object to a JSON-serializable float. Parameters ---------- data : _BaseScalar The value to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- JSON The JSON-serializable form of the complex number, which is a list of two floats, each of which is encoding according to a zarr-format-specific encoding. """ if zarr_format == 2: return complex_float_to_json_v2(self.cast_scalar(data)) elif zarr_format == 3: return complex_float_to_json_v3(self.cast_scalar(data)) raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover @dataclass(frozen=True, kw_only=True) class Complex64(BaseComplex[np.dtypes.Complex64DType, np.complex64]): """ A Zarr data type for arrays containing 64 bit complex floats. Wraps the [`np.dtypes.Complex64DType`][numpy.dtypes.Complex64DType] data type. Scalars for this data type are instances of [`np.complex64`][numpy.complex64]. Attributes ---------- dtype_cls : Type[np.dtypes.Complex64DType] The numpy dtype class for this data type. _zarr_v3_name : ClassVar[Literal["complex64"]] The name of this data type in Zarr V3. _zarr_v2_names : ClassVar[tuple[Literal[">c8"], Literal["c8"], Literal["c8", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 8 @dataclass(frozen=True, kw_only=True) class Complex128(BaseComplex[np.dtypes.Complex128DType, np.complex128], HasEndianness): """ A Zarr data type for arrays containing 64 bit complex floats. Wraps the [`np.dtypes.Complex128DType`][numpy.dtypes.Complex128DType] data type. Scalars for this data type are instances of [`np.complex128`][numpy.complex128]. Attributes ---------- dtype_cls : Type[np.dtypes.Complex128DType] The numpy dtype class for this data type. _zarr_v3_name : ClassVar[Literal["complex128"]] The name of this data type in Zarr V3. _zarr_v2_names : ClassVar[tuple[Literal[">c16"], Literal["c16"], Literal["c16", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 16 zarr-python-3.2.1/src/zarr/core/dtype/npy/float.py000066400000000000000000000325031517635743000221370ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar, Literal, Self, TypeGuard, overload import numpy as np from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasEndianness, HasItemSize, check_dtype_spec_v2, ) from zarr.core.dtype.npy.common import ( FloatLike, check_json_float_v2, check_json_float_v3, check_json_floatish_str, endianness_to_numpy_str, float_from_json_v2, float_from_json_v3, float_to_json_v2, float_to_json_v3, get_endianness_from_numpy_dtype, ) from zarr.core.dtype.wrapper import TBaseDType, ZDType if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat @dataclass(frozen=True) class BaseFloat[ DType: np.dtypes.Float16DType | np.dtypes.Float32DType | np.dtypes.Float64DType, Scalar: np.float16 | np.float32 | np.float64, ](ZDType[DType, Scalar], HasEndianness, HasItemSize): """ A base class for Zarr data types that wrap NumPy float data types. """ # This attribute holds the possible zarr v2 JSON names for the data type _zarr_v2_names: ClassVar[tuple[str, ...]] @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of this ZDType from a NumPy data type. Parameters ---------- dtype : TBaseDType The NumPy data type. Returns ------- Self An instance of this data type. """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> DType: """ Convert the wrapped data type to a NumPy data type. Returns ------- DType The NumPy data type. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) # type: ignore[no-any-return,call-overload] @classmethod def _check_json_v2(cls, data: DTypeJSON) -> TypeGuard[DTypeConfig_V2[str, None]]: """ Check that the input is a valid JSON representation of this data type. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- TypeGuard[DTypeConfig_V2[str, None]] True if the input is a valid JSON representation of this data type, False otherwise. """ return ( check_dtype_spec_v2(data) and data["name"] in cls._zarr_v2_names and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[str]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- TypeGuard[str] True if the input is a valid JSON representation of this class, False otherwise. """ return data == cls._zarr_v3_name @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this ZDType from Zarr v2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. """ if cls._check_json_v2(data): # Going via NumPy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected one of the strings {cls._zarr_v2_names}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this ZDType from Zarr v3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected {cls._zarr_v3_name}." raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[str, None]: ... @overload def to_json(self, zarr_format: Literal[3]) -> str: ... def to_json(self, zarr_format: ZarrFormat) -> DTypeConfig_V2[str, None] | str: """ Convert the wrapped data type to a JSON-serializable form. Parameters ---------- zarr_format : ZarrFormat The zarr format version. Returns ------- DTypeConfig_V2[str, None] or str The JSON-serializable representation of the wrapped data type. Raises ------ ValueError If zarr_format is not 2 or 3. """ if zarr_format == 2: return {"name": self.to_native_dtype().str, "object_codec_id": None} elif zarr_format == 3: return self._zarr_v3_name raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[FloatLike]: """ Check that the input is a valid scalar value. Parameters ---------- data : object The input to check. Returns ------- TypeGuard[FloatLike] True if the input is a valid scalar value, False otherwise. """ if isinstance(data, str): # Only accept strings that are valid float representations (e.g. "NaN", "inf"). # Plain strings that cannot be converted should return False so that cast_scalar # raises TypeError rather than a confusing ValueError. try: self.to_native_dtype().type(data) except (ValueError, OverflowError): return False else: return True return isinstance(data, FloatLike) def _cast_scalar_unchecked(self, data: FloatLike) -> Scalar: """ Cast a scalar value to a NumPy float scalar. Parameters ---------- data : FloatLike The scalar value to cast. Returns ------- Scalar The NumPy float scalar. """ return self.to_native_dtype().type(data) # type: ignore[return-value] def cast_scalar(self, data: object) -> Scalar: """ Cast a scalar value to a NumPy float scalar. Parameters ---------- data : object The scalar value to cast. Returns ------- Scalar The NumPy float scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> Scalar: """ Get the default value, which is 0 cast to this zdtype. Returns ------- Scalar The default value. """ return self._cast_scalar_unchecked(0) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar: """ Read a JSON-serializable value as a NumPy float scalar. Parameters ---------- data : JSON The JSON-serializable value. zarr_format : ZarrFormat The zarr format version. Returns ------- Scalar The NumPy float scalar. """ if zarr_format == 2: if check_json_float_v2(data): return self._cast_scalar_unchecked(float_from_json_v2(data)) elif check_json_floatish_str(data): return self._cast_scalar_unchecked(float(data)) else: raise TypeError( f"Invalid type: {data}. Expected a float or a special string encoding of a float." ) elif zarr_format == 3: if check_json_float_v3(data): return self._cast_scalar_unchecked(float_from_json_v3(data)) elif check_json_floatish_str(data): return self._cast_scalar_unchecked(float(data)) else: raise TypeError( f"Invalid type: {data}. Expected a float or a special string encoding of a float." ) else: raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> float | str: """ Convert an object to a JSON-serializable float. Parameters ---------- data : _BaseScalar The value to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- JSON The JSON-serializable form of the float, which is potentially a number or a string. See the zarr specifications for details on the JSON encoding for floats. """ if zarr_format == 2: return float_to_json_v2(self.cast_scalar(data)) elif zarr_format == 3: return float_to_json_v3(self.cast_scalar(data)) else: raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover @dataclass(frozen=True, kw_only=True) class Float16(BaseFloat[np.dtypes.Float16DType, np.float16]): """ A Zarr data type for arrays containing 16-bit floating point numbers. Wraps the [`np.dtypes.Float16DType`][numpy.dtypes.Float16DType] data type. Scalars for this data type are instances of [`np.float16`][numpy.float16]. Attributes ---------- dtype_cls : Type[np.dtypes.Float16DType] The NumPy dtype class for this data type. References ---------- This class implements the float16 data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.Float16DType _zarr_v3_name = "float16" _zarr_v2_names: ClassVar[tuple[Literal[">f2"], Literal["f2", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 2 @dataclass(frozen=True, kw_only=True) class Float32(BaseFloat[np.dtypes.Float32DType, np.float32]): """ A Zarr data type for arrays containing 32-bit floating point numbers. Wraps the [`np.dtypes.Float32DType`][numpy.dtypes.Float32DType] data type. Scalars for this data type are instances of [`np.float32`][numpy.float32]. Attributes ---------- dtype_cls : Type[np.dtypes.Float32DType] The NumPy dtype class for this data type. References ---------- This class implements the float32 data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.Float32DType _zarr_v3_name = "float32" _zarr_v2_names: ClassVar[tuple[Literal[">f4"], Literal["f4", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 4 @dataclass(frozen=True, kw_only=True) class Float64(BaseFloat[np.dtypes.Float64DType, np.float64]): """ A Zarr data type for arrays containing 64-bit floating point numbers. Wraps the [`np.dtypes.Float64DType`][numpy.dtypes.Float64DType] data type. Scalars for this data type are instances of [`np.float64`][numpy.float64]. Attributes ---------- dtype_cls : Type[np.dtypes.Float64DType] The NumPy dtype class for this data type. References ---------- This class implements the float64 data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.Float64DType _zarr_v3_name = "float64" _zarr_v2_names: ClassVar[tuple[Literal[">f8"], Literal["f8", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 8 zarr-python-3.2.1/src/zarr/core/dtype/npy/int.py000066400000000000000000001355061517635743000216330ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import ( TYPE_CHECKING, ClassVar, Literal, Self, SupportsIndex, SupportsInt, TypeGuard, overload, ) import numpy as np from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasEndianness, HasItemSize, check_dtype_spec_v2, ) from zarr.core.dtype.npy.common import ( check_json_int, check_json_intish_float, check_json_intish_str, endianness_to_numpy_str, get_endianness_from_numpy_dtype, ) from zarr.core.dtype.wrapper import TBaseDType, ZDType if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat _NumpyIntDType = ( np.dtypes.Int8DType | np.dtypes.Int16DType | np.dtypes.Int32DType | np.dtypes.Int64DType | np.dtypes.UInt8DType | np.dtypes.UInt16DType | np.dtypes.UInt32DType | np.dtypes.UInt64DType ) _NumpyIntScalar = ( np.int8 | np.int16 | np.int32 | np.int64 | np.uint8 | np.uint16 | np.uint32 | np.uint64 ) IntLike = SupportsInt | SupportsIndex | bytes | str @dataclass(frozen=True) class BaseInt[ DType: _NumpyIntDType, Scalar: np.int8 | np.int16 | np.int32 | np.int64 | np.uint8 | np.uint16 | np.uint32 | np.uint64, ](ZDType[DType, Scalar], HasItemSize): """ A base class for integer data types in Zarr. This class provides methods for serialization and deserialization of integer types in both Zarr v2 and v3 formats, as well as methods for checking and casting scalars. """ _zarr_v2_names: ClassVar[tuple[str, ...]] @classmethod def _check_json_v2(cls, data: object) -> TypeGuard[DTypeConfig_V2[str, None]]: """ Check that the input is a valid JSON representation of this integer data type in Zarr V2. This method verifies that the provided data matches the expected Zarr V2 representation for this data type. The input data must be a mapping that contains a "name" key that is one of the strings from cls._zarr_v2_names and an "object_codec_id" key that is None. Parameters ---------- data : object The JSON data to check. Returns ------- TypeGuard[DTypeConfig_V2[str, None]] True if the input is a valid representation of this class in Zarr V2, False otherwise. """ return ( check_dtype_spec_v2(data) and data["name"] in cls._zarr_v2_names and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: object) -> TypeGuard[str]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : object The JSON data to check. Returns ------- TypeGuard[str] True if the input is a valid representation of this class in Zarr v3, False otherwise. """ return data == cls._zarr_v3_name def _check_scalar(self, data: object) -> TypeGuard[IntLike]: """ Check if the input object is of an IntLike type. This method verifies whether the provided data can be considered as an integer-like value, which includes objects supporting integer conversion. Parameters ---------- data : object The data to check. Returns ------- TypeGuard[IntLike] True if the data is IntLike, False otherwise. """ return isinstance(data, IntLike) def _cast_scalar_unchecked(self, data: IntLike) -> Scalar: """ Casts a given scalar value to the native integer scalar type without type checking. Parameters ---------- data : IntLike The scalar value to cast. Returns ------- Scalar The casted integer scalar of the native dtype. """ return self.to_native_dtype().type(data) # type: ignore[return-value] def cast_scalar(self, data: object) -> Scalar: """ Attempt to cast a given object to a NumPy integer scalar. Parameters ---------- data : object The data to be cast to a NumPy integer scalar. Returns ------- Scalar The data cast as a NumPy integer scalar. Raises ------ TypeError If the data cannot be converted to a NumPy integer scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> Scalar: """ Get the default value, which is 0 cast to this dtype. Returns ------- Scalar The default value. """ return self._cast_scalar_unchecked(0) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar: """ Read a JSON-serializable value as a NumPy int scalar. Parameters ---------- data : JSON The JSON-serializable value. zarr_format : ZarrFormat The Zarr format version. Returns ------- Scalar The NumPy int scalar. Raises ------ TypeError If the input is not a valid integer type. """ if check_json_int(data): return self._cast_scalar_unchecked(data) if check_json_intish_float(data): return self._cast_scalar_unchecked(int(data)) if check_json_intish_str(data): return self._cast_scalar_unchecked(int(data)) raise TypeError(f"Invalid type: {data}. Expected an integer.") def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int: """ Convert an object to a JSON serializable scalar. For the integer data types, the JSON form is a plain integer. Parameters ---------- data : object The value to convert. zarr_format : ZarrFormat The Zarr format version. Returns ------- int The JSON-serializable form of the scalar. """ return int(self.cast_scalar(data)) @dataclass(frozen=True, kw_only=True) class Int8(BaseInt[np.dtypes.Int8DType, np.int8]): """ A Zarr data type for arrays containing 8-bit signed integers. Wraps the [`np.dtypes.Int8DType`][numpy.dtypes.Int8DType] data type. Scalars for this data type are instances of [`np.int8`][numpy.int8]. Attributes ---------- dtype_cls : np.dtypes.Int8DType The class of the underlying NumPy dtype. References ---------- This class implements the 8-bit signed integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.Int8DType _zarr_v3_name: ClassVar[Literal["int8"]] = "int8" _zarr_v2_names: ClassVar[tuple[Literal["|i1"]]] = ("|i1",) @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an Int8 from an np.dtype('int8') instance. Parameters ---------- dtype : TBaseDType The np.dtype('int8') instance. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data type is not a valid representation of this class Int8. """ if cls._check_native_dtype(dtype): return cls() raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self: Self) -> np.dtypes.Int8DType: """ Convert the Int8 instance to an np.dtype('int8') instance. Returns ------- np.dtypes.Int8DType The np.dtype('int8') instance. """ return self.dtype_cls() @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an Int8 from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class Int8. """ if cls._check_json_v2(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v2_names[0]!r}" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an Int8 from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class Int8. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal["|i1"], None]: ... @overload def to_json(self, zarr_format: Literal[3]) -> Literal["int8"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal["|i1"], None] | Literal["int8"]: """ Convert the data type to a JSON-serializable form. Parameters ---------- zarr_format : ZarrFormat The Zarr format version. Returns ------- ``DTypeConfig_V2[Literal["|i1"], None] | Literal["int8"]`` The JSON-serializable representation of the data type. Raises ------ ValueError If the zarr_format is not 2 or 3. """ if zarr_format == 2: return {"name": self._zarr_v2_names[0], "object_codec_id": None} elif zarr_format == 3: return self._zarr_v3_name raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 1 @dataclass(frozen=True, kw_only=True) class UInt8(BaseInt[np.dtypes.UInt8DType, np.uint8]): """ A Zarr data type for arrays containing 8-bit unsigned integers. Wraps the [`np.dtypes.UInt8DType`][numpy.dtypes.UInt8DType] data type. Scalars for this data type are instances of [`np.uint8`][numpy.uint8]. Attributes ---------- dtype_cls : np.dtypes.UInt8DType The class of the underlying NumPy dtype. References ---------- This class implements the 8-bit unsigned integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.UInt8DType _zarr_v3_name: ClassVar[Literal["uint8"]] = "uint8" _zarr_v2_names: ClassVar[tuple[Literal["|u1"]]] = ("|u1",) @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create a UInt8 from an np.dtype('uint8') instance. """ if cls._check_native_dtype(dtype): return cls() raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self: Self) -> np.dtypes.UInt8DType: """ Create a NumPy unsigned 8-bit integer dtype instance from this UInt8 ZDType. Returns ------- np.dtypes.UInt8DType The NumPy unsigned 8-bit integer dtype. """ return self.dtype_cls() @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v2(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v2_names[0]!r}" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal["|u1"], None]: ... @overload def to_json(self, zarr_format: Literal[3]) -> Literal["uint8"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal["|u1"], None] | Literal["uint8"]: """ Convert the data type to a JSON-serializable form. Parameters ---------- zarr_format : ZarrFormat The Zarr format version. Supported values are 2 and 3. Returns ------- ``DTypeConfig_V2[Literal["|u1"], None] | Literal["uint8"]`` The JSON-serializable representation of the data type. Raises ------ ValueError If `zarr_format` is not 2 or 3. """ if zarr_format == 2: # For Zarr format version 2, return a dictionary with the name and object codec ID. return {"name": self._zarr_v2_names[0], "object_codec_id": None} elif zarr_format == 3: # For Zarr format version 3, return the v3 name as a string. return self._zarr_v3_name # Raise an error if the zarr_format is neither 2 nor 3. raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 1 @dataclass(frozen=True, kw_only=True) class Int16(BaseInt[np.dtypes.Int16DType, np.int16], HasEndianness): """ A Zarr data type for arrays containing 16-bit signed integers. Wraps the [`np.dtypes.Int16DType`][numpy.dtypes.Int16DType] data type. Scalars for this data type are instances of [`np.int16`][numpy.int16]. Attributes ---------- dtype_cls : np.dtypes.Int16DType The class of the underlying NumPy dtype. References ---------- This class implements the 16-bit signed integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.Int16DType _zarr_v3_name: ClassVar[Literal["int16"]] = "int16" _zarr_v2_names: ClassVar[tuple[Literal[">i2"], Literal["i2", " Self: """ Create an instance of this data type from an np.dtype('int16') instance. Parameters ---------- dtype : np.dtype The instance of np.dtype('int16') to create from. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data type is not an instance of np.dtype('int16'). """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.Int16DType: """ Convert the data type to an np.dtype('int16') instance. Returns ------- np.dtype The np.dtype('int16') instance. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v2(data): # Going via NumPy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected one of the strings {cls._zarr_v2_names!r}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal[">i2", " Literal["int16"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal[">i2", "i2", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 2 @dataclass(frozen=True, kw_only=True) class UInt16(BaseInt[np.dtypes.UInt16DType, np.uint16], HasEndianness): """ A Zarr data type for arrays containing 16-bit unsigned integers. Wraps the [`np.dtypes.UInt16DType`][numpy.dtypes.UInt16DType] data type. Scalars for this data type are instances of [`np.uint16`][numpy.uint16]. Attributes ---------- dtype_cls : np.dtypes.UInt16DType The class of the underlying NumPy dtype. References ---------- This class implements the unsigned 16-bit unsigned integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.UInt16DType _zarr_v3_name: ClassVar[Literal["uint16"]] = "uint16" _zarr_v2_names: ClassVar[tuple[Literal[">u2"], Literal["u2", " Self: """ Create an instance of this data type from an np.dtype('uint16') instance. Parameters ---------- dtype : np.dtype The NumPy data type. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data type is not an instance of np.dtype('uint16'). """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.UInt16DType: """ Convert the data type to an np.dtype('uint16') instance. Returns ------- np.dtype The np.dtype('uint16') instance. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v2(data): # Going via NumPy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of UInt16. Got {data!r}, expected one of the strings {cls._zarr_v2_names}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of UInt16. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal[">u2", " Literal["uint16"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal[">u2", "u2", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 2 @dataclass(frozen=True, kw_only=True) class Int32(BaseInt[np.dtypes.Int32DType, np.int32], HasEndianness): """ A Zarr data type for arrays containing 32-bit signed integers. Wraps the [`np.dtypes.Int32DType`][numpy.dtypes.Int32DType] data type. Scalars for this data type are instances of [`np.int32`][numpy.int32]. Attributes ---------- dtype_cls : np.dtypes.Int32DType The class of the underlying NumPy dtype. References ---------- This class implements the 32-bit signed integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.Int32DType _zarr_v3_name: ClassVar[Literal["int32"]] = "int32" _zarr_v2_names: ClassVar[tuple[Literal[">i4"], Literal["i4", " TypeGuard[np.dtypes.Int32DType]: """ A type guard that checks if the input is assignable to the type of ``cls.dtype_class`` This method is overridden for this particular data type because of a Windows-specific issue where np.dtype('i') creates an instance of ``np.dtypes.IntDType``, rather than an instance of ``np.dtypes.Int32DType``, even though both represent 32-bit signed integers. Parameters ---------- dtype : TDType The dtype to check. Returns ------- Bool True if the dtype matches, False otherwise. """ return super()._check_native_dtype(dtype) or dtype == np.dtypes.Int32DType() @classmethod def from_native_dtype(cls: type[Self], dtype: TBaseDType) -> Self: """ Create an Int32 from an np.dtype('int32') instance. Parameters ---------- dtype : TBaseDType The np.dtype('int32') instance. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class Int32. """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self: Self) -> np.dtypes.Int32DType: """ Convert the Int32 instance to an np.dtype('int32') instance. Returns ------- np.dtypes.Int32DType The np.dtype('int32') instance. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an Int32 from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class Int32. """ if cls._check_json_v2(data): # Going via NumPy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected one of the strings {cls._zarr_v2_names!r}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an Int32 from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class Int32. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal[">i4", " Literal["int32"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal[">i4", "i4", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 4 @dataclass(frozen=True, kw_only=True) class UInt32(BaseInt[np.dtypes.UInt32DType, np.uint32], HasEndianness): """ A Zarr data type for arrays containing 32-bit unsigned integers. Wraps the [`np.dtypes.UInt32DType`][numpy.dtypes.UInt32DType] data type. Scalars for this data type are instances of [`np.uint32`][numpy.uint32]. Attributes ---------- dtype_cls : np.dtypes.UInt32DType The class of the underlying NumPy dtype. References ---------- This class implements the 32-bit unsigned integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.UInt32DType _zarr_v3_name: ClassVar[Literal["uint32"]] = "uint32" _zarr_v2_names: ClassVar[tuple[Literal[">u4"], Literal["u4", " TypeGuard[np.dtypes.UInt32DType]: """ A type guard that checks if the input is assignable to the type of ``cls.dtype_class`` This method is overridden for this particular data type because of a Windows-specific issue where ``np.array([1], dtype=np.uint32) & 1`` creates an instance of ``np.dtypes.UIntDType``, rather than an instance of ``np.dtypes.UInt32DType``, even though both represent 32-bit unsigned integers. (In contrast to ``np.dtype('i')``, ``np.dtype('u')`` raises an error.) Parameters ---------- dtype : TDType The dtype to check. Returns ------- Bool True if the dtype matches, False otherwise. """ return super()._check_native_dtype(dtype) or dtype == np.dtypes.UInt32DType() @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create a UInt32 from an np.dtype('uint32') instance. Parameters ---------- dtype : TBaseDType The NumPy data type. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data type is not a valid representation of this class 32-bit unsigned integer. """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.UInt32DType: """ Create a NumPy unsigned 32-bit integer dtype instance from this UInt32 ZDType. Returns ------- np.dtypes.UInt32DType The NumPy unsigned 32-bit integer dtype. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class 32-bit unsigned integer. """ if cls._check_json_v2(data): # Going via NumPy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected one of the strings {cls._zarr_v2_names}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class 32-bit unsigned integer. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal[">u4", " Literal["uint32"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal[">u4", "u4", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 4 @dataclass(frozen=True, kw_only=True) class Int64(BaseInt[np.dtypes.Int64DType, np.int64], HasEndianness): """ A Zarr data type for arrays containing 64-bit signed integers. Wraps the [`np.dtypes.Int64DType`][numpy.dtypes.Int64DType] data type. Scalars for this data type are instances of [`np.int64`][numpy.int64]. Attributes ---------- dtype_cls : np.dtypes.Int64DType The class of the underlying NumPy dtype. References ---------- This class implements the 64-bit signed integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.Int64DType _zarr_v3_name: ClassVar[Literal["int64"]] = "int64" _zarr_v2_names: ClassVar[tuple[Literal[">i8"], Literal["i8", " Self: """ Create an Int64 from an np.dtype('int64') instance. Parameters ---------- dtype : TBaseDType The NumPy data type. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data type is not a valid representation of this class 64-bit signed integer. """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.Int64DType: """ Create a NumPy signed 64-bit integer dtype instance from this Int64 ZDType. Returns ------- np.dtypes.Int64DType The NumPy signed 64-bit integer dtype. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class 64-bit signed integer. """ if cls._check_json_v2(data): # Going via NumPy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected one of the strings {cls._zarr_v2_names}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class 64-bit signed integer. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal[">i8", " Literal["int64"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal[">i8", "i8", " int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 8 @dataclass(frozen=True, kw_only=True) class UInt64(BaseInt[np.dtypes.UInt64DType, np.uint64], HasEndianness): """ A Zarr data type for arrays containing 64-bit unsigned integers. Wraps the [`np.dtypes.UInt64DType`][numpy.dtypes.UInt64DType] data type. Scalars for this data type are instances of [`np.uint64`][numpy.uint64]. Attributes ---------- dtype_cls: np.dtypes.UInt64DType The class of the underlying NumPy dtype. References ---------- This class implements the unsigned 64-bit integer data type defined in Zarr V2 and V3. See the [Zarr V2](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding) and [Zarr V3](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v3/data-types/index.rst) specification documents for details. """ dtype_cls = np.dtypes.UInt64DType _zarr_v3_name: ClassVar[Literal["uint64"]] = "uint64" _zarr_v2_names: ClassVar[tuple[Literal[">u8"], Literal["u8", " np.dtypes.UInt64DType: """ Convert the data type to a native NumPy dtype. Returns ------- np.dtypes.UInt64DType The native NumPy dtype.eeeeeeeeeeeeeeeee """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls().newbyteorder(byte_order) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class unsigned 64-bit integer. """ if cls._check_json_v2(data): # Going via NumPy ensures that we get the endianness correct without # annoying string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected one of the strings {cls._zarr_v2_names}." raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from Zarr V3-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class unsigned 64-bit integer. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[Literal[">u8", " Literal["uint64"]: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[Literal[">u8", "u8", " Self: """ Create an instance of this data type from a native NumPy dtype. Parameters ---------- dtype : TBaseDType The native NumPy dtype. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input dtype is not a valid representation of this class unsigned 64-bit integer. """ if cls._check_native_dtype(dtype): return cls(endianness=get_endianness_from_numpy_dtype(dtype)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 8 zarr-python-3.2.1/src/zarr/core/dtype/npy/string.py000066400000000000000000000606301517635743000223420ustar00rootroot00000000000000from __future__ import annotations import re from dataclasses import dataclass from typing import ( TYPE_CHECKING, ClassVar, Literal, Protocol, Self, TypedDict, TypeGuard, overload, runtime_checkable, ) import numpy as np from zarr.core.common import NamedConfig from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasEndianness, HasItemSize, HasLength, HasObjectCodec, check_dtype_spec_v2, v3_unstable_dtype_warning, ) from zarr.core.dtype.npy.common import ( check_json_str, endianness_to_numpy_str, get_endianness_from_numpy_dtype, ) from zarr.core.dtype.wrapper import ZDType if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat from zarr.core.dtype.wrapper import TBaseDType _NUMPY_SUPPORTS_VLEN_STRING = hasattr(np.dtypes, "StringDType") @runtime_checkable class SupportsStr(Protocol): def __str__(self) -> str: ... class LengthBytesConfig(TypedDict): """ Configuration for a fixed-length string data type in Zarr V3. Attributes ---------- length_bytes : int The length in bytes of the data associated with this configuration. """ length_bytes: int class FixedLengthUTF32JSON_V2(DTypeConfig_V2[str, None]): """ A wrapper around the JSON representation of the ``FixedLengthUTF32`` data type in Zarr V2. The ``name`` field of this class contains the value that would appear under the ``dtype`` field in Zarr V2 array metadata. References ---------- The structure of the ``name`` field is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). Examples -------- ```python { "name": " None: """ We don't allow instances of this class with length less than 1 because there is no way such a data type can contain actual data. """ if self.length < 1: raise ValueError(f"length must be >= 1, got {self.length}.") @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create a FixedLengthUTF32 from a NumPy data type. Parameters ---------- dtype : TBaseDType The NumPy data type. Returns ------- Self An instance of this data type. """ if cls._check_native_dtype(dtype): endianness = get_endianness_from_numpy_dtype(dtype) return cls( length=dtype.itemsize // (cls.code_point_bytes), endianness=endianness, ) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.StrDType[int]: """ Convert the FixedLengthUTF32 instance to a NumPy data type. Returns ------- np.dtypes.StrDType[int] The NumPy data type. """ byte_order = endianness_to_numpy_str(self.endianness) return self.dtype_cls(self.length).newbyteorder(byte_order) @classmethod def _check_json_v2(cls, data: DTypeJSON) -> TypeGuard[FixedLengthUTF32JSON_V2]: """ Check that the input is a valid JSON representation of a NumPy U dtype. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- TypeGuard[FixedLengthUTF32JSON_V2] Whether the input is a valid JSON representation of a NumPy U dtype. """ return ( check_dtype_spec_v2(data) and isinstance(data["name"], str) and re.match(r"^[><]U\d+$", data["name"]) is not None and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[FixedLengthUTF32JSON_V3]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- TypeGuard[FixedLengthUTF32JSONV3] Whether the input is a valid JSON representation of a NumPy U dtype. """ return ( isinstance(data, dict) and set(data.keys()) == {"name", "configuration"} and data["name"] == cls._zarr_v3_name and "configuration" in data and isinstance(data["configuration"], dict) and set(data["configuration"].keys()) == {"length_bytes"} and isinstance(data["configuration"]["length_bytes"], int) ) @overload def to_json(self, zarr_format: Literal[2]) -> DTypeConfig_V2[str, None]: ... @overload def to_json(self, zarr_format: Literal[3]) -> FixedLengthUTF32JSON_V3: ... def to_json( self, zarr_format: ZarrFormat ) -> DTypeConfig_V2[str, None] | FixedLengthUTF32JSON_V3: """ Convert the FixedLengthUTF32 instance to a JSON representation. Parameters ---------- zarr_format : ZarrFormat The Zarr format to use. Returns ------- DTypeConfig_V2[str, None] | FixedLengthUTF32JSON_V3 The JSON representation of the data type. """ if zarr_format == 2: return {"name": self.to_native_dtype().str, "object_codec_id": None} elif zarr_format == 3: v3_unstable_dtype_warning(self) return { "name": self._zarr_v3_name, "configuration": {"length_bytes": self.length * self.code_point_bytes}, } raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create a FixedLengthUTF32 from a JSON representation of a NumPy U dtype. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. """ if cls._check_json_v2(data): # Construct the NumPy dtype instead of string parsing. name = data["name"] return cls.from_native_dtype(np.dtype(name)) raise DataTypeValidationError( f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a string representation of a NumPy U dtype." ) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create a FixedLengthUTF32 from a JSON representation of a NumPy U dtype. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- Self An instance of this data type. """ if cls._check_json_v3(data): return cls(length=data["configuration"]["length_bytes"] // cls.code_point_bytes) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected {cls._zarr_v3_name}." raise DataTypeValidationError(msg) def default_scalar(self) -> np.str_: """ Return the default scalar value for this data type. Returns ------- ``np.str_`` The default scalar value. """ return np.str_("") def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> str: """ Convert the scalar value to a JSON representation. Parameters ---------- data : object The scalar value. zarr_format : ZarrFormat The Zarr format to use. Returns ------- str The JSON representation of the scalar value. """ return str(data) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.str_: """ Convert the JSON representation of a scalar value to the native scalar value. Parameters ---------- data : JSON The JSON data. zarr_format : ZarrFormat The Zarr format to use. Returns ------- ``np.str_`` The native scalar value. """ if check_json_str(data): return self.to_native_dtype().type(data) raise TypeError(f"Invalid type: {data}. Expected a string.") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[SupportsStr]: """ Check that the input is a valid scalar value for this data type. Parameters ---------- data : object The scalar value. Returns ------- TypeGuard[SupportsStr] Whether the input is a valid scalar value for this data type. """ # this is generous for backwards compatibility return isinstance(data, SupportsStr) def cast_scalar(self, data: object) -> np.str_: """ Cast the scalar value to the native scalar value. Parameters ---------- data : object The scalar value. Returns ------- ``np.str_`` The native scalar value. """ if self._check_scalar(data): # We explicitly truncate before casting because of the following NumPy behavior: # >>> x = np.dtype('U3').type('hello world') # >>> x # np.str_('hello world') # >>> x.dtype # dtype('U11') return self.to_native_dtype().type(str(data)[: self.length]) msg = ( # pragma: no cover f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) # pragma: no-cover @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return self.length * self.code_point_bytes def check_vlen_string_json_scalar(data: object) -> TypeGuard[int | str | float]: """ Check if the input is a valid JSON scalar for a variable-length string. This function is generous for backwards compatibility, as Zarr Python v2 would use ints for variable-length string fill values. Parameters ---------- data : object The JSON value to check. Returns ------- TypeGuard[int | str | float] True if the input is a valid scalar for a variable-length string. """ return isinstance(data, int | str | float) class VariableLengthUTF8JSON_V2(DTypeConfig_V2[Literal["|O"], Literal["vlen-utf8"]]): """ A wrapper around the JSON representation of the ``VariableLengthUTF8`` data type in Zarr V2. The ``name`` field of this class contains the value that would appear under the ``dtype`` field in Zarr V2 array metadata. The ``object_codec_id`` field is always ``"vlen-utf8"``. References ---------- The structure of the ``name`` field is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). Examples -------- ```python { "name": "|O", "object_codec_id": "vlen-utf8" } ``` """ # VariableLengthUTF8 is defined in two places, conditioned on the version of NumPy. # If NumPy 2 is installed, then VariableLengthUTF8 is defined with the NumPy variable length # string dtype as the native dtype. Otherwise, VariableLengthUTF8 is defined with the NumPy object # dtype as the native dtype. class UTF8Base[DType: TBaseDType](ZDType[DType, str], HasObjectCodec): """ A base class for variable-length UTF-8 string data types. Not intended for direct use, but as a base for concrete implementations. Attributes ---------- object_codec_id : ClassVar[Literal["vlen-utf8"]] The object codec ID for this data type. References ---------- This data type does not have a Zarr V3 specification. The Zarr V2 data type specification can be found [here](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). """ _zarr_v3_name: ClassVar[Literal["string"]] = "string" object_codec_id: ClassVar[Literal["vlen-utf8"]] = "vlen-utf8" @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of this data type from a compatible NumPy data type. Parameters ---------- dtype : TBaseDType The native data type. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input is not compatible with this data type. """ if cls._check_native_dtype(dtype): return cls() raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) @classmethod def _check_json_v2( cls, data: DTypeJSON, ) -> TypeGuard[VariableLengthUTF8JSON_V2]: """ "Check if the input is a valid JSON representation of a variable-length UTF-8 string dtype for Zarr v2." Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- ``TypeGuard[VariableLengthUTF8JSON_V2]`` Whether the input is a valid JSON representation of a NumPy "object" data type, and that the object codec id is appropriate for variable-length UTF-8 strings. """ return ( check_dtype_spec_v2(data) and data["name"] == "|O" and data["object_codec_id"] == cls.object_codec_id ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[Literal["variable_length_utf8"]]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[Literal["variable_length_utf8"]] Whether the input is a valid JSON representation of a variable length UTF-8 string data type. """ return data == cls._zarr_v3_name @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this class from a JSON representation of a NumPy "object" dtype. Parameters ---------- data : DTypeJSON The JSON data to create an instance from. Returns ------- Self An instance of this data type. """ if cls._check_json_v2(data): return cls() msg = ( f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected the string '|O'" ) raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this class from a JSON representation of a variable length UTF-8 string data type. Parameters ---------- data : DTypeJSON The JSON data to create an instance from. Returns ------- Self An instance of this data type. """ if cls._check_json_v3(data): return cls() msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected {cls._zarr_v3_name}." raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> VariableLengthUTF8JSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> Literal["string"]: ... def to_json(self, zarr_format: ZarrFormat) -> VariableLengthUTF8JSON_V2 | Literal["string"]: """ Convert this data type to a JSON representation. Parameters ---------- zarr_format : int The zarr format to use for the JSON representation. Returns ------- ``VariableLengthUTF8JSON_V2 | Literal["string"]`` The JSON representation of this data type. """ if zarr_format == 2: return {"name": "|O", "object_codec_id": self.object_codec_id} elif zarr_format == 3: return self._zarr_v3_name raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def default_scalar(self) -> str: """ Return the default scalar value for this data type. Returns ------- str The default scalar value. """ return "" def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> str: """ Convert a scalar value to a JSON representation. Parameters ---------- data : object The scalar value to convert. zarr_format : int The zarr format to use for the JSON representation. Returns ------- str The JSON representation of the scalar value. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) raise TypeError(f"Invalid type: {data}. Expected a string.") def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> str: """ Convert a JSON representation of a scalar value to the native scalar type. Parameters ---------- data : JSON The JSON representation of the scalar value. zarr_format : int The zarr format to use for the JSON representation. Returns ------- str The native scalar type of the scalar value. """ if not check_vlen_string_json_scalar(data): raise TypeError(f"Invalid type: {data}. Expected a string or number.") return str(data) def _check_scalar(self, data: object) -> TypeGuard[SupportsStr]: """ Check that the input is a valid scalar value for this data type. Parameters ---------- data : object The scalar value to check. Returns ------- TypeGuard[SupportsStr] Whether the input is a valid scalar value for this data type. """ return isinstance(data, SupportsStr) def _cast_scalar_unchecked(self, data: SupportsStr) -> str: """ Cast a scalar value to a string. Parameters ---------- data : object The scalar value to cast. Returns ------- str The string representation of the scalar value. """ return str(data) def cast_scalar(self, data: object) -> str: """ Cast an object to a string. Parameters ---------- data : object The value to cast. Returns ------- str The input cast to str. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( # pragma: no cover f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) # pragma: no cover if _NUMPY_SUPPORTS_VLEN_STRING: @dataclass(frozen=True, kw_only=True) class VariableLengthUTF8(UTF8Base[np.dtypes.StringDType]): # type: ignore[type-var] """ A Zarr data type for arrays containing variable-length UTF-8 strings. Wraps the ``np.dtypes.StringDType`` data type. Scalars for this data type are instances of ``str``. Attributes ---------- dtype_cls : Type[np.dtypes.StringDType] The NumPy dtype class for this data type. _zarr_v3_name : ClassVar[Literal["variable_length_utf8"]] = "variable_length_utf8" The name of this data type in Zarr V3. object_codec_id : ClassVar[Literal["vlen-utf8"]] = "vlen-utf8" The object codec ID for this data type. """ dtype_cls = np.dtypes.StringDType # type: ignore[assignment] @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of this data type from a compatible NumPy data type. We reject NumPy StringDType instances that have the `na_object` field set, because this is not representable by the Zarr `string` data type. Parameters ---------- dtype : TBaseDType The native data type. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input is not compatible with this data type. ValueError If the input is `numpy.dtypes.StringDType` and has `na_object` set. """ if cls._check_native_dtype(dtype): if hasattr(dtype, "na_object"): msg = ( f"Zarr data type resolution from {dtype} failed. " "Attempted to resolve a zarr data type from a `numpy.dtypes.StringDType` " "with `na_object` set, which is not supported." ) raise ValueError(msg) return cls() raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.StringDType: """ Create a NumPy string dtype from this VariableLengthUTF8 ZDType. Returns ------- np.dtypes.StringDType The NumPy string dtype. """ return self.dtype_cls() else: # Numpy pre-2 does not have a variable length string dtype, so we use the Object dtype instead. @dataclass(frozen=True, kw_only=True) class VariableLengthUTF8(UTF8Base[np.dtypes.ObjectDType]): # type: ignore[no-redef] """ A Zarr data type for arrays containing variable-length UTF-8 strings. Wraps the ``np.dtypes.ObjectDType`` data type. Scalars for this data type are instances of ``str``. Attributes ---------- dtype_cls : Type[np.dtypes.ObjectDType] The NumPy dtype class for this data type. _zarr_v3_name : ClassVar[Literal["variable_length_utf8"]] = "variable_length_utf8" The name of this data type in Zarr V3. object_codec_id : ClassVar[Literal["vlen-utf8"]] = "vlen-utf8" The object codec ID for this data type. """ dtype_cls = np.dtypes.ObjectDType def to_native_dtype(self) -> np.dtypes.ObjectDType: """ Create a NumPy object dtype from this VariableLengthUTF8 ZDType. Returns ------- np.dtypes.ObjectDType The NumPy object dtype. """ return self.dtype_cls() zarr-python-3.2.1/src/zarr/core/dtype/npy/structured.py000066400000000000000000000543431517635743000232440ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar, Literal, Self, TypeGuard, cast, overload import numpy as np from zarr.core.common import NamedConfig from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasItemSize, StructuredName_V2, check_dtype_spec_v2, check_structured_dtype_name_v2, v3_unstable_dtype_warning, ) from zarr.core.dtype.npy.common import ( bytes_from_json, bytes_to_json, check_json_str, ) from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat StructuredScalarLike = list[object] | tuple[object, ...] | bytes | int class StructuredJSON_V2(DTypeConfig_V2[StructuredName_V2, None]): """ A wrapper around the JSON representation of the ``Structured`` data type in Zarr V2. The ``name`` field is a sequence of sequences, where each inner sequence has two values: the field name and the data type name for that field (which could be another sequence). The data type names are strings, and the object codec ID is always None. References ---------- The structure of the ``name`` field is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). Examples -------- ```python { "name": [ ["f0", " None: if len(self.fields) < 1: raise ValueError(f"must have at least one field. Got {self.fields!r}") @classmethod def _check_native_dtype(cls, dtype: TBaseDType) -> TypeGuard[np.dtypes.VoidDType[int]]: """ Check that this dtype is a numpy structured dtype Parameters ---------- dtype : np.dtypes.DTypeLike The dtype to check. Returns ------- TypeGuard[np.dtypes.VoidDType] True if the dtype matches, False otherwise. """ return isinstance(dtype, cls.dtype_cls) and dtype.fields is not None @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create a Structured ZDType from a native NumPy data type. Parameters ---------- dtype : TBaseDType The native data type. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input data type is not an instance of np.dtypes.VoidDType with a non-null ``fields`` attribute. Notes ----- This method attempts to resolve the fields of the structured dtype using the data type registry. """ from zarr.core.dtype import get_data_type_from_native_dtype fields: list[tuple[str, ZDType[TBaseDType, TBaseScalar]]] = [] if cls._check_native_dtype(dtype): # fields of a structured numpy dtype are either 2-tuples or 3-tuples. we only # care about the first element in either case. for key, (dtype_instance, *_) in dtype.fields.items(): # type: ignore[union-attr] dtype_wrapped = get_data_type_from_native_dtype(dtype_instance) fields.append((key, dtype_wrapped)) return cls(fields=tuple(fields)) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> np.dtypes.VoidDType[int]: """ Convert the structured Zarr data type to a native NumPy void dtype. This method constructs a NumPy dtype with fields corresponding to the fields of the structured Zarr data type, by converting each field's data type to its native dtype representation. Returns ------- np.dtypes.VoidDType[int] The native NumPy void dtype representing the structured data type. """ return cast( "np.dtypes.VoidDType[int]", np.dtype([(key, dtype.to_native_dtype()) for (key, dtype) in self.fields]), ) @classmethod def _check_json_v2( cls, data: DTypeJSON, ) -> TypeGuard[StructuredJSON_V2]: """ Check if the input is a valid JSON representation of a Structured data type for Zarr V2. The input data must be a mapping that contains a "name" key that is not a str, and an "object_codec_id" key that is None. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[StructuredJSON_V2] True if the input is a valid JSON representation of a Structured data type for Zarr V2, False otherwise. """ return ( check_dtype_spec_v2(data) and not isinstance(data["name"], str) and check_structured_dtype_name_v2(data["name"]) and data["object_codec_id"] is None ) @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[StructuredJSON_V3]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[StructuredJSON_V3] True if the input is a valid JSON representation of a structured data type for Zarr V3, False otherwise. """ return ( isinstance(data, dict) and set(data.keys()) == {"name", "configuration"} and data["name"] == cls._zarr_v3_name and isinstance(data["configuration"], dict) and set(data["configuration"].keys()) == {"fields"} ) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: # avoid circular import from zarr.core.dtype import get_data_type_from_json if cls._check_json_v2(data): # structured dtypes are constructed directly from a list of lists # note that we do not handle the object codec here! this will prevent structured # dtypes from containing object dtypes. return cls( fields=tuple( # type: ignore[misc] ( # type: ignore[misc] f_name, get_data_type_from_json( {"name": f_dtype, "object_codec_id": None}, zarr_format=2 ), ) for f_name, f_dtype in data["name"] ) ) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a JSON array of arrays" raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: from zarr.core.dtype import get_data_type_from_json if cls._check_json_v3(data): config = data["configuration"] meta_fields = config["fields"] return cls( fields=tuple( (f_name, get_data_type_from_json(f_dtype, zarr_format=3)) # type: ignore[misc] for f_name, f_dtype in meta_fields ) ) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a JSON object with the key {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> StructuredJSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> StructuredJSON_V3: ... def to_json(self, zarr_format: ZarrFormat) -> StructuredJSON_V2 | StructuredJSON_V3: """ Convert the structured data type to a JSON-serializable form. Parameters ---------- zarr_format : ZarrFormat The Zarr format version. Accepted values are 2 and 3. Returns ------- StructuredJSON_V2 | StructuredJSON_V3 The JSON representation of the structured data type. Raises ------ ValueError If the zarr_format is not 2 or 3. """ if zarr_format == 2: fields = [ [f_name, f_dtype.to_json(zarr_format=zarr_format)["name"]] for f_name, f_dtype in self.fields ] return {"name": fields, "object_codec_id": None} elif zarr_format == 3: v3_unstable_dtype_warning(self) fields = [ [f_name, f_dtype.to_json(zarr_format=zarr_format)] # type: ignore[list-item] for f_name, f_dtype in self.fields ] base_dict = { "name": self._zarr_v3_name, "configuration": {"fields": fields}, } return cast("StructuredJSON_V3", base_dict) raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[StructuredScalarLike]: # TODO: implement something more precise here! """ Check that the input is a valid scalar value for this structured data type. Parameters ---------- data : object The scalar value to check. Returns ------- TypeGuard[StructuredScalarLike] Whether the input is a valid scalar value for this structured data type. """ return isinstance(data, (bytes, list, tuple, int, np.void)) def _cast_scalar_unchecked(self, data: StructuredScalarLike) -> np.void: """ Cast a python object to a numpy structured scalar without type checking. Parameters ---------- data : StructuredScalarLike The data to cast. Returns ------- np.void The casted data as a numpy structured scalar. Notes ----- This method does not perform any type checking. The input data must be castable to a numpy structured scalar. """ na_dtype = self.to_native_dtype() if isinstance(data, bytes): res = np.frombuffer(data, dtype=na_dtype)[0] elif isinstance(data, list | tuple): res = np.array([tuple(data)], dtype=na_dtype)[0] else: res = np.array([data], dtype=na_dtype)[0] return cast("np.void", res) def cast_scalar(self, data: object) -> np.void: """ Cast a Python object to a NumPy structured scalar. This function attempts to cast the provided data to a NumPy structured scalar. If the data is compatible with the structured scalar type, it is cast without type checking. Otherwise, a TypeError is raised. Parameters ---------- data : object The data to be cast to a NumPy structured scalar. Returns ------- np.void The data cast as a NumPy structured scalar. Raises ------ TypeError If the data cannot be converted to a NumPy structured scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> np.void: """ Get the default scalar value for this structured data type. Returns ------- np.void The default scalar value, which is the scalar representation of 0 cast to this structured data type. """ return self._cast_scalar_unchecked(0) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.void: """ Read a JSON-serializable value as a NumPy structured scalar. Parameters ---------- data : JSON The JSON-serializable value. zarr_format : ZarrFormat The zarr format version. Returns ------- np.void The NumPy structured scalar. Raises ------ TypeError If the input is not a base64-encoded string. """ if check_json_str(data): as_bytes = bytes_from_json(data, zarr_format=zarr_format) dtype = self.to_native_dtype() return cast("np.void", np.array([as_bytes]).view(dtype)[0]) raise TypeError(f"Invalid type: {data}. Expected a string.") def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> str | dict[str, JSON]: """ Convert a scalar to a JSON-serializable string representation. Parameters ---------- data : object The scalar to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- str | dict[str, JSON] A string representation of the scalar, which is a base64-encoded string of the bytes that make up the scalar. Subclasses may return a dict for V3 format. """ return bytes_to_json(self.cast_scalar(data).tobytes(), zarr_format) @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return self.to_native_dtype().itemsize def has_multi_byte_fields(self) -> bool: """ Check if this structured dtype has any fields with item_size > 1. Returns ------- bool True if any field has item_size > 1, False otherwise. """ return any( isinstance(field_dtype, HasItemSize) and field_dtype.item_size > 1 for _, field_dtype in self.fields ) @dataclass(frozen=True, kw_only=True) class Struct(Structured): """ A Zarr data type for arrays containing structured scalars, AKA "record arrays". Wraps the NumPy `np.dtypes.VoidDType` if the data type has fields. Scalars for this data type are instances of `np.void`, with a ``fields`` attribute. This is the canonical data type registered for structured arrays. It reads both the canonical ``"struct"`` format (object-style fields) and the legacy ``"structured"`` format (tuple-style fields), but always writes the canonical ``"struct"`` format. Attributes ---------- fields : Sequence[tuple[str, ZDType]] The fields of the structured dtype. References ---------- The Zarr V3 specification for this data type is defined in the zarr-extensions repository: https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/struct The Zarr V2 data type specification can be found [here](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). """ _zarr_v3_name: ClassVar[Literal["struct"]] = "struct" # type: ignore[assignment] @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[StructJSON_V3]: # type: ignore[override] return ( isinstance(data, dict) and set(data.keys()) == {"name", "configuration"} and data["name"] in ("struct", "structured") and isinstance(data["configuration"], dict) and set(data["configuration"].keys()) == {"fields"} ) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: from zarr.core.dtype import get_data_type_from_json if cls._check_json_v3(data): config = data["configuration"] meta_fields = config["fields"] parsed_fields: list[tuple[str, ZDType[TBaseDType, TBaseScalar]]] = [] for field in meta_fields: if isinstance(field, dict): f_name = field["name"] f_dtype = field["data_type"] else: # Legacy tuple-style field format from "structured" dtype f_name, f_dtype = field # type: ignore[unreachable] parsed_fields.append((f_name, get_data_type_from_json(f_dtype, zarr_format=3))) # type: ignore[arg-type] return cls(fields=tuple(parsed_fields)) msg = f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a JSON object with the key {cls._zarr_v3_name!r}" raise DataTypeValidationError(msg) @overload # type: ignore[override] def to_json(self, zarr_format: Literal[2]) -> StructuredJSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> StructJSON_V3: ... def to_json(self, zarr_format: ZarrFormat) -> StructuredJSON_V2 | StructJSON_V3: if zarr_format == 2: fields_v2 = [ [f_name, f_dtype.to_json(zarr_format=zarr_format)["name"]] for f_name, f_dtype in self.fields ] return {"name": fields_v2, "object_codec_id": None} elif zarr_format == 3: v3_unstable_dtype_warning(self) fields_v3 = [ {"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}}, ) raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.void: """ Read a JSON-serializable value as a NumPy structured scalar. Parameters ---------- data : JSON The JSON-serializable value. Can be either: - A dict mapping field names to values (primary format for V3) - A base64-encoded string (legacy format, for backward compatibility) zarr_format : ZarrFormat The zarr format version. Returns ------- np.void The NumPy structured scalar. Raises ------ TypeError If the input is not a dict or base64-encoded string. """ if isinstance(data, dict): field_values = [] for field_name, field_dtype in self.fields: if field_name in data: field_values.append( field_dtype.from_json_scalar(data[field_name], zarr_format=zarr_format) ) else: field_values.append(field_dtype.default_scalar()) return self._cast_scalar_unchecked(tuple(field_values)) elif check_json_str(data): as_bytes = bytes_from_json(data, zarr_format=zarr_format) dtype = self.to_native_dtype() return cast("np.void", np.array([as_bytes]).view(dtype)[0]) raise TypeError(f"Invalid type: {data}. Expected a dict or base64-encoded string.") def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> str | dict[str, JSON]: """ Convert a scalar to a JSON-serializable representation. Parameters ---------- data : object The scalar to convert. zarr_format : ZarrFormat The zarr format version. Returns ------- str | dict[str, JSON] For V2: A base64-encoded string of the bytes that make up the scalar. For V3: A dict mapping field names to their JSON-serialized values. """ scalar = self.cast_scalar(data) if zarr_format == 2: return bytes_to_json(scalar.tobytes(), zarr_format) result: dict[str, JSON] = {} for field_name, field_dtype in self.fields: result[field_name] = field_dtype.to_json_scalar( scalar[field_name], zarr_format=zarr_format ) return result zarr-python-3.2.1/src/zarr/core/dtype/npy/time.py000066400000000000000000000663411517635743000217770ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta from typing import ( TYPE_CHECKING, ClassVar, Literal, Self, TypedDict, TypeGuard, cast, get_args, overload, ) import numpy as np from typing_extensions import ReadOnly from zarr.core.common import NamedConfig from zarr.core.dtype.common import ( DataTypeValidationError, DTypeConfig_V2, DTypeJSON, HasEndianness, HasItemSize, 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, ) from zarr.core.dtype.wrapper import TBaseDType, ZDType if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat TimeDeltaLike = str | int | bytes | np.timedelta64 | timedelta | None DateTimeLike = str | int | bytes | np.datetime64 | datetime | None def datetime_from_int(data: int, *, unit: DateTimeUnit, scale_factor: int) -> np.datetime64: """ Convert an integer to a datetime64. Parameters ---------- data : int The integer to convert. unit : DateTimeUnit The unit of the datetime64. scale_factor : int The scale factor of the datetime64. Returns ------- numpy.datetime64 The datetime64 value. """ dtype_name = f"datetime64[{scale_factor}{unit}]" return cast("np.datetime64", np.int64(data).view(dtype_name)) def datetimelike_to_int(data: np.datetime64 | np.timedelta64) -> int: """ Convert a datetime64 or a timedelta64 to an integer. Parameters ---------- data : np.datetime64 | numpy.timedelta64 The value to convert. Returns ------- int An integer representation of the scalar. """ return data.view(np.int64).item() def check_json_time(data: JSON) -> TypeGuard[Literal["NaT"] | int]: """ Type guard to check if the input JSON data is the literal string "NaT" or an integer. """ return check_json_int(data) or data == "NaT" class TimeConfig(TypedDict): """ The configuration for the numpy.timedelta64 or numpy.datetime64 data type in Zarr V3. Attributes ---------- unit : ReadOnly[DateTimeUnit] A string encoding a unit of time. scale_factor : ReadOnly[int] A scale factor. Examples -------- ```python {"unit": "ms", "scale_factor": 1} ``` """ unit: ReadOnly[DateTimeUnit] scale_factor: ReadOnly[int] class DateTime64JSON_V3(NamedConfig[Literal["numpy.datetime64"], TimeConfig]): """ The JSON representation of the ``numpy.datetime64`` data type in Zarr V3. References ---------- This representation is defined in the ``numpy.datetime64`` [specification document](https://zarr-specs.readthedocs.io/en/latest/spec/v3/datatypes.html#numpy-datetime64). Examples -------- ```python { "name": "numpy.datetime64", "configuration": { "unit": "ms", "scale_factor": 1 } } ``` """ class TimeDelta64JSON_V3(NamedConfig[Literal["numpy.timedelta64"], TimeConfig]): """ The JSON representation of the ``TimeDelta64`` data type in Zarr V3. References ---------- This representation is defined in the numpy.timedelta64 [specification document](https://zarr-specs.readthedocs.io/en/latest/spec/v3/datatypes.html#numpy-timedelta64). Examples -------- ```python { "name": "numpy.timedelta64", "configuration": { "unit": "ms", "scale_factor": 1 } } ``` """ class TimeDelta64JSON_V2(DTypeConfig_V2[str, None]): """ A wrapper around the JSON representation of the ``TimeDelta64`` data type in Zarr V2. The ``name`` field of this class contains the value that would appear under the ``dtype`` field in Zarr V2 array metadata. References ---------- The structure of the ``name`` field is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). Examples -------- ```python { "name": " None: if self.scale_factor < 1: raise ValueError(f"scale_factor must be > 0, got {self.scale_factor}.") if self.scale_factor >= 2**31: raise ValueError(f"scale_factor must be < 2147483648, got {self.scale_factor}.") if self.unit not in get_args(DateTimeUnit): raise ValueError(f"unit must be one of {get_args(DateTimeUnit)}, got {self.unit!r}.") @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: """ Create an instance of this class from a native NumPy data type. Parameters ---------- dtype : TBaseDType The native NumPy dtype to convert. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the dtype is not a valid representation of this class. """ if cls._check_native_dtype(dtype): unit, scale_factor = np.datetime_data(dtype.name) unit = cast("DateTimeUnit", unit) return cls( unit=unit, scale_factor=scale_factor, endianness=get_endianness_from_numpy_dtype(dtype), ) raise DataTypeValidationError( f"Invalid data type: {dtype}. Expected an instance of {cls.dtype_cls}" ) def to_native_dtype(self) -> DType: # Numpy does not allow creating datetime64 or timedelta64 via # np.dtypes.{dtype_name}() # so we use np.dtype with a formatted string. """ Convert this data type to a NumPy temporal data type with the appropriate unit and scale factor. Returns ------- DType A NumPy data type object representing the time data type with the specified unit, scale factor, and byte order. """ dtype_string = f"{self._numpy_name}[{self.scale_factor}{self.unit}]" return np.dtype(dtype_string).newbyteorder(endianness_to_numpy_str(self.endianness)) # type: ignore[return-value] def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> int: """ Convert a python object to a JSON representation of a datetime64 or timedelta64 scalar. Parameters ---------- data : object The python object to convert. zarr_format : ZarrFormat The Zarr format version (2 or 3). Returns ------- int The JSON representation of the scalar. """ return datetimelike_to_int(data) # type: ignore[arg-type] @property def item_size(self) -> int: """ The size of a single scalar in bytes. Returns ------- int The size of a single scalar in bytes. """ return 8 @dataclass(frozen=True, kw_only=True, slots=True) class TimeDelta64(TimeDTypeBase[np.dtypes.TimeDelta64DType, np.timedelta64], HasEndianness): """ A Zarr data type for arrays containing NumPy TimeDelta64 data. Wraps the ``np.dtypesTimeDelta64DType`` data type. Scalars for this data type are instances of `np.timedelta64`. Attributes ---------- dtype_cls : Type[np.dtypesTimeDelta64DType] The NumPy dtype class for this data type. scale_factor : int The scale factor for this data type. unit : DateTimeUnit The unit for this data type. References ---------- The Zarr V2 representation of this data type is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). The Zarr V3 representation of this data type is defined in the ``numpy.timedelta64`` [specification document](https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/numpy.timedelta64) """ # mypy infers the type of np.dtypes.TimeDelta64DType to be # "Callable[[Literal['Y', 'M', 'W', 'D'] | Literal['h', 'm', 's', 'ms', 'us', 'ns', 'ps', 'fs', 'as']], Never]" dtype_cls = np.dtypes.TimeDelta64DType # type: ignore[assignment] unit: DateTimeUnit = "generic" scale_factor: int = 1 _zarr_v3_name: ClassVar[Literal["numpy.timedelta64"]] = "numpy.timedelta64" _zarr_v2_names: ClassVar[tuple[Literal[">m8"], Literal["m8", " TypeGuard[TimeDelta64JSON_V2]: """ Validate that the provided JSON input accurately represents a NumPy timedelta64 data type, which could be in the form of strings like "m8[10s]". This method serves as a type guard, helping to refine the type of unknown JSON input by confirming its adherence to the expected format for NumPy timedelta64 data types. The JSON input should contain a "name" key with a value that matches the expected string pattern for NumPy timedelta64 data types. The pattern includes an optional unit enclosed within square brackets, following the base type identifier. Returns ------- bool True if the JSON input is a valid representation of this class, otherwise False. """ if not check_dtype_spec_v2(data): return False name = data["name"] # match m[M], etc # consider making this a standalone function if not isinstance(name, str): return False if not name.startswith(cls._zarr_v2_names): return False if len(name) == 3: # no unit, and # we already checked that this string is either m8 return True else: return name[4:-1].endswith(DATETIME_UNIT) and name[-1] == "]" @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[DateTime64JSON_V3]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Returns ------- TypeGuard[DateTime64JSON_V3] True if the JSON input is a valid representation of this class, otherwise False. """ return ( isinstance(data, dict) and set(data.keys()) == {"name", "configuration"} and data["name"] == cls._zarr_v3_name and isinstance(data["configuration"], dict) and set(data["configuration"].keys()) == {"unit", "scale_factor"} ) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create a TimeDelta64 from a Zarr V2-flavored JSON. Parameters ---------- data : DTypeJSON The JSON data. Returns ------- TimeDelta64 An instance of TimeDelta64. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v2(data): name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = ( f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a string " f"representation of an instance of {cls.dtype_cls}" ) raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create a TimeDelta64 from a Zarr V3-flavored JSON. The JSON representation of a TimeDelta64 in Zarr V3 is a dict with a 'name' key with the value 'numpy.timedelta64', and a 'configuration' key with a value of a dict with a 'unit' key and a 'scale_factor' key. For example: ```json { "name": "numpy.timedelta64", "configuration": { "unit": "generic", "scale_factor": 1 } } ``` """ if cls._check_json_v3(data): unit = data["configuration"]["unit"] scale_factor = data["configuration"]["scale_factor"] return cls(unit=unit, scale_factor=scale_factor) msg = ( f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a dict " f"with a 'name' key with the value 'numpy.timedelta64', " "and a 'configuration' key with a value of a dict with a 'unit' key and a " "'scale_factor' key" ) raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> TimeDelta64JSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> TimeDelta64JSON_V3: ... def to_json(self, zarr_format: ZarrFormat) -> TimeDelta64JSON_V2 | TimeDelta64JSON_V3: """ Serialize this data type to JSON. Parameters ---------- zarr_format : ZarrFormat The Zarr format version (2 or 3). Returns ------- TimeDelta64JSON_V2 | TimeDelta64JSON_V3 The JSON representation of the data type. Raises ------ ValueError If the zarr_format is not 2 or 3. """ if zarr_format == 2: name = self.to_native_dtype().str return {"name": name, "object_codec_id": None} elif zarr_format == 3: return { "name": self._zarr_v3_name, "configuration": {"unit": self.unit, "scale_factor": self.scale_factor}, } raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[TimeDeltaLike]: """ Check if the input is a scalar of this data type. Parameters ---------- data : object The object to check. Returns ------- TypeGuard[TimeDeltaLike] True if the input is a scalar of this data type, False otherwise. """ if data is None: return True return isinstance(data, str | int | bytes | np.timedelta64 | timedelta) def _cast_scalar_unchecked(self, data: TimeDeltaLike) -> np.timedelta64: """ Cast the provided scalar input to a numpy timedelta64 without any type checking. This method assumes that the input data is already a valid scalar of this data type, and does not perform any validation or type checks. It directly casts the input to a numpy timedelta64 scalar using the unit and scale factor defined in the class. Parameters ---------- data : TimeDeltaLike The scalar input data to cast. Returns ------- numpy.timedelta64 The input data cast as a numpy timedelta64 scalar. """ return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}") def cast_scalar(self, data: object) -> np.timedelta64: """ Cast the input to a numpy timedelta64 scalar. If the input is not a scalar of this data type, raise a TypeError. """ if self._check_scalar(data): if isinstance(data, np.timedelta64) and np.isnat(data): return np.timedelta64("NaT", self.unit) return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> np.timedelta64: """ Return a default scalar of this data type. This method provides a default value for the timedelta64 scalar, which is a 'Not-a-Time' (NaT) value. """ return np.timedelta64("NaT", self.unit) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.timedelta64: """ Create a scalar of this data type from JSON input. Parameters ---------- data : JSON The JSON representation of the scalar value. zarr_format : int The zarr format to use for the JSON representation. Returns ------- numpy.timedelta64 The scalar value of this data type. Raises ------ TypeError If the input JSON is not a valid representation of a scalar for this data type. """ if check_json_time(data): return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}") raise TypeError(f"Invalid type: {data}. Expected an integer.") # pragma: no cover @dataclass(frozen=True, kw_only=True, slots=True) class DateTime64(TimeDTypeBase[np.dtypes.DateTime64DType, np.datetime64], HasEndianness): """ A Zarr data type for arrays containing NumPy Datetime64 data. Wraps the ``np.dtypes.TimeDelta64DType`` data type. Scalars for this data type are instances of ``np.datetime64``. Attributes ---------- dtype_cls : Type[np.dtypesTimeDelta64DType] The numpy dtype class for this data type. unit : DateTimeUnit The unit of time for this data type. scale_factor : int The scale factor for the time unit. References ---------- The Zarr V2 representation of this data type is defined in the Zarr V2 [specification document](https://github.com/zarr-developers/zarr-specs/blob/main/docs/v2/v2.0.rst#data-type-encoding). The Zarr V3 representation of this data type is defined in the ``numpy.datetime64`` [specification document](https://github.com/zarr-developers/zarr-extensions/tree/main/data-types/numpy.datetime64) """ dtype_cls = np.dtypes.DateTime64DType # type: ignore[assignment] _zarr_v3_name: ClassVar[Literal["numpy.datetime64"]] = "numpy.datetime64" _zarr_v2_names: ClassVar[tuple[Literal[">M8"], Literal["M8", " TypeGuard[DateTime64JSON_V2]: """ Check that the input is a valid JSON representation of this data type. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[DateTime64JSON_V2] True if the input is a valid JSON representation of a NumPy datetime64 data type, otherwise False. """ if not check_dtype_spec_v2(data): return False name = data["name"] if not isinstance(name, str): return False if not name.startswith(cls._zarr_v2_names): return False if len(name) == 3: # no unit, and # we already checked that this string is either M8 return True else: return name[4:-1].endswith(DATETIME_UNIT) and name[-1] == "]" @classmethod def _check_json_v3(cls, data: DTypeJSON) -> TypeGuard[DateTime64JSON_V3]: """ Check that the input is a valid JSON representation of this class in Zarr V3. Parameters ---------- data : DTypeJSON The JSON data to check. Returns ------- TypeGuard[DateTime64JSON_V3] True if the input is a valid JSON representation of a numpy datetime64 data type in Zarr V3, False otherwise. """ return ( isinstance(data, dict) and set(data.keys()) == {"name", "configuration"} and data["name"] == cls._zarr_v3_name and isinstance(data["configuration"], dict) and set(data["configuration"].keys()) == {"unit", "scale_factor"} ) @classmethod def _from_json_v2(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from a Zarr V2-flavored JSON representation. This method checks if the provided JSON data is a valid representation of this class. If valid, it creates an instance using the native NumPy dtype. Otherwise, it raises a DataTypeValidationError. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v2(data): name = data["name"] return cls.from_native_dtype(np.dtype(name)) msg = ( f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a string " f"representation of an instance of {cls.dtype_cls}" ) raise DataTypeValidationError(msg) @classmethod def _from_json_v3(cls, data: DTypeJSON) -> Self: """ Create an instance of this data type from a Zarr V3-flavored JSON representation. This method checks if the provided JSON data is a valid representation of this class. If valid, it creates an instance using the native NumPy dtype. Otherwise, it raises a DataTypeValidationError. Parameters ---------- data : DTypeJSON The JSON data to parse. Returns ------- Self An instance of this data type. Raises ------ DataTypeValidationError If the input JSON is not a valid representation of this class. """ if cls._check_json_v3(data): unit = data["configuration"]["unit"] scale_factor = data["configuration"]["scale_factor"] return cls(unit=unit, scale_factor=scale_factor) msg = ( f"Invalid JSON representation of {cls.__name__}. Got {data!r}, expected a dict " f"with a 'name' key with the value 'numpy.datetime64', " "and a 'configuration' key with a value of a dict with a 'unit' key and a " "'scale_factor' key" ) raise DataTypeValidationError(msg) @overload def to_json(self, zarr_format: Literal[2]) -> DateTime64JSON_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> DateTime64JSON_V3: ... def to_json(self, zarr_format: ZarrFormat) -> DateTime64JSON_V2 | DateTime64JSON_V3: """ Serialize this data type to JSON. Parameters ---------- zarr_format : ZarrFormat The Zarr format version (2 or 3). Returns ------- DateTime64JSON_V2 | DateTime64JSON_V3 The JSON representation of the data type. Raises ------ ValueError If the zarr_format is not 2 or 3. """ if zarr_format == 2: name = self.to_native_dtype().str return {"name": name, "object_codec_id": None} elif zarr_format == 3: return { "name": self._zarr_v3_name, "configuration": {"unit": self.unit, "scale_factor": self.scale_factor}, } raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def _check_scalar(self, data: object) -> TypeGuard[DateTimeLike]: """ Check if the input is convertible to a scalar of this data type. Parameters ---------- data : object The object to check. Returns ------- TypeGuard[DateTimeLike] True if the input is a scalar of this data type, False otherwise. """ if data is None: return True return isinstance(data, str | int | bytes | np.datetime64 | datetime) def _cast_scalar_unchecked(self, data: DateTimeLike) -> np.datetime64: """ Cast the input to a scalar of this data type without any type checking. Parameters ---------- data : DateTimeLike The scalar data to cast. Returns ------- numpy.datetime64 The input cast to a NumPy datetime scalar. """ return self.to_native_dtype().type(data, f"{self.scale_factor}{self.unit}") def cast_scalar(self, data: object) -> np.datetime64: """ Cast the input to a scalar of this data type after a type check. Parameters ---------- data : object The scalar value to cast. Returns ------- numpy.datetime64 The input cast to a NumPy datetime scalar. Raises ------ TypeError If the data cannot be converted to a numpy datetime scalar. """ if self._check_scalar(data): return self._cast_scalar_unchecked(data) msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {self}." ) raise TypeError(msg) def default_scalar(self) -> np.datetime64: """ Return the default scalar value for this data type. Returns ------- numpy.datetime64 The default scalar value, which is a 'Not-a-Time' (NaT) value """ return np.datetime64("NaT", self.unit) def from_json_scalar(self, data: JSON, *, zarr_format: ZarrFormat) -> np.datetime64: """ Read a JSON-serializable value as a scalar. Parameters ---------- data : JSON The JSON-serializable value. zarr_format : ZarrFormat The zarr format version. Returns ------- numpy.datetime64 The numpy datetime scalar. Raises ------ TypeError If the input is not a valid integer type. """ if check_json_time(data): return self._cast_scalar_unchecked(data) raise TypeError(f"Invalid type: {data}. Expected an integer.") # pragma: no cover zarr-python-3.2.1/src/zarr/core/dtype/registry.py000066400000000000000000000163441517635743000221010ustar00rootroot00000000000000from __future__ import annotations import contextlib from dataclasses import dataclass, field from typing import TYPE_CHECKING, Self import numpy as np from zarr.core.dtype.common import ( DataTypeValidationError, DTypeJSON, ) if TYPE_CHECKING: from importlib.metadata import EntryPoint from zarr.core.common import ZarrFormat from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType # This class is different from the other registry classes, which inherit from # dict. IMO it's simpler to just do a dataclass. But long-term we should # have just 1 registry class in use. @dataclass(frozen=True, kw_only=True) class DataTypeRegistry: """ A registry for ZDType classes. This registry is a mapping from Zarr data type names to their corresponding ZDType classes. Attributes ---------- contents : dict[str, type[ZDType[TBaseDType, TBaseScalar]]] The mapping from Zarr data type names to their corresponding ZDType classes. """ contents: dict[str, type[ZDType[TBaseDType, TBaseScalar]]] = field( default_factory=dict, init=False ) _lazy_load_list: list[EntryPoint] = field(default_factory=list, init=False) def _lazy_load(self) -> None: """ Load all data types from the lazy load list and register them with the registry. After loading, clear the lazy load list. """ for e in self._lazy_load_list: self.register(e.load()._zarr_v3_name, e.load()) self._lazy_load_list.clear() def register(self: Self, key: str, cls: type[ZDType[TBaseDType, TBaseScalar]]) -> None: """ Register a data type with the registry. Parameters ---------- key : str The Zarr V3 name of the data type. cls : type[ZDType[TBaseDType, TBaseScalar]] The class of the data type to register. Notes ----- This method is idempotent. If the data type is already registered, this method does nothing. """ if key not in self.contents or self.contents[key] != cls: self.contents[key] = cls def unregister(self, key: str) -> None: """ Unregister a data type from the registry. Parameters ---------- key : str The key associated with the ZDType class to be unregistered. Returns ------- None Raises ------ KeyError If the data type is not found in the registry. """ if key in self.contents: del self.contents[key] else: raise KeyError(f"Data type '{key}' not found in registry.") def get(self, key: str) -> type[ZDType[TBaseDType, TBaseScalar]]: """ Retrieve a registered ZDType class by its key. Parameters ---------- key : str The key associated with the desired ZDType class. Returns ------- type[ZDType[TBaseDType, TBaseScalar]] The ZDType class registered under the given key. Raises ------ KeyError If the key is not found in the registry. """ return self.contents[key] def match_dtype(self, dtype: TBaseDType) -> ZDType[TBaseDType, TBaseScalar]: """ Match a native data type, e.g. a NumPy data type, to a registered ZDType. Parameters ---------- dtype : TBaseDType The native data type to match. Returns ------- ZDType[TBaseDType, TBaseScalar] The matched ZDType corresponding to the provided NumPy data type. Raises ------ ValueError If the data type is a NumPy "Object" type, which is ambiguous, or if multiple or no Zarr data types are found that match the provided dtype. Notes ----- This function attempts to resolve a Zarr data type from a given native data type. If the dtype is a NumPy "Object" data type, it raises a ValueError, as this type can represent multiple Zarr data types. In such cases, a specific Zarr data type should be explicitly constructed instead of relying on dynamic resolution. If multiple matches are found, it will also raise a ValueError. In this case conflicting data types must be unregistered, or the Zarr data type should be explicitly constructed. """ if dtype == np.dtype("O"): msg = ( f"Zarr data type resolution from {dtype} failed. " 'Attempted to resolve a zarr data type from a numpy "Object" data type, which is ' 'ambiguous, as multiple zarr data types can be represented by the numpy "Object" ' "data type. " "In this case you should construct your array by providing a specific Zarr data " 'type. For a list of Zarr data types that are compatible with the numpy "Object"' "data type, see https://github.com/zarr-developers/zarr-python/issues/3117" ) raise ValueError(msg) matched: list[ZDType[TBaseDType, TBaseScalar]] = [] for val in self.contents.values(): # DataTypeValidationError means "this dtype doesn't match me", which is # expected and suppressed. Other exceptions (e.g. ValueError for a dtype # that matches the type but has an invalid configuration) are propagated # to the caller. with contextlib.suppress(DataTypeValidationError): matched.append(val.from_native_dtype(dtype)) if len(matched) == 1: return matched[0] elif len(matched) > 1: msg = ( f"Zarr data type resolution from {dtype} failed. " f"Multiple data type wrappers found that match dtype '{dtype}': {matched}. " "You should unregister one of these data types, or avoid Zarr data type inference " "entirely by providing a specific Zarr data type when creating your array." "For more information, see https://github.com/zarr-developers/zarr-python/issues/3117" ) raise ValueError(msg) raise ValueError(f"No Zarr data type found that matches dtype '{dtype!r}'") def match_json( self, data: DTypeJSON, *, zarr_format: ZarrFormat ) -> ZDType[TBaseDType, TBaseScalar]: """ Match a JSON representation of a data type to a registered ZDType. Parameters ---------- data : DTypeJSON The JSON representation of a data type to match. zarr_format : ZarrFormat The Zarr format version to consider when matching data types. Returns ------- ZDType[TBaseDType, TBaseScalar] The matched ZDType corresponding to the JSON representation. Raises ------ ValueError If no matching Zarr data type is found for the given JSON data. """ for val in self.contents.values(): try: return val.from_json(data, zarr_format=zarr_format) except DataTypeValidationError: pass raise ValueError(f"No Zarr data type found that matches {data!r}") zarr-python-3.2.1/src/zarr/core/dtype/wrapper.py000066400000000000000000000223131517635743000217020ustar00rootroot00000000000000""" Wrapper for native array data types. The ``ZDType`` class is an abstract base class for wrapping native array data types, e.g. NumPy dtypes. ``ZDType`` provides a common interface for working with data types in a way that is independent of the underlying data type system. The wrapper class encapsulates a native data type. Instances of the class can be created from a native data type instance, and a native data type instance can be created from an instance of the wrapper class. The wrapper class is responsible for: - Serializing and deserializing a native data type to Zarr V2 or Zarr V3 metadata. This ensures that the data type can be properly stored and retrieved from array metadata. - Serializing and deserializing scalar values to Zarr V2 or Zarr V3 metadata. This is important for storing a fill value for an array in a manner that is valid for the data type. You can add support for a new data type in Zarr by subclassing ``ZDType`` wrapper class and adapt its methods to support your native data type. The wrapper class must be added to a data type registry (defined elsewhere) before array creation routines or array reading routines can use your new data type. """ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import ( TYPE_CHECKING, ClassVar, Literal, Self, TypeGuard, overload, ) import numpy as np if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat from zarr.core.dtype.common import DTypeJSON, DTypeSpec_V2, DTypeSpec_V3 # This the upper bound for the scalar types we support. It's numpy scalars + str, # because the new variable-length string dtype in numpy does not have a corresponding scalar type type TBaseScalar = np.generic | str | bytes # This is the bound for the dtypes that we support. If we support non-numpy dtypes, # then this bound will need to be widened. type TBaseDType = np.dtype[np.generic] @dataclass(frozen=True, kw_only=True, slots=True) class ZDType[DType: TBaseDType, Scalar: TBaseScalar](ABC): """ Abstract base class for wrapping native array data types, e.g. numpy dtypes Attributes ---------- dtype_cls : ClassVar[type[TDType]] The wrapped dtype class. This is a class variable. _zarr_v3_name : ClassVar[str] The name given to the data type by a Zarr v3 data type specification. This is a class variable, and it should generally be unique across different data types. """ # this class will create a native data type dtype_cls: ClassVar[type[TBaseDType]] _zarr_v3_name: ClassVar[str] @classmethod def _check_native_dtype(cls: type[Self], dtype: TBaseDType) -> TypeGuard[DType]: """ Check that a native data type matches the dtype_cls class attribute. Used as a type guard. Parameters ---------- dtype : TDType The dtype to check. Returns ------- Bool True if the dtype matches, False otherwise. """ return type(dtype) is cls.dtype_cls @classmethod @abstractmethod def from_native_dtype(cls: type[Self], dtype: TBaseDType) -> Self: """ Create a ZDType instance from a native data type. This method is used when taking a user-provided native data type, like a NumPy data type, and creating the corresponding ZDType instance from them. Parameters ---------- dtype : TDType The native data type object to wrap. Returns ------- Self The ZDType that wraps the native data type. Raises ------ TypeError If the native data type is not consistent with the wrapped data type. """ raise NotImplementedError # pragma: no cover @abstractmethod def to_native_dtype(self: Self) -> DType: """ Return an instance of the wrapped data type. This operation inverts ``from_native_dtype``. Returns ------- TDType The native data type wrapped by this ZDType. """ raise NotImplementedError # pragma: no cover @classmethod @abstractmethod def _from_json_v2(cls: type[Self], data: DTypeJSON) -> Self: raise NotImplementedError # pragma: no cover @classmethod @abstractmethod def _from_json_v3(cls: type[Self], data: DTypeJSON) -> Self: raise NotImplementedError # pragma: no cover @classmethod def from_json(cls: type[Self], data: DTypeJSON, *, zarr_format: ZarrFormat) -> Self: """ Create an instance of this ZDType from JSON data. Parameters ---------- data : DTypeJSON The JSON representation of the data type. zarr_format : ZarrFormat The zarr format version. Returns ------- Self An instance of this data type. """ if zarr_format == 2: return cls._from_json_v2(data) if zarr_format == 3: return cls._from_json_v3(data) raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover @overload def to_json(self, zarr_format: Literal[2]) -> DTypeSpec_V2: ... @overload def to_json(self, zarr_format: Literal[3]) -> DTypeSpec_V3: ... @abstractmethod def to_json(self, zarr_format: ZarrFormat) -> DTypeSpec_V2 | DTypeSpec_V3: """ Serialize this ZDType to JSON. Parameters ---------- zarr_format : ZarrFormat The zarr format version. Returns ------- DTypeJSON_V2 | DTypeJSON_V3 The JSON-serializable representation of the wrapped data type """ raise NotImplementedError # pragma: no cover @abstractmethod def _check_scalar(self, data: object) -> bool: """ Check that a python object is a valid scalar value for the wrapped data type. Parameters ---------- data : object A value to check. Returns ------- Bool True if the object is valid, False otherwise. """ raise NotImplementedError # pragma: no cover @abstractmethod def cast_scalar(self, data: object) -> Scalar: """ Cast a python object to the wrapped scalar type. The type of the provided scalar is first checked for compatibility. If it's incompatible with the associated scalar type, a ``TypeError`` will be raised. Parameters ---------- data : object The python object to cast. Returns ------- TScalar The cast value. """ raise NotImplementedError # pragma: no cover @abstractmethod def default_scalar(self) -> Scalar: """ Get the default scalar value for the wrapped data type. This is a method, rather than an attribute, because the default value for some data types depends on parameters that are not known until a concrete data type is wrapped. For example, data types parametrized by a length like fixed-length strings or bytes will generate scalars consistent with that length. Returns ------- TScalar The default value for this data type. """ raise NotImplementedError # pragma: no cover @abstractmethod def from_json_scalar(self: Self, data: JSON, *, zarr_format: ZarrFormat) -> Scalar: """ Read a JSON-serializable value as a scalar. Parameters ---------- data : JSON A JSON representation of a scalar value. zarr_format : ZarrFormat The zarr format version. This is specified because the JSON serialization of scalars differs between Zarr V2 and Zarr V3. Returns ------- TScalar The deserialized scalar value. """ raise NotImplementedError # pragma: no cover @abstractmethod def to_json_scalar(self, data: object, *, zarr_format: ZarrFormat) -> JSON: """ Serialize a python object to the JSON representation of a scalar. The value will first be cast to the scalar type associated with this ZDType, then serialized to JSON. Parameters ---------- data : object The value to convert. zarr_format : ZarrFormat The zarr format version. This is specified because the JSON serialization of scalars differs between Zarr V2 and Zarr V3. Returns ------- JSON The JSON-serialized scalar. """ raise NotImplementedError # pragma: no cover def scalar_failed_type_check_msg( cls_instance: ZDType[TBaseDType, TBaseScalar], bad_scalar: object ) -> str: """ Generate an error message reporting that a particular value failed a type check when attempting to cast that value to a scalar. """ return ( f"The value {bad_scalar!r} failed a type check. " f"It cannot be safely cast to a scalar compatible with {cls_instance}. " f"Consult the documentation for {cls_instance} to determine the possible values that can " "be cast to scalars of the wrapped data type." ) zarr-python-3.2.1/src/zarr/core/group.py000066400000000000000000004156761517635743000202530ustar00rootroot00000000000000from __future__ import annotations import asyncio import itertools import json import logging import unicodedata import warnings from collections import defaultdict from dataclasses import asdict, dataclass, field, fields, replace from itertools import accumulate from typing import TYPE_CHECKING, Literal, assert_never, cast, overload import numpy as np import numpy.typing as npt import zarr.api.asynchronous as async_api from zarr.abc.metadata import Metadata from zarr.abc.store import Store, set_or_delete from zarr.core._info import GroupInfo from zarr.core.array import ( DEFAULT_FILL_VALUE, Array, AsyncArray, CompressorLike, CompressorsLike, FiltersLike, SerializerLike, ShardsLike, _parse_deprecated_compressor, create_array, ) from zarr.core.attributes import Attributes from zarr.core.buffer import default_buffer_prototype from zarr.core.common import ( JSON, ZARR_JSON, ZARRAY_JSON, ZATTRS_JSON, ZGROUP_JSON, ZMETADATA_V2_JSON, ChunksLike, DimensionNamesLike, NodeType, ShapeLike, ZarrFormat, parse_shapelike, ) from zarr.core.config import config from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.core.metadata.io import save_metadata from zarr.core.sync import SyncMixin, sync from zarr.errors import ( ContainsArrayError, ContainsGroupError, GroupNotFoundError, MetadataValidationError, ZarrUserWarning, ) from zarr.storage import StoreLike, StorePath from zarr.storage._common import ensure_no_existing_node, make_store_path from zarr.storage._utils import _join_paths, _normalize_path_keys, normalize_path if TYPE_CHECKING: from collections.abc import ( AsyncGenerator, AsyncIterator, Coroutine, Generator, Iterable, Iterator, Mapping, ) from typing import Any from zarr.core.array_spec import ArrayConfigLike from zarr.core.buffer import Buffer, BufferPrototype from zarr.core.chunk_key_encodings import ChunkKeyEncodingLike from zarr.core.common import MemoryOrder from zarr.core.dtype import ZDTypeLike from zarr.types import AnyArray, AnyAsyncArray, ArrayV2, ArrayV3, AsyncArrayV2, AsyncArrayV3 logger = logging.getLogger("zarr.group") def parse_zarr_format(data: Any) -> ZarrFormat: """Parse the zarr_format field from metadata.""" if data in (2, 3): return cast("ZarrFormat", data) msg = f"Invalid zarr_format. Expected one of 2 or 3. Got {data}." raise ValueError(msg) def parse_node_type(data: Any) -> NodeType: """Parse the node_type field from metadata.""" if data in ("array", "group"): return cast("Literal['array', 'group']", data) msg = f"Invalid value for 'node_type'. Expected 'array' or 'group'. Got '{data}'." raise MetadataValidationError(msg) # todo: convert None to empty dict def parse_attributes(data: Any) -> dict[str, Any]: """Parse the attributes field from metadata.""" if data is None: return {} elif isinstance(data, dict) and all(isinstance(k, str) for k in data): return data msg = f"Expected dict with string keys. Got {type(data)} instead." raise TypeError(msg) @overload def _parse_async_node(node: AsyncArrayV3) -> ArrayV3: ... @overload def _parse_async_node(node: AsyncArrayV2) -> ArrayV2: ... @overload def _parse_async_node(node: AsyncGroup) -> Group: ... def _parse_async_node( node: AnyAsyncArray | AsyncGroup, ) -> AnyArray | Group: """Wrap an AsyncArray in an Array, or an AsyncGroup in a Group.""" if isinstance(node, AsyncArray): return Array(node) elif isinstance(node, AsyncGroup): return Group(node) else: raise TypeError(f"Unknown node type, got {type(node)}") @dataclass(frozen=True) class ConsolidatedMetadata: """ Consolidated Metadata for this Group. This stores the metadata of child nodes below this group. Any child groups will have their consolidated metadata set appropriately. """ metadata: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata] kind: Literal["inline"] = "inline" must_understand: Literal[False] = False def to_dict(self) -> dict[str, JSON]: return { "kind": self.kind, "must_understand": self.must_understand, "metadata": { k: v.to_dict() for k, v in sorted( self.flattened_metadata.items(), key=lambda item: ( item[0].count("/"), unicodedata.normalize("NFKC", item[0]).casefold(), ), ) }, } @classmethod def from_dict(cls, data: dict[str, JSON]) -> ConsolidatedMetadata: data = dict(data) kind = data.get("kind") if kind != "inline": raise ValueError(f"Consolidated metadata kind='{kind}' is not supported.") raw_metadata = data.get("metadata") if not isinstance(raw_metadata, dict): raise TypeError(f"Unexpected type for 'metadata': {type(raw_metadata)}") metadata: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata] = {} if raw_metadata: for k, v in raw_metadata.items(): if not isinstance(v, dict): raise TypeError( f"Invalid value for metadata items. key='{k}', type='{type(v).__name__}'" ) # zarr_format is present in v2 and v3. zarr_format = parse_zarr_format(v["zarr_format"]) if zarr_format == 3: node_type = parse_node_type(v.get("node_type", None)) if node_type == "group": metadata[k] = GroupMetadata.from_dict(v) elif node_type == "array": metadata[k] = ArrayV3Metadata.from_dict(v) else: assert_never(node_type) elif zarr_format == 2: if "shape" in v: metadata[k] = ArrayV2Metadata.from_dict(v) else: metadata[k] = GroupMetadata.from_dict(v) else: assert_never(zarr_format) cls._flat_to_nested(metadata) return cls(metadata=metadata) @staticmethod def _flat_to_nested( metadata: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata], ) -> None: """ Convert a flat metadata representation to a nested one. Notes ----- Flat metadata is used when persisting the consolidated metadata. The keys include the full path, not just the node name. The key prefixes can be used to determine which nodes are children of which other nodes. Nested metadata is used in-memory. The outermost level will only have the *immediate* children of the Group. All nested child groups will be stored under the consolidated metadata of their immediate parent. """ # We have a flat mapping from {k: v} where the keys include the *full* # path segment: # { # "/a/b": { group_metadata }, # "/a/b/array-0": { array_metadata }, # "/a/b/array-1": { array_metadata }, # } # # We want to reorganize the metadata such that each Group contains the # array metadata of its immediate children. # In the example, the group at `/a/b` will have consolidated metadata # for its children `array-0` and `array-1`. # # metadata = dict(metadata) keys = sorted(metadata, key=lambda k: k.count("/")) grouped = { k: list(v) for k, v in itertools.groupby(keys, key=lambda k: k.rsplit("/", 1)[0]) } # we go top down and directly manipulate metadata. for key, children_keys in grouped.items(): # key is a key like "a", "a/b", "a/b/c" # The basic idea is to find the immediate parent (so "", "a", or "a/b") # and update that node's consolidated metadata to include the metadata # in children_keys *prefixes, name = key.split("/") parent = metadata while prefixes: # e.g. a/b/c has a parent "a/b". Walk through to get # metadata["a"]["b"] part = prefixes.pop(0) # we can assume that parent[part] here is a group # otherwise we wouldn't have a node with this `part` prefix. # We can also assume that the parent node will have consolidated metadata, # because we're walking top to bottom. parent = parent[part].consolidated_metadata.metadata # type: ignore[union-attr] node = parent[name] children_keys = list(children_keys) if isinstance(node, ArrayV2Metadata | ArrayV3Metadata): # These are already present, either thanks to being an array in the # root, or by being collected as a child in the else clause continue children_keys = list(children_keys) # We pop from metadata, since we're *moving* this under group children = { child_key.split("/")[-1]: metadata.pop(child_key) for child_key in children_keys if child_key != key } parent[name] = replace( node, consolidated_metadata=ConsolidatedMetadata(metadata=children) ) @property def flattened_metadata(self) -> dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata]: """ Return the flattened representation of Consolidated Metadata. The returned dictionary will have a key for each child node in the hierarchy under this group. Under the default (nested) representation available through ``self.metadata``, the dictionary only contains keys for immediate children. The keys of the dictionary will include the full path to a child node from the current group, where segments are joined by ``/``. Examples -------- ```python from zarr.core.group import ConsolidatedMetadata, GroupMetadata cm = ConsolidatedMetadata( metadata={ "group-0": GroupMetadata( consolidated_metadata=ConsolidatedMetadata( { "group-0-0": GroupMetadata(), } ) ), "group-1": GroupMetadata(), } ) # {'group-0': GroupMetadata(attributes={}, zarr_format=3, consolidated_metadata=None, node_type='group'), # 'group-0/group-0-0': GroupMetadata(attributes={}, zarr_format=3, consolidated_metadata=None, node_type='group'), # 'group-1': GroupMetadata(attributes={}, zarr_format=3, consolidated_metadata=None, node_type='group')} ``` """ metadata = {} def flatten( key: str, group: GroupMetadata | ArrayV2Metadata | ArrayV3Metadata ) -> dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata]: children: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata] = {} if isinstance(group, ArrayV2Metadata | ArrayV3Metadata): children[key] = group else: if group.consolidated_metadata and group.consolidated_metadata.metadata is not None: children[key] = replace( group, consolidated_metadata=ConsolidatedMetadata(metadata={}) ) for name, val in group.consolidated_metadata.metadata.items(): full_key = f"{key}/{name}" if isinstance(val, GroupMetadata): children.update(flatten(full_key, val)) else: children[full_key] = val else: children[key] = replace(group, consolidated_metadata=None) return children for k, v in self.metadata.items(): metadata.update(flatten(k, v)) return metadata @dataclass(frozen=True) class GroupMetadata(Metadata): """ Metadata for a Group. """ attributes: dict[str, Any] = field(default_factory=dict) zarr_format: ZarrFormat = 3 consolidated_metadata: ConsolidatedMetadata | None = None node_type: Literal["group"] = field(default="group", init=False) def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]: json_indent = config.get("json_indent") if self.zarr_format == 3: return { ZARR_JSON: prototype.buffer.from_bytes( json.dumps(self.to_dict(), indent=json_indent, allow_nan=True).encode() ) } else: items = { ZGROUP_JSON: prototype.buffer.from_bytes( json.dumps({"zarr_format": self.zarr_format}, indent=json_indent).encode() ), ZATTRS_JSON: prototype.buffer.from_bytes( json.dumps(self.attributes, indent=json_indent, allow_nan=True).encode() ), } if self.consolidated_metadata: d = { ZGROUP_JSON: {"zarr_format": self.zarr_format}, ZATTRS_JSON: self.attributes, } consolidated_metadata = self.consolidated_metadata.to_dict()["metadata"] assert isinstance(consolidated_metadata, dict) for k, v in consolidated_metadata.items(): attrs = v.pop("attributes", {}) d[f"{k}/{ZATTRS_JSON}"] = attrs if "shape" in v: # it's an array d[f"{k}/{ZARRAY_JSON}"] = v else: d[f"{k}/{ZGROUP_JSON}"] = { "zarr_format": self.zarr_format, "consolidated_metadata": { "metadata": {}, "must_understand": False, "kind": "inline", }, } items[ZMETADATA_V2_JSON] = prototype.buffer.from_bytes( json.dumps( {"metadata": d, "zarr_consolidated_format": 1}, allow_nan=True ).encode() ) return items def __init__( self, attributes: dict[str, Any] | None = None, zarr_format: ZarrFormat = 3, consolidated_metadata: ConsolidatedMetadata | None = None, ) -> None: attributes_parsed = parse_attributes(attributes) zarr_format_parsed = parse_zarr_format(zarr_format) object.__setattr__(self, "attributes", attributes_parsed) object.__setattr__(self, "zarr_format", zarr_format_parsed) object.__setattr__(self, "consolidated_metadata", consolidated_metadata) @classmethod def from_dict(cls, data: dict[str, Any]) -> GroupMetadata: data = dict(data) assert data.pop("node_type", None) in ("group", None) consolidated_metadata = data.pop("consolidated_metadata", None) if consolidated_metadata: data["consolidated_metadata"] = ConsolidatedMetadata.from_dict(consolidated_metadata) zarr_format = data.get("zarr_format") if zarr_format == 2 or zarr_format is None: # zarr v2 allowed arbitrary keys here. # We don't want the GroupMetadata constructor to fail just because someone put an # extra key in the metadata. expected = {x.name for x in fields(cls)} data = {k: v for k, v in data.items() if k in expected} return cls(**data) def to_dict(self) -> dict[str, Any]: result = asdict(replace(self, consolidated_metadata=None)) if self.consolidated_metadata is not None: result["consolidated_metadata"] = self.consolidated_metadata.to_dict() else: # Leave consolidated metadata unset if it's None result.pop("consolidated_metadata") return result @dataclass(frozen=True) class ImplicitGroupMarker(GroupMetadata): """ Marker for an implicit group. Instances of this class are only used in the context of group creation as a placeholder to represent groups that should only be created if they do not already exist in storage """ @dataclass(frozen=True) class AsyncGroup: """ Asynchronous Group object. """ metadata: GroupMetadata store_path: StorePath # TODO: make this correct and work # TODO: ensure that this can be bound properly to subclass of AsyncGroup @classmethod async def from_store( cls, store: StoreLike, *, attributes: dict[str, Any] | None = None, overwrite: bool = False, zarr_format: ZarrFormat = 3, ) -> AsyncGroup: store_path = await make_store_path(store) if overwrite: if store_path.store.supports_deletes: await store_path.delete_dir() else: await ensure_no_existing_node(store_path, zarr_format=zarr_format) else: await ensure_no_existing_node(store_path, zarr_format=zarr_format) attributes = attributes or {} group = cls( metadata=GroupMetadata(attributes=attributes, zarr_format=zarr_format), store_path=store_path, ) await group._save_metadata(ensure_parents=True) return group @classmethod async def open( cls, store: StoreLike, zarr_format: ZarrFormat | None = 3, use_consolidated: bool | str | None = None, ) -> AsyncGroup: """Open a new AsyncGroup Parameters ---------- store : StoreLike zarr_format : {2, 3}, optional use_consolidated : bool or str, default None Whether to use consolidated metadata. By default, consolidated metadata is used if it's present in the store (in the ``zarr.json`` for Zarr format 3 and in the ``.zmetadata`` file for Zarr format 2) and the Store supports it. To explicitly require consolidated metadata, set ``use_consolidated=True``. In this case, if the Store doesn't support consolidation or consolidated metadata is not found, a ``ValueError`` exception is raised. To explicitly *not* use consolidated metadata, set ``use_consolidated=False``, which will fall back to using the regular, non consolidated metadata. Zarr format 2 allowed configuring the key storing the consolidated metadata (``.zmetadata`` by default). Specify the custom key as ``use_consolidated`` to load consolidated metadata from a non-default key. """ store_path = await make_store_path(store) if not store_path.store.supports_consolidated_metadata: # Fail if consolidated metadata was requested but the Store doesn't support it if use_consolidated: store_name = type(store_path.store).__name__ raise ValueError( f"The Zarr store in use ({store_name}) doesn't support consolidated metadata." ) # if use_consolidated was None (optional), the Store dictates it doesn't want consolidation use_consolidated = False consolidated_key = ZMETADATA_V2_JSON if (zarr_format == 2 or zarr_format is None) and isinstance(use_consolidated, str): consolidated_key = use_consolidated if zarr_format == 2: paths = [store_path / ZGROUP_JSON, store_path / ZATTRS_JSON] if use_consolidated or use_consolidated is None: paths.append(store_path / consolidated_key) zgroup_bytes, zattrs_bytes, *rest = await asyncio.gather( *[path.get() for path in paths] ) if zgroup_bytes is None: raise FileNotFoundError(store_path) if use_consolidated or use_consolidated is None: maybe_consolidated_metadata_bytes = rest[0] else: maybe_consolidated_metadata_bytes = None elif zarr_format == 3: zarr_json_bytes = await (store_path / ZARR_JSON).get() if zarr_json_bytes is None: raise FileNotFoundError(store_path) elif zarr_format is None: ( zarr_json_bytes, zgroup_bytes, zattrs_bytes, maybe_consolidated_metadata_bytes, ) = await asyncio.gather( (store_path / ZARR_JSON).get(), (store_path / ZGROUP_JSON).get(), (store_path / ZATTRS_JSON).get(), (store_path / str(consolidated_key)).get(), ) if zarr_json_bytes is not None and zgroup_bytes is not None: # warn and favor v3 msg = f"Both zarr.json (Zarr format 3) and .zgroup (Zarr format 2) metadata objects exist at {store_path}. Zarr format 3 will be used." warnings.warn(msg, category=ZarrUserWarning, stacklevel=1) if zarr_json_bytes is None and zgroup_bytes is None: raise FileNotFoundError( f"could not find zarr.json or .zgroup objects in {store_path}" ) # set zarr_format based on which keys were found if zarr_json_bytes is not None: zarr_format = 3 else: zarr_format = 2 else: msg = f"Invalid value for 'zarr_format'. Expected 2, 3, or None. Got '{zarr_format}'." # type: ignore[unreachable] raise MetadataValidationError(msg) if zarr_format == 2: # this is checked above, asserting here for mypy assert zgroup_bytes is not None if use_consolidated and maybe_consolidated_metadata_bytes is None: # the user requested consolidated metadata, but it was missing raise ValueError(consolidated_key) elif use_consolidated is False: # the user explicitly opted out of consolidated_metadata. # Discard anything we might have read. maybe_consolidated_metadata_bytes = None return cls._from_bytes_v2( store_path, zgroup_bytes, zattrs_bytes, maybe_consolidated_metadata_bytes ) else: # V3 groups are comprised of a zarr.json object assert zarr_json_bytes is not None if not isinstance(use_consolidated, bool | None): raise TypeError("use_consolidated must be a bool or None for Zarr format 3.") return cls._from_bytes_v3( store_path, zarr_json_bytes, use_consolidated=use_consolidated, ) @classmethod def _from_bytes_v2( cls, store_path: StorePath, zgroup_bytes: Buffer, zattrs_bytes: Buffer | None, consolidated_metadata_bytes: Buffer | None, ) -> AsyncGroup: # V2 groups are comprised of a .zgroup and .zattrs objects zgroup = json.loads(zgroup_bytes.to_bytes()) zattrs = json.loads(zattrs_bytes.to_bytes()) if zattrs_bytes is not None else {} group_metadata = {**zgroup, "attributes": zattrs} if consolidated_metadata_bytes is not None: v2_consolidated_metadata = json.loads(consolidated_metadata_bytes.to_bytes()) v2_consolidated_metadata = v2_consolidated_metadata["metadata"] # We already read zattrs and zgroup. Should we ignore these? v2_consolidated_metadata.pop(".zattrs", None) v2_consolidated_metadata.pop(".zgroup", None) consolidated_metadata: defaultdict[str, dict[str, Any]] = defaultdict(dict) # keys like air/.zarray, air/.zattrs for k, v in v2_consolidated_metadata.items(): path, kind = k.rsplit("/.", 1) if kind == "zarray": consolidated_metadata[path].update(v) elif kind == "zattrs": consolidated_metadata[path]["attributes"] = v elif kind == "zgroup": consolidated_metadata[path].update(v) else: raise ValueError(f"Invalid file type '{kind}' at path '{path}") group_metadata["consolidated_metadata"] = { "metadata": dict(consolidated_metadata), "kind": "inline", "must_understand": False, } return cls.from_dict(store_path, group_metadata) @classmethod def _from_bytes_v3( cls, store_path: StorePath, zarr_json_bytes: Buffer, use_consolidated: bool | None, ) -> AsyncGroup: group_metadata = json.loads(zarr_json_bytes.to_bytes()) if use_consolidated and group_metadata.get("consolidated_metadata") is None: msg = f"Consolidated metadata requested with 'use_consolidated=True' but not found in '{store_path.path}'." raise ValueError(msg) elif use_consolidated is False: # Drop consolidated metadata if it's there. group_metadata.pop("consolidated_metadata", None) return cls.from_dict(store_path, group_metadata) @classmethod def from_dict( cls, store_path: StorePath, data: dict[str, Any], ) -> AsyncGroup: node_type = data.pop("node_type", None) if node_type == "array": msg = f"An array already exists in store {store_path.store} at path {store_path.path}." raise ContainsArrayError(msg) elif node_type not in ("group", None): msg = f"Node type in metadata ({node_type}) is not 'group'" raise GroupNotFoundError(msg) return cls( metadata=GroupMetadata.from_dict(data), store_path=store_path, ) async def setitem(self, key: str, value: Any) -> None: """ Fastpath for creating a new array New arrays will be created with default array settings for the array type. Parameters ---------- key : str Array name value : array-like Array data """ path = self.store_path / key await async_api.save_array( store=path, arr=value, zarr_format=self.metadata.zarr_format, overwrite=True ) async def getitem( self, key: str, ) -> AnyAsyncArray | AsyncGroup: """ Get a subarray or subgroup from the group. Parameters ---------- key : str Array or group name Returns ------- AsyncArray or AsyncGroup """ store_path = self.store_path / key logger.debug("key=%s, store_path=%s", key, store_path) # Consolidated metadata lets us avoid some I/O operations so try that first. if self.metadata.consolidated_metadata is not None: return self._getitem_consolidated(store_path, key, prefix=self.name) try: return await get_node( store=store_path.store, path=store_path.path, zarr_format=self.metadata.zarr_format ) except FileNotFoundError as e: raise KeyError(key) from e def _getitem_consolidated( self, store_path: StorePath, key: str, prefix: str ) -> AnyAsyncArray | AsyncGroup: # getitem, in the special case where we have consolidated metadata. # Note that this is a regular def (non async) function. # This shouldn't do any additional I/O. # the caller needs to verify this! assert self.metadata.consolidated_metadata is not None # we support nested getitems like group/subgroup/array indexers = normalize_path(key).split("/") indexers.reverse() metadata: ArrayV2Metadata | ArrayV3Metadata | GroupMetadata = self.metadata while indexers: indexer = indexers.pop() if isinstance(metadata, ArrayV2Metadata | ArrayV3Metadata): # we've indexed into an array with group["array/subarray"]. Invalid. raise KeyError(key) if metadata.consolidated_metadata is None: # we've indexed into a group without consolidated metadata. # This isn't normal; typically, consolidated metadata # will include explicit markers for when there are no child # nodes as metadata={}. # We have some freedom in exactly how we interpret this case. # For now, we treat None as the same as {}, i.e. we don't # have any children. raise KeyError(key) try: metadata = metadata.consolidated_metadata.metadata[indexer] except KeyError as e: # The Group Metadata has consolidated metadata, but the key # isn't present. We trust this to mean that the key isn't in # the hierarchy, and *don't* fall back to checking the store. msg = f"'{key}' not found in consolidated metadata." raise KeyError(msg) from e # update store_path to ensure that AsyncArray/Group.name is correct if prefix != "/": key = "/".join([prefix.lstrip("/"), key]) store_path = StorePath(store=store_path.store, path=key) if isinstance(metadata, GroupMetadata): return AsyncGroup(metadata=metadata, store_path=store_path) else: return AsyncArray(metadata=metadata, store_path=store_path) async def delitem(self, key: str) -> None: """Delete a group member. Parameters ---------- key : str Array or group name """ store_path = self.store_path / key await store_path.delete_dir() if self.metadata.consolidated_metadata: self.metadata.consolidated_metadata.metadata.pop(key, None) await self._save_metadata() async def get[DefaultT]( self, key: str, default: DefaultT | None = None ) -> AnyAsyncArray | AsyncGroup | DefaultT | None: """Obtain a group member, returning default if not found. Parameters ---------- key : str Group member name. default : object Default value to return if key is not found (default: None). Returns ------- object Group member (AsyncArray or AsyncGroup) or default if not found. """ try: return await self.getitem(key) except KeyError: return default async def _save_metadata(self, ensure_parents: bool = False) -> None: await save_metadata(self.store_path, self.metadata, ensure_parents=ensure_parents) @property def path(self) -> str: """Storage path.""" return self.store_path.path @property def name(self) -> str: """Group name following h5py convention.""" if self.path: # follow h5py convention: add leading slash name = self.path if name[0] != "/": name = f"/{name}" return name return "/" @property def basename(self) -> str: """Final component of name.""" return self.name.split("/")[-1] @property def attrs(self) -> dict[str, Any]: return self.metadata.attributes @property def info(self) -> Any: """ Return a visual representation of the statically known information about a group. Note that this doesn't include dynamic information, like the number of child Groups or Arrays. Returns ------- GroupInfo Related ------- [zarr.AsyncGroup.info_complete][] All information about a group, including dynamic information """ if self.metadata.consolidated_metadata: members = list(self.metadata.consolidated_metadata.flattened_metadata.values()) else: members = None return self._info(members=members) async def info_complete(self) -> Any: """ Return all the information for a group. This includes dynamic information like the number of child Groups or Arrays. If this group doesn't contain consolidated metadata then this will need to read from the backing Store. Returns ------- GroupInfo Related ------- [zarr.AsyncGroup.info][] """ members = [x[1].metadata async for x in self.members(max_depth=None)] return self._info(members=members) def _info( self, members: list[ArrayV2Metadata | ArrayV3Metadata | GroupMetadata] | None = None ) -> Any: kwargs = {} if members is not None: kwargs["_count_members"] = len(members) count_arrays = 0 count_groups = 0 for member in members: if isinstance(member, GroupMetadata): count_groups += 1 else: count_arrays += 1 kwargs["_count_arrays"] = count_arrays kwargs["_count_groups"] = count_groups return GroupInfo( _name=self.store_path.path, _read_only=self.read_only, _store_type=type(self.store_path.store).__name__, _zarr_format=self.metadata.zarr_format, # maybe do a typeddict **kwargs, # type: ignore[arg-type] ) @property def store(self) -> Store: return self.store_path.store @property def read_only(self) -> bool: # Backwards compatibility for 2.x return self.store_path.read_only @property def synchronizer(self) -> None: # Backwards compatibility for 2.x # Not implemented in 3.x yet. return None async def create_group( self, name: str, *, overwrite: bool = False, attributes: dict[str, Any] | None = None, ) -> AsyncGroup: """Create a sub-group. Parameters ---------- name : str Group name. overwrite : bool, optional If True, do not raise an error if the group already exists. attributes : dict, optional Group attributes. Returns ------- g : AsyncGroup """ attributes = attributes or {} return await type(self).from_store( self.store_path / name, attributes=attributes, overwrite=overwrite, zarr_format=self.metadata.zarr_format, ) async def require_group(self, name: str, overwrite: bool = False) -> AsyncGroup: """Obtain a sub-group, creating one if it doesn't exist. Parameters ---------- name : str Group name. overwrite : bool, optional Overwrite any existing group with given `name` if present. Returns ------- g : AsyncGroup """ if overwrite: # TODO: check that overwrite=True errors if an array exists where the group is being created grp = await self.create_group(name, overwrite=True) else: try: item: AsyncGroup | AnyAsyncArray = await self.getitem(name) if not isinstance(item, AsyncGroup): raise TypeError( f"Incompatible object ({item.__class__.__name__}) already exists" ) assert isinstance(item, AsyncGroup) # make mypy happy grp = item except KeyError: grp = await self.create_group(name) return grp async def require_groups(self, *names: str) -> tuple[AsyncGroup, ...]: """Convenience method to require multiple groups in a single call. Parameters ---------- *names : str Group names. Returns ------- Tuple[AsyncGroup, ...] """ if not names: return () return tuple(await asyncio.gather(*(self.require_group(name) for name in names))) async def create_array( self, name: str, *, shape: ShapeLike | None = None, dtype: ZDTypeLike | None = None, data: np.ndarray[Any, np.dtype[Any]] | None = None, chunks: ChunksLike | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", compressor: CompressorLike = "auto", serializer: SerializerLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, ) -> AnyAsyncArray: """Create an array within this group. This method lightly wraps [zarr.core.array.create_array][]. Parameters ---------- name : str The name of the array relative to the group. If ``path`` is ``None``, the array will be located at the root of the store. shape : tuple[int, ...] Shape of the array. dtype : npt.DTypeLike Data type of the array. chunks : tuple[int, ...], optional Chunk shape of the array. If not specified, default are guessed based on the shape and dtype. shards : tuple[int, ...], optional Shard shape of the array. The default value of ``None`` results in no sharding at all. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec], optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. For Zarr format 3, a "compressor" is a codec that takes a bytestream, and returns another bytestream. Multiple compressors my be provided for Zarr format 3. If no ``compressors`` are provided, a default set of compressors will be used. These defaults can be changed by modifying the value of ``array.v3_default_compressors`` in [`zarr.config`][zarr.config]. Use ``None`` to omit default compressors. For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may be provided for Zarr format 2. If no ``compressor`` is provided, a default compressor will be used. in [`zarr.config`][zarr.config]. Use ``None`` to omit the default compressor. compressor : Codec, optional Deprecated in favor of ``compressors``. serializer : dict[str, JSON] | ArrayBytesCodec, optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. If no ``serializer`` is provided, a default serializer will be used. These defaults can be changed by modifying the value of ``array.v3_default_serializer`` in [`zarr.config`][zarr.config]. fill_value : Any, optional Fill value for the array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If no ``order`` is provided, a default order will be used. This default can be changed by modifying the value of ``array.order`` in [`zarr.config`][zarr.config]. attributes : dict, optional Attributes for the array. chunk_key_encoding : ChunkKeyEncoding, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. dimension_names : Iterable[str], optional The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. storage_options : dict, optional If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. config : ArrayConfig or ArrayConfigLike, optional Runtime configuration for the array. write_data : bool If a pre-existing array-like object was provided to this function via the ``data`` parameter then ``write_data`` determines whether the values in that array-like object should be written to the Zarr array created by this function. If ``write_data`` is ``False``, then the array will be left empty. Returns ------- AsyncArray """ compressors = _parse_deprecated_compressor( compressor, compressors, zarr_format=self.metadata.zarr_format ) return await create_array( store=self.store_path, name=name, shape=shape, dtype=dtype, data=data, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=self.metadata.zarr_format, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, storage_options=storage_options, overwrite=overwrite, config=config, write_data=write_data, ) async def require_array( self, name: str, *, shape: ShapeLike, dtype: npt.DTypeLike = None, exact: bool = False, **kwargs: Any, ) -> AnyAsyncArray: """Obtain an array, creating if it doesn't exist. Other `kwargs` are as per [zarr.AsyncGroup.create_array][]. Parameters ---------- name : str Array name. shape : int or tuple of ints Array shape. dtype : str or dtype, optional NumPy dtype. exact : bool, optional If True, require `dtype` to match exactly. If false, require `dtype` can be cast from array dtype. Returns ------- a : AsyncArray """ try: ds = await self.getitem(name) if not isinstance(ds, AsyncArray): raise TypeError(f"Incompatible object ({ds.__class__.__name__}) already exists") shape = parse_shapelike(shape) if shape != ds.shape: raise TypeError(f"Incompatible shape ({ds.shape} vs {shape})") dtype = np.dtype(dtype) if exact: if ds.dtype != dtype: raise TypeError(f"Incompatible dtype ({ds.dtype} vs {dtype})") else: if not np.can_cast(ds.dtype, dtype): raise TypeError(f"Incompatible dtype ({ds.dtype} vs {dtype})") except KeyError: ds = await self.create_array(name, shape=shape, dtype=dtype, **kwargs) return ds async def update_attributes(self, new_attributes: dict[str, Any]) -> AsyncGroup: """Update group attributes. Parameters ---------- new_attributes : dict New attributes to set on the group. Returns ------- self : AsyncGroup """ self.metadata.attributes.update(new_attributes) # Write new metadata await self._save_metadata() return self def __repr__(self) -> str: return f"" async def nmembers( self, max_depth: int | None = 0, ) -> int: """Count the number of members in this group. Parameters ---------- max_depth : int, default 0 The maximum number of levels of the hierarchy to include. By default, (``max_depth=0``) only immediate children are included. Set ``max_depth=None`` to include all nodes, and some positive integer to consider children within that many levels of the root Group. Returns ------- count : int """ # check if we can use consolidated metadata, which requires that we have non-None # consolidated metadata at all points in the hierarchy. if self.metadata.consolidated_metadata is not None: if max_depth is not None and max_depth < 0: raise ValueError(f"max_depth must be None or >= 0. Got '{max_depth}' instead") if max_depth is None: return len(self.metadata.consolidated_metadata.flattened_metadata) else: return len( [ x for x in self.metadata.consolidated_metadata.flattened_metadata if x.count("/") <= max_depth ] ) # TODO: consider using aioitertools.builtins.sum for this # return await aioitertools.builtins.sum((1 async for _ in self.members()), start=0) n = 0 async for _ in self.members(max_depth=max_depth): n += 1 return n async def members( self, max_depth: int | None = 0, *, use_consolidated_for_children: bool = True, ) -> AsyncGenerator[ tuple[str, AnyAsyncArray | AsyncGroup], None, ]: """ Returns an AsyncGenerator over the arrays and groups contained in this group. This method requires that `store_path.store` supports directory listing. The results are not guaranteed to be ordered. Parameters ---------- max_depth : int, default 0 The maximum number of levels of the hierarchy to include. By default, (``max_depth=0``) only immediate children are included. Set ``max_depth=None`` to include all nodes, and some positive integer to consider children within that many levels of the root Group. use_consolidated_for_children : bool, default True Whether to use the consolidated metadata of child groups loaded from the store. Note that this only affects groups loaded from the store. If the current Group already has consolidated metadata, it will always be used. Returns ------- path: A string giving the path to the target, relative to the Group ``self``. value: AsyncArray or AsyncGroup The AsyncArray or AsyncGroup that is a child of ``self``. """ if max_depth is not None and max_depth < 0: raise ValueError(f"max_depth must be None or >= 0. Got '{max_depth}' instead") async for item in self._members( max_depth=max_depth, use_consolidated_for_children=use_consolidated_for_children ): yield item def _members_consolidated( self, max_depth: int | None, prefix: str = "" ) -> Generator[ tuple[str, AnyAsyncArray | AsyncGroup], None, ]: consolidated_metadata = self.metadata.consolidated_metadata do_recursion = max_depth is None or max_depth > 0 # we kind of just want the top-level keys. if consolidated_metadata is not None: for key in consolidated_metadata.metadata: obj = self._getitem_consolidated( self.store_path, key, prefix=self.name ) # Metadata -> Group/Array key = f"{prefix}/{key}".lstrip("/") yield key, obj if do_recursion and isinstance(obj, AsyncGroup): if max_depth is None: new_depth = None else: new_depth = max_depth - 1 yield from obj._members_consolidated(new_depth, prefix=key) async def _members( self, max_depth: int | None, *, use_consolidated_for_children: bool = True ) -> AsyncGenerator[tuple[str, AnyAsyncArray | AsyncGroup], None]: skip_keys: tuple[str, ...] if self.metadata.zarr_format == 2: skip_keys = (".zattrs", ".zgroup", ".zarray", ".zmetadata") elif self.metadata.zarr_format == 3: skip_keys = ("zarr.json",) else: raise ValueError(f"Unknown Zarr format: {self.metadata.zarr_format}") if self.metadata.consolidated_metadata is not None: members = self._members_consolidated(max_depth=max_depth) for member in members: yield member return if not self.store_path.store.supports_listing: msg = ( f"The store associated with this group ({type(self.store_path.store)}) " "does not support listing, " "specifically via the `list_dir` method. " "This function requires a store that supports listing." ) raise ValueError(msg) # enforce a concurrency limit by passing a semaphore to all the recursive functions semaphore = asyncio.Semaphore(config.get("async.concurrency")) async for member in _iter_members_deep( self, max_depth=max_depth, skip_keys=skip_keys, semaphore=semaphore, use_consolidated_for_children=use_consolidated_for_children, ): yield member async def create_hierarchy( self, nodes: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata], *, overwrite: bool = False, ) -> AsyncIterator[tuple[str, AsyncGroup | AnyAsyncArray]]: """ Create a hierarchy of arrays or groups rooted at this group. This function will parse its input to ensure that the hierarchy is complete. Any implicit groups will be inserted as needed. For example, an input like ```{'a/b': GroupMetadata}``` will be parsed to ```{'': GroupMetadata, 'a': GroupMetadata, 'b': Groupmetadata}```. Explicitly specifying a root group, e.g. with ``nodes = {'': GroupMetadata()}`` is an error because this group instance is the root group. After input parsing, this function then creates all the nodes in the hierarchy concurrently. Arrays and Groups are yielded in the order they are created. This order is not stable and should not be relied on. Parameters ---------- nodes : dict[str, GroupMetadata | ArrayV3Metadata | ArrayV2Metadata] A dictionary defining the hierarchy. The keys are the paths of the nodes in the hierarchy, relative to the path of the group. The values are instances of ``GroupMetadata`` or ``ArrayMetadata``. Note that all values must have the same ``zarr_format`` as the parent group -- it is an error to mix zarr versions in the same hierarchy. Leading "/" characters from keys will be removed. overwrite : bool Whether to overwrite existing nodes. Defaults to ``False``, in which case an error is raised instead of overwriting an existing array or group. This function will not erase an existing group unless that group is explicitly named in ``nodes``. If ``nodes`` defines implicit groups, e.g. ``{`'a/b/c': GroupMetadata}``, and a group already exists at path ``a``, then this function will leave the group at ``a`` as-is. Yields ------ tuple[str, AsyncArray | AsyncGroup]. """ # check that all the nodes have the same zarr_format as Self prefix = self.path nodes_parsed = {} for key, value in nodes.items(): if value.zarr_format != self.metadata.zarr_format: msg = ( "The zarr_format of the nodes must be the same as the parent group. " f"The node at {key} has zarr_format {value.zarr_format}, but the parent group" f" has zarr_format {self.metadata.zarr_format}." ) raise ValueError(msg) if normalize_path(key) == "": msg = ( "The input defines a root node, but a root node already exists, namely this Group instance." "It is an error to use this method to create a root node. " "Remove the root node from the input dict, or use a function like " "create_rooted_hierarchy to create a rooted hierarchy." ) raise ValueError(msg) else: nodes_parsed[_join_paths([prefix, key])] = value async for key, node in create_hierarchy( store=self.store, nodes=nodes_parsed, overwrite=overwrite, ): if prefix == "": out_key = key else: out_key = key.removeprefix(prefix + "/") yield out_key, node async def keys(self) -> AsyncGenerator[str, None]: """Iterate over member names.""" async for key, _ in self.members(): yield key async def contains(self, member: str) -> bool: """Check if a member exists in the group. Parameters ---------- member : str Member name. Returns ------- bool """ # TODO: this can be made more efficient. try: await self.getitem(member) except KeyError: return False else: return True async def groups(self) -> AsyncGenerator[tuple[str, AsyncGroup], None]: """Iterate over subgroups.""" async for name, value in self.members(): if isinstance(value, AsyncGroup): yield name, value async def group_keys(self) -> AsyncGenerator[str, None]: """Iterate over group names.""" async for key, _ in self.groups(): yield key async def group_values(self) -> AsyncGenerator[AsyncGroup, None]: """Iterate over group values.""" async for _, group in self.groups(): yield group async def arrays( self, ) -> AsyncGenerator[tuple[str, AnyAsyncArray], None]: """Iterate over arrays.""" async for key, value in self.members(): if isinstance(value, AsyncArray): yield key, value async def array_keys(self) -> AsyncGenerator[str, None]: """Iterate over array names.""" async for key, _ in self.arrays(): yield key async def array_values( self, ) -> AsyncGenerator[AnyAsyncArray, None]: """Iterate over array values.""" async for _, array in self.arrays(): yield array async def tree( self, expand: bool | None = None, level: int | None = None, *, max_nodes: int = 500, plain: bool = False, ) -> Any: """ Return a tree-like representation of a hierarchy. Parameters ---------- expand : bool, optional This keyword is not yet supported. A NotImplementedError is raised if it's used. level : int, optional The maximum depth below this Group to display in the tree. max_nodes : int Maximum number of nodes to display before truncating. Default is 500. plain : bool, optional If True, return a plain-text tree without ANSI styling. This is useful when the output will be consumed by an LLM or written to a file. Default is False. Returns ------- TreeRepr A pretty-printable object displaying the hierarchy. """ from zarr.core._tree import group_tree_async if expand is not None: raise NotImplementedError("'expand' is not yet implemented.") return await group_tree_async(self, max_depth=level, max_nodes=max_nodes, plain=plain) async def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an empty array with the specified shape in this Group. The contents will be filled with the array's fill value or zeros if no fill value is provided. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Notes ----- The contents of an empty Zarr array are not defined. On attempting to retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ return await async_api.empty(shape=shape, store=self.store_path, path=name, **kwargs) async def zeros(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with zero being used as the default value for uninitialized portions of the array. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- AsyncArray The new array. """ return await async_api.zeros(shape=shape, store=self.store_path, path=name, **kwargs) async def ones(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with one being used as the default value for uninitialized portions of the array. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- AsyncArray The new array. """ return await async_api.ones(shape=shape, store=self.store_path, path=name, **kwargs) async def full( self, *, name: str, shape: tuple[int, ...], fill_value: Any | None, **kwargs: Any ) -> AnyAsyncArray: """Create an array, with "fill_value" being used as the default value for uninitialized portions of the array. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. fill_value : scalar Value to fill the array with. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- AsyncArray The new array. """ return await async_api.full( shape=shape, fill_value=fill_value, store=self.store_path, path=name, **kwargs, ) async def empty_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any ) -> AnyAsyncArray: """Create an empty sub-array like `data`. The contents will be filled with the array's fill value or zeros if no fill value is provided. Parameters ---------- name : str Name of the array. data : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- AsyncArray The new array. """ return await async_api.empty_like(a=data, store=self.store_path, path=name, **kwargs) async def zeros_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any ) -> AnyAsyncArray: """Create a sub-array of zeros like `data`. Parameters ---------- name : str Name of the array. data : array-like The array to create the new array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- AsyncArray The new array. """ return await async_api.zeros_like(a=data, store=self.store_path, path=name, **kwargs) async def ones_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any ) -> AnyAsyncArray: """Create a sub-array of ones like `data`. Parameters ---------- name : str Name of the array. data : array-like The array to create the new array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- AsyncArray The new array. """ return await async_api.ones_like(a=data, store=self.store_path, path=name, **kwargs) async def full_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any ) -> AnyAsyncArray: """Create a sub-array like `data` filled with the `fill_value` of `data` . Parameters ---------- name : str Name of the array. data : array-like The array to create the new array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- AsyncArray The new array. """ return await async_api.full_like(a=data, store=self.store_path, path=name, **kwargs) async def move(self, source: str, dest: str) -> None: """Move a sub-group or sub-array from one path to another. Notes ----- Not implemented """ raise NotImplementedError @dataclass(frozen=True) class Group(SyncMixin): """ A Zarr group. """ _async_group: AsyncGroup @classmethod def from_store( cls, store: StoreLike, *, attributes: dict[str, Any] | None = None, zarr_format: ZarrFormat = 3, overwrite: bool = False, ) -> Group: """Instantiate a group from an initialized store. Parameters ---------- store : StoreLike StoreLike containing the Group. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. attributes : dict, optional A dictionary of JSON-serializable values with user-defined attributes. zarr_format : {2, 3}, optional Zarr storage format version. overwrite : bool, optional If True, do not raise an error if the group already exists. Returns ------- Group Group instantiated from the store. Raises ------ ContainsArrayError, ContainsGroupError, ContainsArrayAndGroupError """ attributes = attributes or {} obj = sync( AsyncGroup.from_store( store, attributes=attributes, overwrite=overwrite, zarr_format=zarr_format, ), ) return cls(obj) @classmethod def open( cls, store: StoreLike, zarr_format: ZarrFormat | None = 3, ) -> Group: """Open a group from an initialized store. Parameters ---------- store : StoreLike Store containing the Group. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. zarr_format : {2, 3, None}, optional Zarr storage format version. Returns ------- Group Group instantiated from the store. """ obj = sync(AsyncGroup.open(store, zarr_format=zarr_format)) return cls(obj) def __getitem__(self, path: str) -> AnyArray | Group: """Obtain a group member. Parameters ---------- path : str Group member name. Returns ------- Array | Group Group member (Array or Group) at the specified key Examples -------- ```python import zarr from zarr.core.group import Group group = Group.from_store(zarr.storage.MemoryStore()) group.create_array(name="subarray", shape=(10,), chunks=(10,), dtype="float64") group.create_group(name="subgroup").create_array(name="subarray", shape=(10,), chunks=(10,), dtype="float64") group["subarray"] # group["subgroup"] # group["subgroup"]["subarray"] # ``` """ obj = self._sync(self._async_group.getitem(path)) if isinstance(obj, AsyncArray): return Array(obj) else: return Group(obj) def get[DefaultT]( self, path: str, default: DefaultT | None = None ) -> AnyArray | Group | DefaultT | None: """Obtain a group member, returning default if not found. Parameters ---------- path : str Group member name. default : object Default value to return if key is not found (default: None). Returns ------- object Group member (Array or Group) or default if not found. Examples -------- ```python import zarr from zarr.core.group import Group group = Group.from_store(zarr.storage.MemoryStore()) group.create_array(name="subarray", shape=(10,), chunks=(10,), dtype="float64") group.create_group(name="subgroup") group.get("subarray") # group.get("subgroup") # group.get("nonexistent", None) # None ``` """ try: return self[path] except KeyError: return default def __delitem__(self, key: str) -> None: """Delete a group member. Parameters ---------- key : str Group member name. Examples -------- >>> import zarr >>> group = Group.from_store(zarr.storage.MemoryStore() >>> group.create_array(name="subarray", shape=(10,), chunks=(10,)) >>> del group["subarray"] >>> "subarray" in group False """ self._sync(self._async_group.delitem(key)) def __iter__(self) -> Iterator[str]: """Return an iterator over group member names. Examples -------- >>> import zarr >>> g1 = zarr.group() >>> g2 = g1.create_group('foo') >>> g3 = g1.create_group('bar') >>> d1 = g1.create_array('baz', shape=(10,), chunks=(10,)) >>> d2 = g1.create_array('quux', shape=(10,), chunks=(10,)) >>> for name in g1: ... print(name) baz bar foo quux """ yield from self.keys() def __len__(self) -> int: """Number of members.""" return self.nmembers() def __setitem__(self, key: str, value: Any) -> None: """Fastpath for creating a new array. New arrays will be created using default settings for the array type. If you need to create an array with custom settings, use the `create_array` method. Parameters ---------- key : str Array name. value : Any Array data. Examples -------- >>> import zarr >>> group = zarr.group() >>> group["foo"] = zarr.zeros((10,)) >>> group["foo"] """ self._sync(self._async_group.setitem(key, value)) def __repr__(self) -> str: return f"" async def update_attributes_async(self, new_attributes: dict[str, Any]) -> Group: """Update the attributes of this group. Examples -------- >>> import zarr >>> group = zarr.group() >>> await group.update_attributes_async({"foo": "bar"}) >>> group.attrs.asdict() {'foo': 'bar'} """ new_metadata = replace(self.metadata, attributes=new_attributes) # Write new metadata to_save = new_metadata.to_buffer_dict(default_buffer_prototype()) awaitables = [set_or_delete(self.store_path / key, value) for key, value in to_save.items()] await asyncio.gather(*awaitables) async_group = replace(self._async_group, metadata=new_metadata) return replace(self, _async_group=async_group) @property def store_path(self) -> StorePath: """Path-like interface for the Store.""" return self._async_group.store_path @property def metadata(self) -> GroupMetadata: """Group metadata.""" return self._async_group.metadata @property def path(self) -> str: """Storage path.""" return self._async_group.path @property def name(self) -> str: """Group name following h5py convention.""" return self._async_group.name @property def basename(self) -> str: """Final component of name.""" return self._async_group.basename @property def attrs(self) -> Attributes: """Attributes of this Group""" return Attributes(self) @property def info(self) -> Any: """ Return the statically known information for a group. Returns ------- GroupInfo Related ------- [zarr.Group.info_complete][] All information about a group, including dynamic information like the children members. """ return self._async_group.info def info_complete(self) -> Any: """ Return information for a group. If this group doesn't contain consolidated metadata then this will need to read from the backing Store. Returns ------- GroupInfo Related ------- [zarr.Group.info][] """ return self._sync(self._async_group.info_complete()) @property def store(self) -> Store: # Backwards compatibility for 2.x return self._async_group.store @property def read_only(self) -> bool: # Backwards compatibility for 2.x return self._async_group.read_only @property def synchronizer(self) -> None: # Backwards compatibility for 2.x # Not implemented in 3.x yet. return self._async_group.synchronizer def update_attributes(self, new_attributes: dict[str, Any]) -> Group: """Update the attributes of this group. Examples -------- >>> import zarr >>> group = zarr.group() >>> group.update_attributes({"foo": "bar"}) >>> group.attrs.asdict() {'foo': 'bar'} """ self._sync(self._async_group.update_attributes(new_attributes)) return self def nmembers(self, max_depth: int | None = 0) -> int: """Count the number of members in this group. Parameters ---------- max_depth : int, default 0 The maximum number of levels of the hierarchy to include. By default, (``max_depth=0``) only immediate children are included. Set ``max_depth=None`` to include all nodes, and some positive integer to consider children within that many levels of the root Group. Returns ------- count : int """ return self._sync(self._async_group.nmembers(max_depth=max_depth)) def members( self, max_depth: int | None = 0, *, use_consolidated_for_children: bool = True ) -> tuple[tuple[str, AnyArray | Group], ...]: """ Returns an AsyncGenerator over the arrays and groups contained in this group. This method requires that `store_path.store` supports directory listing. The results are not guaranteed to be ordered. Parameters ---------- max_depth : int, default 0 The maximum number of levels of the hierarchy to include. By default, (``max_depth=0``) only immediate children are included. Set ``max_depth=None`` to include all nodes, and some positive integer to consider children within that many levels of the root Group. use_consolidated_for_children : bool, default True Whether to use the consolidated metadata of child groups loaded from the store. Note that this only affects groups loaded from the store. If the current Group already has consolidated metadata, it will always be used. Returns ------- path: A string giving the path to the target, relative to the Group ``self``. value: AsyncArray or AsyncGroup The AsyncArray or AsyncGroup that is a child of ``self``. """ _members = self._sync_iter(self._async_group.members(max_depth=max_depth)) return tuple((kv[0], _parse_async_node(kv[1])) for kv in _members) def create_hierarchy( self, nodes: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata], *, overwrite: bool = False, ) -> Iterator[tuple[str, Group | AnyArray]]: """ Create a hierarchy of arrays or groups rooted at this group. This function will parse its input to ensure that the hierarchy is complete. Any implicit groups will be inserted as needed. For example, an input like ```{'a/b': GroupMetadata}``` will be parsed to ```{'': GroupMetadata, 'a': GroupMetadata, 'b': Groupmetadata}```. Explicitly specifying a root group, e.g. with ``nodes = {'': GroupMetadata()}`` is an error because this group instance is the root group. After input parsing, this function then creates all the nodes in the hierarchy concurrently. Arrays and Groups are yielded in the order they are created. This order is not stable and should not be relied on. Parameters ---------- nodes : dict[str, GroupMetadata | ArrayV3Metadata | ArrayV2Metadata] A dictionary defining the hierarchy. The keys are the paths of the nodes in the hierarchy, relative to the path of the group. The values are instances of ``GroupMetadata`` or ``ArrayMetadata``. Note that all values must have the same ``zarr_format`` as the parent group -- it is an error to mix zarr versions in the same hierarchy. Leading "/" characters from keys will be removed. overwrite : bool Whether to overwrite existing nodes. Defaults to ``False``, in which case an error is raised instead of overwriting an existing array or group. This function will not erase an existing group unless that group is explicitly named in ``nodes``. If ``nodes`` defines implicit groups, e.g. ``{`'a/b/c': GroupMetadata}``, and a group already exists at path ``a``, then this function will leave the group at ``a`` as-is. Yields ------ tuple[str, Array | Group]. Examples -------- >>> import zarr >>> from zarr.core.group import GroupMetadata >>> root = zarr.create_group(store={}) >>> for key, val in root.create_hierarchy({'a/b/c': GroupMetadata()}): ... print(key, val) ... """ for key, node in self._sync_iter( self._async_group.create_hierarchy(nodes, overwrite=overwrite) ): yield (key, _parse_async_node(node)) def keys(self) -> Generator[str, None]: """Return an iterator over group member names. Examples -------- >>> import zarr >>> g1 = zarr.group() >>> g2 = g1.create_group('foo') >>> g3 = g1.create_group('bar') >>> d1 = g1.create_array('baz', shape=(10,), chunks=(10,)) >>> d2 = g1.create_array('quux', shape=(10,), chunks=(10,)) >>> for name in g1.keys(): ... print(name) baz bar foo quux """ yield from self._sync_iter(self._async_group.keys()) def __contains__(self, member: str) -> bool: """Test for group membership. Examples -------- >>> import zarr >>> g1 = zarr.group() >>> g2 = g1.create_group('foo') >>> d1 = g1.create_array('bar', shape=(10,), chunks=(10,)) >>> 'foo' in g1 True >>> 'bar' in g1 True >>> 'baz' in g1 False """ return self._sync(self._async_group.contains(member)) def groups(self) -> Generator[tuple[str, Group], None]: """Return the sub-groups of this group as a generator of (name, group) pairs. Examples -------- >>> import zarr >>> group = zarr.group() >>> group.create_group("subgroup") >>> for name, subgroup in group.groups(): ... print(name, subgroup) subgroup """ for name, async_group in self._sync_iter(self._async_group.groups()): yield name, Group(async_group) def group_keys(self) -> Generator[str, None]: """Return an iterator over group member names. Examples -------- >>> import zarr >>> group = zarr.group() >>> group.create_group("subgroup") >>> for name in group.group_keys(): ... print(name) subgroup """ for name, _ in self.groups(): yield name def group_values(self) -> Generator[Group, None]: """Return an iterator over group members. Examples -------- >>> import zarr >>> group = zarr.group() >>> group.create_group("subgroup") >>> for subgroup in group.group_values(): ... print(subgroup) """ for _, group in self.groups(): yield group def arrays(self) -> Generator[tuple[str, AnyArray], None]: """Return the sub-arrays of this group as a generator of (name, array) pairs Examples -------- >>> import zarr >>> group = zarr.group() >>> group.create_array("subarray", shape=(10,), chunks=(10,)) >>> for name, subarray in group.arrays(): ... print(name, subarray) subarray """ for name, async_array in self._sync_iter(self._async_group.arrays()): yield name, Array(async_array) def array_keys(self) -> Generator[str, None]: """Return an iterator over group member names. Examples -------- >>> import zarr >>> group = zarr.group() >>> group.create_array("subarray", shape=(10,), chunks=(10,)) >>> for name in group.array_keys(): ... print(name) subarray """ for name, _ in self.arrays(): yield name def array_values(self) -> Generator[AnyArray, None]: """Return an iterator over group members. Examples -------- >>> import zarr >>> group = zarr.group() >>> group.create_array("subarray", shape=(10,), chunks=(10,)) >>> for subarray in group.array_values(): ... print(subarray) """ for _, array in self.arrays(): yield array def tree( self, expand: bool | None = None, level: int | None = None, *, max_nodes: int = 500, plain: bool = False, ) -> Any: """ Return a tree-like representation of a hierarchy. Parameters ---------- expand : bool, optional This keyword is not yet supported. A NotImplementedError is raised if it's used. level : int, optional The maximum depth below this Group to display in the tree. max_nodes : int Maximum number of nodes to display before truncating. Default is 500. plain : bool, optional If True, return a plain-text tree without ANSI styling. This is useful when the output will be consumed by an LLM or written to a file. Default is False. Returns ------- TreeRepr A pretty-printable object displaying the hierarchy. """ return self._sync( self._async_group.tree(expand=expand, level=level, max_nodes=max_nodes, plain=plain) ) def create_group(self, name: str, **kwargs: Any) -> Group: """Create a sub-group. Parameters ---------- name : str Name of the new subgroup. Returns ------- Group Examples -------- >>> import zarr >>> group = zarr.group() >>> subgroup = group.create_group("subgroup") >>> subgroup """ return Group(self._sync(self._async_group.create_group(name, **kwargs))) def require_group(self, name: str, **kwargs: Any) -> Group: """Obtain a sub-group, creating one if it doesn't exist. Parameters ---------- name : str Group name. Returns ------- g : Group """ return Group(self._sync(self._async_group.require_group(name, **kwargs))) def require_groups(self, *names: str) -> tuple[Group, ...]: """Convenience method to require multiple groups in a single call. Parameters ---------- *names : str Group names. Returns ------- groups : tuple of Groups """ return tuple(map(Group, self._sync(self._async_group.require_groups(*names)))) def create( self, name: str, *, shape: ShapeLike | None = None, dtype: ZDTypeLike | None = None, data: np.ndarray[Any, np.dtype[Any]] | None = None, chunks: ChunksLike | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", compressor: CompressorLike = "auto", serializer: SerializerLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, ) -> AnyArray: """Create an array within this group. This method lightly wraps [`zarr.core.array.create_array`][]. Parameters ---------- name : str The name of the array relative to the group. If ``path`` is ``None``, the array will be located at the root of the store. shape : ShapeLike, optional Shape of the array. Must be ``None`` if ``data`` is provided. dtype : npt.DTypeLike | None Data type of the array. Must be ``None`` if ``data`` is provided. data : Array-like data to use for initializing the array. If this parameter is provided, the ``shape`` and ``dtype`` parameters must be ``None``. chunks : tuple[int, ...], optional Chunk shape of the array. If not specified, default are guessed based on the shape and dtype. shards : tuple[int, ...], optional Shard shape of the array. The default value of ``None`` results in no sharding at all. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec], optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. For Zarr format 3, a "compressor" is a codec that takes a bytestream, and returns another bytestream. Multiple compressors my be provided for Zarr format 3. If no ``compressors`` are provided, a default set of compressors will be used. These defaults can be changed by modifying the value of ``array.v3_default_compressors`` in [`zarr.config`][]. Use ``None`` to omit default compressors. For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may be provided for Zarr format 2. If no ``compressor`` is provided, a default compressor will be used. in [`zarr.config`][]. Use ``None`` to omit the default compressor. compressor : Codec, optional Deprecated in favor of ``compressors``. serializer : dict[str, JSON] | ArrayBytesCodec, optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. If no ``serializer`` is provided, a default serializer will be used. These defaults can be changed by modifying the value of ``array.v3_default_serializer`` in [`zarr.config`][]. fill_value : Any, optional Fill value for the array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If no ``order`` is provided, a default order will be used. This default can be changed by modifying the value of ``array.order`` in [`zarr.config`][]. attributes : dict, optional Attributes for the array. chunk_key_encoding : ChunkKeyEncoding, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. dimension_names : Iterable[str], optional The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. storage_options : dict, optional If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. config : ArrayConfig or ArrayConfigLike, optional Runtime configuration for the array. write_data : bool If a pre-existing array-like object was provided to this function via the ``data`` parameter then ``write_data`` determines whether the values in that array-like object should be written to the Zarr array created by this function. If ``write_data`` is ``False``, then the array will be left empty. Returns ------- AsyncArray """ return self.create_array( name, shape=shape, dtype=dtype, data=data, chunks=chunks, shards=shards, filters=filters, compressors=compressors, compressor=compressor, serializer=serializer, fill_value=fill_value, order=order, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, storage_options=storage_options, overwrite=overwrite, config=config, write_data=write_data, ) def create_array( self, name: str, *, shape: ShapeLike | None = None, dtype: ZDTypeLike | None = None, data: np.ndarray[Any, np.dtype[Any]] | None = None, chunks: ChunksLike | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", compressor: CompressorLike = "auto", serializer: SerializerLike = "auto", fill_value: Any | None = DEFAULT_FILL_VALUE, order: MemoryOrder | None = None, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, ) -> AnyArray: """Create an array within this group. This method lightly wraps [zarr.core.array.create_array][]. Parameters ---------- name : str The name of the array relative to the group. If ``path`` is ``None``, the array will be located at the root of the store. shape : ShapeLike, optional Shape of the array. Must be ``None`` if ``data`` is provided. dtype : npt.DTypeLike | None Data type of the array. Must be ``None`` if ``data`` is provided. data : Array-like data to use for initializing the array. If this parameter is provided, the ``shape`` and ``dtype`` parameters must be ``None``. chunks : tuple[int, ...], optional Chunk shape of the array. If not specified, default are guessed based on the shape and dtype. shards : tuple[int, ...], optional Shard shape of the array. The default value of ``None`` results in no sharding at all. filters : Iterable[Codec] | Literal["auto"], optional Iterable of filters to apply to each chunk of the array, in order, before serializing that chunk to bytes. For Zarr format 3, a "filter" is a codec that takes an array and returns an array, and these values must be instances of [`zarr.abc.codec.ArrayArrayCodec`][], or a dict representations of [`zarr.abc.codec.ArrayArrayCodec`][]. For Zarr format 2, a "filter" can be any numcodecs codec; you should ensure that the the order if your filters is consistent with the behavior of each filter. The default value of ``"auto"`` instructs Zarr to use a default used based on the data type of the array and the Zarr format specified. For all data types in Zarr V3, and most data types in Zarr V2, the default filters are empty. The only cases where default filters are not empty is when the Zarr format is 2, and the data type is a variable-length data type like [`zarr.dtype.VariableLengthUTF8`][] or [`zarr.dtype.VariableLengthUTF8`][]. In these cases, the default filters contains a single element which is a codec specific to that particular data type. To create an array with no filters, provide an empty iterable or the value ``None``. compressors : Iterable[Codec], optional List of compressors to apply to the array. Compressors are applied in order, and after any filters are applied (if any are specified) and the data is serialized into bytes. For Zarr format 3, a "compressor" is a codec that takes a bytestream, and returns another bytestream. Multiple compressors my be provided for Zarr format 3. If no ``compressors`` are provided, a default set of compressors will be used. These defaults can be changed by modifying the value of ``array.v3_default_compressors`` in [`zarr.config`][zarr.config]. Use ``None`` to omit default compressors. For Zarr format 2, a "compressor" can be any numcodecs codec. Only a single compressor may be provided for Zarr format 2. If no ``compressor`` is provided, a default compressor will be used. in [`zarr.config`][zarr.config]. Use ``None`` to omit the default compressor. compressor : Codec, optional Deprecated in favor of ``compressors``. serializer : dict[str, JSON] | ArrayBytesCodec, optional Array-to-bytes codec to use for encoding the array data. Zarr format 3 only. Zarr format 2 arrays use implicit array-to-bytes conversion. If no ``serializer`` is provided, a default serializer will be used. These defaults can be changed by modifying the value of ``array.v3_default_serializer`` in [`zarr.config`][zarr.config]. fill_value : Any, optional Fill value for the array. order : {"C", "F"}, optional The memory of the array (default is "C"). For Zarr format 2, this parameter sets the memory order of the array. For Zarr format 3, this parameter is deprecated, because memory order is a runtime parameter for Zarr format 3 arrays. The recommended way to specify the memory order for Zarr format 3 arrays is via the ``config`` parameter, e.g. ``{'config': 'C'}``. If no ``order`` is provided, a default order will be used. This default can be changed by modifying the value of ``array.order`` in [`zarr.config`][zarr.config]. attributes : dict, optional Attributes for the array. chunk_key_encoding : ChunkKeyEncoding, optional A specification of how the chunk keys are represented in storage. For Zarr format 3, the default is ``{"name": "default", "separator": "/"}}``. For Zarr format 2, the default is ``{"name": "v2", "separator": "."}}``. dimension_names : Iterable[str], optional The names of the dimensions (default is None). Zarr format 3 only. Zarr format 2 arrays should not use this parameter. storage_options : dict, optional If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. overwrite : bool, default False Whether to overwrite an array with the same name in the store, if one exists. config : ArrayConfig or ArrayConfigLike, optional Runtime configuration for the array. write_data : bool If a pre-existing array-like object was provided to this function via the ``data`` parameter then ``write_data`` determines whether the values in that array-like object should be written to the Zarr array created by this function. If ``write_data`` is ``False``, then the array will be left empty. Returns ------- AsyncArray """ compressors = _parse_deprecated_compressor( compressor, compressors, zarr_format=self.metadata.zarr_format ) return Array( self._sync( self._async_group.create_array( name=name, shape=shape, dtype=dtype, data=data, chunks=chunks, shards=shards, fill_value=fill_value, attributes=attributes, chunk_key_encoding=chunk_key_encoding, compressors=compressors, serializer=serializer, dimension_names=dimension_names, order=order, filters=filters, overwrite=overwrite, storage_options=storage_options, config=config, write_data=write_data, ) ) ) def require_array(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> AnyArray: """Obtain an array, creating if it doesn't exist. Other `kwargs` are as per [zarr.Group.create_array][]. Parameters ---------- name : str Array name. **kwargs : See [zarr.Group.create_array][]. Returns ------- a : Array """ return Array(self._sync(self._async_group.require_array(name, shape=shape, **kwargs))) def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an empty array with the specified shape in this Group. The contents will be filled with the array's fill value or zeros if no fill value is provided. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Notes ----- The contents of an empty Zarr array are not defined. On attempting to retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ return Array(self._sync(self._async_group.empty(name=name, shape=shape, **kwargs))) def zeros(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array, with zero being used as the default value for uninitialized portions of the array. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- Array The new array. """ return Array(self._sync(self._async_group.zeros(name=name, shape=shape, **kwargs))) def ones(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array, with one being used as the default value for uninitialized portions of the array. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- Array The new array. """ return Array(self._sync(self._async_group.ones(name=name, shape=shape, **kwargs))) def full( self, *, name: str, shape: tuple[int, ...], fill_value: Any | None, **kwargs: Any ) -> AnyArray: """Create an array, with "fill_value" being used as the default value for uninitialized portions of the array. Parameters ---------- name : str Name of the array. shape : int or tuple of int Shape of the empty array. fill_value : scalar Value to fill the array with. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- Array The new array. """ return Array( self._sync( self._async_group.full(name=name, shape=shape, fill_value=fill_value, **kwargs) ) ) def empty_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create an empty sub-array like `data`. The contents will be filled with the array's fill value or zeros if no fill value is provided. Parameters ---------- name : str Name of the array. data : array-like The array to create an empty array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- Array The new array. Notes ----- The contents of an empty Zarr array are not defined. On attempting to retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ return Array(self._sync(self._async_group.empty_like(name=name, data=data, **kwargs))) def zeros_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create a sub-array of zeros like `data`. Parameters ---------- name : str Name of the array. data : array-like The array to create the new array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- Array The new array. """ return Array(self._sync(self._async_group.zeros_like(name=name, data=data, **kwargs))) def ones_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create a sub-array of ones like `data`. Parameters ---------- name : str Name of the array. data : array-like The array to create the new array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- Array The new array. """ return Array(self._sync(self._async_group.ones_like(name=name, data=data, **kwargs))) def full_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create a sub-array like `data` filled with the `fill_value` of `data` . Parameters ---------- name : str Name of the array. data : array-like The array to create the new array like. **kwargs Keyword arguments passed to [zarr.api.asynchronous.create][]. Returns ------- Array The new array. """ return Array(self._sync(self._async_group.full_like(name=name, data=data, **kwargs))) def move(self, source: str, dest: str) -> None: """Move a sub-group or sub-array from one path to another. Notes ----- Not implemented """ return self._sync(self._async_group.move(source, dest)) async def create_hierarchy( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, ) -> AsyncIterator[tuple[str, AsyncGroup | AnyAsyncArray]]: """ Create a complete zarr hierarchy from a collection of metadata objects. This function will parse its input to ensure that the hierarchy is complete. Any implicit groups will be inserted as needed. For example, an input like ```{'a/b': GroupMetadata}``` will be parsed to ```{'': GroupMetadata, 'a': GroupMetadata, 'b': Groupmetadata}``` After input parsing, this function then creates all the nodes in the hierarchy concurrently. Arrays and Groups are yielded in the order they are created. This order is not stable and should not be relied on. Parameters ---------- store : Store The storage backend to use. nodes : dict[str, GroupMetadata | ArrayV3Metadata | ArrayV2Metadata] A dictionary defining the hierarchy. The keys are the paths of the nodes in the hierarchy, relative to the root of the ``Store``. The root of the store can be specified with the empty string ``''``. The values are instances of ``GroupMetadata`` or ``ArrayMetadata``. Note that all values must have the same ``zarr_format`` -- it is an error to mix zarr versions in the same hierarchy. Leading "/" characters from keys will be removed. overwrite : bool Whether to overwrite existing nodes. Defaults to ``False``, in which case an error is raised instead of overwriting an existing array or group. This function will not erase an existing group unless that group is explicitly named in ``nodes``. If ``nodes`` defines implicit groups, e.g. ``{`'a/b/c': GroupMetadata}``, and a group already exists at path ``a``, then this function will leave the group at ``a`` as-is. Yields ------ tuple[str, AsyncGroup | AsyncArray] This function yields (path, node) pairs, in the order the nodes were created. Examples -------- >>> from zarr.api.asynchronous import create_hierarchy >>> from zarr.storage import MemoryStore >>> from zarr.core.group import GroupMetadata >>> import asyncio >>> store = MemoryStore() >>> nodes = {'a': GroupMetadata(attributes={'name': 'leaf'})} >>> async def run(): ... print(dict([x async for x in create_hierarchy(store=store, nodes=nodes)])) >>> asyncio.run(run()) # {'a': , '': } """ # normalize the keys to be valid paths nodes_normed_keys = _normalize_path_keys(nodes) # ensure that all nodes have the same zarr_format, and add implicit groups as needed nodes_parsed = _parse_hierarchy_dict(data=nodes_normed_keys) redundant_implicit_groups = [] # empty hierarchies should be a no-op if len(nodes_parsed) > 0: # figure out which zarr format we are using zarr_format = next(iter(nodes_parsed.values())).zarr_format # check which implicit groups will require materialization implicit_group_keys = tuple( filter(lambda k: isinstance(nodes_parsed[k], ImplicitGroupMarker), nodes_parsed) ) # read potential group metadata for each implicit group maybe_extant_group_coros = ( _read_group_metadata(store, k, zarr_format=zarr_format) for k in implicit_group_keys ) maybe_extant_groups = await asyncio.gather( *maybe_extant_group_coros, return_exceptions=True ) for key, value in zip(implicit_group_keys, maybe_extant_groups, strict=True): if isinstance(value, BaseException): if isinstance(value, FileNotFoundError): # this is fine -- there was no group there, so we will create one pass else: raise value else: # a loop exists already at ``key``, so we can avoid creating anything there redundant_implicit_groups.append(key) if overwrite: # we will remove any nodes that collide with arrays and non-implicit groups defined in # nodes # track the keys of nodes we need to delete to_delete_keys = [] to_delete_keys.extend( [k for k, v in nodes_parsed.items() if k not in implicit_group_keys] ) await asyncio.gather(*(store.delete_dir(key) for key in to_delete_keys)) else: # This type is long. coros: ( Generator[Coroutine[Any, Any, ArrayV2Metadata | GroupMetadata], None, None] | Generator[Coroutine[Any, Any, ArrayV3Metadata | GroupMetadata], None, None] ) if zarr_format == 2: coros = (_read_metadata_v2(store=store, path=key) for key in nodes_parsed) elif zarr_format == 3: coros = (_read_metadata_v3(store=store, path=key) for key in nodes_parsed) else: # pragma: no cover raise ValueError(f"Invalid zarr_format: {zarr_format}") # pragma: no cover extant_node_query = dict( zip( nodes_parsed.keys(), await asyncio.gather(*coros, return_exceptions=True), strict=False, ) ) # iterate over the existing arrays / groups and figure out which of them conflict # with the arrays / groups we want to create for key, extant_node in extant_node_query.items(): proposed_node = nodes_parsed[key] if isinstance(extant_node, BaseException): if isinstance(extant_node, FileNotFoundError): # ignore FileNotFoundError, because they represent nodes we can safely create pass else: # Any other exception is a real error raise extant_node else: # this is a node that already exists, but a node with the same key was specified # in nodes_parsed. if isinstance(extant_node, GroupMetadata): # a group already exists where we want to create a group if isinstance(proposed_node, ImplicitGroupMarker): # we have proposed an implicit group, which is OK -- we will just skip # creating this particular metadata document redundant_implicit_groups.append(key) else: # we have proposed an explicit group, which is an error, given that a # group already exists. msg = f"A group exists in store {store!r} at path {key!r}." raise ContainsGroupError(msg) elif isinstance(extant_node, ArrayV2Metadata | ArrayV3Metadata): # we are trying to overwrite an existing array. this is an error. msg = f"An array exists in store {store!r} at path {key!r}." raise ContainsArrayError(msg) nodes_explicit: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata] = {} for k, v in nodes_parsed.items(): if k not in redundant_implicit_groups: if isinstance(v, ImplicitGroupMarker): nodes_explicit[k] = GroupMetadata(zarr_format=v.zarr_format) else: nodes_explicit[k] = v async for key, node in create_nodes(store=store, nodes=nodes_explicit): yield key, node async def create_nodes( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], ) -> AsyncIterator[tuple[str, AsyncGroup | AnyAsyncArray]]: """Create a collection of arrays and / or groups concurrently. Note: no attempt is made to validate that these arrays and / or groups collectively form a valid Zarr hierarchy. It is the responsibility of the caller of this function to ensure that the ``nodes`` parameter satisfies any correctness constraints. Parameters ---------- store : Store The storage backend to use. nodes : dict[str, GroupMetadata | ArrayV3Metadata | ArrayV2Metadata] A dictionary defining the hierarchy. The keys are the paths of the nodes in the hierarchy, and the values are the metadata of the nodes. The metadata must be either an instance of GroupMetadata, ArrayV3Metadata or ArrayV2Metadata. Yields ------ AsyncGroup | AsyncArray The created nodes in the order they are created. """ # Note: the only way to alter this value is via the config. If that's undesirable for some reason, # then we should consider adding a keyword argument this this function semaphore = asyncio.Semaphore(config.get("async.concurrency")) create_tasks: list[Coroutine[None, None, str]] = [] for key, value in nodes.items(): # make the key absolute create_tasks.extend(_persist_metadata(store, key, value, semaphore=semaphore)) created_object_keys = [] for coro in asyncio.as_completed(create_tasks): created_key = await coro # we need this to track which metadata documents were written so that we can yield a # complete v2 Array / Group class after both .zattrs and the metadata JSON was created. created_object_keys.append(created_key) # get the node name from the object key if len(created_key.split("/")) == 1: # this is the root node meta_out = nodes[""] node_name = "" else: # turn "foo/" into "foo" node_name = created_key[: created_key.rfind("/")] meta_out = nodes[node_name] if meta_out.zarr_format == 3: yield node_name, _build_node(store=store, path=node_name, metadata=meta_out) else: # For zarr v2 # we only want to yield when both the metadata and attributes are created # so we track which keys have been created, and wait for both the meta key and # the attrs key to be created before yielding back the AsyncArray / AsyncGroup attrs_done = _join_paths([node_name, ZATTRS_JSON]) in created_object_keys if isinstance(meta_out, GroupMetadata): meta_done = _join_paths([node_name, ZGROUP_JSON]) in created_object_keys else: meta_done = _join_paths([node_name, ZARRAY_JSON]) in created_object_keys if meta_done and attrs_done: yield node_name, _build_node(store=store, path=node_name, metadata=meta_out) continue def _get_roots( data: Iterable[str], ) -> tuple[str, ...]: """ Return the keys of the root(s) of the hierarchy. A root is a key with the fewest number of path segments. """ if "" in data: return ("",) keys_split = sorted((key.split("/") for key in data), key=len) groups: defaultdict[int, list[str]] = defaultdict(list) for key_split in keys_split: groups[len(key_split)].append("/".join(key_split)) return tuple(groups[min(groups.keys())]) def _parse_hierarchy_dict( *, data: Mapping[str, ImplicitGroupMarker | GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], ) -> dict[str, ImplicitGroupMarker | GroupMetadata | ArrayV2Metadata | ArrayV3Metadata]: """ Take an input with type Mapping[str, ArrayMetadata | GroupMetadata] and parse it into a dict of str: node pairs that models a valid, complete Zarr hierarchy. If the input represents a complete Zarr hierarchy, i.e. one with no implicit groups, then return a dict with the exact same data as the input. Otherwise, return a dict derived from the input with GroupMetadata inserted as needed to make the hierarchy complete. For example, an input of {'a/b': ArrayMetadata} is incomplete, because it references two groups (the root group '' and a group at 'a') that are not specified in the input. Applying this function to that input will result in a return value of {'': GroupMetadata, 'a': GroupMetadata, 'a/b': ArrayMetadata}, i.e. the implied groups were added. The input is also checked for the following conditions; an error is raised if any are violated: - No arrays can contain group or arrays (i.e., all arrays must be leaf nodes). - All arrays and groups must have the same ``zarr_format`` value. This function ensures that the input is transformed into a specification of a complete and valid Zarr hierarchy. """ # ensure that all nodes have the same zarr format data_purified = _ensure_consistent_zarr_format(data) # ensure that keys are normalized to zarr paths data_normed_keys = _normalize_path_keys(data_purified) # insert an implicit root group if a root was not specified # but not if an empty dict was provided, because any empty hierarchy has no nodes if len(data_normed_keys) > 0 and "" not in data_normed_keys: z_format = next(iter(data_normed_keys.values())).zarr_format data_normed_keys = data_normed_keys | {"": ImplicitGroupMarker(zarr_format=z_format)} out: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata] = {**data_normed_keys} for k, v in data_normed_keys.items(): key_split = k.split("/") # get every parent path *subpaths, _ = accumulate(key_split, lambda a, b: _join_paths([a, b])) for subpath in subpaths: # If a component is not already in the output dict, add ImplicitGroupMetadata if subpath not in out: out[subpath] = ImplicitGroupMarker(zarr_format=v.zarr_format) else: if not isinstance(out[subpath], GroupMetadata | ImplicitGroupMarker): msg = ( f"The node at {subpath} contains other nodes, but it is not a Zarr group. " "This is invalid. Only Zarr groups can contain other nodes." ) raise ValueError(msg) return out def _ensure_consistent_zarr_format( data: Mapping[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], ) -> Mapping[str, GroupMetadata | ArrayV2Metadata] | Mapping[str, GroupMetadata | ArrayV3Metadata]: """ Ensure that all values of the input dict have the same zarr format. If any do not, then a value error is raised. """ observed_zarr_formats: dict[ZarrFormat, list[str]] = {2: [], 3: []} for k, v in data.items(): observed_zarr_formats[v.zarr_format].append(k) if len(observed_zarr_formats[2]) > 0 and len(observed_zarr_formats[3]) > 0: msg = ( "Got data with both Zarr v2 and Zarr v3 nodes, which is invalid. " f"The following keys map to Zarr v2 nodes: {observed_zarr_formats.get(2)}. " f"The following keys map to Zarr v3 nodes: {observed_zarr_formats.get(3)}." "Ensure that all nodes have the same Zarr format." ) raise ValueError(msg) return cast( "Mapping[str, GroupMetadata | ArrayV2Metadata] | Mapping[str, GroupMetadata | ArrayV3Metadata]", data, ) async def _getitem_semaphore( node: AsyncGroup, key: str, semaphore: asyncio.Semaphore | None ) -> AnyAsyncArray | AsyncGroup: """ Wrap Group.getitem with an optional semaphore. If the semaphore parameter is an asyncio.Semaphore instance, then the getitem operation is performed inside an async context manager provided by that semaphore. If the semaphore parameter is None, then getitem is invoked without a context manager. """ if semaphore is not None: async with semaphore: return await node.getitem(key) else: return await node.getitem(key) async def _iter_members( node: AsyncGroup, skip_keys: tuple[str, ...], semaphore: asyncio.Semaphore | None, ) -> AsyncGenerator[tuple[str, AnyAsyncArray | AsyncGroup], None]: """ Iterate over the arrays and groups contained in a group. Parameters ---------- node : AsyncGroup The group to traverse. skip_keys : tuple[str, ...] A tuple of keys to skip when iterating over the possible members of the group. semaphore : asyncio.Semaphore | None An optional semaphore to use for concurrency control. Yields ------ tuple[str, AnyAsyncArray | AsyncGroup] """ # retrieve keys from storage keys = [key async for key in node.store.list_dir(node.path)] keys_filtered = tuple(filter(lambda v: v not in skip_keys, keys)) node_tasks = tuple( asyncio.create_task(_getitem_semaphore(node, key, semaphore), name=key) for key in keys_filtered ) for fetched_node_coro in asyncio.as_completed(node_tasks): try: fetched_node = await fetched_node_coro except KeyError as e: # keyerror is raised when `key` names an object (in the object storage sense), # as opposed to a prefix, in the store under the prefix associated with this group # in which case `key` cannot be the name of a sub-array or sub-group. warnings.warn( f"Object at {e.args[0]} is not recognized as a component of a Zarr hierarchy.", ZarrUserWarning, stacklevel=1, ) continue match fetched_node: case AsyncArray() | AsyncGroup(): yield fetched_node.basename, fetched_node case _: raise ValueError(f"Unexpected type: {type(fetched_node)}") async def _iter_members_deep( group: AsyncGroup, *, max_depth: int | None, skip_keys: tuple[str, ...], semaphore: asyncio.Semaphore | None = None, use_consolidated_for_children: bool = True, ) -> AsyncGenerator[tuple[str, AnyAsyncArray | AsyncGroup], None]: """ Iterate over the arrays and groups contained in a group, and optionally the arrays and groups contained in those groups. Parameters ---------- group : AsyncGroup The group to traverse. max_depth : int | None The maximum depth of recursion. skip_keys : tuple[str, ...] A tuple of keys to skip when iterating over the possible members of the group. semaphore : asyncio.Semaphore | None An optional semaphore to use for concurrency control. use_consolidated_for_children : bool, default True Whether to use the consolidated metadata of child groups loaded from the store. Note that this only affects groups loaded from the store. If the current Group already has consolidated metadata, it will always be used. Yields ------ tuple[str, AnyAsyncArray | AsyncGroup] """ to_recurse = {} do_recursion = max_depth is None or max_depth > 0 if max_depth is None: new_depth = None else: new_depth = max_depth - 1 async for name, node in _iter_members(group, skip_keys=skip_keys, semaphore=semaphore): is_group = isinstance(node, AsyncGroup) if ( is_group and not use_consolidated_for_children and node.metadata.consolidated_metadata is not None ): node = cast("AsyncGroup", node) # We've decided not to trust consolidated metadata at this point, because we're # reconsolidating the metadata, for example. node = replace(node, metadata=replace(node.metadata, consolidated_metadata=None)) yield name, node if is_group and do_recursion: node = cast("AsyncGroup", node) to_recurse[name] = _iter_members_deep( node, max_depth=new_depth, skip_keys=skip_keys, semaphore=semaphore ) for prefix, subgroup_iter in to_recurse.items(): async for name, node in subgroup_iter: key = f"{prefix}/{name}".lstrip("/") yield key, node async def _read_metadata_v3(store: Store, path: str) -> ArrayV3Metadata | GroupMetadata: """ Given a store_path, return ArrayV3Metadata or GroupMetadata defined by the metadata document stored at store_path.path / zarr.json. If no such document is found, raise a FileNotFoundError. """ zarr_json_bytes = await store.get( _join_paths([path, ZARR_JSON]), prototype=default_buffer_prototype() ) if zarr_json_bytes is None: raise FileNotFoundError(path) else: zarr_json = json.loads(zarr_json_bytes.to_bytes()) return _build_metadata_v3(zarr_json) async def _read_metadata_v2(store: Store, path: str) -> ArrayV2Metadata | GroupMetadata: """ Given a store_path, return ArrayV2Metadata or GroupMetadata defined by the metadata document stored at store_path.path / (.zgroup | .zarray). If no such document is found, raise a FileNotFoundError. """ # TODO: consider first fetching array metadata, and only fetching group metadata when we don't # find an array zarray_bytes, zgroup_bytes, zattrs_bytes = await asyncio.gather( store.get(_join_paths([path, ZARRAY_JSON]), prototype=default_buffer_prototype()), store.get(_join_paths([path, ZGROUP_JSON]), prototype=default_buffer_prototype()), store.get(_join_paths([path, ZATTRS_JSON]), prototype=default_buffer_prototype()), ) if zattrs_bytes is None: zattrs = {} else: zattrs = json.loads(zattrs_bytes.to_bytes()) # TODO: decide how to handle finding both array and group metadata. The spec does not seem to # consider this situation. A practical approach would be to ignore that combination, and only # return the array metadata. if zarray_bytes is not None: zmeta = json.loads(zarray_bytes.to_bytes()) else: if zgroup_bytes is None: # neither .zarray or .zgroup were found results in KeyError raise FileNotFoundError(path) else: zmeta = json.loads(zgroup_bytes.to_bytes()) return _build_metadata_v2(zmeta, zattrs) async def _read_group_metadata_v2(store: Store, path: str) -> GroupMetadata: """ Read group metadata or error """ meta = await _read_metadata_v2(store=store, path=path) if not isinstance(meta, GroupMetadata): raise FileNotFoundError(f"Group metadata was not found in {store} at {path}") return meta async def _read_group_metadata_v3(store: Store, path: str) -> GroupMetadata: """ Read group metadata or error """ meta = await _read_metadata_v3(store=store, path=path) if not isinstance(meta, GroupMetadata): raise FileNotFoundError(f"Group metadata was not found in {store} at {path}") return meta async def _read_group_metadata( store: Store, path: str, *, zarr_format: ZarrFormat ) -> GroupMetadata: if zarr_format == 2: return await _read_group_metadata_v2(store=store, path=path) return await _read_group_metadata_v3(store=store, path=path) def _build_metadata_v3(zarr_json: dict[str, JSON]) -> ArrayV3Metadata | GroupMetadata: """ Convert a dict representation of Zarr V3 metadata into the corresponding metadata class. """ if "node_type" not in zarr_json: msg = "Required key 'node_type' is missing from the provided metadata document." raise MetadataValidationError(msg) match zarr_json: case {"node_type": "array"}: return ArrayV3Metadata.from_dict(zarr_json) case {"node_type": "group"}: return GroupMetadata.from_dict(zarr_json) case _: # pragma: no cover raise ValueError( "invalid value for `node_type` key in metadata document" ) # pragma: no cover def _build_metadata_v2( zarr_json: dict[str, JSON], attrs_json: dict[str, JSON] ) -> ArrayV2Metadata | GroupMetadata: """ Convert a dict representation of Zarr V2 metadata into the corresponding metadata class. """ match zarr_json: case {"shape": _}: return ArrayV2Metadata.from_dict(zarr_json | {"attributes": attrs_json}) case _: # pragma: no cover return GroupMetadata.from_dict(zarr_json | {"attributes": attrs_json}) @overload def _build_node(*, store: Store, path: str, metadata: ArrayV2Metadata) -> AsyncArrayV2: ... @overload def _build_node(*, store: Store, path: str, metadata: ArrayV3Metadata) -> AsyncArrayV3: ... @overload def _build_node(*, store: Store, path: str, metadata: GroupMetadata) -> AsyncGroup: ... def _build_node( *, store: Store, path: str, metadata: ArrayV3Metadata | ArrayV2Metadata | GroupMetadata ) -> AnyAsyncArray | AsyncGroup: """ Take a metadata object and return a node (AsyncArray or AsyncGroup). """ store_path = StorePath(store=store, path=path) match metadata: case ArrayV2Metadata() | ArrayV3Metadata(): return AsyncArray(metadata, store_path=store_path) case GroupMetadata(): return AsyncGroup(metadata, store_path=store_path) case _: # pragma: no cover raise ValueError(f"Unexpected metadata type: {type(metadata)}") # pragma: no cover async def _get_node_v2(store: Store, path: str) -> AsyncArrayV2 | AsyncGroup: """ Read a Zarr v2 AsyncArray or AsyncGroup from a path in a Store. Parameters ---------- store : Store The store-like object to read from. path : str The path to the node to read. Returns ------- AsyncArray | AsyncGroup """ metadata = await _read_metadata_v2(store=store, path=path) return _build_node(store=store, path=path, metadata=metadata) async def _get_node_v3(store: Store, path: str) -> AsyncArrayV3 | AsyncGroup: """ Read a Zarr v3 AsyncArray or AsyncGroup from a path in a Store. Parameters ---------- store : Store The store-like object to read from. path : str The path to the node to read. Returns ------- AsyncArray | AsyncGroup """ metadata = await _read_metadata_v3(store=store, path=path) return _build_node(store=store, path=path, metadata=metadata) async def get_node(store: Store, path: str, zarr_format: ZarrFormat) -> AnyAsyncArray | AsyncGroup: """ Get an AsyncArray or AsyncGroup from a path in a Store. Parameters ---------- store : Store The store-like object to read from. path : str The path to the node to read. zarr_format : {2, 3} The zarr format of the node to read. Returns ------- AsyncArray | AsyncGroup """ match zarr_format: case 2: return await _get_node_v2(store=store, path=path) case 3: return await _get_node_v3(store=store, path=path) case _: # pragma: no cover raise ValueError(f"Unexpected zarr format: {zarr_format}") # pragma: no cover async def _set_return_key( *, store: Store, key: str, value: Buffer, semaphore: asyncio.Semaphore | None = None ) -> str: """ Write a value to storage at the given key. The key is returned. Useful when saving values via routines that return results in execution order, like asyncio.as_completed, because in this case we need to know which key was saved in order to yield the right object to the caller. Parameters ---------- store : Store The store to save the value to. key : str The key to save the value to. value : Buffer The value to save. semaphore : asyncio.Semaphore | None An optional semaphore to use to limit the number of concurrent writes. """ if semaphore is not None: async with semaphore: await store.set(key, value) else: await store.set(key, value) return key def _persist_metadata( store: Store, path: str, metadata: ArrayV2Metadata | ArrayV3Metadata | GroupMetadata, semaphore: asyncio.Semaphore | None = None, ) -> tuple[Coroutine[None, None, str], ...]: """ Prepare to save a metadata document to storage, returning a tuple of coroutines that must be awaited. """ to_save = metadata.to_buffer_dict(default_buffer_prototype()) return tuple( _set_return_key(store=store, key=_join_paths([path, key]), value=value, semaphore=semaphore) for key, value in to_save.items() ) async def create_rooted_hierarchy( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, ) -> AsyncGroup | AnyAsyncArray: """ Create an ``AsyncGroup`` or ``AsyncArray`` from a store and a dict of metadata documents. This function ensures that its input contains a specification of a root node, calls ``create_hierarchy`` to create nodes, and returns the root node of the hierarchy. """ roots = _get_roots(nodes.keys()) if len(roots) != 1: msg = ( "The input does not specify a root node. " "This function can only create hierarchies that contain a root node, which is " "defined as a group that is ancestral to all the other arrays and " "groups in the hierarchy, or a single array." ) raise ValueError(msg) else: root_key = roots[0] nodes_created = [ x async for x in create_hierarchy(store=store, nodes=nodes, overwrite=overwrite) ] return dict(nodes_created)[root_key] zarr-python-3.2.1/src/zarr/core/indexing.py000066400000000000000000001671301517635743000207110ustar00rootroot00000000000000from __future__ import annotations import itertools import numbers import operator from collections.abc import Iterator, Sequence from dataclasses import dataclass from enum import Enum from functools import lru_cache, reduce from types import EllipsisType from typing import ( TYPE_CHECKING, Any, Literal, NamedTuple, Protocol, TypeGuard, cast, runtime_checkable, ) import numpy as np import numpy.typing as npt from zarr.core.common import ceildiv, product from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.errors import ( ArrayIndexError, BoundsCheckError, NegativeStepError, VindexInvalidSelectionError, ) if TYPE_CHECKING: from zarr.core.array import AsyncArray from zarr.core.buffer import NDArrayLikeOrScalar from zarr.core.chunk_grids import ChunkGrid, DimensionGrid from zarr.types import AnyArray IntSequence = list[int] | npt.NDArray[np.intp] ArrayOfIntOrBool = npt.NDArray[np.intp] | npt.NDArray[np.bool_] BasicSelector = int | slice | EllipsisType Selector = BasicSelector | ArrayOfIntOrBool BasicSelection = BasicSelector | tuple[BasicSelector, ...] # also used for BlockIndex CoordinateSelection = IntSequence | tuple[IntSequence, ...] MaskSelection = npt.NDArray[np.bool_] OrthogonalSelection = Selector | tuple[Selector, ...] Selection = BasicSelection | CoordinateSelection | MaskSelection | OrthogonalSelection CoordinateSelectionNormalized = tuple[npt.NDArray[np.intp], ...] SelectionNormalized = tuple[Selector, ...] | ArrayOfIntOrBool SelectionWithFields = Selection | str | Sequence[str] SelectorTuple = tuple[Selector, ...] | npt.NDArray[np.intp] | slice Fields = str | list[str] | tuple[str, ...] def err_too_many_indices(selection: Any, shape: tuple[int, ...]) -> None: raise IndexError(f"too many indices for array; expected {len(shape)}, got {len(selection)}") def _zarr_array_to_int_or_bool_array(arr: AnyArray) -> npt.NDArray[np.intp] | npt.NDArray[np.bool_]: if arr.dtype.kind in ("i", "b"): return np.asarray(arr) else: raise IndexError( f"Invalid array dtype: {arr.dtype}. Arrays used as indices must be of integer or boolean type" ) @runtime_checkable class Indexer(Protocol): shape: tuple[int, ...] drop_axes: tuple[int, ...] def __iter__(self) -> Iterator[ChunkProjection]: ... type _ArrayIndexingOrder = Literal["lexicographic"] def _iter_grid( grid_shape: Sequence[int], *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, order: _ArrayIndexingOrder = "lexicographic", ) -> Iterator[tuple[int, ...]]: """ Iterate over the elements of grid of integers, with the option to restrict the domain of iteration to a contiguous subregion of that grid. Parameters ---------- grid_shape : Sequence[int] The size of the domain to iterate over. origin : Sequence[int] | None, default=None The first coordinate of the domain to return. selection_shape : Sequence[int] | None, default=None The shape of the selection. order : Literal["lexicographic"], default="lexicographic" The linear indexing order to use. Returns ------- Iterator[tuple[int, ...]] An iterator over tuples of integers Examples -------- ```python from zarr.core.indexing import _iter_grid tuple(_iter_grid((1,))) # ((0,),) tuple(_iter_grid((2,3))) # ((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)) tuple(_iter_grid((2,3), origin=(1,1))) # ((1, 1), (1, 2)) tuple(_iter_grid((2,3), origin=(0,0), selection_shape=(2,2))) # ((0, 0), (0, 1), (1, 0), (1, 1)) ``` """ if origin is None: origin_parsed = (0,) * len(grid_shape) else: if len(origin) != len(grid_shape): msg = ( "Shape and origin parameters must have the same length." f"Got {len(grid_shape)} elements in shape, but {len(origin)} elements in origin." ) raise ValueError(msg) origin_parsed = tuple(origin) if selection_shape is None: selection_shape_parsed = tuple( g - o for o, g in zip(origin_parsed, grid_shape, strict=True) ) else: selection_shape_parsed = tuple(selection_shape) if order == "lexicographic": dimensions: tuple[range, ...] = () for idx, (o, gs, ss) in enumerate( zip(origin_parsed, grid_shape, selection_shape_parsed, strict=True) ): if o + ss > gs: raise IndexError( f"Invalid selection shape ({ss}) for origin ({o}) and grid shape ({gs}) at axis {idx}." ) dimensions += (range(o, o + ss),) return itertools.product(*(dimensions)) else: msg = f"Indexing order {order} is not supported at this time." # type: ignore[unreachable] # pragma: no cover raise NotImplementedError(msg) # pragma: no cover def _iter_regions( domain_shape: Sequence[int], region_shape: Sequence[int], *, origin: Sequence[int] | None = None, selection_shape: Sequence[int] | None = None, order: _ArrayIndexingOrder = "lexicographic", trim_excess: bool = True, ) -> Iterator[tuple[slice, ...]]: """ Iterate over contiguous regions on a grid of integers, with the option to restrict the domain of iteration to a contiguous subregion of that grid. Parameters ---------- domain_shape : Sequence[int] The size of the domain to iterate over. region_shape : Sequence[int] The shape of the region to iterate over. origin : Sequence[int] | None, default=None The location, in grid coordinates, of the first region to return. selection_shape : Sequence[int] | None, default=None The shape of the selection, in grid coordinates. order : Literal["lexicographic"], default="lexicographic" The linear indexing order to use. Yields ------ Iterator[tuple[slice, ...]] An iterator over tuples of slices, where each slice spans a separate contiguous region Examples -------- ```python from zarr.core.indexing import _iter_regions tuple(_iter_regions((1,), (1,))) # ((slice(0, 1, 1),),) tuple(_iter_regions((2, 3), (1, 2))) # ((slice(0, 1, 1), slice(0, 2, 1)), (slice(1, 2, 1), slice(0, 2, 1))) tuple(_iter_regions((2,3), (1,2), origin=(1,1))) # ((slice(1, 2, 1), slice(1, 3, 1)), (slice(2, 3, 1), slice(1, 3, 1))) tuple(_iter_regions((2,3), (1,2), origin=(0,0), selection_shape=(2,2))) # ((slice(0, 1, 1), slice(0, 2, 1)), (slice(1, 2, 1), slice(0, 2, 1))) ``` """ grid_shape = tuple(itertools.starmap(ceildiv, zip(domain_shape, region_shape, strict=True))) for grid_position in _iter_grid( grid_shape=grid_shape, origin=origin, selection_shape=selection_shape, order=order ): out: list[slice] = [] for g_pos, r_shape, d_shape in zip(grid_position, region_shape, domain_shape, strict=True): start = g_pos * r_shape stop = start + r_shape if trim_excess: stop = min(stop, d_shape) out.append(slice(start, stop, 1)) yield tuple(out) def is_integer(x: Any) -> TypeGuard[int]: """True if x is an integer (both pure Python or NumPy).""" return isinstance(x, numbers.Integral) and not is_bool(x) def is_bool(x: Any) -> TypeGuard[bool | np.bool_]: """True if x is a boolean (both pure Python or NumPy).""" return type(x) in [bool, np.bool_] def is_integer_list(x: Any) -> TypeGuard[list[int]]: """True if x is a list of integers.""" return isinstance(x, list) and len(x) > 0 and all(is_integer(i) for i in x) def is_bool_list(x: Any) -> TypeGuard[list[bool | np.bool_]]: """True if x is a list of boolean.""" return isinstance(x, list) and len(x) > 0 and all(is_bool(i) for i in x) def is_integer_array(x: Any, ndim: int | None = None) -> TypeGuard[npt.NDArray[np.intp]]: t = not np.isscalar(x) and hasattr(x, "shape") and hasattr(x, "dtype") and x.dtype.kind in "ui" if ndim is not None: t = t and hasattr(x, "shape") and len(x.shape) == ndim return t def is_bool_array(x: Any, ndim: int | None = None) -> TypeGuard[npt.NDArray[np.bool_]]: t = hasattr(x, "shape") and hasattr(x, "dtype") and x.dtype == bool if ndim is not None: t = t and hasattr(x, "shape") and len(x.shape) == ndim return t def is_int_or_bool_iterable(x: Any) -> bool: return is_integer_list(x) or is_integer_array(x) or is_bool_array(x) or is_bool_list(x) def is_scalar(value: Any, dtype: np.dtype[Any]) -> bool: if np.isscalar(value): return True if hasattr(value, "shape") and value.shape == (): return True return isinstance(value, tuple) and dtype.names is not None and len(value) == len(dtype.names) def is_pure_fancy_indexing(selection: Any, ndim: int) -> bool: """Check whether a selection contains only scalars or integer/bool array-likes. Parameters ---------- selection : tuple, slice, or scalar A valid selection value for indexing into arrays. Returns ------- is_pure : bool True if the selection is a pure fancy indexing expression (ie not mixed with boolean or slices). """ if is_bool_array(selection): # is mask selection return True if ndim == 1 and ( is_integer_list(selection) or is_integer_array(selection) or is_bool_list(selection) ): return True # if not, we go through the normal path below, because a 1-tuple # of integers is also allowed. no_slicing = ( isinstance(selection, tuple) and len(selection) == ndim and not (any(isinstance(elem, slice) or elem is Ellipsis for elem in selection)) ) return ( no_slicing and all( is_integer(elem) or is_integer_list(elem) or is_integer_array(elem) for elem in selection ) and any(is_integer_list(elem) or is_integer_array(elem) for elem in selection) ) def is_pure_orthogonal_indexing(selection: Selection, ndim: int) -> TypeGuard[OrthogonalSelection]: if not ndim: return False selection_normalized = (selection,) if not isinstance(selection, tuple) else selection # Case 1: Selection contains of iterable of integers or boolean if len(selection_normalized) == ndim and all( is_int_or_bool_iterable(s) for s in selection_normalized ): return True # Case 2: selection contains either zero or one integer iterables. # All other selection elements are slices or integers return ( len(selection_normalized) <= ndim and sum(is_int_or_bool_iterable(s) for s in selection_normalized) <= 1 and all( is_int_or_bool_iterable(s) or isinstance(s, int | slice) for s in selection_normalized ) ) def normalize_integer_selection(dim_sel: int, dim_len: int) -> int: # normalize type to int dim_sel = int(dim_sel) # handle wraparound if dim_sel < 0: dim_sel = dim_len + dim_sel # handle out of bounds if dim_sel >= dim_len or dim_sel < 0: msg = f"index out of bounds for dimension with length {dim_len}" raise BoundsCheckError(msg) return dim_sel class ChunkDimProjection(NamedTuple): """A mapping from chunk to output array for a single dimension. Attributes ---------- dim_chunk_ix Index of chunk. dim_chunk_sel Selection of items from chunk array. dim_out_sel Selection of items in target (output) array. """ dim_chunk_ix: int dim_chunk_sel: Selector dim_out_sel: Selector | None is_complete_chunk: bool @dataclass(frozen=True) class IntDimIndexer: dim_sel: int dim_len: int dim_grid: DimensionGrid nitems: int = 1 def __init__(self, dim_sel: int, dim_len: int, dim_grid: DimensionGrid) -> None: object.__setattr__(self, "dim_sel", normalize_integer_selection(dim_sel, dim_len)) object.__setattr__(self, "dim_len", dim_len) object.__setattr__(self, "dim_grid", dim_grid) def __iter__(self) -> Iterator[ChunkDimProjection]: g = self.dim_grid dim_chunk_ix = g.index_to_chunk(self.dim_sel) dim_offset = g.chunk_offset(dim_chunk_ix) dim_chunk_sel = self.dim_sel - dim_offset dim_out_sel = None is_complete_chunk = g.data_size(dim_chunk_ix) == 1 yield ChunkDimProjection(dim_chunk_ix, dim_chunk_sel, dim_out_sel, is_complete_chunk) @dataclass(frozen=True) class SliceDimIndexer: dim_len: int nitems: int nchunks: int dim_grid: DimensionGrid start: int stop: int step: int def __init__( self, dim_sel: slice, dim_len: int, dim_grid: DimensionGrid, ) -> None: # normalize start, stop, step = dim_sel.indices(dim_len) if step < 1: raise NegativeStepError("only slices with step >= 1 are supported.") object.__setattr__(self, "start", start) object.__setattr__(self, "stop", stop) object.__setattr__(self, "step", step) object.__setattr__(self, "dim_len", dim_len) object.__setattr__(self, "dim_grid", dim_grid) object.__setattr__(self, "nitems", max(0, ceildiv((stop - start), step))) object.__setattr__(self, "nchunks", dim_grid.nchunks) def __iter__(self) -> Iterator[ChunkDimProjection]: # figure out the range of chunks we need to visit if self.start >= self.stop: return # empty slice g = self.dim_grid dim_chunk_ix_from = g.index_to_chunk(self.start) if self.start > 0 else 0 dim_chunk_ix_to = g.index_to_chunk(self.stop - 1) + 1 if self.stop > 0 else 0 # iterate over chunks in range for dim_chunk_ix in range(dim_chunk_ix_from, dim_chunk_ix_to): # compute offsets for chunk within overall array dim_offset = g.chunk_offset(dim_chunk_ix) # determine chunk length, accounting for trailing chunk dim_chunk_len = g.data_size(dim_chunk_ix) dim_limit = dim_offset + dim_chunk_len if self.start < dim_offset: # selection starts before current chunk dim_chunk_sel_start = 0 remainder = (dim_offset - self.start) % self.step if remainder: dim_chunk_sel_start += self.step - remainder # compute number of previous items, provides offset into output array dim_out_offset = ceildiv((dim_offset - self.start), self.step) else: # selection starts within current chunk dim_chunk_sel_start = self.start - dim_offset dim_out_offset = 0 if self.stop > dim_limit: # selection ends after current chunk dim_chunk_sel_stop = dim_chunk_len else: # selection ends within current chunk dim_chunk_sel_stop = self.stop - dim_offset dim_chunk_sel = slice(dim_chunk_sel_start, dim_chunk_sel_stop, self.step) dim_chunk_nitems = ceildiv((dim_chunk_sel_stop - dim_chunk_sel_start), self.step) # If there are no elements on the selection within this chunk, then skip if dim_chunk_nitems == 0: continue dim_out_sel = slice(dim_out_offset, dim_out_offset + dim_chunk_nitems) is_complete_chunk = ( dim_chunk_sel_start == 0 and (self.stop >= dim_limit) and self.step in [1, None] ) yield ChunkDimProjection(dim_chunk_ix, dim_chunk_sel, dim_out_sel, is_complete_chunk) def check_selection_length(selection: SelectionNormalized, shape: tuple[int, ...]) -> None: if len(selection) > len(shape): err_too_many_indices(selection, shape) def replace_ellipsis(selection: Any, shape: tuple[int, ...]) -> SelectionNormalized: selection = ensure_tuple(selection) # count number of ellipsis present n_ellipsis = sum(1 for i in selection if i is Ellipsis) if n_ellipsis > 1: # more than 1 is an error raise IndexError("an index can only have a single ellipsis ('...')") elif n_ellipsis == 1: # locate the ellipsis, count how many items to left and right n_items_l = selection.index(Ellipsis) # items to left of ellipsis n_items_r = len(selection) - (n_items_l + 1) # items to right of ellipsis n_items = len(selection) - 1 # all non-ellipsis items if n_items >= len(shape): # ellipsis does nothing, just remove it selection = tuple(i for i in selection if i != Ellipsis) else: # replace ellipsis with as many slices are needed for number of dims new_item = selection[:n_items_l] + ((slice(None),) * (len(shape) - n_items)) if n_items_r: new_item += selection[-n_items_r:] selection = new_item # fill out selection if not completely specified if len(selection) < len(shape): selection += (slice(None),) * (len(shape) - len(selection)) # check selection not too long check_selection_length(selection, shape) return cast("SelectionNormalized", selection) def replace_lists(selection: SelectionNormalized) -> SelectionNormalized: return tuple( np.asarray(dim_sel) if isinstance(dim_sel, list) else dim_sel for dim_sel in selection ) def ensure_tuple(v: Any) -> SelectionNormalized: if not isinstance(v, tuple): v = (v,) return cast("SelectionNormalized", v) class ChunkProjection(NamedTuple): """A mapping of items from chunk to output array. Can be used to extract items from the chunk array for loading into an output array. Can also be used to extract items from a value array for setting/updating in a chunk array. Attributes ---------- chunk_coords Indices of chunk. chunk_selection Selection of items from chunk array. out_selection Selection of items in target (output) array. is_complete_chunk: True if a complete chunk is indexed """ chunk_coords: tuple[int, ...] chunk_selection: tuple[Selector, ...] | npt.NDArray[np.intp] out_selection: tuple[Selector, ...] | npt.NDArray[np.intp] | slice is_complete_chunk: bool def is_slice(s: Any) -> TypeGuard[slice]: return isinstance(s, slice) def is_contiguous_slice(s: Any) -> TypeGuard[slice]: return is_slice(s) and (s.step is None or s.step == 1) def is_positive_slice(s: Any) -> TypeGuard[slice]: return is_slice(s) and (s.step is None or s.step >= 1) def is_contiguous_selection(selection: Any) -> TypeGuard[slice]: selection = ensure_tuple(selection) return all((is_integer_array(s) or is_contiguous_slice(s) or s == Ellipsis) for s in selection) def is_basic_selection(selection: Any) -> TypeGuard[BasicSelection]: selection = ensure_tuple(selection) return all(is_integer(s) or is_positive_slice(s) for s in selection) @dataclass(frozen=True) class BasicIndexer(Indexer): dim_indexers: list[IntDimIndexer | SliceDimIndexer] shape: tuple[int, ...] drop_axes: tuple[int, ...] def __init__( self, selection: BasicSelection, shape: tuple[int, ...], chunk_grid: ChunkGrid, ) -> None: dim_grids = chunk_grid._dimensions # handle ellipsis selection_normalized = replace_ellipsis(selection, shape) # setup per-dimension indexers dim_indexers: list[IntDimIndexer | SliceDimIndexer] = [] for dim_sel, dim_len, dim_grid in zip(selection_normalized, shape, dim_grids, strict=True): dim_indexer: IntDimIndexer | SliceDimIndexer if is_integer(dim_sel): dim_indexer = IntDimIndexer(dim_sel, dim_len, dim_grid) elif is_slice(dim_sel): dim_indexer = SliceDimIndexer(dim_sel, dim_len, dim_grid) else: raise IndexError( "unsupported selection item for basic indexing; " f"expected integer or slice, got {type(dim_sel)!r}" ) dim_indexers.append(dim_indexer) object.__setattr__(self, "dim_indexers", dim_indexers) object.__setattr__( self, "shape", tuple(s.nitems for s in self.dim_indexers if not isinstance(s, IntDimIndexer)), ) object.__setattr__(self, "drop_axes", ()) def __iter__(self) -> Iterator[ChunkProjection]: for dim_projections in itertools.product(*self.dim_indexers): chunk_coords = tuple(p.dim_chunk_ix for p in dim_projections) chunk_selection = tuple(p.dim_chunk_sel for p in dim_projections) out_selection = tuple( p.dim_out_sel for p in dim_projections if p.dim_out_sel is not None ) is_complete_chunk = all(p.is_complete_chunk for p in dim_projections) yield ChunkProjection(chunk_coords, chunk_selection, out_selection, is_complete_chunk) @dataclass(frozen=True) class BoolArrayDimIndexer: dim_sel: npt.NDArray[np.bool_] dim_len: int dim_grid: DimensionGrid nchunks: int chunk_nitems: npt.NDArray[Any] chunk_nitems_cumsum: npt.NDArray[Any] nitems: int dim_chunk_ixs: npt.NDArray[np.intp] def __init__( self, dim_sel: npt.NDArray[np.bool_], dim_len: int, dim_grid: DimensionGrid, ) -> None: # check number of dimensions if not is_bool_array(dim_sel, 1): raise IndexError("Boolean arrays in an orthogonal selection must be 1-dimensional only") # check shape if dim_sel.shape[0] != dim_len: raise IndexError( f"Boolean array has the wrong length for dimension; expected {dim_len}, got {dim_sel.shape[0]}" ) g = dim_grid nchunks = g.nchunks # precompute number of selected items for each chunk chunk_nitems = np.zeros(nchunks, dtype="i8") for dim_chunk_ix in range(nchunks): dim_offset = g.chunk_offset(dim_chunk_ix) chunk_len = g.data_size(dim_chunk_ix) chunk_nitems[dim_chunk_ix] = np.count_nonzero( dim_sel[dim_offset : dim_offset + chunk_len] ) chunk_nitems_cumsum = np.cumsum(chunk_nitems) nitems = chunk_nitems_cumsum[-1] dim_chunk_ixs = np.nonzero(chunk_nitems)[0] # store attributes object.__setattr__(self, "dim_sel", dim_sel) object.__setattr__(self, "dim_len", dim_len) object.__setattr__(self, "dim_grid", dim_grid) object.__setattr__(self, "nchunks", nchunks) object.__setattr__(self, "chunk_nitems", chunk_nitems) object.__setattr__(self, "chunk_nitems_cumsum", chunk_nitems_cumsum) object.__setattr__(self, "nitems", nitems) object.__setattr__(self, "dim_chunk_ixs", dim_chunk_ixs) def __iter__(self) -> Iterator[ChunkDimProjection]: g = self.dim_grid # iterate over chunks with at least one item for dim_chunk_ix in self.dim_chunk_ixs: # find region in chunk dim_offset = g.chunk_offset(dim_chunk_ix) chunk_len = g.data_size(dim_chunk_ix) dim_chunk_sel = self.dim_sel[dim_offset : dim_offset + chunk_len] # pad out if boundary chunk (codec buffer may be larger than valid data region) codec_size = g.chunk_size(dim_chunk_ix) if dim_chunk_sel.shape[0] < codec_size: tmp = np.zeros(codec_size, dtype=bool) tmp[: dim_chunk_sel.shape[0]] = dim_chunk_sel dim_chunk_sel = tmp # find region in output if dim_chunk_ix == 0: start = 0 else: start = self.chunk_nitems_cumsum[dim_chunk_ix - 1] stop = self.chunk_nitems_cumsum[dim_chunk_ix] dim_out_sel = slice(start, stop) is_complete_chunk = False # TODO yield ChunkDimProjection(dim_chunk_ix, dim_chunk_sel, dim_out_sel, is_complete_chunk) class Order(Enum): """ Enum for indexing order. """ UNKNOWN = 0 INCREASING = 1 DECREASING = 2 UNORDERED = 3 @staticmethod def check(a: npt.NDArray[Any]) -> Order: diff = np.diff(a) diff_positive = diff >= 0 n_diff_positive = np.count_nonzero(diff_positive) all_increasing = n_diff_positive == len(diff_positive) any_increasing = n_diff_positive > 0 if all_increasing: order = Order.INCREASING elif any_increasing: order = Order.UNORDERED else: order = Order.DECREASING return order def wraparound_indices(x: npt.NDArray[Any], dim_len: int) -> None: loc_neg = x < 0 if np.any(loc_neg): x[loc_neg] += dim_len def boundscheck_indices(x: npt.NDArray[Any], dim_len: int) -> None: if np.any(x < 0) or np.any(x >= dim_len): msg = f"index out of bounds for dimension with length {dim_len}" raise BoundsCheckError(msg) @dataclass(frozen=True) class IntArrayDimIndexer: """Integer array selection against a single dimension.""" dim_len: int dim_grid: DimensionGrid nchunks: int nitems: int order: Order dim_sel: npt.NDArray[np.intp] dim_out_sel: npt.NDArray[np.intp] chunk_nitems: int dim_chunk_ixs: npt.NDArray[np.intp] chunk_nitems_cumsum: npt.NDArray[np.intp] def __init__( self, dim_sel: npt.NDArray[np.intp], dim_len: int, dim_grid: DimensionGrid, wraparound: bool = True, boundscheck: bool = True, order: Order = Order.UNKNOWN, ) -> None: # ensure 1d array dim_sel = np.asanyarray(dim_sel) if not is_integer_array(dim_sel, 1): raise IndexError("integer arrays in an orthogonal selection must be 1-dimensional only") nitems = len(dim_sel) g = dim_grid nchunks = g.nchunks # handle wraparound if wraparound: wraparound_indices(dim_sel, dim_len) # handle out of bounds if boundscheck: boundscheck_indices(dim_sel, dim_len) # determine which chunk is needed for each selection item # note: for dense integer selections, the division operation here is the # bottleneck dim_sel_chunk = g.indices_to_chunks(dim_sel) # determine order of indices if order == Order.UNKNOWN: order = Order.check(dim_sel) order = Order(order) if order == Order.INCREASING: dim_out_sel = None elif order == Order.DECREASING: dim_sel = dim_sel[::-1] # TODO should be possible to do this without creating an arange dim_out_sel = np.arange(nitems - 1, -1, -1) else: # sort indices to group by chunk dim_out_sel = np.argsort(dim_sel_chunk) dim_sel = np.take(dim_sel, dim_out_sel) # precompute number of selected items for each chunk chunk_nitems = np.bincount(dim_sel_chunk, minlength=nchunks) # find chunks that we need to visit dim_chunk_ixs = np.nonzero(chunk_nitems)[0] # compute offsets into the output array chunk_nitems_cumsum = np.cumsum(chunk_nitems) # store attributes object.__setattr__(self, "dim_len", dim_len) object.__setattr__(self, "dim_grid", dim_grid) object.__setattr__(self, "nchunks", nchunks) object.__setattr__(self, "nitems", nitems) object.__setattr__(self, "order", order) object.__setattr__(self, "dim_sel", dim_sel) object.__setattr__(self, "dim_out_sel", dim_out_sel) object.__setattr__(self, "chunk_nitems", chunk_nitems) object.__setattr__(self, "dim_chunk_ixs", dim_chunk_ixs) object.__setattr__(self, "chunk_nitems_cumsum", chunk_nitems_cumsum) def __iter__(self) -> Iterator[ChunkDimProjection]: g = self.dim_grid for dim_chunk_ix in self.dim_chunk_ixs: dim_out_sel: slice | npt.NDArray[np.intp] # find region in output if dim_chunk_ix == 0: start = 0 else: start = self.chunk_nitems_cumsum[dim_chunk_ix - 1] stop = self.chunk_nitems_cumsum[dim_chunk_ix] if self.order == Order.INCREASING: dim_out_sel = slice(start, stop) else: dim_out_sel = self.dim_out_sel[start:stop] # find region in chunk dim_offset = g.chunk_offset(dim_chunk_ix) dim_chunk_sel = self.dim_sel[start:stop] - dim_offset is_complete_chunk = False # TODO yield ChunkDimProjection(dim_chunk_ix, dim_chunk_sel, dim_out_sel, is_complete_chunk) def slice_to_range(s: slice, length: int) -> range: return range(*s.indices(length)) def ix_(selection: Any, shape: tuple[int, ...]) -> npt.NDArray[np.intp]: """Convert an orthogonal selection to a numpy advanced (fancy) selection, like ``numpy.ix_`` but with support for slices and single ints.""" # normalisation selection = replace_ellipsis(selection, shape) # replace slice and int as these are not supported by numpy.ix_ selection = [ slice_to_range(dim_sel, dim_len) if isinstance(dim_sel, slice) else [dim_sel] if is_integer(dim_sel) else dim_sel for dim_sel, dim_len in zip(selection, shape, strict=True) ] # now get numpy to convert to a coordinate selection selection = np.ix_(*selection) return cast("npt.NDArray[np.intp]", selection) def oindex(a: npt.NDArray[Any], selection: Selection) -> npt.NDArray[Any]: """Implementation of orthogonal indexing with slices and ints.""" selection = replace_ellipsis(selection, a.shape) drop_axes = tuple(i for i, s in enumerate(selection) if is_integer(s)) selection = ix_(selection, a.shape) result = a[selection] if drop_axes: result = result.squeeze(axis=drop_axes) return result def oindex_set(a: npt.NDArray[Any], selection: Selection, value: Any) -> None: selection = replace_ellipsis(selection, a.shape) drop_axes = tuple(i for i, s in enumerate(selection) if is_integer(s)) selection = ix_(selection, a.shape) if not np.isscalar(value) and drop_axes: value = np.asanyarray(value) value_selection: list[Selector | None] = [slice(None)] * len(a.shape) for i in drop_axes: value_selection[i] = np.newaxis value = value[tuple(value_selection)] a[selection] = value @dataclass(frozen=True) class OrthogonalIndexer(Indexer): dim_indexers: list[IntDimIndexer | SliceDimIndexer | IntArrayDimIndexer | BoolArrayDimIndexer] dim_grids: tuple[DimensionGrid, ...] shape: tuple[int, ...] is_advanced: bool drop_axes: tuple[int, ...] def __init__(self, selection: Selection, shape: tuple[int, ...], chunk_grid: ChunkGrid) -> None: dim_grids = chunk_grid._dimensions # handle ellipsis selection = replace_ellipsis(selection, shape) # normalize list to array selection = replace_lists(selection) # setup per-dimension indexers dim_indexers: list[ IntDimIndexer | SliceDimIndexer | IntArrayDimIndexer | BoolArrayDimIndexer ] = [] for dim_sel, dim_len, dim_grid in zip(selection, shape, dim_grids, strict=True): dim_indexer: IntDimIndexer | SliceDimIndexer | IntArrayDimIndexer | BoolArrayDimIndexer if is_integer(dim_sel): dim_indexer = IntDimIndexer(dim_sel, dim_len, dim_grid) elif isinstance(dim_sel, slice): dim_indexer = SliceDimIndexer(dim_sel, dim_len, dim_grid) elif is_integer_array(dim_sel): dim_indexer = IntArrayDimIndexer(dim_sel, dim_len, dim_grid) elif is_bool_array(dim_sel): dim_indexer = BoolArrayDimIndexer(dim_sel, dim_len, dim_grid) else: raise IndexError( "unsupported selection item for orthogonal indexing; " "expected integer, slice, integer array or Boolean " f"array, got {type(dim_sel)!r}" ) dim_indexers.append(dim_indexer) shape = tuple(s.nitems for s in dim_indexers if not isinstance(s, IntDimIndexer)) is_advanced = not is_basic_selection(selection) if is_advanced: drop_axes = tuple( i for i, dim_indexer in enumerate(dim_indexers) if isinstance(dim_indexer, IntDimIndexer) ) else: drop_axes = () object.__setattr__(self, "dim_indexers", dim_indexers) object.__setattr__(self, "dim_grids", dim_grids) object.__setattr__(self, "shape", shape) object.__setattr__(self, "is_advanced", is_advanced) object.__setattr__(self, "drop_axes", drop_axes) def __iter__(self) -> Iterator[ChunkProjection]: for dim_projections in itertools.product(*self.dim_indexers): chunk_coords = tuple(p.dim_chunk_ix for p in dim_projections) chunk_selection: tuple[Selector, ...] | npt.NDArray[Any] = tuple( p.dim_chunk_sel for p in dim_projections ) out_selection: tuple[Selector, ...] | npt.NDArray[Any] = tuple( p.dim_out_sel for p in dim_projections if p.dim_out_sel is not None ) # handle advanced indexing arrays orthogonally if self.is_advanced: # NumPy can handle a single array-indexed dimension directly, # which preserves full slices and avoids an # unnecessary advanced-indexing copy. Integer-indexed # dimensions still need the ix_ path for downstream squeezing. # Example: we skip `ix_` for array[:, :, [1, 2, 3]] n_array_dims = sum(isinstance(sel, np.ndarray) for sel in chunk_selection) if n_array_dims > 1 or self.drop_axes: # N.B., numpy doesn't support orthogonal indexing directly # for multiple array-indexed dimensions, so we need to # convert the orthogonal selection into coordinate arrays. chunk_shape = tuple( g.chunk_size(p.dim_chunk_ix) for g, p in zip(self.dim_grids, dim_projections, strict=True) ) chunk_selection = ix_(chunk_selection, chunk_shape) # special case for non-monotonic indices if not is_basic_selection(out_selection): out_selection = ix_(out_selection, self.shape) is_complete_chunk = all(p.is_complete_chunk for p in dim_projections) yield ChunkProjection(chunk_coords, chunk_selection, out_selection, is_complete_chunk) @dataclass(frozen=True) class OIndex: array: AnyArray # TODO: develop Array generic and move zarr.Array[np.intp] | zarr.Array[np.bool_] to ArrayOfIntOrBool def __getitem__(self, selection: OrthogonalSelection | AnyArray) -> NDArrayLikeOrScalar: from zarr.core.array import Array # if input is a Zarr array, we materialize it now. if isinstance(selection, Array): selection = _zarr_array_to_int_or_bool_array(selection) fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) return self.array.get_orthogonal_selection( cast("OrthogonalSelection", new_selection), fields=fields ) def __setitem__(self, selection: OrthogonalSelection, value: npt.ArrayLike) -> None: fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) return self.array.set_orthogonal_selection( cast("OrthogonalSelection", new_selection), value, fields=fields ) @dataclass(frozen=True) class AsyncOIndex[T_ArrayMetadata: (ArrayV2Metadata, ArrayV3Metadata)]: array: AsyncArray[T_ArrayMetadata] async def getitem(self, selection: OrthogonalSelection | AnyArray) -> NDArrayLikeOrScalar: from zarr.core.array import Array # if input is a Zarr array, we materialize it now. if isinstance(selection, Array): selection = _zarr_array_to_int_or_bool_array(selection) fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) return await self.array.get_orthogonal_selection( cast(OrthogonalSelection, new_selection), fields=fields ) @dataclass(frozen=True) class BlockIndexer(Indexer): dim_indexers: list[SliceDimIndexer] shape: tuple[int, ...] drop_axes: tuple[int, ...] def __init__( self, selection: BasicSelection, shape: tuple[int, ...], chunk_grid: ChunkGrid ) -> None: dim_grids = chunk_grid._dimensions # handle ellipsis selection_normalized = replace_ellipsis(selection, shape) # normalize list to array selection_normalized = replace_lists(selection_normalized) # setup per-dimension indexers dim_indexers = [] for dim_sel, dim_len, dim_grid in zip(selection_normalized, shape, dim_grids, strict=True): dim_numchunks = dim_grid.nchunks if is_integer(dim_sel): if dim_sel < 0: dim_sel = dim_numchunks + dim_sel if dim_sel < 0 or dim_sel >= dim_numchunks: raise BoundsCheckError( f"block index out of bounds for dimension with {dim_numchunks} chunk(s)" ) start = dim_grid.chunk_offset(dim_sel) stop = start + dim_grid.chunk_size(dim_sel) slice_ = slice(start, stop) elif is_slice(dim_sel): start = dim_sel.start if dim_sel.start is not None else 0 stop = dim_sel.stop if dim_sel.stop is not None else dim_numchunks if dim_sel.step not in {1, None}: raise IndexError( "unsupported selection item for block indexing; " f"expected integer or slice with step=1, got {type(dim_sel)!r}" ) # Can't reuse wraparound_indices because it expects a numpy array # We have integers here. if start < 0: start = dim_numchunks + start if stop < 0: stop = dim_numchunks + stop start = dim_grid.chunk_offset(start) if start < dim_numchunks else dim_len stop = dim_grid.chunk_offset(stop) if stop < dim_numchunks else dim_len slice_ = slice(start, stop) else: raise IndexError( "unsupported selection item for block indexing; " f"expected integer or slice, got {type(dim_sel)!r}" ) dim_indexer = SliceDimIndexer(slice_, dim_len, dim_grid) dim_indexers.append(dim_indexer) if slice_.start >= dim_len or slice_.start < 0: msg = f"index out of bounds for dimension with length {dim_len}" raise BoundsCheckError(msg) shape = tuple(s.nitems for s in dim_indexers) object.__setattr__(self, "dim_indexers", dim_indexers) object.__setattr__(self, "shape", shape) object.__setattr__(self, "drop_axes", ()) def __iter__(self) -> Iterator[ChunkProjection]: for dim_projections in itertools.product(*self.dim_indexers): chunk_coords = tuple(p.dim_chunk_ix for p in dim_projections) chunk_selection = tuple(p.dim_chunk_sel for p in dim_projections) out_selection = tuple( p.dim_out_sel for p in dim_projections if p.dim_out_sel is not None ) is_complete_chunk = all(p.is_complete_chunk for p in dim_projections) yield ChunkProjection(chunk_coords, chunk_selection, out_selection, is_complete_chunk) @dataclass(frozen=True) class BlockIndex: array: AnyArray def __getitem__(self, selection: BasicSelection) -> NDArrayLikeOrScalar: fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) return self.array.get_block_selection(cast("BasicSelection", new_selection), fields=fields) def __setitem__(self, selection: BasicSelection, value: npt.ArrayLike) -> None: fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) return self.array.set_block_selection( cast("BasicSelection", new_selection), value, fields=fields ) def is_coordinate_selection( selection: SelectionNormalized, shape: tuple[int, ...] ) -> TypeGuard[CoordinateSelectionNormalized]: return ( isinstance(selection, tuple) and len(selection) == len(shape) and all(is_integer(dim_sel) or is_integer_array(dim_sel) for dim_sel in selection) ) def is_mask_selection(selection: Selection, shape: tuple[int, ...]) -> TypeGuard[MaskSelection]: return ( isinstance(selection, tuple) and len(selection) == 1 and is_bool_array(selection[0]) and selection[0].shape == shape ) @dataclass(frozen=True) class CoordinateIndexer(Indexer): sel_shape: tuple[int, ...] selection: CoordinateSelectionNormalized sel_sort: npt.NDArray[np.intp] | None chunk_nitems_cumsum: npt.NDArray[np.intp] chunk_rixs: npt.NDArray[np.intp] chunk_mixs: tuple[npt.NDArray[np.intp], ...] shape: tuple[int, ...] dim_grids: tuple[DimensionGrid, ...] drop_axes: tuple[int, ...] def __init__( self, selection: CoordinateSelection, shape: tuple[int, ...], chunk_grid: ChunkGrid ) -> None: dim_grids = chunk_grid._dimensions cdata_shape: tuple[int, ...] if shape == (): cdata_shape = (1,) else: cdata_shape = tuple(g.nchunks for g in dim_grids) nchunks = reduce(operator.mul, cdata_shape, 1) # some initial normalization selection_normalized = cast("CoordinateSelectionNormalized", ensure_tuple(selection)) selection_normalized = tuple( np.asarray([i]) if is_integer(i) else i for i in selection_normalized ) selection_normalized = cast( "CoordinateSelectionNormalized", replace_lists(selection_normalized) ) # validation if not is_coordinate_selection(selection_normalized, shape): raise IndexError( "invalid coordinate selection; expected one integer " "(coordinate) array per dimension of the target array, " f"got {selection!r}" ) # handle wraparound, boundscheck for dim_sel, dim_len in zip(selection_normalized, shape, strict=True): # handle wraparound wraparound_indices(dim_sel, dim_len) # handle out of bounds boundscheck_indices(dim_sel, dim_len) # compute chunk index for each point in the selection chunks_multi_index = tuple( g.indices_to_chunks(dim_sel) for (dim_sel, g) in zip(selection_normalized, dim_grids, strict=True) ) # broadcast selection - this will raise error if array dimensions don't match selection_broadcast = tuple(np.broadcast_arrays(*selection_normalized)) chunks_multi_index_broadcast = np.broadcast_arrays(*chunks_multi_index) # remember shape of selection, because we will flatten indices for processing sel_shape = selection_broadcast[0].shape or (1,) # flatten selection selection_broadcast = tuple(dim_sel.reshape(-1) for dim_sel in selection_broadcast) chunks_multi_index_broadcast = tuple( dim_chunks.reshape(-1) for dim_chunks in chunks_multi_index_broadcast ) # ravel chunk indices chunks_raveled_indices = np.ravel_multi_index( chunks_multi_index_broadcast, dims=cdata_shape ) # group points by chunk if np.any(np.diff(chunks_raveled_indices) < 0): # optimisation, only sort if needed sel_sort = np.argsort(chunks_raveled_indices) selection_broadcast = tuple(dim_sel[sel_sort] for dim_sel in selection_broadcast) else: sel_sort = None shape = selection_broadcast[0].shape or (1,) # precompute number of selected items for each chunk chunk_nitems = np.bincount(chunks_raveled_indices, minlength=nchunks) chunk_nitems_cumsum = np.cumsum(chunk_nitems) # locate the chunks we need to process chunk_rixs = np.nonzero(chunk_nitems)[0] # unravel chunk indices chunk_mixs = np.unravel_index(chunk_rixs, cdata_shape) object.__setattr__(self, "sel_shape", sel_shape) object.__setattr__(self, "selection", selection_broadcast) object.__setattr__(self, "sel_sort", sel_sort) object.__setattr__(self, "chunk_nitems_cumsum", chunk_nitems_cumsum) object.__setattr__(self, "chunk_rixs", chunk_rixs) object.__setattr__(self, "chunk_mixs", chunk_mixs) object.__setattr__(self, "dim_grids", dim_grids) object.__setattr__(self, "shape", shape) object.__setattr__(self, "drop_axes", ()) def __iter__(self) -> Iterator[ChunkProjection]: # iterate over chunks for i, chunk_rix in enumerate(self.chunk_rixs): chunk_coords = tuple(m[i] for m in self.chunk_mixs) if chunk_rix == 0: start = 0 else: start = self.chunk_nitems_cumsum[chunk_rix - 1] stop = self.chunk_nitems_cumsum[chunk_rix] out_selection: slice | npt.NDArray[np.intp] if self.sel_sort is None: out_selection = slice(start, stop) else: out_selection = self.sel_sort[start:stop] chunk_offsets = tuple( g.chunk_offset(dim_chunk_ix) for dim_chunk_ix, g in zip(chunk_coords, self.dim_grids, strict=True) ) chunk_selection = tuple( dim_sel[start:stop] - dim_chunk_offset for (dim_sel, dim_chunk_offset) in zip(self.selection, chunk_offsets, strict=True) ) is_complete_chunk = False # TODO yield ChunkProjection(chunk_coords, chunk_selection, out_selection, is_complete_chunk) @dataclass(frozen=True) class MaskIndexer(CoordinateIndexer): def __init__( self, selection: MaskSelection, shape: tuple[int, ...], chunk_grid: ChunkGrid ) -> None: # some initial normalization selection_normalized = cast("tuple[MaskSelection]", ensure_tuple(selection)) selection_normalized = cast("tuple[MaskSelection]", replace_lists(selection_normalized)) # validation if not is_mask_selection(selection_normalized, shape): raise IndexError( "invalid mask selection; expected one Boolean (mask)" f"array with the same shape as the target array, got {selection_normalized!r}" ) # convert to indices selection_indices = np.nonzero(selection_normalized[0]) # delegate the rest to superclass super().__init__(selection_indices, shape, chunk_grid) @dataclass(frozen=True) class VIndex: array: AnyArray # TODO: develop Array generic and move zarr.Array[np.intp] | zarr.Array[np.bool_] to ArrayOfIntOrBool def __getitem__( self, selection: CoordinateSelection | MaskSelection | AnyArray ) -> NDArrayLikeOrScalar: from zarr.core.array import Array # if input is a Zarr array, we materialize it now. if isinstance(selection, Array): selection = _zarr_array_to_int_or_bool_array(selection) fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) if is_coordinate_selection(new_selection, self.array.shape): return self.array.get_coordinate_selection(new_selection, fields=fields) elif is_mask_selection(new_selection, self.array.shape): return self.array.get_mask_selection(new_selection, fields=fields) else: msg = ( "unsupported selection type for vectorized indexing; only " "coordinate selection (tuple of integer arrays) and mask selection " f"(single Boolean array) are supported; got {new_selection!r}" ) raise VindexInvalidSelectionError(msg) def __setitem__( self, selection: CoordinateSelection | MaskSelection, value: npt.ArrayLike ) -> None: fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) if is_coordinate_selection(new_selection, self.array.shape): self.array.set_coordinate_selection(new_selection, value, fields=fields) elif is_mask_selection(new_selection, self.array.shape): self.array.set_mask_selection(new_selection, value, fields=fields) else: msg = ( "unsupported selection type for vectorized indexing; only " "coordinate selection (tuple of integer arrays) and mask selection " f"(single Boolean array) are supported; got {new_selection!r}" ) raise VindexInvalidSelectionError(msg) @dataclass(frozen=True) class AsyncVIndex[T_ArrayMetadata: (ArrayV2Metadata, ArrayV3Metadata)]: array: AsyncArray[T_ArrayMetadata] # TODO: develop Array generic and move zarr.Array[np.intp] | zarr.Array[np.bool_] to ArrayOfIntOrBool async def getitem( self, selection: CoordinateSelection | MaskSelection | AnyArray ) -> NDArrayLikeOrScalar: # TODO deduplicate these internals with the sync version of getitem # TODO requires solving this circular sync issue: https://github.com/zarr-developers/zarr-python/pull/3083#discussion_r2230737448 from zarr.core.array import Array # if input is a Zarr array, we materialize it now. if isinstance(selection, Array): selection = _zarr_array_to_int_or_bool_array(selection) fields, new_selection = pop_fields(selection) new_selection = ensure_tuple(new_selection) new_selection = replace_lists(new_selection) if is_coordinate_selection(new_selection, self.array.shape): return await self.array.get_coordinate_selection(new_selection, fields=fields) elif is_mask_selection(new_selection, self.array.shape): return await self.array.get_mask_selection(new_selection, fields=fields) else: msg = ( "unsupported selection type for vectorized indexing; only " "coordinate selection (tuple of integer arrays) and mask selection " f"(single Boolean array) are supported; got {new_selection!r}" ) raise VindexInvalidSelectionError(msg) def check_fields(fields: Fields | None, dtype: np.dtype[Any]) -> np.dtype[Any]: # early out if fields is None: return dtype # check type if not isinstance(fields, str | list | tuple): raise IndexError( f"'fields' argument must be a string or list of strings; found {type(fields)!r}" ) if fields: if dtype.names is None: raise IndexError("invalid 'fields' argument, array does not have any fields") try: if isinstance(fields, str): # single field selection out_dtype = dtype[fields] else: # multiple field selection out_dtype = np.dtype([(f, dtype[f]) for f in fields]) except KeyError as e: raise IndexError(f"invalid 'fields' argument, field not found: {e!r}") from e else: return out_dtype else: return dtype def check_no_multi_fields(fields: Fields | None) -> Fields | None: if isinstance(fields, list): if len(fields) == 1: return fields[0] elif len(fields) > 1: raise IndexError("multiple fields are not supported for this operation") return fields def pop_fields(selection: SelectionWithFields) -> tuple[Fields | None, Selection]: if isinstance(selection, str): # single field selection return selection, () elif not isinstance(selection, tuple): # single selection item, no fields # leave selection as-is return None, cast("Selection", selection) else: # multiple items, split fields from selection items fields: Fields = [f for f in selection if isinstance(f, str)] fields = fields[0] if len(fields) == 1 else fields selection_tuple = tuple(s for s in selection if not isinstance(s, str)) selection = cast( "Selection", selection_tuple[0] if len(selection_tuple) == 1 else selection_tuple ) return fields, selection def make_slice_selection(selection: Any) -> list[slice]: ls: list[slice] = [] for dim_selection in selection: if is_integer(dim_selection): ls.append(slice(int(dim_selection), int(dim_selection) + 1, 1)) elif isinstance(dim_selection, np.ndarray): if len(dim_selection) == 1: ls.append(slice(int(dim_selection[0]), int(dim_selection[0]) + 1, 1)) else: raise ArrayIndexError else: ls.append(dim_selection) return ls def decode_morton(z: int, chunk_shape: tuple[int, ...]) -> tuple[int, ...]: # Inspired by compressed morton code as implemented in Neuroglancer # https://github.com/google/neuroglancer/blob/master/src/neuroglancer/datasource/precomputed/volume.md#compressed-morton-code bits = tuple((c - 1).bit_length() for c in chunk_shape) max_coords_bits = max(bits) input_bit = 0 input_value = z out = [0] * len(chunk_shape) for coord_bit in range(max_coords_bits): for dim in range(len(chunk_shape)): if coord_bit < bits[dim]: bit = (input_value >> input_bit) & 1 out[dim] |= bit << coord_bit input_bit += 1 return tuple(out) def decode_morton_vectorized( z: npt.NDArray[np.intp], chunk_shape: tuple[int, ...] ) -> npt.NDArray[np.intp]: """Vectorized Morton code decoding for multiple z values. Parameters ---------- z : ndarray 1D array of Morton codes to decode. chunk_shape : tuple of int Shape defining the coordinate space. Returns ------- ndarray 2D array of shape (len(z), len(chunk_shape)) containing decoded coordinates. """ n_dims = len(chunk_shape) bits = tuple((c - 1).bit_length() for c in chunk_shape) max_coords_bits = max(bits) if bits else 0 out = np.zeros((len(z), n_dims), dtype=np.intp) input_bit = 0 for coord_bit in range(max_coords_bits): for dim in range(n_dims): if coord_bit < bits[dim]: # Extract bit at position input_bit from all z values bit_values = (z >> input_bit) & 1 # Place bit at coord_bit position in dimension dim out[:, dim] |= bit_values << coord_bit input_bit += 1 return out @lru_cache(maxsize=16) def _morton_order(chunk_shape: tuple[int, ...]) -> npt.NDArray[np.intp]: n_total = product(chunk_shape) n_dims = len(chunk_shape) if n_total == 0: out = np.empty((0, n_dims), dtype=np.intp) out.flags.writeable = False return out # Ceiling hypercube: smallest power-of-2 hypercube whose Morton codes span # all valid coordinates in chunk_shape. (c-1).bit_length() gives the number # of bits needed to index c values (0 for singleton dims). n_z = 2**total_bits # is the size of this hypercube. total_bits = sum((c - 1).bit_length() for c in chunk_shape) n_z = 1 << total_bits if total_bits > 0 else 1 # Decode all Morton codes in the ceiling hypercube, then filter to valid coords. # This is fully vectorized. For shapes with n_z >> n_total (e.g. (33,33,33): # n_z=262144, n_total=35937), consider the argsort strategy below. order: npt.NDArray[np.intp] if n_z <= 4 * n_total: # Ceiling strategy: decode all n_z codes vectorized, filter in-bounds. # Works well when the overgeneration ratio n_z/n_total is small (≤4). z_values = np.arange(n_z, dtype=np.intp) all_coords = decode_morton_vectorized(z_values, chunk_shape) shape_arr = np.array(chunk_shape, dtype=np.intp) valid_mask = np.all(all_coords < shape_arr, axis=1) order = all_coords[valid_mask] else: # Argsort strategy: enumerate all n_total valid coordinates directly, # encode each to a Morton code, then sort by code. Avoids the 8x or # larger overgeneration penalty for near-miss shapes like (33,33,33). # Cost: O(n_total * bits) encode + O(n_total log n_total) sort, # vs O(n_z * bits) = O(8 * n_total * bits) for ceiling. grids = np.meshgrid(*[np.arange(c, dtype=np.intp) for c in chunk_shape], indexing="ij") all_coords = np.stack([g.ravel() for g in grids], axis=1) # Encode all coordinates to Morton codes (vectorized). bits_per_dim = tuple((c - 1).bit_length() for c in chunk_shape) max_coord_bits = max(bits_per_dim) z_codes = np.zeros(n_total, dtype=np.intp) output_bit = 0 for coord_bit in range(max_coord_bits): for dim in range(n_dims): if coord_bit < bits_per_dim[dim]: z_codes |= ((all_coords[:, dim] >> coord_bit) & 1) << output_bit output_bit += 1 sort_idx: npt.NDArray[np.intp] = np.argsort(z_codes, kind="stable") order = np.asarray(all_coords[sort_idx], dtype=np.intp) order.flags.writeable = False return order @lru_cache(maxsize=16) def _morton_order_keys(chunk_shape: tuple[int, ...]) -> tuple[tuple[int, ...], ...]: return tuple(tuple(int(x) for x in row) for row in _morton_order(chunk_shape)) def morton_order_iter(chunk_shape: tuple[int, ...]) -> Iterator[tuple[int, ...]]: return iter(_morton_order_keys(tuple(chunk_shape))) def c_order_iter(chunks_per_shard: tuple[int, ...]) -> Iterator[tuple[int, ...]]: return itertools.product(*(range(x) for x in chunks_per_shard)) def get_indexer( selection: SelectionWithFields, shape: tuple[int, ...], chunk_grid: ChunkGrid ) -> Indexer: _, pure_selection = pop_fields(selection) if is_pure_fancy_indexing(pure_selection, len(shape)): new_selection = ensure_tuple(selection) new_selection = replace_lists(new_selection) if is_coordinate_selection(new_selection, shape): return CoordinateIndexer(cast("CoordinateSelection", selection), shape, chunk_grid) elif is_mask_selection(new_selection, shape): return MaskIndexer(cast("MaskSelection", selection), shape, chunk_grid) else: msg = ( "unsupported selection type for vectorized indexing; only " "coordinate selection (tuple of integer arrays) and mask selection " f"(single Boolean array) are supported; got {new_selection!r}" ) raise VindexInvalidSelectionError(msg) elif is_pure_orthogonal_indexing(pure_selection, len(shape)): return OrthogonalIndexer(cast("OrthogonalSelection", selection), shape, chunk_grid) else: return BasicIndexer(cast("BasicSelection", selection), shape, chunk_grid) zarr-python-3.2.1/src/zarr/core/metadata/000077500000000000000000000000001517635743000203025ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/core/metadata/__init__.py000066400000000000000000000006041517635743000224130ustar00rootroot00000000000000from .v2 import ArrayV2Metadata, ArrayV2MetadataDict from .v3 import ArrayMetadataJSON_V3, ArrayV3Metadata ArrayMetadata = ArrayV2Metadata | ArrayV3Metadata type ArrayMetadataDict = ArrayV2MetadataDict | ArrayMetadataJSON_V3 __all__ = [ "ArrayMetadata", "ArrayMetadataDict", "ArrayMetadataJSON_V3", "ArrayV2Metadata", "ArrayV2MetadataDict", "ArrayV3Metadata", ] zarr-python-3.2.1/src/zarr/core/metadata/common.py000066400000000000000000000003771517635743000221530ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from zarr.core.common import JSON def parse_attributes(data: dict[str, JSON] | None) -> dict[str, JSON]: if data is None: return {} return data zarr-python-3.2.1/src/zarr/core/metadata/io.py000066400000000000000000000061721517635743000212710ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import TYPE_CHECKING from zarr.abc.store import set_or_delete from zarr.core.buffer.core import default_buffer_prototype from zarr.errors import ContainsArrayError from zarr.storage._common import StorePath, ensure_no_existing_node if TYPE_CHECKING: from zarr.core.common import ZarrFormat from zarr.core.group import GroupMetadata from zarr.core.metadata import ArrayMetadata def _build_parents(store_path: StorePath, zarr_format: ZarrFormat) -> dict[str, GroupMetadata]: from zarr.core.group import GroupMetadata path = store_path.path if not path: return {} required_parts = path.split("/")[:-1] # the root group parents = {"": GroupMetadata(zarr_format=zarr_format)} for i, part in enumerate(required_parts): parent_path = "/".join(required_parts[:i] + [part]) parents[parent_path] = GroupMetadata(zarr_format=zarr_format) return parents async def save_metadata( store_path: StorePath, metadata: ArrayMetadata | GroupMetadata, ensure_parents: bool = False ) -> None: """Asynchronously save the array or group metadata. Parameters ---------- store_path : StorePath Location to save metadata. metadata : ArrayMetadata | GroupMetadata Metadata to save. ensure_parents : bool, optional Create any missing parent groups, and check no existing parents are arrays. Raises ------ ValueError """ to_save = metadata.to_buffer_dict(default_buffer_prototype()) set_awaitables = [set_or_delete(store_path / key, value) for key, value in to_save.items()] if ensure_parents: # To enable zarr.create(store, path="a/b/c"), we need to create all the intermediate groups. parents = _build_parents(store_path, metadata.zarr_format) ensure_array_awaitables = [] for parent_path, parent_metadata in parents.items(): parent_store_path = StorePath(store_path.store, parent_path) # Error if an array already exists at any parent location. Only groups can have child nodes. ensure_array_awaitables.append( ensure_no_existing_node( parent_store_path, parent_metadata.zarr_format, node_type="array" ) ) set_awaitables.extend( [ (parent_store_path / key).set_if_not_exists(value) for key, value in parent_metadata.to_buffer_dict( default_buffer_prototype() ).items() ] ) # Checks for parent arrays must happen first, before any metadata is modified try: await asyncio.gather(*ensure_array_awaitables) except ContainsArrayError as e: # clear awaitables to avoid RuntimeWarning: coroutine was never awaited for awaitable in set_awaitables: awaitable.close() raise ValueError( f"A parent of {store_path} is an array - only groups may have child nodes." ) from e await asyncio.gather(*set_awaitables) zarr-python-3.2.1/src/zarr/core/metadata/v2.py000066400000000000000000000312671517635743000212140ustar00rootroot00000000000000from __future__ import annotations import warnings from collections.abc import Iterable, Sequence from functools import cached_property from typing import TYPE_CHECKING, Any, TypedDict, cast from zarr.abc.metadata import Metadata from zarr.abc.numcodec import Numcodec, _is_numcodec from zarr.core.dtype import get_data_type_from_json from zarr.core.dtype.common import OBJECT_CODEC_IDS, DTypeSpec_V2 from zarr.errors import ZarrUserWarning from zarr.registry import get_numcodec if TYPE_CHECKING: from typing import Literal, Self import numpy.typing as npt from zarr.core.buffer import Buffer, BufferPrototype from zarr.core.chunk_grids import ChunkGrid from zarr.core.dtype.wrapper import ( TBaseDType, TBaseScalar, ZDType, ) import json from dataclasses import dataclass, field, fields, replace import numpy as np from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.chunk_key_encodings import parse_separator from zarr.core.common import ( JSON, ZARRAY_JSON, ZATTRS_JSON, MemoryOrder, parse_shapelike, ) 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 @dataclass(frozen=True, kw_only=True) class ArrayV2Metadata(Metadata): shape: tuple[int, ...] chunks: tuple[int, ...] dtype: ZDType[TBaseDType, TBaseScalar] fill_value: int | float | str | bytes | None = None order: MemoryOrder = "C" filters: tuple[Numcodec, ...] | None = None dimension_separator: Literal[".", "/"] = "." compressor: Numcodec | None attributes: dict[str, JSON] = field(default_factory=dict) zarr_format: Literal[2] = field(init=False, default=2) def __init__( self, *, shape: tuple[int, ...], dtype: ZDType[TBaseDType, TBaseScalar], chunks: tuple[int, ...], fill_value: Any, order: MemoryOrder, dimension_separator: Literal[".", "/"] = ".", compressor: CompressorLikev2 = None, filters: Iterable[Numcodec | dict[str, JSON]] | None = None, attributes: dict[str, JSON] | None = None, ) -> None: """ Metadata for a Zarr format 2 array. """ shape_parsed = parse_shapelike(shape) chunks_parsed = parse_shapelike(chunks) compressor_parsed = parse_compressor(compressor) order_parsed = parse_indexing_order(order) dimension_separator_parsed = parse_separator(dimension_separator) filters_parsed = parse_filters(filters) fill_value_parsed: TBaseScalar | None if fill_value is not None: fill_value_parsed = dtype.cast_scalar(fill_value) else: fill_value_parsed = fill_value attributes_parsed = parse_attributes(attributes) object.__setattr__(self, "shape", shape_parsed) object.__setattr__(self, "dtype", dtype) object.__setattr__(self, "chunks", chunks_parsed) object.__setattr__(self, "compressor", compressor_parsed) object.__setattr__(self, "order", order_parsed) object.__setattr__(self, "dimension_separator", dimension_separator_parsed) object.__setattr__(self, "filters", filters_parsed) object.__setattr__(self, "fill_value", fill_value_parsed) object.__setattr__(self, "attributes", attributes_parsed) # ensure that the metadata document is consistent _ = parse_metadata(self) @property def ndim(self) -> int: return len(self.shape) @cached_property def chunk_grid(self) -> ChunkGrid: """Backwards-compatible chunk grid property. .. deprecated:: Access the chunk grid via the array layer instead. This property will be removed in a future release. """ from zarr.core.chunk_grids import ChunkGrid warnings.warn( "ArrayV2Metadata.chunk_grid is deprecated. " "Use ChunkGrid.from_metadata(metadata) instead.", DeprecationWarning, stacklevel=2, ) return ChunkGrid.from_sizes(self.shape, tuple(self.chunks)) @property def shards(self) -> tuple[int, ...] | None: return None def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]: zarray_dict = self.to_dict() zattrs_dict = zarray_dict.pop("attributes", {}) json_indent = config.get("json_indent") return { ZARRAY_JSON: prototype.buffer.from_bytes( json.dumps(zarray_dict, indent=json_indent, allow_nan=True).encode() ), ZATTRS_JSON: prototype.buffer.from_bytes( json.dumps(zattrs_dict, indent=json_indent, allow_nan=True).encode() ), } @classmethod def from_dict(cls, data: dict[str, Any]) -> ArrayV2Metadata: # Make a copy to protect the original from modification. _data = data.copy() # Check that the zarr_format attribute is correct. _ = parse_zarr_format(_data.pop("zarr_format")) # To resolve a numpy object dtype array, we need to search for an object codec, # which could be in filters or as a compressor. # we will reference a hard-coded collection of object codec ids for this search. _filters, _compressor = (data.get("filters"), data.get("compressor")) if _filters is not None: _filters = cast("tuple[dict[str, JSON], ...]", _filters) object_codec_id = get_object_codec_id(tuple(_filters) + (_compressor,)) else: object_codec_id = get_object_codec_id((_compressor,)) # we add a layer of indirection here around the dtype attribute of the array metadata # because we also need to know the object codec id, if any, to resolve the data type dtype_spec: DTypeSpec_V2 = { "name": data["dtype"], "object_codec_id": object_codec_id, } dtype = get_data_type_from_json(dtype_spec, zarr_format=2) _data["dtype"] = dtype fill_value_encoded = _data.get("fill_value") if fill_value_encoded is not None: fill_value = dtype.from_json_scalar(fill_value_encoded, zarr_format=2) _data["fill_value"] = fill_value # zarr v2 allowed arbitrary keys here. # We don't want the ArrayV2Metadata constructor to fail just because someone put an # extra key in the metadata. expected = {x.name for x in fields(cls)} expected |= {"dtype", "chunks"} # check if `filters` is an empty sequence; if so use None instead and raise a warning filters = _data.get("filters") if ( isinstance(filters, Sequence) and not isinstance(filters, (str, bytes)) and len(filters) == 0 ): msg = ( "Found an empty list of filters in the array metadata document. " "This is contrary to the Zarr V2 specification, and will cause an error in the future. " "Use None (or Null in a JSON document) instead of an empty list of filters." ) warnings.warn(msg, ZarrUserWarning, stacklevel=1) _data["filters"] = None _data = {k: v for k, v in _data.items() if k in expected} return cls(**_data) def to_dict(self) -> dict[str, JSON]: zarray_dict = super().to_dict() if _is_numcodec(zarray_dict["compressor"]): codec_config = zarray_dict["compressor"].get_config() # Hotfix for https://github.com/zarr-developers/zarr-python/issues/2647 if codec_config["id"] == "zstd" and not codec_config.get("checksum", False): codec_config.pop("checksum") zarray_dict["compressor"] = codec_config if zarray_dict["filters"] is not None: raw_filters = zarray_dict["filters"] # TODO: remove this when we can stratically type the output JSON data structure # entirely if not isinstance(raw_filters, list | tuple): raise TypeError("Invalid type for filters. Expected a list or tuple.") new_filters = [] for f in raw_filters: if _is_numcodec(f): new_filters.append(f.get_config()) else: new_filters.append(f) zarray_dict["filters"] = new_filters # serialize the fill value after dtype-specific JSON encoding if self.fill_value is not None: fill_value = self.dtype.to_json_scalar(self.fill_value, zarr_format=2) zarray_dict["fill_value"] = fill_value # pull the "name" attribute out of the dtype spec returned by self.dtype.to_json zarray_dict["dtype"] = self.dtype.to_json(zarr_format=2)["name"] return zarray_dict def get_chunk_spec( self, _chunk_coords: tuple[int, ...], array_config: ArrayConfig, prototype: BufferPrototype ) -> ArraySpec: return ArraySpec( shape=self.chunks, dtype=self.dtype, fill_value=self.fill_value, config=array_config, prototype=prototype, ) def encode_chunk_key(self, chunk_coords: tuple[int, ...]) -> str: chunk_identifier = self.dimension_separator.join(map(str, chunk_coords)) return "0" if chunk_identifier == "" else chunk_identifier def update_shape(self, shape: tuple[int, ...]) -> Self: return replace(self, shape=shape) def update_attributes(self, attributes: dict[str, JSON]) -> Self: return replace(self, attributes=attributes) def parse_dtype(data: npt.DTypeLike) -> np.dtype[Any]: if isinstance(data, list): # this is a valid _VoidDTypeLike check data = [tuple(d) for d in data] return np.dtype(data) def parse_zarr_format(data: object) -> Literal[2]: if data == 2: return 2 raise ValueError(f"Invalid value. Expected 2. Got {data}.") def parse_filters(data: object) -> tuple[Numcodec, ...] | None: """ Parse a potential tuple of filters """ out: list[Numcodec] = [] if data is None: return data if isinstance(data, Iterable): for idx, val in enumerate(data): if _is_numcodec(val): out.append(val) elif isinstance(val, dict): out.append(get_numcodec(val)) # type: ignore[arg-type] else: msg = f"Invalid filter at index {idx}. Expected a numcodecs.abc.Codec or a dict representation of numcodecs.abc.Codec. Got {type(val)} instead." raise TypeError(msg) if len(out) == 0: # Per the v2 spec, an empty tuple is not allowed -- use None to express "no filters" return None else: return tuple(out) # take a single codec instance and wrap it in a tuple if _is_numcodec(data): return (data,) msg = f"Invalid filters. Expected None, an iterable of numcodecs.abc.Codec or dict representations of numcodecs.abc.Codec. Got {type(data)} instead." raise TypeError(msg) def parse_compressor(data: object) -> Numcodec | None: """ Parse a potential compressor. """ if data is None or _is_numcodec(data): return data if isinstance(data, dict): return get_numcodec(data) # type: ignore[arg-type] msg = f"Invalid compressor. Expected None, a numcodecs.abc.Codec, or a dict representation of a numcodecs.abc.Codec. Got {type(data)} instead." raise ValueError(msg) def parse_metadata(data: ArrayV2Metadata) -> ArrayV2Metadata: if (l_chunks := len(data.chunks)) != (l_shape := len(data.shape)): msg = ( f"The `shape` and `chunks` attributes must have the same length. " f"`chunks` has length {l_chunks}, but `shape` has length {l_shape}." ) raise ValueError(msg) return data def get_object_codec_id(maybe_object_codecs: Sequence[JSON]) -> str | None: """ Inspect a sequence of codecs / filters for an "object codec", i.e. a codec that can serialize object arrays to contiguous bytes. Zarr python maintains a hard-coded set of object codec ids. If any element from the input has an id that matches one of the hard-coded object codec ids, that id is returned immediately. """ object_codec_id = None for maybe_object_codec in maybe_object_codecs: if ( isinstance(maybe_object_codec, dict) and maybe_object_codec.get("id") in OBJECT_CODEC_IDS ): return cast("str", maybe_object_codec["id"]) return object_codec_id zarr-python-3.2.1/src/zarr/core/metadata/v3.py000066400000000000000000000663461517635743000212230ustar00rootroot00000000000000from __future__ import annotations 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_extensions import TypedDict from zarr.abc.codec import ArrayArrayCodec, ArrayBytesCodec, BytesBytesCodec, Codec from zarr.abc.metadata import Metadata from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer.core import default_buffer_prototype from zarr.core.chunk_key_encodings import ( ChunkKeyEncoding, ChunkKeyEncodingLike, parse_chunk_key_encoding, ) from zarr.core.common import ( JSON, ZARR_JSON, ChunksLike, DimensionNamesLike, NamedConfig, NamedRequiredConfig, compress_rle, expand_rle, parse_named_configuration, parse_shapelike, validate_rectilinear_edges, validate_rectilinear_kind, ) from zarr.core.config import config from zarr.core.dtype import VariableLengthUTF8, ZDType, get_data_type_from_json from zarr.core.dtype.common import check_dtype_spec_v3 from zarr.core.metadata.common import parse_attributes from zarr.errors import MetadataValidationError, NodeTypeValidationError, UnknownCodecError from zarr.registry import get_codec_class if TYPE_CHECKING: from typing import Self from zarr.core.buffer import Buffer, BufferPrototype from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar def parse_zarr_format(data: object) -> Literal[3]: if data == 3: return 3 msg = f"Invalid value for 'zarr_format'. Expected '3'. Got '{data}'." raise MetadataValidationError(msg) def parse_node_type_array(data: object) -> Literal["array"]: if data == "array": return "array" msg = f"Invalid value for 'node_type'. Expected 'array'. Got '{data}'." raise NodeTypeValidationError(msg) def parse_codecs(data: object) -> tuple[Codec, ...]: out: tuple[Codec, ...] = () if not isinstance(data, Iterable): raise TypeError(f"Expected iterable, got {type(data)}") for c in data: if isinstance( c, ArrayArrayCodec | ArrayBytesCodec | BytesBytesCodec ): # Can't use Codec here because of mypy limitation out += (c,) else: name_parsed, _ = parse_named_configuration(c, require_configuration=False) try: out += (get_codec_class(name_parsed).from_dict(c),) except KeyError as e: raise UnknownCodecError(f"Unknown codec: {e.args[0]!r}") from e return out def validate_array_bytes_codec(codecs: tuple[Codec, ...]) -> ArrayBytesCodec: # ensure that we have at least one ArrayBytesCodec abcs: list[ArrayBytesCodec] = [codec for codec in codecs if isinstance(codec, ArrayBytesCodec)] if len(abcs) == 0: raise ValueError("At least one ArrayBytesCodec is required.") elif len(abcs) > 1: raise ValueError("Only one ArrayBytesCodec is allowed.") return abcs[0] def validate_codecs(codecs: tuple[Codec, ...], dtype: ZDType[TBaseDType, TBaseScalar]) -> None: """Check that the codecs are valid for the given dtype""" from zarr.codecs.sharding import ShardingCodec abc = validate_array_bytes_codec(codecs) # Recursively resolve array-bytes codecs within sharding codecs while isinstance(abc, ShardingCodec): abc = validate_array_bytes_codec(abc.codecs) # we need to have special codecs if we are decoding vlen strings or bytestrings # TODO: use codec ID instead of class name codec_class_name = abc.__class__.__name__ # TODO: Fix typing here if isinstance(dtype, VariableLengthUTF8) and not codec_class_name == "VLenUTF8Codec": # type: ignore[unreachable] raise ValueError( f"For string dtype, ArrayBytesCodec must be `VLenUTF8Codec`, got `{codec_class_name}`." ) def parse_dimension_names(data: object) -> tuple[str | None, ...] | None: if data is None: return data elif isinstance(data, Iterable) and all(isinstance(x, type(None) | str) for x in data): return tuple(data) else: msg = f"Expected either None or an iterable of str, got {type(data)}" raise TypeError(msg) def parse_storage_transformers(data: object) -> tuple[dict[str, JSON], ...]: """ Parse storage_transformers. Zarr python cannot use storage transformers at this time, so this function doesn't attempt to validate them. """ if data is None: return () if isinstance(data, Iterable): if len(tuple(data)) >= 1: return data # type: ignore[return-value] else: return () raise TypeError( f"Invalid storage_transformers. Expected an iterable of dicts. Got {type(data)} instead." ) 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. """ must_understand: Literal[False] def check_allowed_extra_field(data: object) -> TypeGuard[AllowedExtraField]: """ Check if the extra field is allowed according to the Zarr v3 spec. The object must be a mapping with a "must_understand" key set to `False`. """ return isinstance(data, Mapping) and data.get("must_understand") is False def parse_extra_fields( data: Mapping[str, AllowedExtraField] | None, ) -> dict[str, AllowedExtraField]: if data is None: return {} else: conflict_keys = ARRAY_METADATA_KEYS & set(data.keys()) if len(conflict_keys) > 0: msg = ( "Invalid extra fields. " "The following keys: " f"{sorted(conflict_keys)} " "are invalid because they collide with keys reserved for use by the " "array metadata document." ) raise ValueError(msg) return dict(data) # JSON type for a single dimension's rectilinear spec: # bare int (uniform shorthand), or list of ints / [value, count] RLE pairs. RectilinearDimSpecJSON = int | list[int | list[int]] class RegularChunkGridMetadataConfig(TypedDict): chunk_shape: Sequence[int] class RectilinearChunkGridMetadataConfig(TypedDict): kind: Literal["inline"] chunk_shapes: Sequence[RectilinearDimSpecJSON] RegularChunkGridMetadataJSON = NamedRequiredConfig[ Literal["regular"], RegularChunkGridMetadataConfig ] RectilinearChunkGridMetadataJSON = NamedRequiredConfig[ Literal["rectilinear"], RectilinearChunkGridMetadataConfig ] def _parse_chunk_shape(chunk_shape: Iterable[int]) -> tuple[int, ...]: """Validate and normalize a regular chunk shape. Delegates to ``_validate_chunk_shapes`` — a regular chunk shape is just a sequence of bare ints (one per dimension), each of which must be >= 1. """ result = _validate_chunk_shapes(tuple(chunk_shape)) # Regular grids only have bare ints — cast is safe after validation return cast(tuple[int, ...], result) def _validate_chunk_shapes( chunk_shapes: Sequence[int | Sequence[int]], ) -> tuple[int | tuple[int, ...], ...]: """Validate per-dimension chunk specifications. Each element is either a bare ``int`` (regular step size, must be >= 1) or a sequence of explicit edge lengths (all must be >= 1, non-empty). """ result: list[int | tuple[int, ...]] = [] for dim_idx, dim_spec in enumerate(chunk_shapes): if isinstance(dim_spec, int): if dim_spec < 1: raise ValueError( f"Dimension {dim_idx}: integer chunk edge length must be >= 1, got {dim_spec}" ) result.append(dim_spec) else: edges = tuple(dim_spec) if not edges: raise ValueError(f"Dimension {dim_idx} has no chunk edges.") bad = [i for i, e in enumerate(edges) if e < 1] if bad: raise ValueError( f"Dimension {dim_idx} has invalid edge lengths at indices {bad}: " f"{[edges[i] for i in bad]}" ) result.append(edges) return tuple(result) @dataclass(frozen=True, kw_only=True) class RegularChunkGridMetadata(Metadata): """Metadata-only description of a regular chunk grid. Stores just the chunk shape — no array extent, no runtime logic. This is what lives on ``ArrayV3Metadata.chunk_grid``. """ chunk_shape: tuple[int, ...] def __post_init__(self) -> None: chunk_shape_parsed = _parse_chunk_shape(self.chunk_shape) object.__setattr__(self, "chunk_shape", chunk_shape_parsed) @property def ndim(self) -> int: return len(self.chunk_shape) def to_dict(self) -> RegularChunkGridMetadataJSON: # type: ignore[override] return { "name": "regular", "configuration": {"chunk_shape": self.chunk_shape}, } @classmethod def from_dict(cls, data: RegularChunkGridMetadataJSON) -> Self: # type: ignore[override] parse_named_configuration(data, "regular") # validate name configuration = data["configuration"] return cls(chunk_shape=_parse_chunk_shape(configuration["chunk_shape"])) @dataclass(frozen=True, kw_only=True) class RectilinearChunkGridMetadata(Metadata): """Metadata-only description of a rectilinear chunk grid. Each element of ``chunk_shapes`` is either: - A bare ``int`` — a regular step size that repeats to cover the axis (the spec's single-integer shorthand). - A ``tuple[int, ...]`` — explicit per-chunk edge lengths (already expanded from any RLE encoding). This distinction matters for faithful round-tripping: a bare int serializes back as a bare int, while a single-element tuple serializes as a list. """ chunk_shapes: tuple[int | tuple[int, ...], ...] def __post_init__(self) -> None: from zarr.core.config import config if not config.get("array.rectilinear_chunks"): raise ValueError( "Rectilinear chunk grids are experimental and disabled by default. " "Enable them with: zarr.config.set({'array.rectilinear_chunks': True}) " "or set the environment variable ZARR_ARRAY__RECTILINEAR_CHUNKS=True" ) object.__setattr__(self, "chunk_shapes", _validate_chunk_shapes(self.chunk_shapes)) @property def ndim(self) -> int: return len(self.chunk_shapes) def to_dict(self) -> RectilinearChunkGridMetadataJSON: # type: ignore[override] serialized_dims: list[RectilinearDimSpecJSON] = [] for dim_spec in self.chunk_shapes: if isinstance(dim_spec, int): # Bare int shorthand — serialize as-is serialized_dims.append(dim_spec) else: rle = compress_rle(dim_spec) # Use RLE only if it's actually shorter if len(rle) < len(dim_spec): serialized_dims.append(rle) else: serialized_dims.append(list(dim_spec)) return { "name": "rectilinear", "configuration": { "kind": "inline", "chunk_shapes": tuple(serialized_dims), }, } def update_shape( self, old_shape: tuple[int, ...], new_shape: tuple[int, ...] ) -> RectilinearChunkGridMetadata: """Return a new RectilinearChunkGridMetadata with edges adjusted for *new_shape*. - Bare-int dimensions stay as bare ints (they cover any extent). - Explicit-edge dimensions: if the new extent exceeds the sum of edges, a new chunk is appended to cover the additional extent. Otherwise edges are kept as-is (the spec allows trailing edges beyond the array extent). """ new_chunk_shapes: list[int | tuple[int, ...]] = [] for dim_spec, new_ext in zip(self.chunk_shapes, new_shape, strict=True): if isinstance(dim_spec, int): # Bare int covers any extent — no change needed new_chunk_shapes.append(dim_spec) else: edge_sum = sum(dim_spec) if new_ext > edge_sum: new_chunk_shapes.append((*dim_spec, new_ext - edge_sum)) else: new_chunk_shapes.append(dim_spec) return RectilinearChunkGridMetadata(chunk_shapes=tuple(new_chunk_shapes)) @classmethod def from_dict(cls, data: RectilinearChunkGridMetadataJSON) -> Self: # type: ignore[override] parse_named_configuration(data, "rectilinear") # validate name configuration = data["configuration"] validate_rectilinear_kind(configuration.get("kind")) raw_shapes = configuration["chunk_shapes"] parsed: list[int | tuple[int, ...]] = [] for dim_spec in raw_shapes: if isinstance(dim_spec, int): if dim_spec < 1: raise ValueError(f"Integer chunk edge length must be >= 1, got {dim_spec}") parsed.append(dim_spec) elif isinstance(dim_spec, list): parsed.append(tuple(expand_rle(dim_spec))) else: raise TypeError( f"Invalid chunk_shapes entry: expected int or list, got {type(dim_spec)}" ) return cls(chunk_shapes=tuple(parsed)) ChunkGridMetadata = RegularChunkGridMetadata | RectilinearChunkGridMetadata def resolve_chunks( chunks: ChunksLike, shape: tuple[int, ...], typesize: int, ) -> ChunkGridMetadata: """Construct a chunk grid from user-facing input (e.g. ``create_array(chunks=...)``). Nested sequences like ``[[10, 20], [5, 5]]`` produce a ``RectilinearChunkGridMetadata``. Flat inputs like ``(10, 10)`` or a scalar ``int`` produce a ``RegularChunkGridMetadata`` after normalization via :func:`~zarr.core.chunk_grids.normalize_chunks`. See Also -------- parse_chunk_grid : Deserialize a chunk grid from stored JSON metadata. """ from zarr.core.chunk_grids import _is_rectilinear_chunks, normalize_chunks if _is_rectilinear_chunks(chunks): return RectilinearChunkGridMetadata(chunk_shapes=tuple(tuple(c) for c in chunks)) return RegularChunkGridMetadata(chunk_shape=normalize_chunks(chunks, shape, typesize)) def parse_chunk_grid( data: dict[str, JSON] | ChunkGridMetadata | NamedConfig[str, Any], ) -> ChunkGridMetadata: """Deserialize a chunk grid from stored JSON metadata or pass through an existing instance. See Also -------- resolve_chunks : Construct a chunk grid from user-facing input. """ if isinstance(data, (RegularChunkGridMetadata, RectilinearChunkGridMetadata)): return data name, _ = parse_named_configuration(data) if name == "regular": return RegularChunkGridMetadata.from_dict(data) # type: ignore[arg-type] if name == "rectilinear": 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. 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, ...]] """ The names of the fields of the array metadata document defined in the zarr V3 spec. """ ARRAY_METADATA_KEYS: Final[set[str]] = { "zarr_format", "node_type", "data_type", "shape", "chunk_grid", "chunk_key_encoding", "fill_value", "codecs", "attributes", "storage_transformers", "dimension_names", } @dataclass(frozen=True, kw_only=True) class ArrayV3Metadata(Metadata): shape: tuple[int, ...] data_type: ZDType[TBaseDType, TBaseScalar] chunk_grid: ChunkGridMetadata chunk_key_encoding: ChunkKeyEncoding fill_value: Any codecs: tuple[Codec, ...] attributes: dict[str, Any] = field(default_factory=dict) dimension_names: tuple[str | None, ...] | None = None zarr_format: Literal[3] = field(default=3, init=False) node_type: Literal["array"] = field(default="array", init=False) storage_transformers: tuple[dict[str, JSON], ...] extra_fields: dict[str, AllowedExtraField] def __init__( self, *, shape: Iterable[int], data_type: ZDType[TBaseDType, TBaseScalar], chunk_grid: dict[str, JSON] | ChunkGridMetadata | NamedConfig[str, Any], chunk_key_encoding: ChunkKeyEncodingLike, fill_value: object, codecs: Iterable[Codec | dict[str, JSON] | NamedConfig[str, Any] | str], attributes: dict[str, JSON] | None, dimension_names: DimensionNamesLike, storage_transformers: Iterable[dict[str, JSON]] | None = None, extra_fields: Mapping[str, AllowedExtraField] | None = None, ) -> None: """ Because the class is a frozen dataclass, we set attributes using object.__setattr__ """ shape_parsed = parse_shapelike(shape) chunk_grid_parsed = parse_chunk_grid(chunk_grid) chunk_key_encoding_parsed = parse_chunk_key_encoding(chunk_key_encoding) dimension_names_parsed = parse_dimension_names(dimension_names) # Note: relying on a type method is numpy-specific fill_value_parsed = data_type.cast_scalar(fill_value) attributes_parsed = parse_attributes(attributes) codecs_parsed_partial = parse_codecs(codecs) storage_transformers_parsed = parse_storage_transformers(storage_transformers) extra_fields_parsed = parse_extra_fields(extra_fields) array_spec = ArraySpec( shape=shape_parsed, dtype=data_type, fill_value=fill_value_parsed, config=ArrayConfig.from_dict({}), # TODO: config is not needed here. prototype=default_buffer_prototype(), # TODO: prototype is not needed here. ) # Thread the spec through evolution: each codec must be evolved against # the spec it will actually see at run-time, not the original array spec. # Earlier array->array codecs may transform the dtype (e.g. cast_value), # so the spec passed to later codecs must reflect those transformations. # Per-codec validate() must run before resolve_metadata(), since the # latter may rely on invariants the former checks (e.g. cast_value # rejects complex source dtypes that would otherwise crash _do_cast). evolved: list[Codec] = [] spec = array_spec for c in codecs_parsed_partial: evolved_codec = c.evolve_from_array_spec(spec) evolved_codec.validate(shape=spec.shape, dtype=spec.dtype, chunk_grid=chunk_grid_parsed) evolved.append(evolved_codec) spec = evolved_codec.resolve_metadata(spec) codecs_parsed = tuple(evolved) validate_codecs(codecs_parsed_partial, data_type) object.__setattr__(self, "shape", shape_parsed) object.__setattr__(self, "data_type", data_type) object.__setattr__(self, "chunk_grid", chunk_grid_parsed) object.__setattr__(self, "chunk_key_encoding", chunk_key_encoding_parsed) object.__setattr__(self, "codecs", codecs_parsed) object.__setattr__(self, "dimension_names", dimension_names_parsed) object.__setattr__(self, "fill_value", fill_value_parsed) object.__setattr__(self, "attributes", attributes_parsed) object.__setattr__(self, "storage_transformers", storage_transformers_parsed) object.__setattr__(self, "extra_fields", extra_fields_parsed) self._validate_metadata() def _validate_metadata(self) -> None: if len(self.shape) != self.chunk_grid.ndim: raise ValueError("`chunk_grid` and `shape` need to have the same number of dimensions.") if isinstance(self.chunk_grid, RectilinearChunkGridMetadata): validate_rectilinear_edges(self.chunk_grid.chunk_shapes, self.shape) if self.dimension_names is not None and len(self.shape) != len(self.dimension_names): raise ValueError( "`dimension_names` and `shape` need to have the same number of dimensions." ) if self.fill_value is None: raise ValueError("`fill_value` is required.") for codec in self.codecs: codec.validate(shape=self.shape, dtype=self.data_type, chunk_grid=self.chunk_grid) @property def ndim(self) -> int: return len(self.shape) @property def dtype(self) -> ZDType[TBaseDType, TBaseScalar]: return self.data_type # TODO: move these properties to the Array class. # They require knowledge of codecs (ShardingCodec) and don't belong on a metadata DTO. @property def chunks(self) -> tuple[int, ...]: if not isinstance(self.chunk_grid, RegularChunkGridMetadata): msg = ( "The `chunks` attribute is only defined for arrays using regular chunk grids. " "This array has a rectilinear chunk grid. Use `read_chunk_sizes` for general access." ) raise NotImplementedError(msg) from zarr.codecs.sharding import ShardingCodec if len(self.codecs) == 1 and isinstance(self.codecs[0], ShardingCodec): return self.codecs[0].chunk_shape return self.chunk_grid.chunk_shape @property def shards(self) -> tuple[int, ...] | None: from zarr.codecs.sharding import ShardingCodec if len(self.codecs) == 1 and isinstance(self.codecs[0], ShardingCodec): if not isinstance(self.chunk_grid, RegularChunkGridMetadata): msg = ( "The `shards` attribute is only defined for arrays using regular chunk grids. " "This array has a rectilinear chunk grid. Use `write_chunk_sizes` for general access." ) raise NotImplementedError(msg) return self.chunk_grid.chunk_shape return None @property def inner_codecs(self) -> tuple[Codec, ...]: from zarr.codecs.sharding import ShardingCodec if len(self.codecs) == 1 and isinstance(self.codecs[0], ShardingCodec): return self.codecs[0].codecs return self.codecs def encode_chunk_key(self, chunk_coords: tuple[int, ...]) -> str: return self.chunk_key_encoding.encode_chunk_key(chunk_coords) def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]: json_indent = config.get("json_indent") d = self.to_dict() return { ZARR_JSON: prototype.buffer.from_bytes( json.dumps(d, allow_nan=True, indent=json_indent).encode() ) } @classmethod def from_dict(cls, data: dict[str, JSON]) -> Self: # make a copy because we are modifying the dict _data = data.copy() # check that the zarr_format attribute is correct _ = parse_zarr_format(_data.pop("zarr_format")) # check that the node_type attribute is correct _ = parse_node_type_array(_data.pop("node_type")) data_type_json = _data.pop("data_type") if not check_dtype_spec_v3(data_type_json): raise ValueError(f"Invalid data_type: {data_type_json!r}") data_type = get_data_type_from_json(data_type_json, zarr_format=3) # check that the fill value is consistent with the data type try: fill = _data.pop("fill_value") fill_value_parsed = data_type.from_json_scalar(fill, zarr_format=3) except ValueError as e: raise TypeError(f"Invalid fill_value: {fill!r}") from e # check if there are extra keys extra_keys = set(_data.keys()) - ARRAY_METADATA_KEYS allowed_extra_fields: dict[str, AllowedExtraField] = {} invalid_extra_fields = {} for key in extra_keys: val = _data[key] if check_allowed_extra_field(val): allowed_extra_fields[key] = val else: invalid_extra_fields[key] = val if len(invalid_extra_fields) > 0: msg = ( "Got a Zarr V3 metadata document with the following disallowed extra fields:" f"{sorted(invalid_extra_fields.keys())}." 'Extra fields are not allowed unless they are a dict with a "must_understand" key' "which is assigned the value `False`." ) raise MetadataValidationError(msg) # TODO: replace this with a real type check! _data_typed = cast(ArrayMetadataJSON_V3, _data) return cls( shape=_data_typed["shape"], chunk_grid=_data_typed["chunk_grid"], # type: ignore[arg-type] chunk_key_encoding=_data_typed["chunk_key_encoding"], # type: ignore[arg-type] codecs=_data_typed["codecs"], attributes=_data_typed.get("attributes", {}), # type: ignore[arg-type] dimension_names=_data_typed.get("dimension_names", None), fill_value=fill_value_parsed, data_type=data_type, extra_fields=allowed_extra_fields, storage_transformers=_data_typed.get("storage_transformers", ()), # type: ignore[arg-type] ) def to_dict(self) -> dict[str, JSON]: out_dict = super().to_dict() extra_fields = out_dict.pop("extra_fields") out_dict = out_dict | extra_fields # type: ignore[operator] out_dict["chunk_grid"] = self.chunk_grid.to_dict() out_dict["fill_value"] = self.data_type.to_json_scalar( self.fill_value, zarr_format=self.zarr_format ) if not isinstance(out_dict, dict): raise TypeError(f"Expected dict. Got {type(out_dict)}.") # if `dimension_names` is `None`, we do not include it in # the metadata document if out_dict["dimension_names"] is None: out_dict.pop("dimension_names") # TODO: replace the `to_dict` / `from_dict` on the `Metadata`` class with # to_json, from_json, and have ZDType inherit from `Metadata` # until then, we have this hack here, which relies on the fact that to_dict will pass through # any non-`Metadata` fields as-is. dtype_meta = out_dict["data_type"] if isinstance(dtype_meta, ZDType): out_dict["data_type"] = dtype_meta.to_json(zarr_format=3) # type: ignore[unreachable] return out_dict def update_shape(self, shape: tuple[int, ...]) -> Self: chunk_grid = self.chunk_grid if isinstance(chunk_grid, RectilinearChunkGridMetadata): chunk_grid = chunk_grid.update_shape(self.shape, shape) return replace(self, shape=shape, chunk_grid=chunk_grid) def update_attributes(self, attributes: dict[str, JSON]) -> Self: return replace(self, attributes=attributes) zarr-python-3.2.1/src/zarr/core/sync.py000066400000000000000000000162221517635743000200530ustar00rootroot00000000000000from __future__ import annotations import asyncio import atexit import logging import os import threading from concurrent.futures import ThreadPoolExecutor, wait from typing import TYPE_CHECKING from typing_extensions import ParamSpec from zarr.core.config import config if TYPE_CHECKING: from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine from typing import Any logger = logging.getLogger(__name__) P = ParamSpec("P") # From https://github.com/fsspec/filesystem_spec/blob/master/fsspec/asyn.py iothread: list[threading.Thread | None] = [None] # dedicated IO thread loop: list[asyncio.AbstractEventLoop | None] = [ None ] # global event loop for any non-async instance _lock: threading.Lock | None = None # global lock placeholder _executor: ThreadPoolExecutor | None = None # global executor placeholder class SyncError(Exception): pass def _get_lock() -> threading.Lock: """Allocate or return a threading lock. The lock is allocated on first use to allow setting one lock per forked process. """ global _lock if not _lock: _lock = threading.Lock() return _lock def _get_executor() -> ThreadPoolExecutor: """Return Zarr Thread Pool Executor The executor is allocated on first use. """ global _executor if not _executor: max_workers = config.get("threading.max_workers", None) logger.debug("Creating Zarr ThreadPoolExecutor with max_workers=%s", max_workers) _executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="zarr_pool") _get_loop().set_default_executor(_executor) return _executor def cleanup_resources() -> None: global _executor if _executor: _executor.shutdown(wait=True, cancel_futures=True) _executor = None if loop[0] is not None: with _get_lock(): # Stop the event loop safely loop[0].call_soon_threadsafe(loop[0].stop) # Stop loop from another thread if iothread[0] is not None: iothread[0].join(timeout=0.2) # Add a timeout to avoid hanging if iothread[0].is_alive(): logger.warning( "Thread did not finish cleanly; forcefully closing the event loop." ) # Forcefully close the event loop to release resources loop[0].close() # dereference the loop and iothread loop[0] = None iothread[0] = None atexit.register(cleanup_resources) def reset_resources_after_fork() -> None: """ Ensure that global resources are reset after a fork. Without this function, forked processes will retain invalid references to the parent process's resources. """ global loop, iothread, _executor # These lines are excluded from coverage because this function only runs in a child process, # which is not observed by the test coverage instrumentation. Despite the apparent lack of # test coverage, this function should be adequately tested by any test that uses Zarr IO with # multiprocessing. loop[0] = None # pragma: no cover iothread[0] = None # pragma: no cover _executor = None # pragma: no cover # this is only available on certain operating systems if hasattr(os, "register_at_fork"): os.register_at_fork(after_in_child=reset_resources_after_fork) async def _runner[T](coro: Coroutine[Any, Any, T]) -> T | BaseException: """ Await a coroutine and return the result of running it. If awaiting the coroutine raises an exception, the exception will be returned. """ try: return await coro except Exception as ex: return ex def sync[T]( coro: Coroutine[Any, Any, T], loop: asyncio.AbstractEventLoop | None = None, timeout: float | None = None, ) -> T: """ Make loop run coroutine until it returns. Runs in other thread """ if loop is None: # NB: if the loop is not running *yet*, it is OK to submit work # and we will wait for it loop = _get_loop() if _executor is None and config.get("threading.max_workers", None) is not None: # trigger executor creation and attach to loop _ = _get_executor() if not isinstance(loop, asyncio.AbstractEventLoop): raise TypeError(f"loop cannot be of type {type(loop)}") if loop.is_closed(): raise RuntimeError("Loop is not running") try: loop0 = asyncio.events.get_running_loop() if loop0 is loop: raise SyncError("Calling sync() from within a running loop") except RuntimeError: pass future = asyncio.run_coroutine_threadsafe(_runner(coro), loop) finished, unfinished = wait([future], return_when=asyncio.ALL_COMPLETED, timeout=timeout) if len(unfinished) > 0: raise TimeoutError(f"Coroutine {coro} failed to finish within {timeout} s") assert len(finished) == 1 return_result = next(iter(finished)).result() if isinstance(return_result, BaseException): raise return_result else: return return_result def _get_loop() -> asyncio.AbstractEventLoop: """Create or return the default fsspec IO loop The loop will be running on a separate thread. """ if loop[0] is None: with _get_lock(): # repeat the check just in case the loop got filled between the # previous two calls from another thread if loop[0] is None: logger.debug("Creating Zarr event loop") new_loop = asyncio.new_event_loop() loop[0] = new_loop iothread[0] = threading.Thread(target=new_loop.run_forever, name="zarr_io") assert iothread[0] is not None iothread[0].daemon = True iothread[0].start() assert loop[0] is not None return loop[0] async def _collect_aiterator[T](data: AsyncIterator[T]) -> tuple[T, ...]: """ Collect an entire async iterator into a tuple """ result = [x async for x in data] return tuple(result) def collect_aiterator[T](data: AsyncIterator[T]) -> tuple[T, ...]: """ Synchronously collect an entire async iterator into a tuple. """ return sync(_collect_aiterator(data)) class SyncMixin: def _sync[T](self, coroutine: Coroutine[Any, Any, T]) -> T: # TODO: refactor this to to take *args and **kwargs and pass those to the method # this should allow us to better type the sync wrapper return sync( coroutine, timeout=config.get("async.timeout"), ) def _sync_iter[T](self, async_iterator: AsyncIterator[T]) -> list[T]: async def iter_to_list() -> list[T]: return [item async for item in async_iterator] return self._sync(iter_to_list()) async def _with_semaphore[T]( func: Callable[[], Awaitable[T]], semaphore: asyncio.Semaphore | None = None ) -> T: """ Await the result of invoking the no-argument-callable ``func`` within the context manager provided by a Semaphore, if one is provided. Otherwise, just await the result of invoking ``func``. """ if semaphore is None: return await func() async with semaphore: return await func() zarr-python-3.2.1/src/zarr/core/sync_group.py000066400000000000000000000136571517635743000213000ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from zarr.core.group import Group, GroupMetadata, _parse_async_node from zarr.core.group import create_hierarchy as create_hierarchy_async from zarr.core.group import create_nodes as create_nodes_async from zarr.core.group import create_rooted_hierarchy as create_rooted_hierarchy_async from zarr.core.group import get_node as get_node_async from zarr.core.sync import _collect_aiterator, sync if TYPE_CHECKING: from collections.abc import Iterator from zarr.abc.store import Store from zarr.core.common import ZarrFormat from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.types import AnyArray def create_nodes( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata] ) -> Iterator[tuple[str, Group | AnyArray]]: """Create a collection of arrays and / or groups concurrently. Note: no attempt is made to validate that these arrays and / or groups collectively form a valid Zarr hierarchy. It is the responsibility of the caller of this function to ensure that the ``nodes`` parameter satisfies any correctness constraints. Parameters ---------- store : Store The storage backend to use. nodes : dict[str, GroupMetadata | ArrayV3Metadata | ArrayV2Metadata] A dictionary defining the hierarchy. The keys are the paths of the nodes in the hierarchy, and the values are the metadata of the nodes. The metadata must be either an instance of GroupMetadata, ArrayV3Metadata or ArrayV2Metadata. Yields ------ Group | Array The created nodes. """ coro = create_nodes_async(store=store, nodes=nodes) for key, value in sync(_collect_aiterator(coro)): yield key, _parse_async_node(value) def create_hierarchy( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, ) -> Iterator[tuple[str, Group | AnyArray]]: """ Create a complete zarr hierarchy from a collection of metadata objects. This function will parse its input to ensure that the hierarchy is complete. Any implicit groups will be inserted as needed. For example, an input like ```{'a/b': GroupMetadata}``` will be parsed to ```{'': GroupMetadata, 'a': GroupMetadata, 'b': Groupmetadata}``` After input parsing, this function then creates all the nodes in the hierarchy concurrently. Arrays and Groups are yielded in the order they are created. This order is not stable and should not be relied on. Parameters ---------- store : Store The storage backend to use. nodes : dict[str, GroupMetadata | ArrayV3Metadata | ArrayV2Metadata] A dictionary defining the hierarchy. The keys are the paths of the nodes in the hierarchy, relative to the root of the ``Store``. The root of the store can be specified with the empty string ``''``. The values are instances of ``GroupMetadata`` or ``ArrayMetadata``. Note that all values must have the same ``zarr_format`` -- it is an error to mix zarr versions in the same hierarchy. Leading "/" characters from keys will be removed. overwrite : bool Whether to overwrite existing nodes. Defaults to ``False``, in which case an error is raised instead of overwriting an existing array or group. This function will not erase an existing group unless that group is explicitly named in ``nodes``. If ``nodes`` defines implicit groups, e.g. ``{`'a/b/c': GroupMetadata}``, and a group already exists at path ``a``, then this function will leave the group at ``a`` as-is. Yields ------ tuple[str, Group | Array] This function yields (path, node) pairs, in the order the nodes were created. Examples -------- ```python from zarr import create_hierarchy from zarr.storage import MemoryStore from zarr.core.group import GroupMetadata store = MemoryStore() nodes = {'a': GroupMetadata(attributes={'name': 'leaf'})} nodes_created = dict(create_hierarchy(store=store, nodes=nodes)) print(nodes) # {'a': GroupMetadata(attributes={'name': 'leaf'}, zarr_format=3, consolidated_metadata=None, node_type='group')} ``` """ coro = create_hierarchy_async(store=store, nodes=nodes, overwrite=overwrite) for key, value in sync(_collect_aiterator(coro)): yield key, _parse_async_node(value) def create_rooted_hierarchy( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, ) -> Group | AnyArray: """ Create a Zarr hierarchy with a root, and return the root node, which could be a ``Group`` or ``Array`` instance. Parameters ---------- store : Store The storage backend to use. nodes : dict[str, GroupMetadata | ArrayV3Metadata | ArrayV2Metadata] A dictionary defining the hierarchy. The keys are the paths of the nodes in the hierarchy, and the values are the metadata of the nodes. The metadata must be either an instance of GroupMetadata, ArrayV3Metadata or ArrayV2Metadata. overwrite : bool Whether to overwrite existing nodes. Default is ``False``. Returns ------- Group | Array """ async_node = sync(create_rooted_hierarchy_async(store=store, nodes=nodes, overwrite=overwrite)) return _parse_async_node(async_node) def get_node(store: Store, path: str, zarr_format: ZarrFormat) -> AnyArray | Group: """ Get an Array or Group from a path in a Store. Parameters ---------- store : Store The store-like object to read from. path : str The path to the node to read. zarr_format : {2, 3} The zarr format of the node to read. Returns ------- Array | Group """ return _parse_async_node(sync(get_node_async(store=store, path=path, zarr_format=zarr_format))) zarr-python-3.2.1/src/zarr/dtype.py000066400000000000000000000035461517635743000173010ustar00rootroot00000000000000from zarr.core.dtype import ( Bool, Complex64, Complex128, DataTypeValidationError, DateTime64, DateTime64JSON_V2, DateTime64JSON_V3, FixedLengthUTF32, FixedLengthUTF32JSON_V2, FixedLengthUTF32JSON_V3, Float16, Float32, Float64, Int8, Int16, Int32, Int64, NullTerminatedBytes, NullterminatedBytesJSON_V2, NullTerminatedBytesJSON_V3, RawBytes, RawBytesJSON_V2, RawBytesJSON_V3, Struct, StructJSON_V3, Structured, StructuredJSON_V2, StructuredJSON_V3, TimeDelta64, TimeDelta64JSON_V2, TimeDelta64JSON_V3, UInt8, UInt16, UInt32, UInt64, VariableLengthBytes, VariableLengthBytesJSON_V2, VariableLengthUTF8, VariableLengthUTF8JSON_V2, ZDType, data_type_registry, # Import for backwards compatibility, but not included in __all__ # so it doesn't show up in the docs parse_data_type, # noqa: F401 parse_dtype, ) __all__ = [ "Bool", "Complex64", "Complex128", "DataTypeValidationError", "DateTime64", "DateTime64JSON_V2", "DateTime64JSON_V3", "FixedLengthUTF32", "FixedLengthUTF32JSON_V2", "FixedLengthUTF32JSON_V3", "Float16", "Float32", "Float64", "Int8", "Int16", "Int32", "Int64", "NullTerminatedBytes", "NullTerminatedBytesJSON_V3", "NullterminatedBytesJSON_V2", "RawBytes", "RawBytesJSON_V2", "RawBytesJSON_V3", "Struct", "StructJSON_V3", "Structured", "StructuredJSON_V2", "StructuredJSON_V3", "TimeDelta64", "TimeDelta64JSON_V2", "TimeDelta64JSON_V3", "UInt8", "UInt16", "UInt32", "UInt64", "VariableLengthBytes", "VariableLengthBytesJSON_V2", "VariableLengthUTF8", "VariableLengthUTF8JSON_V2", "ZDType", "data_type_registry", "parse_dtype", ] zarr-python-3.2.1/src/zarr/errors.py000066400000000000000000000074221517635743000174650ustar00rootroot00000000000000__all__ = [ "ArrayIndexError", "ArrayNotFoundError", "BaseZarrError", "BoundsCheckError", "ChunkNotFoundError", "ContainsArrayAndGroupError", "ContainsArrayError", "ContainsGroupError", "GroupNotFoundError", "MetadataValidationError", "NegativeStepError", "NodeTypeValidationError", "UnstableSpecificationWarning", "VindexInvalidSelectionError", "ZarrDeprecationWarning", "ZarrFutureWarning", "ZarrRuntimeWarning", ] class BaseZarrError(ValueError): """ Base error which all zarr errors are sub-classed from. """ _msg: str = "{}" def __init__(self, *args: object) -> None: """ If a single argument is passed, treat it as a pre-formatted message. If multiple arguments are passed, they are used as arguments for a template string class variable. This behavior is deprecated. """ if len(args) == 1: super().__init__(args[0]) else: super().__init__(self._msg.format(*args)) class NodeNotFoundError(BaseZarrError, FileNotFoundError): """ Raised when a node (array or group) is not found at a certain path. """ class ArrayNotFoundError(NodeNotFoundError): """ Raised when an array isn't found at a certain path. """ _msg = "No array found in store {!r} at path {!r}" class GroupNotFoundError(NodeNotFoundError): """ Raised when a group isn't found at a certain path. """ _msg = "No group found in store {!r} at path {!r}" class ContainsGroupError(BaseZarrError): """Raised when a group already exists at a certain path.""" _msg = "A group exists in store {!r} at path {!r}." class ContainsArrayError(BaseZarrError): """Raised when an array already exists at a certain path.""" _msg = "An array exists in store {!r} at path {!r}." class ContainsArrayAndGroupError(BaseZarrError): """Raised when both array and group metadata are found at the same path.""" _msg = ( "Array and group metadata documents (.zarray and .zgroup) were both found in store " "{!r} at path {!r}. " "Only one of these files may be present in a given directory / prefix. " "Remove the .zarray file, or the .zgroup file, or both." ) class MetadataValidationError(BaseZarrError): """Raised when the Zarr metadata is invalid in some way""" _msg = "Invalid value for '{}'. Expected '{}'. Got '{}'." class UnknownCodecError(BaseZarrError): """ Raised when an unknown codec was used. """ class NodeTypeValidationError(MetadataValidationError): """ Specialized exception when the node_type of the metadata document is incorrect. This can be raised when the value is invalid or unexpected given the context, for example an 'array' node when we expected a 'group'. """ class ZarrFutureWarning(FutureWarning): """ A warning intended for end users raised to indicate deprecated features. """ class UnstableSpecificationWarning(ZarrFutureWarning): """ A warning raised to indicate that a feature is outside the Zarr specification. """ class ZarrDeprecationWarning(DeprecationWarning): """ A warning raised to indicate that a feature will be removed in a future release. """ class ZarrUserWarning(UserWarning): """ A warning raised to report problems with user code. """ class ZarrRuntimeWarning(RuntimeWarning): """ A warning for dubious runtime behavior. """ class VindexInvalidSelectionError(IndexError): ... class NegativeStepError(IndexError): ... class BoundsCheckError(IndexError): ... class ArrayIndexError(IndexError): ... class ChunkNotFoundError(BaseZarrError): """ Raised when a chunk that was expected to exist in storage was not retrieved successfully. """ zarr-python-3.2.1/src/zarr/experimental/000077500000000000000000000000001517635743000202675ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/experimental/__init__.py000066400000000000000000000002671517635743000224050ustar00rootroot00000000000000"""The experimental module is a site for exporting new or experimental Zarr features.""" from zarr.core.chunk_grids import ChunkGrid, ChunkSpec __all__ = ["ChunkGrid", "ChunkSpec"] zarr-python-3.2.1/src/zarr/experimental/cache_store.py000066400000000000000000000416021517635743000231230ustar00rootroot00000000000000from __future__ import annotations import asyncio import logging import time from collections import OrderedDict from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Literal, Self from zarr.abc.store import ByteRequest, Store from zarr.storage._wrapper import WrapperStore logger = logging.getLogger(__name__) if TYPE_CHECKING: from zarr.core.buffer.core import Buffer, BufferPrototype # A cache entry identifier. Plain ``str`` for full-key entries that live in # the Store-backed cache; ``(str, ByteRequest)`` for byte-range entries that # live in the in-memory range cache. _CacheEntryKey = str | tuple[str, ByteRequest] @dataclass(slots=True) class _CacheState: cache_order: OrderedDict[_CacheEntryKey, None] = field(default_factory=OrderedDict) current_size: int = 0 key_sizes: dict[_CacheEntryKey, int] = field(default_factory=dict) lock: asyncio.Lock = field(default_factory=asyncio.Lock) hits: int = 0 misses: int = 0 evictions: int = 0 key_insert_times: dict[_CacheEntryKey, float] = field(default_factory=dict) range_cache: dict[str, dict[ByteRequest, Buffer]] = field(default_factory=dict) class CacheStore(WrapperStore[Store]): """ A dual-store caching implementation for Zarr stores. This cache wraps any Store implementation and uses a separate Store instance as the cache backend. This provides persistent caching capabilities with time-based expiration, size-based eviction, and flexible cache storage options. Full-key reads are cached in the Store-backed cache. Byte-range reads are cached in a separate in-memory dictionary so that partial reads never pollute the filesystem (or other persistent backend). Both caches share the same ``max_size`` budget and LRU eviction policy. Parameters ---------- store : Store The underlying store to wrap with caching cache_store : Store The store to use for caching (can be any Store implementation) max_age_seconds : int | None, optional Maximum age of cached entries in seconds. None means no expiration. Default is None. max_size : int | None, optional Maximum size of the cache in bytes. When exceeded, least recently used items are evicted. None means unlimited size. Default is None. Note: Individual values larger than max_size will not be cached. cache_set_data : bool, optional Whether to cache data when it's written to the store. Default is True. Examples -------- ```python import zarr from zarr.storage import MemoryStore from zarr.experimental.cache_store import CacheStore # Create a cached store source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( store=source_store, cache_store=cache_store, max_age_seconds=60, max_size=1024*1024 ) # Use it like any other store array = zarr.create(shape=(100,), store=cached_store) array[:] = 42 ``` """ _cache: Store max_age_seconds: int | Literal["infinity"] max_size: int | None cache_set_data: bool _state: _CacheState def __init__( self, store: Store, *, cache_store: Store, max_age_seconds: int | str = "infinity", max_size: int | None = None, cache_set_data: bool = True, ) -> None: super().__init__(store) if not cache_store.supports_deletes: msg = ( f"The provided cache store {cache_store} does not support deletes. " "The cache_store must support deletes for CacheStore to function properly." ) raise ValueError(msg) self._cache = cache_store # Validate and set max_age_seconds if isinstance(max_age_seconds, str): if max_age_seconds != "infinity": raise ValueError("max_age_seconds string value must be 'infinity'") self.max_age_seconds = "infinity" else: self.max_age_seconds = max_age_seconds self.max_size = max_size self.cache_set_data = cache_set_data self._state = _CacheState() def _with_store(self, store: Store) -> Self: # Cannot support this operation because it would share a cache, but have a new store # So cache keys would conflict raise NotImplementedError("CacheStore does not support this operation.") def with_read_only(self, read_only: bool = False) -> Self: # Create a new cache store that shares the same cache and mutable state store = type(self)( store=self._store.with_read_only(read_only), cache_store=self._cache, max_age_seconds=self.max_age_seconds, max_size=self.max_size, cache_set_data=self.cache_set_data, ) store._state = self._state return store def _is_key_fresh(self, entry_key: _CacheEntryKey) -> bool: """Check if a cached entry is still fresh based on max_age_seconds. Uses monotonic time for accurate elapsed time measurement. """ if self.max_age_seconds == "infinity": return True now = time.monotonic() elapsed = now - self._state.key_insert_times.get(entry_key, 0) return elapsed < self.max_age_seconds async def _accommodate_value(self, value_size: int) -> None: """Ensure there is enough space in the cache for a new value. Must be called while holding self._state.lock. """ if self.max_size is None: return # Remove least recently used items until we have enough space while self._state.current_size + value_size > self.max_size and self._state.cache_order: # Get the least recently used key (first in OrderedDict) lru_key = next(iter(self._state.cache_order)) await self._evict_key(lru_key) async def _evict_key(self, entry_key: _CacheEntryKey) -> None: """Evict a cache entry. Must be called while holding self._state.lock. For ``str`` keys the entry is deleted from the Store-backed cache. For ``(str, ByteRequest)`` keys the entry is removed from the in-memory range cache. """ key_size = self._state.key_sizes.get(entry_key, 0) if isinstance(entry_key, str): await self._cache.delete(entry_key) else: base_key, byte_range = entry_key per_key = self._state.range_cache.get(base_key) if per_key is not None: per_key.pop(byte_range, None) if not per_key: del self._state.range_cache[base_key] self._state.cache_order.pop(entry_key, None) self._state.key_insert_times.pop(entry_key, None) self._state.key_sizes.pop(entry_key, None) self._state.current_size = max(0, self._state.current_size - key_size) self._state.evictions += 1 async def _track_entry(self, entry_key: _CacheEntryKey, value: Buffer) -> bool: """Register *entry_key* in the shared size / LRU tracking. Returns ``True`` if the entry was tracked, ``False`` if the value exceeds ``max_size`` and was skipped. Callers should roll back any data they already stored when this returns ``False``. This method holds the lock for the entire operation to ensure atomicity. """ value_size = len(value) # Check if value exceeds max size if self.max_size is not None and value_size > self.max_size: return False async with self._state.lock: # If key already exists, subtract old size first if entry_key in self._state.key_sizes: old_size = self._state.key_sizes[entry_key] self._state.current_size -= old_size # Make room for the new value await self._accommodate_value(value_size) # Update tracking atomically self._state.cache_order[entry_key] = None self._state.current_size += value_size self._state.key_sizes[entry_key] = value_size self._state.key_insert_times[entry_key] = time.monotonic() return True async def _update_access_order(self, entry_key: _CacheEntryKey) -> None: """Update the access order for LRU tracking.""" if entry_key in self._state.cache_order: async with self._state.lock: self._state.cache_order.move_to_end(entry_key) def _remove_from_tracking(self, entry_key: _CacheEntryKey) -> None: """Remove an entry from all tracking structures. Must be called while holding self._state.lock. """ self._state.cache_order.pop(entry_key, None) self._state.key_insert_times.pop(entry_key, None) self._state.key_sizes.pop(entry_key, None) def _invalidate_range_entries(self, key: str) -> None: """Remove all byte-range entries for *key* from the range cache and tracking. Must be called while holding self._state.lock. """ per_key = self._state.range_cache.pop(key, None) if per_key is not None: for byte_range in per_key: entry_key: _CacheEntryKey = (key, byte_range) entry_size = self._state.key_sizes.pop(entry_key, 0) self._state.cache_order.pop(entry_key, None) self._state.key_insert_times.pop(entry_key, None) self._state.current_size = max(0, self._state.current_size - entry_size) # ------------------------------------------------------------------ # get helpers # ------------------------------------------------------------------ async def _cache_miss( self, key: str, byte_range: ByteRequest | None, result: Buffer | None ) -> None: """Handle a cache miss by storing or cleaning up after a source-store fetch.""" if result is None: if byte_range is None: await self._cache.delete(key) async with self._state.lock: self._remove_from_tracking(key) else: entry_key: _CacheEntryKey = (key, byte_range) async with self._state.lock: per_key = self._state.range_cache.get(key) if per_key is not None: per_key.pop(byte_range, None) if not per_key: del self._state.range_cache[key] self._remove_from_tracking(entry_key) else: if byte_range is None: await self._cache.set(key, result) await self._track_entry(key, result) else: entry_key = (key, byte_range) self._state.range_cache.setdefault(key, {})[byte_range] = result tracked = await self._track_entry(entry_key, result) if not tracked: # Value too large for the cache — roll back the insertion per_key = self._state.range_cache.get(key) if per_key is not None: per_key.pop(byte_range, None) if not per_key: del self._state.range_cache[key] async def _get_try_cache( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: """Try to get data from cache first, falling back to source store.""" if byte_range is None: # Full-key read — use Store-backed cache maybe_cached = await self._cache.get(key, prototype) if maybe_cached is not None: self._state.hits += 1 await self._update_access_order(key) return maybe_cached else: # Byte-range read — use in-memory range cache entry_key: _CacheEntryKey = (key, byte_range) per_key = self._state.range_cache.get(key) if per_key is not None: cached_buf = per_key.get(byte_range) if cached_buf is not None: self._state.hits += 1 await self._update_access_order(entry_key) return cached_buf # Cache miss — fetch from source store self._state.misses += 1 result = await super().get(key, prototype, byte_range) await self._cache_miss(key, byte_range, result) return result async def _get_no_cache( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: """Get data directly from source store and update cache.""" self._state.misses += 1 result = await super().get(key, prototype, byte_range) await self._cache_miss(key, byte_range, result) return result async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None, ) -> Buffer | None: """ Retrieve data from the store, using cache when appropriate. Parameters ---------- key : str The key to retrieve prototype : BufferPrototype Buffer prototype for creating the result buffer byte_range : ByteRequest, optional Byte range to retrieve Returns ------- Buffer | None The retrieved data, or None if not found """ entry_key: _CacheEntryKey = (key, byte_range) if byte_range is not None else key if not self._is_key_fresh(entry_key): return await self._get_no_cache(key, prototype, byte_range) else: return await self._get_try_cache(key, prototype, byte_range) async def set(self, key: str, value: Buffer) -> None: """ Store data in the underlying store and optionally in cache. Parameters ---------- key : str The key to store under value : Buffer The data to store """ await super().set(key, value) # Invalidate all cached byte-range entries (source data changed) async with self._state.lock: self._invalidate_range_entries(key) if self.cache_set_data: await self._cache.set(key, value) await self._track_entry(key, value) else: await self._cache.delete(key) async with self._state.lock: self._remove_from_tracking(key) async def delete(self, key: str) -> None: """ Delete data from both the underlying store and cache. Parameters ---------- key : str The key to delete """ await super().delete(key) # Invalidate all cached byte-range entries async with self._state.lock: self._invalidate_range_entries(key) await self._cache.delete(key) async with self._state.lock: self._remove_from_tracking(key) def cache_info(self) -> dict[str, Any]: """Return information about the cache state.""" return { "cache_store_type": type(self._cache).__name__, "max_age_seconds": "infinity" if self.max_age_seconds == "infinity" else self.max_age_seconds, "max_size": self.max_size, "current_size": self._state.current_size, "cache_set_data": self.cache_set_data, "tracked_keys": len(self._state.key_insert_times), "cached_keys": len(self._state.cache_order), } def cache_stats(self) -> dict[str, Any]: """Return cache performance statistics.""" total_requests = self._state.hits + self._state.misses hit_rate = self._state.hits / total_requests if total_requests > 0 else 0.0 return { "hits": self._state.hits, "misses": self._state.misses, "evictions": self._state.evictions, "total_requests": total_requests, "hit_rate": hit_rate, } async def clear_cache(self) -> None: """Clear all cached data and tracking information.""" # Clear the cache store if it supports clear if hasattr(self._cache, "clear"): await self._cache.clear() # Reset tracking async with self._state.lock: self._state.key_insert_times.clear() self._state.cache_order.clear() self._state.key_sizes.clear() self._state.range_cache.clear() self._state.current_size = 0 def __repr__(self) -> str: """Return string representation of the cache store.""" return ( f"{self.__class__.__name__}(" f"store={self._store!r}, " f"cache_store={self._cache!r}, " f"max_age_seconds={self.max_age_seconds}, " f"max_size={self.max_size}, " f"current_size={self._state.current_size}, " f"cached_keys={len(self._state.cache_order)})" ) zarr-python-3.2.1/src/zarr/metadata/000077500000000000000000000000001517635743000173525ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/metadata/__init__.py000066400000000000000000000000001517635743000214510ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/metadata/migrate_v3.py000066400000000000000000000244211517635743000217670ustar00rootroot00000000000000import asyncio import logging from typing import cast import numcodecs.abc import zarr from zarr import Group from zarr.abc.codec import ArrayArrayCodec, BytesBytesCodec, Codec from zarr.abc.store import Store from zarr.codecs.blosc import BloscCodec, BloscShuffle from zarr.codecs.bytes import BytesCodec from zarr.codecs.gzip import GzipCodec from zarr.codecs.transpose import TransposeCodec from zarr.codecs.zstd import ZstdCodec from zarr.core.buffer.core import default_buffer_prototype from zarr.core.chunk_key_encodings import V2ChunkKeyEncoding from zarr.core.common import ( ZARR_JSON, ZARRAY_JSON, ZATTRS_JSON, ZGROUP_JSON, ZMETADATA_V2_JSON, ZarrFormat, ) from zarr.core.dtype.common import HasEndianness from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType from zarr.core.group import GroupMetadata from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGridMetadata from zarr.core.sync import sync from zarr.registry import get_codec_class from zarr.storage import StorePath from zarr.types import AnyArray _logger = logging.getLogger(__name__) def migrate_v2_to_v3( *, input_store: Store, output_store: Store | None = None, dry_run: bool = False, ) -> None: """Migrate all v2 metadata in a Zarr store to v3. This will create a zarr.json file at each level of a Zarr hierarchy (for every group / array). v2 files (.zarray, .zattrs etc.) will be left as-is. Parameters ---------- input_store : Store Input Zarr to migrate. output_store : Store, optional Output location to write v3 metadata (no array data will be copied). If not provided, v3 metadata will be written to input_store. dry_run : bool, optional Enable a 'dry run' - files that would be created are logged, but no files are created or changed. """ zarr_v2 = zarr.open(store=input_store, mode="r+") if output_store is not None: # w- access to not allow overwrite of existing data output_path = sync(StorePath.open(output_store, path="", mode="w-")) else: output_path = zarr_v2.store_path migrate_to_v3(zarr_v2, output_path, dry_run=dry_run) def migrate_to_v3(zarr_v2: AnyArray | Group, output_path: StorePath, dry_run: bool = False) -> None: """Migrate all v2 metadata in a Zarr array/group to v3. Note - if a group is provided, then all arrays / groups within this group will also be converted. A zarr.json file will be created for each level and written to output_path, with any v2 files (.zarray, .zattrs etc.) left as-is. Parameters ---------- zarr_v2 : Array | Group An array or group with zarr_format = 2 output_path : StorePath The store path to write generated v3 metadata to. dry_run : bool, optional Enable a 'dry run' - files that would be created are logged, but no files are created or changed. """ if not zarr_v2.metadata.zarr_format == 2: raise TypeError("Only arrays / groups with zarr v2 metadata can be converted") if isinstance(zarr_v2.metadata, GroupMetadata): _convert_group(zarr_v2, output_path, dry_run) else: _convert_array(zarr_v2, output_path, dry_run) async def remove_metadata( store: Store, zarr_format: ZarrFormat, force: bool = False, dry_run: bool = False, ) -> None: """Remove all v2 (.zarray, .zattrs, .zgroup, .zmetadata) or v3 (zarr.json) metadata files from the given Zarr. Note - this will remove metadata files at all levels of the hierarchy (every group and array). Parameters ---------- store : Store Zarr to remove metadata from. zarr_format : ZarrFormat Which format's metadata to remove - 2 or 3. force : bool, optional When False, metadata can only be removed if a valid alternative exists e.g. deletion of v2 metadata will only be allowed when v3 metadata is also present. When True, metadata can be removed when there is no alternative. dry_run : bool, optional Enable a 'dry run' - files that would be deleted are logged, but no files are removed or changed. """ if not store.supports_deletes: raise ValueError("Store must support deletes to remove metadata") store_path = await StorePath.open(store, path="", mode="r+") metadata_files_all = { 2: [ZARRAY_JSON, ZATTRS_JSON, ZGROUP_JSON, ZMETADATA_V2_JSON], 3: [ZARR_JSON], } if zarr_format == 2: alternative_metadata = 3 else: alternative_metadata = 2 awaitables = [] async for file_path in store.list(): parent_path, _, file_name = file_path.rpartition("/") if file_name not in metadata_files_all[zarr_format]: continue if force or await _metadata_exists( cast(ZarrFormat, alternative_metadata), store_path / parent_path ): _logger.info("Deleting metadata at %s", store_path / file_path) if not dry_run: awaitables.append((store_path / file_path).delete()) else: raise ValueError( f"Cannot remove v{zarr_format} metadata at {store_path / file_path} - no v{alternative_metadata} " "metadata exists. To delete anyway, use the 'force' option." ) await asyncio.gather(*awaitables) def _convert_group(zarr_v2: Group, output_path: StorePath, dry_run: bool) -> None: if zarr_v2.metadata.consolidated_metadata is not None: raise NotImplementedError("Migration of consolidated metadata isn't supported.") # process members of the group for key in zarr_v2: migrate_to_v3(zarr_v2[key], output_path=output_path / key, dry_run=dry_run) # write group's converted metadata group_metadata_v3 = GroupMetadata( attributes=zarr_v2.metadata.attributes, zarr_format=3, consolidated_metadata=None ) sync(_save_v3_metadata(group_metadata_v3, output_path, dry_run=dry_run)) def _convert_array(zarr_v2: AnyArray, output_path: StorePath, dry_run: bool) -> None: array_metadata_v3 = _convert_array_metadata(cast(ArrayV2Metadata, zarr_v2.metadata)) sync(_save_v3_metadata(array_metadata_v3, output_path, dry_run=dry_run)) async def _metadata_exists(zarr_format: ZarrFormat, store_path: StorePath) -> bool: metadata_files_required = {2: [ZARRAY_JSON, ZGROUP_JSON], 3: [ZARR_JSON]} for metadata_file in metadata_files_required[zarr_format]: if await (store_path / metadata_file).exists(): return True return False def _convert_array_metadata(metadata_v2: ArrayV2Metadata) -> ArrayV3Metadata: chunk_key_encoding = V2ChunkKeyEncoding(separator=metadata_v2.dimension_separator) codecs: list[Codec] = [] # array-array codecs if metadata_v2.order == "F": # F is equivalent to order: n-1, ... 1, 0 codecs.append(TransposeCodec(order=list(range(len(metadata_v2.shape) - 1, -1, -1)))) if metadata_v2.filters is not None: codecs.extend(_convert_filters(metadata_v2.filters)) # array-bytes codecs if not isinstance(metadata_v2.dtype, HasEndianness): codecs.append(BytesCodec(endian=None)) else: codecs.append(BytesCodec(endian=metadata_v2.dtype.endianness)) # bytes-bytes codecs if metadata_v2.compressor is not None: bytes_bytes_codec = _convert_compressor(metadata_v2.compressor, metadata_v2.dtype) codecs.append(bytes_bytes_codec) return ArrayV3Metadata( shape=metadata_v2.shape, data_type=metadata_v2.dtype, chunk_grid=RegularChunkGridMetadata(chunk_shape=metadata_v2.chunks), chunk_key_encoding=chunk_key_encoding, fill_value=metadata_v2.fill_value, codecs=codecs, attributes=metadata_v2.attributes, dimension_names=None, storage_transformers=None, ) def _convert_filters(filters: tuple[numcodecs.abc.Codec, ...]) -> list[ArrayArrayCodec]: filters_codecs = [_find_numcodecs_zarr3(filter) for filter in filters] for codec in filters_codecs: if not isinstance(codec, ArrayArrayCodec): raise TypeError(f"Filter {type(codec)} is not an ArrayArrayCodec") return cast(list[ArrayArrayCodec], filters_codecs) def _convert_compressor( compressor: numcodecs.abc.Codec, dtype: ZDType[TBaseDType, TBaseScalar] ) -> BytesBytesCodec: match compressor.codec_id: case "blosc": return BloscCodec( typesize=dtype.to_native_dtype().itemsize, cname=compressor.cname, clevel=compressor.clevel, shuffle=BloscShuffle.from_int(compressor.shuffle), blocksize=compressor.blocksize, ) case "zstd": return ZstdCodec( level=compressor.level, checksum=compressor.checksum, ) case "gzip": return GzipCodec(level=compressor.level) case _: # If possible, find matching zarr.codecs.numcodecs codec compressor_codec = _find_numcodecs_zarr3(compressor) if not isinstance(compressor_codec, BytesBytesCodec): raise TypeError(f"Compressor {type(compressor_codec)} is not a BytesBytesCodec") return compressor_codec def _find_numcodecs_zarr3(numcodecs_codec: numcodecs.abc.Codec) -> Codec: """Find matching zarr.codecs.numcodecs codec (if it exists)""" numcodec_name = f"numcodecs.{numcodecs_codec.codec_id}" numcodec_dict = { "name": numcodec_name, "configuration": numcodecs_codec.get_config(), } try: codec_v3 = get_codec_class(numcodec_name) except KeyError as exc: raise ValueError( f"Couldn't find corresponding zarr.codecs.numcodecs codec for {numcodecs_codec.codec_id}" ) from exc return codec_v3.from_dict(numcodec_dict) async def _save_v3_metadata( metadata_v3: ArrayV3Metadata | GroupMetadata, output_path: StorePath, dry_run: bool = False ) -> None: zarr_json_path = output_path / ZARR_JSON if await zarr_json_path.exists(): raise ValueError(f"{ZARR_JSON} already exists at {zarr_json_path}") _logger.info("Saving metadata to %s", zarr_json_path) to_save = metadata_v3.to_buffer_dict(default_buffer_prototype()) if not dry_run: await zarr_json_path.set_if_not_exists(to_save[ZARR_JSON]) zarr-python-3.2.1/src/zarr/py.typed000066400000000000000000000000001517635743000172570ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/registry.py000066400000000000000000000262111517635743000200160ustar00rootroot00000000000000from __future__ import annotations import warnings from collections import defaultdict from importlib.metadata import entry_points as get_entry_points from typing import TYPE_CHECKING, Any from zarr.core.config import BadConfigError, config from zarr.core.dtype import data_type_registry from zarr.errors import ZarrUserWarning if TYPE_CHECKING: from importlib.metadata import EntryPoint from zarr.abc.codec import ( ArrayArrayCodec, ArrayBytesCodec, BytesBytesCodec, Codec, CodecJSON_V2, CodecPipeline, ) from zarr.abc.numcodec import Numcodec from zarr.core.buffer import Buffer, NDBuffer from zarr.core.chunk_key_encodings import ChunkKeyEncoding from zarr.core.common import JSON __all__ = [ "Registry", "get_buffer_class", "get_chunk_key_encoding_class", "get_codec_class", "get_ndbuffer_class", "get_pipeline_class", "register_buffer", "register_chunk_key_encoding", "register_codec", "register_ndbuffer", "register_pipeline", ] class Registry[T](dict[str, type[T]]): def __init__(self) -> None: super().__init__() self.lazy_load_list: list[EntryPoint] = [] def lazy_load(self, use_entrypoint_name: bool = False) -> None: for e in self.lazy_load_list: self.register(e.load(), qualname=e.name if use_entrypoint_name else None) self.lazy_load_list.clear() def register(self, cls: type[T], qualname: str | None = None) -> None: if qualname is None: qualname = fully_qualified_name(cls) self[qualname] = cls _codec_registries: dict[str, Registry[Codec]] = defaultdict(Registry) _pipeline_registry: Registry[CodecPipeline] = Registry() _buffer_registry: Registry[Buffer] = Registry() _ndbuffer_registry: Registry[NDBuffer] = Registry() _chunk_key_encoding_registry: Registry[ChunkKeyEncoding] = Registry() """ The registry module is responsible for managing implementations of codecs, pipelines, buffers, ndbuffers, and chunk key encodings and collecting them from entrypoints. The implementation used is determined by the config. The registry module is also responsible for managing dtypes. """ def _collect_entrypoints() -> list[Registry[Any]]: """ Collects codecs, pipelines, dtypes, buffers and ndbuffers from entrypoints. Entry points can either be single items or groups of items. Allowed syntax for entry_points.txt is e.g. [zarr.codecs] gzip = package:EntrypointGzipCodec1 [zarr.codecs.gzip] some_name = package:EntrypointGzipCodec2 another = package:EntrypointGzipCodec3 [zarr] buffer = package:TestBuffer1 [zarr.buffer] xyz = package:TestBuffer2 abc = package:TestBuffer3 ... """ entry_points = get_entry_points() _buffer_registry.lazy_load_list.extend(entry_points.select(group="zarr.buffer")) _buffer_registry.lazy_load_list.extend(entry_points.select(group="zarr", name="buffer")) _ndbuffer_registry.lazy_load_list.extend(entry_points.select(group="zarr.ndbuffer")) _ndbuffer_registry.lazy_load_list.extend(entry_points.select(group="zarr", name="ndbuffer")) data_type_registry._lazy_load_list.extend(entry_points.select(group="zarr.data_type")) data_type_registry._lazy_load_list.extend(entry_points.select(group="zarr", name="data_type")) _chunk_key_encoding_registry.lazy_load_list.extend( entry_points.select(group="zarr.chunk_key_encoding") ) _chunk_key_encoding_registry.lazy_load_list.extend( entry_points.select(group="zarr", name="chunk_key_encoding") ) _pipeline_registry.lazy_load_list.extend(entry_points.select(group="zarr.codec_pipeline")) _pipeline_registry.lazy_load_list.extend( entry_points.select(group="zarr", name="codec_pipeline") ) for e in entry_points.select(group="zarr.codecs"): _codec_registries[e.name].lazy_load_list.append(e) for group in entry_points.groups: if group.startswith("zarr.codecs."): codec_name = group.split(".")[2] _codec_registries[codec_name].lazy_load_list.extend(entry_points.select(group=group)) return [ *_codec_registries.values(), _pipeline_registry, _buffer_registry, _ndbuffer_registry, _chunk_key_encoding_registry, ] def _reload_config() -> None: config.refresh() def fully_qualified_name(cls: type) -> str: module = cls.__module__ return f"{module}.{cls.__qualname__}" def register_codec(key: str, codec_cls: type[Codec], *, qualname: str | None = None) -> None: if key not in _codec_registries: _codec_registries[key] = Registry() _codec_registries[key].register(codec_cls, qualname=qualname) def register_pipeline(pipe_cls: type[CodecPipeline]) -> None: _pipeline_registry.register(pipe_cls) def register_ndbuffer(cls: type[NDBuffer], qualname: str | None = None) -> None: _ndbuffer_registry.register(cls, qualname) def register_buffer(cls: type[Buffer], qualname: str | None = None) -> None: _buffer_registry.register(cls, qualname) def register_chunk_key_encoding(key: str, cls: type) -> None: _chunk_key_encoding_registry.register(cls, key) def get_codec_class(key: str, reload_config: bool = False) -> type[Codec]: if reload_config: _reload_config() if key in _codec_registries: # logger.debug("Auto loading codec '%s' from entrypoint", codec_id) _codec_registries[key].lazy_load() codec_classes = _codec_registries[key] if not codec_classes: raise KeyError(key) config_entry = config.get("codecs", {}).get(key) if config_entry is None: if len(codec_classes) == 1: return next(iter(codec_classes.values())) warnings.warn( f"Codec '{key}' not configured in config. Selecting any implementation.", stacklevel=2, category=ZarrUserWarning, ) return list(codec_classes.values())[-1] selected_codec_cls = codec_classes[config_entry] if selected_codec_cls: return selected_codec_cls raise KeyError(key) def _resolve_codec(data: dict[str, JSON]) -> Codec: """ Get a codec instance from a dict representation of that codec. """ # TODO: narrow the type of the input to only those dicts that map on to codec class instances. return get_codec_class(data["name"]).from_dict(data) # type: ignore[arg-type] def _parse_bytes_bytes_codec(data: dict[str, JSON] | Codec) -> BytesBytesCodec: """ Normalize the input to a ``BytesBytesCodec`` instance. If the input is already a ``BytesBytesCodec``, it is returned as is. If the input is a dict, it is converted to a ``BytesBytesCodec`` instance via the ``_resolve_codec`` function. """ from zarr.abc.codec import BytesBytesCodec if isinstance(data, dict): result = _resolve_codec(data) if not isinstance(result, BytesBytesCodec): msg = f"Expected a dict representation of a BytesBytesCodec; got a dict representation of a {type(result)} instead." raise TypeError(msg) else: if not isinstance(data, BytesBytesCodec): raise TypeError(f"Expected a BytesBytesCodec. Got {type(data)} instead.") result = data return result def _parse_array_bytes_codec(data: dict[str, JSON] | Codec) -> ArrayBytesCodec: """ Normalize the input to a ``ArrayBytesCodec`` instance. If the input is already a ``ArrayBytesCodec``, it is returned as is. If the input is a dict, it is converted to a ``ArrayBytesCodec`` instance via the ``_resolve_codec`` function. """ from zarr.abc.codec import ArrayBytesCodec if isinstance(data, dict): result = _resolve_codec(data) if not isinstance(result, ArrayBytesCodec): msg = f"Expected a dict representation of an ArrayBytesCodec; got a dict representation of a {type(result)} instead." raise TypeError(msg) else: if not isinstance(data, ArrayBytesCodec): raise TypeError(f"Expected an ArrayBytesCodec. Got {type(data)} instead.") result = data return result def _parse_array_array_codec(data: dict[str, JSON] | Codec) -> ArrayArrayCodec: """ Normalize the input to a ``ArrayArrayCodec`` instance. If the input is already a ``ArrayArrayCodec``, it is returned as is. If the input is a dict, it is converted to a ``ArrayArrayCodec`` instance via the ``_resolve_codec`` function. """ from zarr.abc.codec import ArrayArrayCodec if isinstance(data, dict): result = _resolve_codec(data) if not isinstance(result, ArrayArrayCodec): msg = f"Expected a dict representation of an ArrayArrayCodec; got a dict representation of a {type(result)} instead." raise TypeError(msg) else: if not isinstance(data, ArrayArrayCodec): raise TypeError(f"Expected an ArrayArrayCodec. Got {type(data)} instead.") result = data return result def get_pipeline_class(reload_config: bool = False) -> type[CodecPipeline]: if reload_config: _reload_config() _pipeline_registry.lazy_load() path = config.get("codec_pipeline.path") pipeline_class = _pipeline_registry.get(path) if pipeline_class: return pipeline_class raise BadConfigError( f"Pipeline class '{path}' not found in registered pipelines: {list(_pipeline_registry)}." ) def get_buffer_class(reload_config: bool = False) -> type[Buffer]: if reload_config: _reload_config() _buffer_registry.lazy_load() path = config.get("buffer") buffer_class = _buffer_registry.get(path) if buffer_class: return buffer_class raise BadConfigError( f"Buffer class '{path}' not found in registered buffers: {list(_buffer_registry)}." ) def get_ndbuffer_class(reload_config: bool = False) -> type[NDBuffer]: if reload_config: _reload_config() _ndbuffer_registry.lazy_load() path = config.get("ndbuffer") ndbuffer_class = _ndbuffer_registry.get(path) if ndbuffer_class: return ndbuffer_class raise BadConfigError( f"NDBuffer class '{path}' not found in registered buffers: {list(_ndbuffer_registry)}." ) def get_chunk_key_encoding_class(key: str) -> type[ChunkKeyEncoding]: _chunk_key_encoding_registry.lazy_load(use_entrypoint_name=True) if key not in _chunk_key_encoding_registry: raise KeyError( f"Chunk key encoding '{key}' not found in registered chunk key encodings: {list(_chunk_key_encoding_registry)}." ) return _chunk_key_encoding_registry[key] _collect_entrypoints() def get_numcodec(data: CodecJSON_V2[str]) -> Numcodec: """ Resolve a numcodec codec from the numcodecs registry. This requires the Numcodecs package to be installed. Parameters ---------- data : CodecJSON_V2 The JSON metadata for the codec. Returns ------- codec : Numcodec Examples -------- ```python from zarr.registry import get_numcodec codec = get_numcodec({'id': 'zlib', 'level': 1}) codec # Zlib(level=1) ``` """ from numcodecs.registry import get_codec return get_codec(data) # type: ignore[no-any-return] zarr-python-3.2.1/src/zarr/storage/000077500000000000000000000000001517635743000172365ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/storage/__init__.py000066400000000000000000000025411517635743000213510ustar00rootroot00000000000000import sys import warnings from types import ModuleType from typing import Any from zarr.errors import ZarrDeprecationWarning from zarr.storage._common import StoreLike, StorePath from zarr.storage._fsspec import FsspecStore from zarr.storage._local import LocalStore from zarr.storage._logging import LoggingStore from zarr.storage._memory import GpuMemoryStore, ManagedMemoryStore, MemoryStore from zarr.storage._obstore import ObjectStore from zarr.storage._wrapper import WrapperStore from zarr.storage._zip import ZipStore __all__ = [ "FsspecStore", "GpuMemoryStore", "LocalStore", "LoggingStore", "ManagedMemoryStore", "MemoryStore", "ObjectStore", "StoreLike", "StorePath", "WrapperStore", "ZipStore", ] class VerboseModule(ModuleType): def __setattr__(self, attr: str, value: Any) -> None: if attr == "default_compressor": warnings.warn( "setting zarr.storage.default_compressor is deprecated, use " "zarr.config to configure array.v2_default_compressor " "e.g. config.set({'codecs.zstd':'numcodecs.Zstd', 'array.v2_default_compressor.numeric': 'zstd'})", ZarrDeprecationWarning, stacklevel=1, ) else: super().__setattr__(attr, value) sys.modules[__name__].__class__ = VerboseModule zarr-python-3.2.1/src/zarr/storage/_common.py000066400000000000000000000534771517635743000212570ustar00rootroot00000000000000from __future__ import annotations import importlib.util import json from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Self from zarr.abc.store import ( ByteRequest, Store, SupportsDeleteSync, SupportsGetSync, SupportsSetSync, ) from zarr.core.buffer import Buffer, default_buffer_prototype from zarr.core.common import ( ANY_ACCESS_MODE, ZARR_JSON, ZARRAY_JSON, ZGROUP_JSON, AccessModeLiteral, ZarrFormat, ) from zarr.errors import ContainsArrayAndGroupError, ContainsArrayError, ContainsGroupError from zarr.storage._local import LocalStore from zarr.storage._memory import ManagedMemoryStore, MemoryStore from zarr.storage._utils import _join_paths, normalize_path, parse_store_url _has_fsspec = importlib.util.find_spec("fsspec") if _has_fsspec: from fsspec.mapping import FSMap else: FSMap = None if TYPE_CHECKING: from zarr.core.buffer import BufferPrototype class StorePath: """ Path-like interface for a Store. Parameters ---------- store : Store The store to use. path : str The path within the store. """ store: Store path: str def __init__(self, store: Store, path: str = "") -> None: self.store = store self.path = normalize_path(path) @property def read_only(self) -> bool: return self.store.read_only @classmethod async def _create_open_instance(cls, store: Store, path: str) -> Self: """Helper to create and return a StorePath instance.""" await store._ensure_open() return cls(store, path) @classmethod async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = None) -> Self: """ Open StorePath based on the provided mode. * If the mode is None, return an opened version of the store with no changes. * If the mode is 'r+', 'w-', 'w', or 'a' and the store is read-only, raise a ValueError. * If the mode is 'r' and the store is not read-only, return a copy of the store with read_only set to True. * If the mode is 'w-' and the store is not read-only and the StorePath contains keys, raise a FileExistsError. * If the mode is 'w' and the store is not read-only, delete all keys nested within the StorePath. Parameters ---------- mode : AccessModeLiteral The mode to use when initializing the store path. The accepted values are: - ``'r'``: read only (must exist) - ``'r+'``: read/write (must exist) - ``'a'``: read/write (create if doesn't exist) - ``'w'``: read/write (overwrite if exists) - ``'w-'``: read/write (create if doesn't exist). Raises ------ FileExistsError If the mode is 'w-' and the store path already exists. ValueError If the mode is not "r" and the store is read-only, or """ # fastpath if mode is None if mode is None: return await cls._create_open_instance(store, path) if mode not in ANY_ACCESS_MODE: raise ValueError(f"Invalid mode: {mode}, expected one of {ANY_ACCESS_MODE}") if store.read_only: # Don't allow write operations on a read-only store if mode != "r": raise ValueError( f"Store is read-only but mode is {mode!r}. Create a writable store or use 'r' mode." ) self = await cls._create_open_instance(store, path) elif mode == "r": # Create read-only copy for read mode on writable store try: read_only_store = store.with_read_only(True) except NotImplementedError as e: raise ValueError( "Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store. " "Please use a read-only store or a storage class that implements .with_read_only()." ) from e self = await cls._create_open_instance(read_only_store, path) else: # writable store and writable mode self = await cls._create_open_instance(store, path) # Handle mode-specific operations match mode: case "w-": if not await self.is_empty(): raise FileExistsError( f"Cannot create '{path}' with mode 'w-' because it already contains data. " f"Use mode 'w' to overwrite or 'a' to append." ) case "w": await self.delete_dir() return self async def get( self, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: """ Read bytes from the store. Parameters ---------- prototype : BufferPrototype, optional The buffer prototype to use when reading the bytes. byte_range : ByteRequest, optional The range of bytes to read. Returns ------- buffer : Buffer or None The read bytes, or None if the key does not exist. """ if prototype is None: prototype = default_buffer_prototype() return await self.store.get(self.path, prototype=prototype, byte_range=byte_range) async def set(self, value: Buffer) -> None: """ Write bytes to the store. Parameters ---------- value : Buffer The buffer to write. """ await self.store.set(self.path, value) async def delete(self) -> None: """ Delete the key from the store. Raises ------ NotImplementedError If the store does not support deletion. """ await self.store.delete(self.path) async def delete_dir(self) -> None: """ Delete all keys with the given prefix from the store. """ await self.store.delete_dir(self.path) async def set_if_not_exists(self, default: Buffer) -> None: """ Store a key to ``value`` if the key is not already present. Parameters ---------- default : Buffer The buffer to store if the key is not already present. """ await self.store.set_if_not_exists(self.path, default) async def exists(self) -> bool: """ Check if the key exists in the store. Returns ------- bool True if the key exists in the store, False otherwise. """ return await self.store.exists(self.path) async def is_empty(self) -> bool: """ Check if any keys exist in the store with the given prefix. Returns ------- bool True if no keys exist in the store with the given prefix, False otherwise. """ return await self.store.is_empty(self.path) # ------------------------------------------------------------------- # Synchronous IO delegation # ------------------------------------------------------------------- def get_sync( self, *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: """Synchronous read — delegates to ``self.store.get_sync(self.path, ...)``.""" if not isinstance(self.store, SupportsGetSync): raise TypeError(f"Store {type(self.store).__name__} does not support synchronous get.") if prototype is None: prototype = default_buffer_prototype() return self.store.get_sync(self.path, prototype=prototype, byte_range=byte_range) def set_sync(self, value: Buffer) -> None: """Synchronous write — delegates to ``self.store.set_sync(self.path, value)``.""" if not isinstance(self.store, SupportsSetSync): raise TypeError(f"Store {type(self.store).__name__} does not support synchronous set.") self.store.set_sync(self.path, value) def delete_sync(self) -> None: """Synchronous delete — delegates to ``self.store.delete_sync(self.path)``.""" if not isinstance(self.store, SupportsDeleteSync): raise TypeError( f"Store {type(self.store).__name__} does not support synchronous delete." ) self.store.delete_sync(self.path) def __truediv__(self, other: str) -> StorePath: """Combine this store path with another path""" return self.__class__(self.store, _join_paths([self.path, other])) def __str__(self) -> str: return _join_paths([str(self.store), self.path]) def __repr__(self) -> str: return f"StorePath({self.store.__class__.__name__}, '{self}')" def __eq__(self, other: object) -> bool: """ Check if two StorePath objects are equal. Returns ------- bool True if the two objects are equal, False otherwise. Notes ----- Two StorePath objects are considered equal if their stores are equal and their paths are equal. """ try: return self.store == other.store and self.path == other.path # type: ignore[attr-defined, no-any-return] except Exception: pass return False type StoreLike = Store | StorePath | FSMap | Path | str | dict[str, Buffer] async def make_store( store_like: StoreLike | None, *, mode: AccessModeLiteral | None = None, storage_options: dict[str, Any] | None = None, ) -> Store: """ Convert a `StoreLike` object into a Store object. `StoreLike` objects are converted to `Store` as follows: - `Store` or `StorePath` = `Store` object. - `Path` or `str` = `LocalStore` object. - `str` that starts with a protocol = `FsspecStore` object. - `dict[str, Buffer]` = `MemoryStore` object. - `None` = `MemoryStore` object. - `FSMap` = `FsspecStore` object. Parameters ---------- store_like : StoreLike | None The `StoreLike` object to convert to a `Store` object. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. mode : StoreAccessMode | None, optional The mode to use when creating the `Store` object. If None, the default mode is 'r'. storage_options : dict[str, Any] | None, optional The storage options to use when creating the `RemoteStore` object. If None, the default storage options are used. Returns ------- Store The converted Store object. Raises ------ TypeError If the StoreLike object is not one of the supported types, or if storage_options is provided but not used. """ from zarr.storage._fsspec import FsspecStore # circular import # Parse URL early so we can reuse the result for both validation and routing parsed = parse_store_url(store_like) if isinstance(store_like, str) else None # Check if storage_options is valid for this store_like if storage_options is not None: is_fsspec_uri = parsed is not None and parsed.scheme not in ("", "memory", "file") if not is_fsspec_uri: raise TypeError( "'storage_options' was provided but unused. " "'storage_options' is only used when the store is passed as an FSSpec URI string.", ) assert mode in (None, "r", "r+", "a", "w", "w-") _read_only = mode == "r" if isinstance(store_like, StorePath): # Get underlying store return store_like.store elif isinstance(store_like, Store): # Already a Store return store_like elif isinstance(store_like, dict): # Already a dictionary that can be a MemoryStore # # We deliberate only consider dict[str, Buffer] here, and not arbitrary mutable mappings. # By only allowing dictionaries, which are in-memory, we know that MemoryStore appropriate. return await MemoryStore.open(store_dict=store_like, read_only=_read_only) elif store_like is None: # Create a new in-memory store return await make_store({}, mode=mode, storage_options=storage_options) elif isinstance(store_like, Path): # Create a new LocalStore return await LocalStore.open(root=store_like, mode=mode, read_only=_read_only) elif isinstance(store_like, str) and parsed is not None: if parsed.scheme == "memory" and not _has_fsspec: # Create or get a ManagedMemoryStore return ManagedMemoryStore(name=parsed.name, path=parsed.path, read_only=_read_only) elif parsed.scheme == "file" or not parsed.scheme: # Local filesystem path — use parsed.path to strip the file:// scheme return await make_store(Path(parsed.path), mode=mode, storage_options=storage_options) else: # Assume fsspec can handle it (s3://, gs://, http://, etc.) return FsspecStore.from_url( store_like, storage_options=storage_options, read_only=_read_only ) elif _has_fsspec and isinstance(store_like, FSMap): return FsspecStore.from_mapper(store_like, read_only=_read_only) else: raise TypeError(f"Unsupported type for store_like: '{type(store_like).__name__}'") async def make_store_path( store_like: StoreLike | None, *, path: str | None = "", mode: AccessModeLiteral | None = None, storage_options: dict[str, Any] | None = None, ) -> StorePath: """ Convert a `StoreLike` object into a StorePath object. This function takes a `StoreLike` object and returns a `StorePath` object. See `make_store` for details of which `Store` is used for each type of `store_like` object. Parameters ---------- store_like : StoreLike or None, default=None The `StoreLike` object to convert to a `StorePath` object. See the [storage documentation in the user guide][user-guide-store-like] for a description of all valid StoreLike values. path : str | None, optional The path to use when creating the `StorePath` object. If None, the default path is the empty string. mode : StoreAccessMode | None, optional The mode to use when creating the `StorePath` object. If None, the default mode is 'r'. storage_options : dict[str, Any] | None, optional The storage options to use when creating the `RemoteStore` object. If None, the default storage options are used. Returns ------- StorePath The converted StorePath object. Raises ------ TypeError If the StoreLike object is not one of the supported types, or if storage_options is provided but not used. ValueError If path is provided for a store that does not support it. See Also -------- make_store """ path_normalized = normalize_path(path) if isinstance(store_like, StorePath): # Already a StorePath if storage_options: raise TypeError( "'storage_options' was provided but unused. " "'storage_options' is only used when the store is passed as an FSSpec URI string.", ) return store_like / path_normalized elif _has_fsspec and isinstance(store_like, FSMap) and path: raise ValueError( "'path' was provided but is not used for FSMap store_like objects. Specify the path when creating the FSMap instance instead." ) else: store = await make_store(store_like, mode=mode, storage_options=storage_options) return await StorePath.open(store, path=path_normalized, mode=mode) async def ensure_no_existing_node( store_path: StorePath, zarr_format: ZarrFormat, node_type: Literal["array", "group"] | None = None, ) -> None: """ Check if a store_path is safe for array / group creation. Returns `None` or raises an exception. Parameters ---------- store_path : StorePath The storage location to check. zarr_format : ZarrFormat The Zarr format to check. node_type : str | None, optional Raise an error if an "array", or "group" exists. By default (when None), raises an error for either. Raises ------ ContainsArrayError, ContainsGroupError, ContainsArrayAndGroupError """ if zarr_format == 2: extant_node = await _contains_node_v2(store_path) elif zarr_format == 3: extant_node = await _contains_node_v3(store_path) match extant_node: case "array": if node_type != "group": msg = f"An array exists in store {store_path.store!r} at path {store_path.path!r}." raise ContainsArrayError(msg) case "group": if node_type != "array": msg = f"A group exists in store {store_path.store!r} at path {store_path.path!r}." raise ContainsGroupError(msg) case "nothing": return case _: msg = f"Invalid value for extant_node: {extant_node}" # type: ignore[unreachable] raise ValueError(msg) async def _contains_node_v3(store_path: StorePath) -> Literal["array", "group", "nothing"]: """ Check if a store_path contains nothing, an array, or a group. This function returns the string "array", "group", or "nothing" to denote containing an array, a group, or nothing. Parameters ---------- store_path : StorePath The location in storage to check. Returns ------- Literal["array", "group", "nothing"] A string representing the zarr node found at store_path. """ result: Literal["array", "group", "nothing"] = "nothing" extant_meta_bytes = await (store_path / ZARR_JSON).get() # if no metadata document could be loaded, then we just return "nothing" if extant_meta_bytes is not None: try: extant_meta_json = json.loads(extant_meta_bytes.to_bytes()) # avoid constructing a full metadata document here in the name of speed. if extant_meta_json["node_type"] == "array": result = "array" elif extant_meta_json["node_type"] == "group": result = "group" except (KeyError, json.JSONDecodeError): # either of these errors is consistent with no array or group present. pass return result async def _contains_node_v2(store_path: StorePath) -> Literal["array", "group", "nothing"]: """ Check if a store_path contains nothing, an array, a group, or both. If both an array and a group are detected, a `ContainsArrayAndGroup` exception is raised. Otherwise, this function returns the string "array", "group", or "nothing" to denote containing an array, a group, or nothing. Parameters ---------- store_path : StorePath The location in storage to check. Returns ------- Literal["array", "group", "nothing"] A string representing the zarr node found at store_path. """ _array = await contains_array(store_path=store_path, zarr_format=2) _group = await contains_group(store_path=store_path, zarr_format=2) if _array and _group: msg = ( "Array and group metadata documents (.zarray and .zgroup) were both found in store " f"{store_path.store!r} at path {store_path.path!r}. " "Only one of these files may be present in a given directory / prefix. " "Remove the .zarray file, or the .zgroup file, or both." ) raise ContainsArrayAndGroupError(msg) elif _array: return "array" elif _group: return "group" else: return "nothing" async def contains_array(store_path: StorePath, zarr_format: ZarrFormat) -> bool: """ Check if an array exists at a given StorePath. Parameters ---------- store_path : StorePath The StorePath to check for an existing group. zarr_format : The zarr format to check for. Returns ------- bool True if the StorePath contains a group, False otherwise. """ if zarr_format == 3: extant_meta_bytes = await (store_path / ZARR_JSON).get() if extant_meta_bytes is None: return False else: try: extant_meta_json = json.loads(extant_meta_bytes.to_bytes()) # we avoid constructing a full metadata document here in the name of speed. if extant_meta_json["node_type"] == "array": return True except (ValueError, KeyError): return False elif zarr_format == 2: return await (store_path / ZARRAY_JSON).exists() msg = f"Invalid zarr_format provided. Got {zarr_format}, expected 2 or 3" raise ValueError(msg) async def contains_group(store_path: StorePath, zarr_format: ZarrFormat) -> bool: """ Check if a group exists at a given StorePath. Parameters ---------- store_path : StorePath The StorePath to check for an existing group. zarr_format : The zarr format to check for. Returns ------- bool True if the StorePath contains a group, False otherwise """ if zarr_format == 3: extant_meta_bytes = await (store_path / ZARR_JSON).get() if extant_meta_bytes is None: return False else: try: extant_meta_json = json.loads(extant_meta_bytes.to_bytes()) # we avoid constructing a full metadata document here in the name of speed. result: bool = extant_meta_json["node_type"] == "group" except (ValueError, KeyError): return False else: return result elif zarr_format == 2: return await (store_path / ZGROUP_JSON).exists() msg = f"Invalid zarr_format provided. Got {zarr_format}, expected 2 or 3" # type: ignore[unreachable] raise ValueError(msg) zarr-python-3.2.1/src/zarr/storage/_fsspec.py000066400000000000000000000345551517635743000212460ustar00rootroot00000000000000from __future__ import annotations import json import warnings from contextlib import suppress from typing import TYPE_CHECKING, Any from packaging.version import parse as parse_version from zarr.abc.store import ( ByteRequest, OffsetByteRequest, RangeByteRequest, Store, SuffixByteRequest, ) from zarr.core.buffer import Buffer from zarr.errors import ZarrUserWarning from zarr.storage._utils import _dereference_path if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterable from fsspec import AbstractFileSystem from fsspec.asyn import AsyncFileSystem from fsspec.mapping import FSMap from zarr.core.buffer import BufferPrototype ALLOWED_EXCEPTIONS: tuple[type[Exception], ...] = ( FileNotFoundError, IsADirectoryError, NotADirectoryError, ) def _make_async(fs: AbstractFileSystem) -> AsyncFileSystem: """Convert a sync FSSpec filesystem to an async FFSpec filesystem If the filesystem class supports async operations, a new async instance is created from the existing instance. If the filesystem class does not support async operations, the existing instance is wrapped with AsyncFileSystemWrapper. """ import fsspec fsspec_version = parse_version(fsspec.__version__) if fs.async_impl and fs.asynchronous: # Already an async instance of an async filesystem, nothing to do return fs if fs.async_impl: # Convert sync instance of an async fs to an async instance fs_dict = json.loads(fs.to_json()) fs_dict["asynchronous"] = True return fsspec.AbstractFileSystem.from_json(json.dumps(fs_dict)) if fsspec_version < parse_version("2024.12.0"): raise ImportError( f"The filesystem '{fs}' is synchronous, and the required " "AsyncFileSystemWrapper is not available. Upgrade fsspec to version " "2024.12.0 or later to enable this functionality." ) from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper return AsyncFileSystemWrapper(fs, asynchronous=True) class FsspecStore(Store): """ Store for remote data based on FSSpec. Parameters ---------- fs : AsyncFileSystem The Async FSSpec filesystem to use with this store. read_only : bool Whether the store is read-only path : str The root path of the store. This should be a relative path and must not include the filesystem scheme. allowed_exceptions : tuple[type[Exception], ...] When fetching data, these cases will be deemed to correspond to missing keys. Attributes ---------- fs allowed_exceptions supports_writes supports_deletes supports_listing Raises ------ TypeError If the Filesystem does not support async operations. ValueError If the path argument includes a scheme. Warns ----- ZarrUserWarning If the file system (fs) was not created with `asynchronous=True`. See Also -------- FsspecStore.from_upath FsspecStore.from_url """ # based on FSSpec supports_writes: bool = True supports_deletes: bool = True supports_listing: bool = True fs: AsyncFileSystem allowed_exceptions: tuple[type[Exception], ...] path: str def __init__( self, fs: AsyncFileSystem, read_only: bool = False, path: str = "/", allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS, ) -> None: super().__init__(read_only=read_only) self.fs = fs self.path = path self.allowed_exceptions = allowed_exceptions if not self.fs.async_impl: raise TypeError("Filesystem needs to support async operations.") if not self.fs.asynchronous: warnings.warn( f"fs ({fs}) was not created with `asynchronous=True`, this may lead to surprising behavior", category=ZarrUserWarning, stacklevel=2, ) @classmethod def from_upath( cls, upath: Any, read_only: bool = False, allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS, ) -> FsspecStore: """ Create an FsspecStore from a upath object. Parameters ---------- upath : UPath The upath to the root of the store. read_only : bool Whether the store is read-only, defaults to False. allowed_exceptions : tuple, optional The exceptions that are allowed to be raised when accessing the store. Defaults to ALLOWED_EXCEPTIONS. Returns ------- FsspecStore """ return cls( fs=upath.fs, path=upath.path.rstrip("/"), read_only=read_only, allowed_exceptions=allowed_exceptions, ) @classmethod def from_mapper( cls, fs_map: FSMap, read_only: bool = False, allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS, ) -> FsspecStore: """ Create an FsspecStore from an FSMap object. Parameters ---------- fs_map : FSMap Fsspec mutable mapping object. read_only : bool Whether the store is read-only, defaults to False. allowed_exceptions : tuple, optional The exceptions that are allowed to be raised when accessing the store. Defaults to ALLOWED_EXCEPTIONS. Returns ------- FsspecStore """ fs = _make_async(fs_map.fs) return cls( fs=fs, path=fs_map.root, read_only=read_only, allowed_exceptions=allowed_exceptions, ) @classmethod def from_url( cls, url: str, storage_options: dict[str, Any] | None = None, read_only: bool = False, allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS, ) -> FsspecStore: """ Create an FsspecStore from a URL. The type of store is determined from the URL scheme. Parameters ---------- url : str The URL to the root of the store. storage_options : dict, optional The options to pass to fsspec when creating the filesystem. read_only : bool Whether the store is read-only, defaults to False. allowed_exceptions : tuple, optional The exceptions that are allowed to be raised when accessing the store. Defaults to ALLOWED_EXCEPTIONS. Returns ------- FsspecStore """ try: from fsspec import url_to_fs except ImportError: # before fsspec==2024.3.1 from fsspec.core import url_to_fs opts = storage_options or {} opts = {"asynchronous": True, **opts} fs, path = url_to_fs(url, **opts) if not fs.async_impl: fs = _make_async(fs) return cls(fs=fs, path=path, read_only=read_only, allowed_exceptions=allowed_exceptions) def with_read_only(self, read_only: bool = False) -> FsspecStore: # docstring inherited return type(self)( fs=self.fs, path=self.path, allowed_exceptions=self.allowed_exceptions, read_only=read_only, ) async def clear(self) -> None: # docstring inherited try: for subpath in await self.fs._find(self.path, withdirs=True): if subpath != self.path: await self.fs._rm(subpath, recursive=True) except FileNotFoundError: pass def __repr__(self) -> str: return f"" def __eq__(self, other: object) -> bool: return ( isinstance(other, type(self)) and self.path == other.path and self.read_only == other.read_only and self.fs == other.fs ) async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None, ) -> Buffer | None: # docstring inherited if not self._is_open: await self._open() path = _dereference_path(self.path, key) try: if byte_range is None: value = prototype.buffer.from_bytes(await self.fs._cat_file(path)) elif isinstance(byte_range, RangeByteRequest): value = prototype.buffer.from_bytes( await self.fs._cat_file( path, start=byte_range.start, end=byte_range.end, ) ) elif isinstance(byte_range, OffsetByteRequest): value = prototype.buffer.from_bytes( await self.fs._cat_file(path, start=byte_range.offset, end=None) ) elif isinstance(byte_range, SuffixByteRequest): value = prototype.buffer.from_bytes( await self.fs._cat_file(path, start=-byte_range.suffix, end=None) ) else: raise ValueError(f"Unexpected byte_range, got {byte_range}.") except self.allowed_exceptions: return None except OSError as e: if "not satisfiable" in str(e): # this is an s3-specific condition we probably don't want to leak return prototype.buffer.from_bytes(b"") raise else: return value async def set( self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None, ) -> None: # docstring inherited if not self._is_open: await self._open() self._check_writable() if not isinstance(value, Buffer): raise TypeError( f"FsspecStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead." ) path = _dereference_path(self.path, key) # write data if byte_range: raise NotImplementedError await self.fs._pipe_file(path, value.to_bytes()) async def delete(self, key: str) -> None: # docstring inherited self._check_writable() path = _dereference_path(self.path, key) try: await self.fs._rm(path) except FileNotFoundError: pass except self.allowed_exceptions: pass async def delete_dir(self, prefix: str) -> None: # docstring inherited if not self.supports_deletes: raise NotImplementedError( "This method is only available for stores that support deletes." ) self._check_writable() path_to_delete = _dereference_path(self.path, prefix) with suppress(*self.allowed_exceptions): await self.fs._rm(path_to_delete, recursive=True) async def exists(self, key: str) -> bool: # docstring inherited path = _dereference_path(self.path, key) exists: bool = await self.fs._exists(path) return exists async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: # docstring inherited if key_ranges: # _cat_ranges expects a list of paths, start, and end ranges, so we need to reformat each ByteRequest. key_ranges = list(key_ranges) paths: list[str] = [] starts: list[int | None] = [] stops: list[int | None] = [] for key, byte_range in key_ranges: paths.append(_dereference_path(self.path, key)) if byte_range is None: starts.append(None) stops.append(None) elif isinstance(byte_range, RangeByteRequest): starts.append(byte_range.start) stops.append(byte_range.end) elif isinstance(byte_range, OffsetByteRequest): starts.append(byte_range.offset) stops.append(None) elif isinstance(byte_range, SuffixByteRequest): starts.append(-byte_range.suffix) stops.append(None) else: raise ValueError(f"Unexpected byte_range, got {byte_range}.") else: return [] # TODO: expectations for exceptions or missing keys? res = await self.fs._cat_ranges(paths, starts, stops, on_error="return") # the following is an s3-specific condition we probably don't want to leak res = [b"" if (isinstance(r, OSError) and "not satisfiable" in str(r)) else r for r in res] for r in res: if isinstance(r, Exception) and not isinstance(r, self.allowed_exceptions): raise r return [None if isinstance(r, Exception) else prototype.buffer.from_bytes(r) for r in res] async def list(self) -> AsyncIterator[str]: # docstring inherited allfiles = await self.fs._find(self.path, detail=False, withdirs=False) for onefile in (a.removeprefix(f"{self.path}/") for a in allfiles): yield onefile async def list_dir(self, prefix: str) -> AsyncIterator[str]: # docstring inherited prefix = f"{self.path}/{prefix.rstrip('/')}" try: allfiles = await self.fs._ls(prefix, detail=False) except FileNotFoundError: return for onefile in (a.replace(f"{prefix}/", "") for a in allfiles): yield onefile.removeprefix(self.path).removeprefix("/") async def list_prefix(self, prefix: str) -> AsyncIterator[str]: # docstring inherited for onefile in await self.fs._find( f"{self.path}/{prefix}", detail=False, maxdepth=None, withdirs=False ): yield onefile.removeprefix(f"{self.path}/") async def getsize(self, key: str) -> int: path = _dereference_path(self.path, key) info = await self.fs._info(path) size = info.get("size") if size is None: # Not all filesystems support size. Fall back to reading the entire object return await super().getsize(key) else: # fsspec doesn't have typing. We'll need to assume or verify this is true return int(size) zarr-python-3.2.1/src/zarr/storage/_local.py000066400000000000000000000474661517635743000210620ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib import io import os import shutil import sys import uuid from pathlib import Path from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Self from zarr.abc.store import ( ByteRequest, OffsetByteRequest, RangeByteRequest, Store, SuffixByteRequest, ) from zarr.core.buffer import Buffer from zarr.core.buffer.core import default_buffer_prototype from zarr.core.common import AccessModeLiteral, concurrent_map if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterable, Iterator from zarr.core.buffer import BufferPrototype def _get(path: Path, prototype: BufferPrototype, byte_range: ByteRequest | None) -> Buffer: if byte_range is None: return prototype.buffer.from_bytes(path.read_bytes()) with path.open("rb") as f: size = f.seek(0, io.SEEK_END) if isinstance(byte_range, RangeByteRequest): f.seek(byte_range.start) return prototype.buffer.from_bytes(f.read(byte_range.end - f.tell())) elif isinstance(byte_range, OffsetByteRequest): f.seek(byte_range.offset) elif isinstance(byte_range, SuffixByteRequest): f.seek(max(0, size - byte_range.suffix)) else: raise TypeError(f"Unexpected byte_range, got {byte_range}.") return prototype.buffer.from_bytes(f.read()) if sys.platform == "win32": # Per the os.rename docs: # On Windows, if dst exists a FileExistsError is always raised. _safe_move = os.rename else: # On Unix, os.rename silently replace files, so instead we use os.link like # atomicwrites: # https://github.com/untitaker/python-atomicwrites/blob/1.4.1/atomicwrites/__init__.py#L59-L60 # This also raises FileExistsError if dst exists. def _safe_move(src: Path, dst: Path) -> None: os.link(src, dst) os.unlink(src) @contextlib.contextmanager def _atomic_write( path: Path, mode: Literal["r+b", "wb"], exclusive: bool = False, ) -> Iterator[BinaryIO]: tmp_path = path.with_suffix(f".{uuid.uuid4().hex}.partial") try: with tmp_path.open(mode) as f: yield f if exclusive: _safe_move(tmp_path, path) else: tmp_path.replace(path) except Exception: tmp_path.unlink(missing_ok=True) raise def _put(path: Path, value: Buffer, exclusive: bool = False) -> int: path.parent.mkdir(parents=True, exist_ok=True) # write takes any object supporting the buffer protocol view = value.as_buffer_like() with _atomic_write(path, "wb", exclusive=exclusive) as f: return f.write(view) class LocalStore(Store): """ Store for the local file system. Parameters ---------- root : str or Path Directory to use as root of store. read_only : bool Whether the store is read-only Attributes ---------- supports_writes supports_deletes supports_listing root """ supports_writes: bool = True supports_deletes: bool = True supports_listing: bool = True root: Path def __init__(self, root: Path | str, *, read_only: bool = False) -> None: super().__init__(read_only=read_only) if isinstance(root, str): root = Path(root) if not isinstance(root, Path): raise TypeError( f"'root' must be a string or Path instance. Got an instance of {type(root)} instead." ) self.root = root def with_read_only(self, read_only: bool = False) -> Self: # docstring inherited return type(self)( root=self.root, read_only=read_only, ) @classmethod async def open( cls, root: Path | str, *, read_only: bool = False, mode: AccessModeLiteral | None = None ) -> Self: """ Create and open the store. Parameters ---------- root : str or Path Directory to use as root of store. read_only : bool Whether the store is read-only mode : Mode in which to create the store. This only affects opening the store, and the final read-only state of the store is controlled through the read_only parameter. Returns ------- Store The opened store instance. """ # If mode = 'r+', want to open in read only mode (fail if exists), # but return a writeable store if mode is not None: read_only_creation = mode in ["r", "r+"] else: read_only_creation = read_only store = cls(root, read_only=read_only_creation) await store._open() # Set read_only state store = store.with_read_only(read_only) await store._open() return store async def _open(self, *, mode: AccessModeLiteral | None = None) -> None: if not self.read_only: self.root.mkdir(parents=True, exist_ok=True) if not self.root.exists(): raise FileNotFoundError(f"{self.root} does not exist") return await super()._open() async def clear(self) -> None: # docstring inherited self._check_writable() shutil.rmtree(self.root) self.root.mkdir() def __str__(self) -> str: return f"file://{self.root.as_posix()}" def __repr__(self) -> str: return f"LocalStore('{self}')" def __eq__(self, other: object) -> bool: return isinstance(other, type(self)) and self.root == other.root # ------------------------------------------------------------------- # Synchronous store methods # ------------------------------------------------------------------- def _ensure_open_sync(self) -> None: if not self._is_open: if not self.read_only: self.root.mkdir(parents=True, exist_ok=True) if not self.root.exists(): raise FileNotFoundError(f"{self.root} does not exist") self._is_open = True def get_sync( self, key: str, *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: if prototype is None: prototype = default_buffer_prototype() self._ensure_open_sync() assert isinstance(key, str) path = self.root / key try: return _get(path, prototype, byte_range) except (FileNotFoundError, IsADirectoryError, NotADirectoryError): return None def set_sync(self, key: str, value: Buffer) -> None: self._ensure_open_sync() self._check_writable() assert isinstance(key, str) if not isinstance(value, Buffer): raise TypeError( f"LocalStore.set(): `value` must be a Buffer instance. " f"Got an instance of {type(value)} instead." ) path = self.root / key _put(path, value) def delete_sync(self, key: str) -> None: self._ensure_open_sync() self._check_writable() path = self.root / key if path.is_dir(): shutil.rmtree(path) else: path.unlink(missing_ok=True) async def get( self, key: str, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: # docstring inherited if prototype is None: prototype = default_buffer_prototype() if not self._is_open: await self._open() assert isinstance(key, str) path = self.root / key try: return await asyncio.to_thread(_get, path, prototype, byte_range) except (FileNotFoundError, IsADirectoryError, NotADirectoryError): return None async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: # docstring inherited args = [] for key, byte_range in key_ranges: assert isinstance(key, str) path = self.root / key args.append((_get, path, prototype, byte_range)) return await concurrent_map(args, asyncio.to_thread, limit=None) # TODO: fix limit async def set(self, key: str, value: Buffer) -> None: # docstring inherited return await self._set(key, value) async def set_if_not_exists(self, key: str, value: Buffer) -> None: # docstring inherited try: return await self._set(key, value, exclusive=True) except FileExistsError: pass async def _set(self, key: str, value: Buffer, exclusive: bool = False) -> None: if not self._is_open: await self._open() self._check_writable() assert isinstance(key, str) if not isinstance(value, Buffer): raise TypeError( f"LocalStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead." ) path = self.root / key await asyncio.to_thread(_put, path, value, exclusive=exclusive) async def delete(self, key: str) -> None: """ Remove a key from the store. Parameters ---------- key : str Notes ----- If ``key`` is a directory within this store, the entire directory at ``store.root / key`` is deleted. """ # docstring inherited self._check_writable() path = self.root / key if path.is_dir(): # TODO: support deleting directories? shutil.rmtree? shutil.rmtree(path) else: await asyncio.to_thread(path.unlink, True) # Q: we may want to raise if path is missing async def delete_dir(self, prefix: str) -> None: # docstring inherited self._check_writable() path = self.root / prefix if path.is_dir(): shutil.rmtree(path) elif path.is_file(): raise ValueError(f"delete_dir was passed a {prefix=!r} that is a file") else: # Non-existent directory # This path is tested by test_group:test_create_creates_parents for one pass async def exists(self, key: str) -> bool: # docstring inherited path = self.root / key return await asyncio.to_thread(path.is_file) async def list(self) -> AsyncIterator[str]: # docstring inherited to_strip = self.root.as_posix() + "/" for p in list(self.root.rglob("*")): if p.is_file(): yield p.as_posix().replace(to_strip, "") async def list_prefix(self, prefix: str) -> AsyncIterator[str]: # docstring inherited to_strip = self.root.as_posix() + "/" prefix = prefix.rstrip("/") for p in (self.root / prefix).rglob("*"): if p.is_file(): yield p.as_posix().replace(to_strip, "") async def list_dir(self, prefix: str) -> AsyncIterator[str]: # docstring inherited base = self.root / prefix try: key_iter = base.iterdir() for key in key_iter: yield key.relative_to(base).as_posix() except (FileNotFoundError, NotADirectoryError): pass async def _get_bytes( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> bytes: """ Retrieve raw bytes from the local store asynchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_bytes`` for full documentation. Parameters ---------- key : str, optional The key identifying the data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Returns ------- bytes The raw bytes stored at the given key. Raises ------ FileNotFoundError If the key does not exist in the store. See Also -------- Store.get_bytes : Base implementation with full documentation. get_bytes_sync : Synchronous version of this method. Examples -------- >>> store = await LocalStore.open("data") >>> await store.set("data", Buffer.from_bytes(b"hello")) >>> # No need to specify prototype for LocalStore >>> data = await store.get_bytes("data") >>> print(data) b'hello' """ if prototype is None: prototype = default_buffer_prototype() return await super()._get_bytes(key, prototype=prototype, byte_range=byte_range) def _get_bytes_sync( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> bytes: """ Retrieve raw bytes from the local store synchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_bytes`` for full documentation. Parameters ---------- key : str, optional The key identifying the data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Returns ------- bytes The raw bytes stored at the given key. Raises ------ FileNotFoundError If the key does not exist in the store. Warnings -------- Do not call this method from async functions. Use ``get_bytes()`` instead. See Also -------- Store.get_bytes_sync : Base implementation with full documentation. get_bytes : Asynchronous version of this method. Examples -------- >>> store = LocalStore("data") >>> store.set("data", Buffer.from_bytes(b"hello")) >>> # No need to specify prototype for LocalStore >>> data = store.get_bytes("data") >>> print(data) b'hello' """ if prototype is None: prototype = default_buffer_prototype() return super()._get_bytes_sync(key, prototype=prototype, byte_range=byte_range) async def _get_json( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Any: """ Retrieve and parse JSON data from the local store asynchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_json`` for full documentation. Parameters ---------- key : str, optional The key identifying the JSON data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Note: Using byte ranges with JSON may result in invalid JSON. Returns ------- Any The parsed JSON data. This follows the behavior of ``json.loads()`` and can be any JSON-serializable type: dict, list, str, int, float, bool, or None. Raises ------ FileNotFoundError If the key does not exist in the store. json.JSONDecodeError If the stored data is not valid JSON. See Also -------- Store.get_json : Base implementation with full documentation. get_json_sync : Synchronous version of this method. get_bytes : Method for retrieving raw bytes without parsing. Examples -------- >>> store = await LocalStore.open("data") >>> import json >>> metadata = {"zarr_format": 3, "node_type": "array"} >>> await store.set("zarr.json", Buffer.from_bytes(json.dumps(metadata).encode())) >>> # No need to specify prototype for LocalStore >>> data = await store.get_json("zarr.json") >>> print(data) {'zarr_format': 3, 'node_type': 'array'} """ if prototype is None: prototype = default_buffer_prototype() return await super()._get_json(key, prototype=prototype, byte_range=byte_range) def _get_json_sync( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Any: """ Retrieve and parse JSON data from the local store synchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_json`` for full documentation. Parameters ---------- key : str, optional The key identifying the JSON data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Note: Using byte ranges with JSON may result in invalid JSON. Returns ------- Any The parsed JSON data. This follows the behavior of ``json.loads()`` and can be any JSON-serializable type: dict, list, str, int, float, bool, or None. Raises ------ FileNotFoundError If the key does not exist in the store. json.JSONDecodeError If the stored data is not valid JSON. Warnings -------- Do not call this method from async functions. Use ``get_json()`` instead. See Also -------- Store.get_json_sync : Base implementation with full documentation. get_json : Asynchronous version of this method. get_bytes_sync : Method for retrieving raw bytes without parsing. Examples -------- >>> store = LocalStore("data") >>> import json >>> metadata = {"zarr_format": 3, "node_type": "array"} >>> store.set("zarr.json", Buffer.from_bytes(json.dumps(metadata).encode())) >>> # No need to specify prototype for LocalStore >>> data = store.get_json("zarr.json") >>> print(data) {'zarr_format': 3, 'node_type': 'array'} """ if prototype is None: prototype = default_buffer_prototype() return super()._get_json_sync(key, prototype=prototype, byte_range=byte_range) async def move(self, dest_root: Path | str) -> None: """ Move the store to another path. The old root directory is deleted. """ if isinstance(dest_root, str): dest_root = Path(dest_root) os.makedirs(dest_root.parent, exist_ok=True) if os.path.exists(dest_root): raise FileExistsError(f"Destination root {dest_root} already exists.") shutil.move(self.root, dest_root) self.root = dest_root async def getsize(self, key: str) -> int: return os.path.getsize(self.root / key) zarr-python-3.2.1/src/zarr/storage/_logging.py000066400000000000000000000165261517635743000214070ustar00rootroot00000000000000from __future__ import annotations import inspect import logging import sys import time from collections import defaultdict from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Self from zarr.abc.store import Store from zarr.storage._wrapper import WrapperStore if TYPE_CHECKING: from collections.abc import AsyncGenerator, Generator, Iterable from zarr.abc.store import ByteRequest from zarr.core.buffer import Buffer, BufferPrototype counter: defaultdict[str, int] class LoggingStore[T_Store: Store](WrapperStore[T_Store]): """ Store that logs all calls to another wrapped store. Parameters ---------- store : Store Store to wrap log_level : str Log level log_handler : logging.Handler Log handler Attributes ---------- counter : dict Counter of number of times each method has been called """ counter: defaultdict[str, int] def __init__( self, store: T_Store, log_level: str = "DEBUG", log_handler: logging.Handler | None = None, ) -> None: super().__init__(store) self.counter = defaultdict(int) self.log_level = log_level self.log_handler = log_handler self._configure_logger(log_level, log_handler) def _configure_logger( self, log_level: str = "DEBUG", log_handler: logging.Handler | None = None ) -> None: self.log_level = log_level self.logger = logging.getLogger(f"LoggingStore({self._store})") self.logger.setLevel(log_level) if not self.logger.hasHandlers(): if not log_handler: log_handler = self._default_handler() # Add handler to logger self.logger.addHandler(log_handler) def _default_handler(self) -> logging.Handler: """Define a default log handler""" handler = logging.StreamHandler(stream=sys.stdout) handler.setLevel(self.log_level) handler.setFormatter( logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") ) return handler def _with_store(self, store: T_Store) -> Self: return type(self)(store=store, log_level=self.log_level, log_handler=self.log_handler) @contextmanager def log(self, hint: Any = "") -> Generator[None, None, None]: """Context manager to log method calls Each call to the wrapped store is logged to the configured logger and added to the counter dict. """ method = inspect.stack()[2].function op = f"{type(self._store).__name__}.{method}" if hint: op = f"{op}({hint})" self.logger.info(" Calling %s", op) start_time = time.time() try: self.counter[method] += 1 yield finally: end_time = time.time() self.logger.info("Finished %s [%.2f s]", op, end_time - start_time) @classmethod async def open(cls: type[Self], store_cls: type[T_Store], *args: Any, **kwargs: Any) -> Self: log_level = kwargs.pop("log_level", "DEBUG") log_handler = kwargs.pop("log_handler", None) store = store_cls(*args, **kwargs) await store._open() return cls(store=store, log_level=log_level, log_handler=log_handler) @property def supports_writes(self) -> bool: with self.log(): return self._store.supports_writes @property def supports_deletes(self) -> bool: with self.log(): return self._store.supports_deletes @property def supports_listing(self) -> bool: with self.log(): return self._store.supports_listing @property def read_only(self) -> bool: with self.log(): return self._store.read_only @property def _is_open(self) -> bool: with self.log(): return self._store._is_open @_is_open.setter def _is_open(self, value: bool) -> None: raise NotImplementedError("LoggingStore must be opened via the `_open` method") async def _open(self) -> None: with self.log(): return await self._store._open() async def _ensure_open(self) -> None: with self.log(): return await self._store._ensure_open() async def is_empty(self, prefix: str = "") -> bool: # docstring inherited with self.log(): return await self._store.is_empty(prefix=prefix) async def clear(self) -> None: # docstring inherited with self.log(): return await self._store.clear() def __str__(self) -> str: return f"logging-{self._store}" def __repr__(self) -> str: return f"LoggingStore({self._store.__class__.__name__}, '{self._store}')" def __eq__(self, other: object) -> bool: with self.log(other): return type(self) is type(other) and self._store.__eq__(other._store) # type: ignore[attr-defined] async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None, ) -> Buffer | None: # docstring inherited with self.log(key): return await self._store.get(key=key, prototype=prototype, byte_range=byte_range) async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: # docstring inherited keys = ",".join([k[0] for k in key_ranges]) with self.log(keys): return await self._store.get_partial_values(prototype=prototype, key_ranges=key_ranges) async def exists(self, key: str) -> bool: # docstring inherited with self.log(key): return await self._store.exists(key) async def set(self, key: str, value: Buffer) -> None: # docstring inherited with self.log(key): return await self._store.set(key=key, value=value) async def set_if_not_exists(self, key: str, value: Buffer) -> None: # docstring inherited with self.log(key): return await self._store.set_if_not_exists(key=key, value=value) async def delete(self, key: str) -> None: # docstring inherited with self.log(key): return await self._store.delete(key=key) async def list(self) -> AsyncGenerator[str, None]: # docstring inherited with self.log(): async for key in self._store.list(): yield key async def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]: # docstring inherited with self.log(prefix): async for key in self._store.list_prefix(prefix=prefix): yield key async def list_dir(self, prefix: str) -> AsyncGenerator[str, None]: # docstring inherited with self.log(prefix): async for key in self._store.list_dir(prefix=prefix): yield key async def delete_dir(self, prefix: str) -> None: # docstring inherited with self.log(prefix): await self._store.delete_dir(prefix=prefix) async def getsize(self, key: str) -> int: with self.log(key): return await self._store.getsize(key) async def getsize_prefix(self, prefix: str) -> int: with self.log(prefix): return await self._store.getsize_prefix(prefix) zarr-python-3.2.1/src/zarr/storage/_memory.py000066400000000000000000000742441517635743000212720ustar00rootroot00000000000000from __future__ import annotations import os import threading import weakref from logging import getLogger from typing import TYPE_CHECKING, Any, Self from zarr.abc.store import ByteRequest, Store from zarr.core.buffer import Buffer, gpu from zarr.core.buffer.core import default_buffer_prototype from zarr.core.common import concurrent_map from zarr.storage._utils import ( _join_paths, _normalize_byte_range_index, normalize_path, parse_store_url, ) if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterable, MutableMapping from zarr.core.buffer import BufferPrototype logger = getLogger(__name__) class MemoryStore(Store): """ Store for local memory. Parameters ---------- store_dict : dict Initial data read_only : bool Whether the store is read-only Attributes ---------- supports_writes supports_deletes supports_listing """ supports_writes: bool = True supports_deletes: bool = True supports_listing: bool = True _store_dict: MutableMapping[str, Buffer] def __init__( self, store_dict: MutableMapping[str, Buffer] | None = None, *, read_only: bool = False, ) -> None: super().__init__(read_only=read_only) if store_dict is None: store_dict = {} self._store_dict = store_dict def with_read_only(self, read_only: bool = False) -> MemoryStore: # docstring inherited return type(self)( store_dict=self._store_dict, read_only=read_only, ) async def clear(self) -> None: # docstring inherited self._store_dict.clear() def __str__(self) -> str: return f"memory://{id(self._store_dict)}" def __repr__(self) -> str: return f"MemoryStore('{self}')" def __eq__(self, other: object) -> bool: return ( isinstance(other, type(self)) and self._store_dict == other._store_dict and self.read_only == other.read_only ) # ------------------------------------------------------------------- # Synchronous store methods # ------------------------------------------------------------------- def get_sync( self, key: str, *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: if prototype is None: prototype = default_buffer_prototype() if not self._is_open: self._is_open = True assert isinstance(key, str) try: value = self._store_dict[key] start, stop = _normalize_byte_range_index(value, byte_range) return prototype.buffer.from_buffer(value[start:stop]) except KeyError: return None def set_sync(self, key: str, value: Buffer) -> None: self._check_writable() if not self._is_open: self._is_open = True assert isinstance(key, str) if not isinstance(value, Buffer): raise TypeError( f"MemoryStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead." ) self._store_dict[key] = value def delete_sync(self, key: str) -> None: self._check_writable() if not self._is_open: self._is_open = True try: del self._store_dict[key] except KeyError: logger.debug("Key %s does not exist.", key) async def get( self, key: str, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: # docstring inherited if prototype is None: prototype = default_buffer_prototype() if not self._is_open: await self._open() assert isinstance(key, str) try: value = self._store_dict[key] start, stop = _normalize_byte_range_index(value, byte_range) return prototype.buffer.from_buffer(value[start:stop]) except KeyError: return None async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: # docstring inherited # All the key-ranges arguments goes with the same prototype async def _get(key: str, byte_range: ByteRequest | None) -> Buffer | None: return await self.get(key, prototype=prototype, byte_range=byte_range) return await concurrent_map(key_ranges, _get, limit=None) async def exists(self, key: str) -> bool: # docstring inherited return key in self._store_dict async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None: # docstring inherited self._check_writable() await self._ensure_open() assert isinstance(key, str) if not isinstance(value, Buffer): raise TypeError( f"MemoryStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead." ) if byte_range is not None: buf = self._store_dict[key] buf[byte_range[0] : byte_range[1]] = value self._store_dict[key] = buf else: self._store_dict[key] = value async def set_if_not_exists(self, key: str, value: Buffer) -> None: # docstring inherited self._check_writable() await self._ensure_open() self._store_dict.setdefault(key, value) async def delete(self, key: str) -> None: # docstring inherited self._check_writable() try: del self._store_dict[key] except KeyError: logger.debug("Key %s does not exist.", key) async def list(self) -> AsyncIterator[str]: # docstring inherited for key in self._store_dict: yield key async def list_prefix(self, prefix: str) -> AsyncIterator[str]: # docstring inherited # note: we materialize all dict keys into a list here so we can mutate the dict in-place (e.g. in delete_prefix) for key in list(self._store_dict): if key.startswith(prefix): yield key async def list_dir(self, prefix: str) -> AsyncIterator[str]: # docstring inherited prefix = prefix.rstrip("/") if prefix == "": keys_unique = {k.split("/")[0] for k in self._store_dict} else: # Our dictionary doesn't contain directory markers, but we want to include # a pseudo directory when there's a nested item and we're listing an # intermediate level. keys_unique = { key.removeprefix(f"{prefix}/").split("/")[0] for key in self._store_dict if key.startswith(f"{prefix}/") and key != prefix } for key in keys_unique: yield key async def _get_bytes( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> bytes: """ Retrieve raw bytes from the memory store asynchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_bytes`` for full documentation. Parameters ---------- key : str, optional The key identifying the data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Returns ------- bytes The raw bytes stored at the given key. Raises ------ FileNotFoundError If the key does not exist in the store. See Also -------- Store.get_bytes : Base implementation with full documentation. get_bytes_sync : Synchronous version of this method. Examples -------- >>> store = await MemoryStore.open() >>> await store.set("data", Buffer.from_bytes(b"hello")) >>> # No need to specify prototype for MemoryStore >>> data = await store.get_bytes("data") >>> print(data) b'hello' """ if prototype is None: prototype = default_buffer_prototype() return await super()._get_bytes(key, prototype=prototype, byte_range=byte_range) def _get_bytes_sync( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> bytes: """ Retrieve raw bytes from the memory store synchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_bytes`` for full documentation. Parameters ---------- key : str, optional The key identifying the data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Returns ------- bytes The raw bytes stored at the given key. Raises ------ FileNotFoundError If the key does not exist in the store. Warnings -------- Do not call this method from async functions. Use ``get_bytes()`` instead. See Also -------- Store.get_bytes_sync : Base implementation with full documentation. get_bytes : Asynchronous version of this method. Examples -------- >>> store = MemoryStore() >>> store.set("data", Buffer.from_bytes(b"hello")) >>> # No need to specify prototype for MemoryStore >>> data = store.get_bytes("data") >>> print(data) b'hello' """ if prototype is None: prototype = default_buffer_prototype() return super()._get_bytes_sync(key, prototype=prototype, byte_range=byte_range) async def _get_json( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Any: """ Retrieve and parse JSON data from the memory store asynchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_json`` for full documentation. Parameters ---------- key : str, optional The key identifying the JSON data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Note: Using byte ranges with JSON may result in invalid JSON. Returns ------- Any The parsed JSON data. This follows the behavior of ``json.loads()`` and can be any JSON-serializable type: dict, list, str, int, float, bool, or None. Raises ------ FileNotFoundError If the key does not exist in the store. json.JSONDecodeError If the stored data is not valid JSON. See Also -------- Store.get_json : Base implementation with full documentation. get_json_sync : Synchronous version of this method. get_bytes : Method for retrieving raw bytes without parsing. Examples -------- >>> store = await MemoryStore.open() >>> import json >>> metadata = {"zarr_format": 3, "node_type": "array"} >>> await store.set("zarr.json", Buffer.from_bytes(json.dumps(metadata).encode())) >>> # No need to specify prototype for MemoryStore >>> data = await store.get_json("zarr.json") >>> print(data) {'zarr_format': 3, 'node_type': 'array'} """ if prototype is None: prototype = default_buffer_prototype() return await super()._get_json(key, prototype=prototype, byte_range=byte_range) def _get_json_sync( self, key: str = "", *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Any: """ Retrieve and parse JSON data from the memory store synchronously. This is a convenience override that makes the ``prototype`` parameter optional by defaulting to the standard buffer prototype. See the base ``Store.get_json`` for full documentation. Parameters ---------- key : str, optional The key identifying the JSON data to retrieve. Defaults to an empty string. prototype : BufferPrototype, optional The buffer prototype to use for reading the data. If None, uses ``default_buffer_prototype()``. byte_range : ByteRequest, optional If specified, only retrieve a portion of the stored data. Note: Using byte ranges with JSON may result in invalid JSON. Returns ------- Any The parsed JSON data. This follows the behavior of ``json.loads()`` and can be any JSON-serializable type: dict, list, str, int, float, bool, or None. Raises ------ FileNotFoundError If the key does not exist in the store. json.JSONDecodeError If the stored data is not valid JSON. Warnings -------- Do not call this method from async functions. Use ``get_json()`` instead. See Also -------- Store.get_json_sync : Base implementation with full documentation. get_json : Asynchronous version of this method. get_bytes_sync : Method for retrieving raw bytes without parsing. Examples -------- >>> store = MemoryStore() >>> import json >>> metadata = {"zarr_format": 3, "node_type": "array"} >>> store.set("zarr.json", Buffer.from_bytes(json.dumps(metadata).encode())) >>> # No need to specify prototype for MemoryStore >>> data = store.get_json("zarr.json") >>> print(data) {'zarr_format': 3, 'node_type': 'array'} """ if prototype is None: prototype = default_buffer_prototype() return super()._get_json_sync(key, prototype=prototype, byte_range=byte_range) class GpuMemoryStore(MemoryStore): """ Store for GPU memory. Stores every chunk in GPU memory irrespective of the original location. The dictionary of buffers to initialize this memory store with *must* be GPU Buffers. Writing data to this store through ``.set`` will move the buffer to the GPU if necessary. Parameters ---------- store_dict : MutableMapping, optional A mutable mapping with string keys and [zarr.core.buffer.gpu.Buffer][] values. read_only : bool Whether to open the store in read-only mode. """ _store_dict: MutableMapping[str, gpu.Buffer] # type: ignore[assignment] def __init__( self, store_dict: MutableMapping[str, gpu.Buffer] | None = None, *, read_only: bool = False, ) -> None: super().__init__(store_dict=store_dict, read_only=read_only) # type: ignore[arg-type] def __str__(self) -> str: return f"gpumemory://{id(self._store_dict)}" def __repr__(self) -> str: return f"GpuMemoryStore('{self}')" @classmethod def from_dict(cls, store_dict: MutableMapping[str, Buffer]) -> Self: """ Create a GpuMemoryStore from a dictionary of buffers at any location. The dictionary backing the newly created ``GpuMemoryStore`` will not be the same as ``store_dict``. Parameters ---------- store_dict : mapping A mapping of strings keys to arbitrary Buffers. The buffer data will be moved into a [`gpu.Buffer`][zarr.core.buffer.gpu.Buffer]. Returns ------- GpuMemoryStore """ gpu_store_dict = {k: gpu.Buffer.from_buffer(v) for k, v in store_dict.items()} return cls(gpu_store_dict) async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None: # docstring inherited self._check_writable() assert isinstance(key, str) if not isinstance(value, Buffer): raise TypeError( f"GpuMemoryStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead." ) # Convert to gpu.Buffer gpu_value = value if isinstance(value, gpu.Buffer) else gpu.Buffer.from_buffer(value) await super().set(key, gpu_value, byte_range=byte_range) # ----------------------------------------------------------------------------- # ManagedMemoryStore and its registry # ----------------------------------------------------------------------------- # ManagedMemoryStore owns the lifecycle of its backing dict, enabling proper # weakref-based tracking. This allows memory:// URLs to be resolved back to # the store's dict within the same process. class _ManagedStoreDict(dict[str, Buffer]): """ A dict subclass that supports weak references. Regular dicts don't support weakrefs, but we need to track managed store dicts in a WeakValueDictionary so they can be garbage collected when no longer referenced. This subclass adds the necessary __weakref__ slot. """ __slots__ = ("__weakref__",) class _ManagedStoreDictRegistry: """ Registry for managed store dicts. This registry is the source of truth for managed store dicts. It creates new dicts, tracks them via weak references, and looks them up by name. """ def __init__(self) -> None: self._registry: weakref.WeakValueDictionary[str, _ManagedStoreDict] = ( weakref.WeakValueDictionary() ) self._counter = 0 self._lock = threading.Lock() def _generate_name(self) -> str: """Generate a unique name for a store. Must be called while holding `self._lock`. """ name = str(self._counter) self._counter += 1 return name def get_or_create(self, name: str | None = None) -> tuple[_ManagedStoreDict, str]: """ Get an existing managed dict by name, or create a new one. Thread-safe: uses a lock to prevent TOCTOU races between checking for an existing entry and inserting a new one. Parameters ---------- name : str | None The name for the store. If None, a unique name is auto-generated. If a store with this name already exists, returns the existing store. Names cannot contain '/' characters. Returns ------- tuple[_ManagedStoreDict, str] The store dict and its name. Raises ------ ValueError If the name contains '/' characters. """ with self._lock: if name is None: name = self._generate_name() elif "/" in name: raise ValueError( f"Store name cannot contain '/': {name!r}. " "Use the 'path' parameter to specify a path within the store." ) existing = self._registry.get(name) if existing is not None: return existing, name store_dict = _ManagedStoreDict() self._registry[name] = store_dict return store_dict, name def get(self, name: str) -> _ManagedStoreDict | None: """ Look up a managed store dict by name. Parameters ---------- name : str The name of the store. Returns ------- _ManagedStoreDict | None The store dict if found, None otherwise. """ return self._registry.get(name) _managed_store_dict_registry = _ManagedStoreDictRegistry() class ManagedMemoryStore(MemoryStore): """ A memory store that owns and manages the lifecycle of its backing dict. Unlike ``MemoryStore`` which accepts any ``MutableMapping``, this store creates and owns its backing dict internally. This enables proper lifecycle management and allows the store to be looked up by its ``memory://`` URL within the same process. Parameters ---------- name : str | None The name for this store, used in the ``memory://`` URL. If None, a unique name is auto-generated. If a store with this name already exists, the new store will share the same backing dict. path : str The root path for this store. All keys will be prefixed with this path. read_only : bool Whether the store is read-only. Attributes ---------- name : str The name of this store. path : str The root path of this store. Notes ----- The backing dict is tracked via weak references and will be garbage collected when no ``ManagedMemoryStore`` instances reference it. URLs pointing to a garbage-collected store will fail to resolve. See Also -------- MemoryStore : A memory store that accepts any MutableMapping. Examples -------- >>> store = ManagedMemoryStore(name="my-data") >>> str(store) 'memory://my-data' >>> # Later, resolve the URL back to the store's dict >>> store2 = ManagedMemoryStore.from_url("memory://my-data") >>> store2._store_dict is store._store_dict True >>> # Create a store with a path prefix >>> store3 = ManagedMemoryStore.from_url("memory://my-data/subdir") >>> store3.path 'subdir' """ _store_dict: _ManagedStoreDict _name: str path: str def __init__(self, name: str | None = None, *, path: str = "", read_only: bool = False) -> None: # Skip MemoryStore.__init__ and call Store.__init__ directly # because we manage _store_dict via the registry, not via a user-supplied # MutableMapping. If MemoryStore.__init__ ever adds logic beyond setting # _store_dict, that logic must be replicated here. Store.__init__(self, read_only=read_only) # Get or create a managed dict from the registry self._store_dict, self._name = _managed_store_dict_registry.get_or_create(name) self.path = normalize_path(path) def __str__(self) -> str: return _join_paths([f"memory://{self._name}", self.path]) def __repr__(self) -> str: return f"ManagedMemoryStore('{self}')" def __eq__(self, other: object) -> bool: return ( isinstance(other, type(self)) and self._store_dict is other._store_dict and self.path == other.path and self.read_only == other.read_only ) @property def name(self) -> str: """The name of this store, used in the memory:// URL.""" return self._name @classmethod def _from_managed_dict( cls, managed_dict: _ManagedStoreDict, name: str, *, path: str = "", read_only: bool = False, ) -> ManagedMemoryStore: """Internal: create a store from an existing managed dict.""" store = object.__new__(cls) Store.__init__(store, read_only=read_only) store._store_dict = managed_dict store._name = name store.path = normalize_path(path) return store def with_read_only(self, read_only: bool = False) -> ManagedMemoryStore: # docstring inherited return type(self)._from_managed_dict( self._store_dict, self._name, path=self.path, read_only=read_only ) @classmethod def from_url(cls, url: str, *, read_only: bool = False) -> ManagedMemoryStore: """ Create a ManagedMemoryStore from a memory:// URL. This looks up the backing dict in the process-wide registry and creates a new store instance that shares the same dict. Parameters ---------- url : str A URL like "memory://my-store" or "memory://my-store/path/to/data" identifying the store and optional path prefix. read_only : bool Whether the new store should be read-only. Returns ------- ManagedMemoryStore A store sharing the same backing dict as the original. Raises ------ ValueError If the URL is not a valid memory:// URL or the store has been garbage collected. """ parsed = parse_store_url(url) if parsed.scheme != "memory": raise ValueError( f"Expected a 'memory://' URL, got scheme {parsed.scheme!r} in '{url}'." ) name = parsed.name or "" managed_dict = _managed_store_dict_registry.get(name) if managed_dict is None: raise ValueError( f"Memory store not found for URL '{url}'. " "The store may have been garbage collected." ) return cls._from_managed_dict(managed_dict, name, path=parsed.path, read_only=read_only) # Override MemoryStore methods to use path prefix and check process async def get( self, key: str, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: # docstring inherited return await super().get( _join_paths([self.path, key]), prototype=prototype, byte_range=byte_range ) async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: # docstring inherited key_ranges = [(_join_paths([self.path, key]), byte_range) for key, byte_range in key_ranges] return await super().get_partial_values(prototype, key_ranges) async def exists(self, key: str) -> bool: # docstring inherited return await super().exists(_join_paths([self.path, key])) async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None: # docstring inherited return await super().set(_join_paths([self.path, key]), value, byte_range=byte_range) async def set_if_not_exists(self, key: str, value: Buffer) -> None: # docstring inherited return await super().set_if_not_exists(_join_paths([self.path, key]), value) async def delete(self, key: str) -> None: # docstring inherited return await super().delete(_join_paths([self.path, key])) async def list(self) -> AsyncIterator[str]: # docstring inherited prefix = f"{self.path}/" if self.path else "" async for key in super().list(): if key.startswith(prefix): yield key.removeprefix(prefix) async def list_prefix(self, prefix: str) -> AsyncIterator[str]: # docstring inherited # Manual concatenation instead of _join_paths because we need "path/" # as the prefix when prefix is empty (to list all keys under self.path) full_prefix = f"{self.path}/{prefix}" if self.path else prefix path_prefix = f"{self.path}/" if self.path else "" async for key in super().list_prefix(full_prefix): yield key.removeprefix(path_prefix) async def list_dir(self, prefix: str) -> AsyncIterator[str]: # docstring inherited full_prefix = _join_paths([self.path, prefix]) async for key in super().list_dir(full_prefix): yield key def __reduce__( self, ) -> tuple[type[ManagedMemoryStore], tuple[str | None], dict[str, Any]]: """ Support pickling of ManagedMemoryStore. On unpickle, the store will reconnect to an existing store with the same name if one exists in the registry, or create a new empty store otherwise. Note that the backing dict data is NOT serialized - only the store's identity (name, path, read_only) is preserved. If the original store has been garbage collected, the unpickled store will have an empty dict. The current process ID is preserved so that cross-process unpickling can be detected and will raise an error at unpickle time. """ return ( self.__class__, (self._name,), { "path": self.path, "read_only": self.read_only, "created_pid": os.getpid(), }, ) def __setstate__(self, state: dict[str, Any]) -> None: """Restore state after unpickling. The pickle protocol calls ``cls(name)`` (via ``__reduce__``'s args) then ``__setstate__(state)``. ``__init__`` already set up ``_store_dict`` and ``_name`` from the registry — we just restore path and read_only here. """ # Check for cross-process usage first, before mutating state created_pid = state.get("created_pid") if created_pid is not None and created_pid != os.getpid(): raise RuntimeError( f"ManagedMemoryStore '{self._name}' was created in process {created_pid} " f"but is being unpickled in process {os.getpid()}. " "ManagedMemoryStore instances cannot be shared across processes because " "their backing dict is not serialized. Use a persistent store (e.g., " "LocalStore, ZipStore) for cross-process data sharing." ) self.path = normalize_path(state.get("path", "")) # Use the Store-level _read_only attribute directly because # Store.__init__ was already called by __init__ during unpickling self._read_only = state.get("read_only", False) zarr-python-3.2.1/src/zarr/storage/_obstore.py000066400000000000000000000420411517635743000214250ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib import pickle from collections import defaultdict from itertools import chain from operator import itemgetter from typing import TYPE_CHECKING, Self, TypedDict from zarr.abc.store import ( ByteRequest, OffsetByteRequest, RangeByteRequest, Store, SuffixByteRequest, ) from zarr.core.common import concurrent_map from zarr.core.config import config from zarr.storage._utils import _relativize_path if TYPE_CHECKING: from collections.abc import AsyncGenerator, Coroutine, Iterable, Sequence from typing import Any from obstore import ListResult, ListStream, ObjectMeta, OffsetRange, SuffixRange from obstore.store import ObjectStore as _UpstreamObjectStore from zarr.core.buffer import Buffer, BufferPrototype __all__ = ["ObjectStore"] _ALLOWED_EXCEPTIONS: tuple[type[Exception], ...] = ( FileNotFoundError, IsADirectoryError, NotADirectoryError, ) class ObjectStore[T_Store: "_UpstreamObjectStore"](Store): """ Store that uses obstore for fast read/write from AWS, GCP, Azure. Parameters ---------- store : obstore.store.ObjectStore An obstore store instance that is set up with the proper credentials. read_only : bool Whether to open the store in read-only mode. Warnings -------- ObjectStore is experimental and subject to API changes without notice. Please raise an issue with any comments/concerns about the store. """ store: T_Store """The underlying obstore instance.""" def __eq__(self, value: object) -> bool: if not isinstance(value, ObjectStore): return False if not self.read_only == value.read_only: return False return self.store == value.store # type: ignore[no-any-return] def __init__(self, store: T_Store, *, read_only: bool = False) -> None: if not store.__class__.__module__.startswith("obstore"): raise TypeError(f"expected ObjectStore class, got {store!r}") super().__init__(read_only=read_only) self.store = store def with_read_only(self, read_only: bool = False) -> Self: # docstring inherited return type(self)( store=self.store, read_only=read_only, ) def __str__(self) -> str: return f"object_store://{self.store}" def __repr__(self) -> str: return f"{type(self).__name__}({self})" def __getstate__(self) -> dict[Any, Any]: state = self.__dict__.copy() state["store"] = pickle.dumps(self.store) return state def __setstate__(self, state: dict[Any, Any]) -> None: state["store"] = pickle.loads(state["store"]) self.__dict__.update(state) async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: # docstring inherited import obstore as obs try: if byte_range is None: resp = await obs.get_async(self.store, key) return prototype.buffer.from_bytes(await resp.bytes_async()) # type: ignore[arg-type] elif isinstance(byte_range, RangeByteRequest): bytes = await obs.get_range_async( self.store, key, start=byte_range.start, end=byte_range.end ) return prototype.buffer.from_bytes(bytes) # type: ignore[arg-type] elif isinstance(byte_range, OffsetByteRequest): resp = await obs.get_async( self.store, key, options={"range": {"offset": byte_range.offset}} ) return prototype.buffer.from_bytes(await resp.bytes_async()) # type: ignore[arg-type] elif isinstance(byte_range, SuffixByteRequest): # some object stores (Azure) don't support suffix requests. In this # case, our workaround is to first get the length of the object and then # manually request the byte range at the end. try: resp = await obs.get_async( self.store, key, options={"range": {"suffix": byte_range.suffix}} ) return prototype.buffer.from_bytes(await resp.bytes_async()) # type: ignore[arg-type] except obs.exceptions.NotSupportedError: head_resp = await obs.head_async(self.store, key) file_size = head_resp["size"] suffix_len = byte_range.suffix buffer = await obs.get_range_async( self.store, key, start=file_size - suffix_len, length=suffix_len, ) return prototype.buffer.from_bytes(buffer) # type: ignore[arg-type] else: raise ValueError(f"Unexpected byte_range, got {byte_range}") except _ALLOWED_EXCEPTIONS: return None async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: # docstring inherited return await _get_partial_values(self.store, prototype=prototype, key_ranges=key_ranges) async def exists(self, key: str) -> bool: # docstring inherited import obstore as obs try: await obs.head_async(self.store, key) except FileNotFoundError: return False else: return True @property def supports_writes(self) -> bool: # docstring inherited return True async def set(self, key: str, value: Buffer) -> None: # docstring inherited import obstore as obs self._check_writable() buf = value.as_buffer_like() await obs.put_async(self.store, key, buf) async def set_if_not_exists(self, key: str, value: Buffer) -> None: # docstring inherited import obstore as obs self._check_writable() buf = value.as_buffer_like() with contextlib.suppress(obs.exceptions.AlreadyExistsError): await obs.put_async(self.store, key, buf, mode="create") @property def supports_deletes(self) -> bool: # docstring inherited return True async def delete(self, key: str) -> None: # docstring inherited import obstore as obs self._check_writable() # Some obstore stores such as local filesystems, GCP and Azure raise an error # when deleting a non-existent key, while others such as S3 and in-memory do # not. We suppress the error to make the behavior consistent across all obstore # stores. This is also in line with the behavior of the other Zarr store adapters. with contextlib.suppress(FileNotFoundError): await obs.delete_async(self.store, key) async def delete_dir(self, prefix: str) -> None: # docstring inherited import obstore as obs self._check_writable() if prefix != "" and not prefix.endswith("/"): prefix += "/" metas = await obs.list(self.store, prefix).collect_async() keys = [(m["path"],) for m in metas] await concurrent_map(keys, self.delete, limit=config.get("async.concurrency")) @property def supports_listing(self) -> bool: # docstring inherited return True async def _list(self, prefix: str | None = None) -> AsyncGenerator[ObjectMeta, None]: import obstore as obs objects: ListStream[Sequence[ObjectMeta]] = obs.list(self.store, prefix=prefix) async for batch in objects: for item in batch: yield item def list(self) -> AsyncGenerator[str, None]: # docstring inherited return (obj["path"] async for obj in self._list()) def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]: # docstring inherited return (obj["path"] async for obj in self._list(prefix)) def list_dir(self, prefix: str) -> AsyncGenerator[str, None]: # docstring inherited import obstore as obs coroutine = obs.list_with_delimiter_async(self.store, prefix=prefix) return _transform_list_dir(coroutine, prefix) async def getsize(self, key: str) -> int: # docstring inherited import obstore as obs resp = await obs.head_async(self.store, key) return resp["size"] async def getsize_prefix(self, prefix: str) -> int: # docstring inherited sizes = [obj["size"] async for obj in self._list(prefix=prefix)] return sum(sizes) async def _transform_list_dir( list_result_coroutine: Coroutine[Any, Any, ListResult[Sequence[ObjectMeta]]], prefix: str ) -> AsyncGenerator[str, None]: """ Transform the result of list_with_delimiter into an async generator of paths. """ list_result = await list_result_coroutine # We assume that the underlying object-store implementation correctly handles the # prefix, so we don't double-check that the returned results actually start with the # given prefix. prefix = prefix.rstrip("/") for path in chain( list_result["common_prefixes"], map(itemgetter("path"), list_result["objects"]) ): yield _relativize_path(path=path, prefix=prefix) class _BoundedRequest(TypedDict): """Range request with a known start and end byte. These requests can be multiplexed natively on the Rust side with `obstore.get_ranges_async`. """ original_request_index: int """The positional index in the original key_ranges input""" start: int """Start byte offset.""" end: int """End byte offset.""" class _OtherRequest(TypedDict): """Offset or suffix range requests. These requests cannot be concurrent on the Rust side, and each need their own call to `obstore.get_async`, passing in the `range` parameter. """ original_request_index: int """The positional index in the original key_ranges input""" path: str """The path to request from.""" range: OffsetRange | None # Note: suffix requests are handled separately because some object stores (Azure) # don't support them """The range request type.""" class _SuffixRequest(TypedDict): """Offset or suffix range requests. These requests cannot be concurrent on the Rust side, and each need their own call to `obstore.get_async`, passing in the `range` parameter. """ original_request_index: int """The positional index in the original key_ranges input""" path: str """The path to request from.""" range: SuffixRange """The suffix range.""" class _Response(TypedDict): """A response buffer associated with the original index that it should be restored to.""" original_request_index: int """The positional index in the original key_ranges input""" buffer: Buffer """The buffer returned from obstore's range request.""" async def _make_bounded_requests( store: _UpstreamObjectStore, path: str, requests: list[_BoundedRequest], prototype: BufferPrototype, semaphore: asyncio.Semaphore, ) -> list[_Response]: """Make all bounded requests for a specific file. `obstore.get_ranges_async` allows for making concurrent requests for multiple ranges within a single file, and will e.g. merge concurrent requests. This only uses one single Python coroutine. """ import obstore as obs starts = [r["start"] for r in requests] ends = [r["end"] for r in requests] async with semaphore: responses = await obs.get_ranges_async(store, path=path, starts=starts, ends=ends) buffer_responses: list[_Response] = [] for request, response in zip(requests, responses, strict=True): buffer_responses.append( { "original_request_index": request["original_request_index"], "buffer": prototype.buffer.from_bytes(response), # type: ignore[arg-type] } ) return buffer_responses async def _make_other_request( store: _UpstreamObjectStore, request: _OtherRequest, prototype: BufferPrototype, semaphore: asyncio.Semaphore, ) -> list[_Response]: """Make offset or full-file requests. We return a `list[_Response]` for symmetry with `_make_bounded_requests` so that all futures can be gathered together. """ import obstore as obs async with semaphore: if request["range"] is None: resp = await obs.get_async(store, request["path"]) else: resp = await obs.get_async(store, request["path"], options={"range": request["range"]}) buffer = await resp.bytes_async() return [ { "original_request_index": request["original_request_index"], "buffer": prototype.buffer.from_bytes(buffer), # type: ignore[arg-type] } ] async def _make_suffix_request( store: _UpstreamObjectStore, request: _SuffixRequest, prototype: BufferPrototype, semaphore: asyncio.Semaphore, ) -> list[_Response]: """Make suffix requests. This is separated out from `_make_other_request` because some object stores (Azure) don't support suffix requests. In this case, our workaround is to first get the length of the object and then manually request the byte range at the end. We return a `list[_Response]` for symmetry with `_make_bounded_requests` so that all futures can be gathered together. """ import obstore as obs async with semaphore: try: resp = await obs.get_async(store, request["path"], options={"range": request["range"]}) buffer = await resp.bytes_async() except obs.exceptions.NotSupportedError: head_resp = await obs.head_async(store, request["path"]) file_size = head_resp["size"] suffix_len = request["range"]["suffix"] buffer = await obs.get_range_async( store, request["path"], start=file_size - suffix_len, length=suffix_len, ) return [ { "original_request_index": request["original_request_index"], "buffer": prototype.buffer.from_bytes(buffer), # type: ignore[arg-type] } ] async def _get_partial_values( store: _UpstreamObjectStore, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: """Make multiple range requests. ObjectStore has a `get_ranges` method that will additionally merge nearby ranges, but it's _per_ file. So we need to split these key_ranges into **per-file** key ranges, and then reassemble the results in the original order. We separate into different requests: - One call to `obstore.get_ranges_async` **per target file** - One call to `obstore.get_async` for each other request. """ key_ranges = list(key_ranges) per_file_bounded_requests: dict[str, list[_BoundedRequest]] = defaultdict(list) other_requests: list[_OtherRequest] = [] suffix_requests: list[_SuffixRequest] = [] for idx, (path, byte_range) in enumerate(key_ranges): if byte_range is None: other_requests.append( { "original_request_index": idx, "path": path, "range": None, } ) elif isinstance(byte_range, RangeByteRequest): per_file_bounded_requests[path].append( {"original_request_index": idx, "start": byte_range.start, "end": byte_range.end} ) elif isinstance(byte_range, OffsetByteRequest): other_requests.append( { "original_request_index": idx, "path": path, "range": {"offset": byte_range.offset}, } ) elif isinstance(byte_range, SuffixByteRequest): suffix_requests.append( { "original_request_index": idx, "path": path, "range": {"suffix": byte_range.suffix}, } ) else: raise ValueError(f"Unsupported range input: {byte_range}") semaphore = asyncio.Semaphore(config.get("async.concurrency")) futs: list[Coroutine[Any, Any, list[_Response]]] = [] for path, bounded_ranges in per_file_bounded_requests.items(): futs.append( _make_bounded_requests(store, path, bounded_ranges, prototype, semaphore=semaphore) ) for request in other_requests: futs.append(_make_other_request(store, request, prototype, semaphore=semaphore)) # noqa: PERF401 for suffix_request in suffix_requests: futs.append(_make_suffix_request(store, suffix_request, prototype, semaphore=semaphore)) # noqa: PERF401 buffers: list[Buffer | None] = [None] * len(key_ranges) for responses in await asyncio.gather(*futs): for resp in responses: buffers[resp["original_request_index"]] = resp["buffer"] return buffers zarr-python-3.2.1/src/zarr/storage/_utils.py000066400000000000000000000225261517635743000211160ustar00rootroot00000000000000from __future__ import annotations import importlib import re from pathlib import Path, PureWindowsPath from urllib.parse import urlparse if importlib.util.find_spec("upath"): from upath.core import UPath else: class UPath: # type: ignore[no-redef] pass import sys from typing import TYPE_CHECKING, NamedTuple from zarr.abc.store import OffsetByteRequest, RangeByteRequest, SuffixByteRequest if TYPE_CHECKING: from collections.abc import Iterable, Mapping from zarr.abc.store import ByteRequest from zarr.core.buffer import Buffer class ParsedStoreUrl(NamedTuple): """ Parsed components of a store URL. Attributes ---------- scheme : str The URL scheme (e.g., "memory", "file", "s3"). Empty string for local paths. name : str | None The store name/host component. For memory:// URLs this is the store name. None if empty. path : str The path component within the store. raw : str The original URL string. """ scheme: str name: str | None path: str raw: str def parse_store_url(url: str) -> ParsedStoreUrl: """ Parse a store URL into its components. Parameters ---------- url : str A URL like "memory://store-name/path" or "s3://bucket/key" or a local path. Returns ------- ParsedStoreUrl Named tuple with scheme, name, path, and raw URL. Examples -------- >>> parse_store_url("memory://mystore") ParsedStoreUrl(scheme='memory', name='mystore', path='', raw='memory://mystore') >>> parse_store_url("memory://mystore/path/to/data") ParsedStoreUrl(scheme='memory', name='mystore', path='path/to/data', raw='memory://mystore/path/to/data') >>> parse_store_url("s3://bucket/key") ParsedStoreUrl(scheme='s3', name='bucket', path='key', raw='s3://bucket/key') >>> parse_store_url("/local/path") ParsedStoreUrl(scheme='', name=None, path='/local/path', raw='/local/path') Note that ``memory://name/path`` and ``memory:///path`` are different: the first has ``name="name"`` and ``path="path"``, while the second has ``name=None`` and ``path="/path"`` (no host component between ``//`` and ``/``). """ # On Windows, bare paths like "C:\foo" or "C:/foo" cause urlparse to # misinterpret the drive letter as a URL scheme. Detect this early and # return a local-path result without going through urlparse. if sys.platform == "win32" and PureWindowsPath(url).drive: return ParsedStoreUrl(scheme="", name=None, path=url, raw=url) parsed = urlparse(url) # netloc is the "host" part (store name for memory://, bucket for s3://, etc.) name = parsed.netloc or None # For URLs with a scheme and netloc (like memory://store/path or s3://bucket/key), # strip the leading slash from the path component. # For local paths (no scheme), preserve the path as-is. if parsed.scheme and parsed.netloc: path = parsed.path.lstrip("/") else: path = parsed.path return ParsedStoreUrl(scheme=parsed.scheme, name=name, path=path, raw=url) def normalize_path(path: str | bytes | Path | None) -> str: if path is None: result = "" elif isinstance(path, bytes): result = str(path, "ascii") # handle pathlib.Path elif isinstance(path, Path | UPath): result = str(path) elif isinstance(path, str): result = path else: raise TypeError(f'Object {path} has an invalid type for "path": {type(path).__name__}') # convert backslash to forward slash result = result.replace("\\", "/") # remove leading and trailing slashes result = result.strip("/") # collapse any repeated slashes pat = re.compile(r"//+") result = pat.sub("/", result) # disallow path segments with just '.' or '..' segments = result.split("/") if any(s in {".", ".."} for s in segments): raise ValueError( f"The path {path!r} is invalid because its string representation contains '.' or '..' segments." ) return result def _normalize_byte_range_index(data: Buffer, byte_range: ByteRequest | None) -> tuple[int, int]: """ Convert a ByteRequest into an explicit start and stop """ if byte_range is None: start = 0 stop = len(data) + 1 elif isinstance(byte_range, RangeByteRequest): start = byte_range.start stop = byte_range.end elif isinstance(byte_range, OffsetByteRequest): start = byte_range.offset stop = len(data) + 1 elif isinstance(byte_range, SuffixByteRequest): start = len(data) - byte_range.suffix stop = len(data) + 1 else: raise ValueError(f"Unexpected byte_range, got {byte_range}.") return (start, stop) def _join_paths(paths: Iterable[str]) -> str: """ Filter out instances of '' and join the remaining strings with '/'. Parameters ---------- paths : Iterable[str] Returns ------- str Examples -------- ```python from zarr.storage._utils import _join_paths _join_paths(["", "a", "b"]) # 'a/b' _join_paths(["a", "b", "c"]) # 'a/b/c' ``` """ return "/".join(filter(lambda v: v != "", paths)) def _dereference_path(root: str, path: str) -> str: """ Combine a store-side root with a key into a single fully-qualified path. Unlike `_join_paths`, this is purpose-built for the case where `root` is an opaque backend-side prefix that may use `"/"` as a sentinel for "root of the filesystem" (notably for fsspec's `ReferenceFileSystem`). A trailing `"/"` is stripped from `root` before joining; if `root` is then empty, the bare `path` is returned so that joining `"/"` with `"key"` yields `"key"` rather than `"//key"`. A trailing `"/"` on the result is also stripped. Leading slashes on `root` are preserved -- a backend-side path like `"/home/foo/data.zarr"` is an absolute filesystem path for `LocalFileSystem` and must not lose its leading separator. Parameters ---------- root : str The backend-side root of a store. May be `""`, `"/"`, an absolute filesystem path, or a backend-specific prefix. path : str The key within the store, typically a zarr key like `"zarr.json"` or `"a/b/c/zarr.json"`. Returns ------- str `root` and `path` joined by a single `"/"`, with the `"/"` sentinel collapsed and trailing slashes removed. Examples -------- ```python from zarr.storage._utils import _dereference_path _dereference_path("/", "zarr.json") # 'zarr.json' _dereference_path("", "zarr.json") # 'zarr.json' _dereference_path("/home/foo", "zarr.json") # '/home/foo/zarr.json' _dereference_path("/home/foo/", "zarr.json") # '/home/foo/zarr.json' _dereference_path("bucket/p", "zarr.json") # 'bucket/p/zarr.json' ``` """ root = root.rstrip("/") path = f"{root}/{path}" if root else path return path.rstrip("/") def _relativize_path(*, path: str, prefix: str) -> str: """ Make a "/"-delimited path relative to some prefix. If the prefix is '', then the path is returned as-is. Otherwise, the prefix is removed from the path as well as the separator string "/". If ``prefix`` is not the empty string and ``path`` does not start with ``prefix`` followed by a "/" character, then an error is raised. This function assumes that the prefix does not end with "/". Parameters ---------- path : str The path to make relative to the prefix. prefix : str The prefix to make the path relative to. Returns ------- str Examples -------- ```python from zarr.storage._utils import _relativize_path _relativize_path(path="a/b", prefix="") # 'a/b' _relativize_path(path="a/b/c", prefix="a/b") # 'c' ``` """ if prefix == "": return path else: _prefix = f"{prefix}/" if not path.startswith(_prefix): raise ValueError(f"The first component of {path} does not start with {prefix}.") return path.removeprefix(_prefix) def _normalize_paths(paths: Iterable[str]) -> tuple[str, ...]: """ Normalize the input paths according to the normalization scheme used for zarr node paths. If any two paths normalize to the same value, raise a ValueError. """ path_map: dict[str, str] = {} for path in paths: parsed = normalize_path(path) if parsed in path_map: msg = ( f"After normalization, the value '{path}' collides with '{path_map[parsed]}'. " f"Both '{path}' and '{path_map[parsed]}' normalize to the same value: '{parsed}'. " f"You should use either '{path}' or '{path_map[parsed]}', but not both." ) raise ValueError(msg) path_map[parsed] = path return tuple(path_map.keys()) def _normalize_path_keys[T](data: Mapping[str, T]) -> dict[str, T]: """ Normalize the keys of the input dict according to the normalization scheme used for zarr node paths. If any two keys in the input normalize to the same value, raise a ValueError. Returns a dict where the keys are the elements of the input and the values are the normalized form of each key. """ parsed_keys = _normalize_paths(data.keys()) return dict(zip(parsed_keys, data.values(), strict=True)) zarr-python-3.2.1/src/zarr/storage/_wrapper.py000066400000000000000000000113151517635743000214300ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, cast if TYPE_CHECKING: from collections.abc import AsyncGenerator, AsyncIterator, Iterable from types import TracebackType from typing import Any, Self from zarr.abc.buffer import Buffer from zarr.abc.store import ByteRequest from zarr.core.buffer import BufferPrototype from zarr.abc.store import Store class WrapperStore[T_Store: Store](Store): """ Store that wraps an existing Store. By default all of the store methods are delegated to the wrapped store instance, which is accessible via the ``._store`` attribute of this class. Use this class to modify or extend the behavior of the other store classes. """ _store: T_Store def __init__(self, store: T_Store) -> None: self._store = store def _with_store(self, store: T_Store) -> Self: """ Constructs a new instance of the wrapper store with the same details but a new store. """ return type(self)(store=store) @classmethod async def open(cls: type[Self], store_cls: type[T_Store], *args: Any, **kwargs: Any) -> Self: store = store_cls(*args, **kwargs) await store._open() return cls(store=store) def with_read_only(self, read_only: bool = False) -> Self: return self._with_store(cast(T_Store, self._store.with_read_only(read_only))) def __enter__(self) -> Self: return self._with_store(self._store.__enter__()) def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: return self._store.__exit__(exc_type, exc_value, traceback) async def _open(self) -> None: await self._store._open() async def _ensure_open(self) -> None: await self._store._ensure_open() async def is_empty(self, prefix: str) -> bool: return await self._store.is_empty(prefix) @property def _is_open(self) -> bool: return self._store._is_open @_is_open.setter def _is_open(self, value: bool) -> None: raise NotImplementedError("WrapperStore must be opened via the `_open` method") async def clear(self) -> None: return await self._store.clear() @property def read_only(self) -> bool: return self._store.read_only def _check_writable(self) -> None: return self._store._check_writable() def __eq__(self, value: object) -> bool: return type(self) is type(value) and self._store.__eq__(value._store) # type: ignore[attr-defined] def __str__(self) -> str: return f"wrapping-{self._store}" def __repr__(self) -> str: return f"WrapperStore({self._store.__class__.__name__}, '{self._store}')" async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: return await self._store.get(key, prototype, byte_range) async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: return await self._store.get_partial_values(prototype, key_ranges) async def exists(self, key: str) -> bool: return await self._store.exists(key) async def set(self, key: str, value: Buffer) -> None: await self._store.set(key, value) async def set_if_not_exists(self, key: str, value: Buffer) -> None: return await self._store.set_if_not_exists(key, value) async def _set_many(self, values: Iterable[tuple[str, Buffer]]) -> None: await self._store._set_many(values) @property def supports_writes(self) -> bool: return self._store.supports_writes @property def supports_deletes(self) -> bool: return self._store.supports_deletes async def delete(self, key: str) -> None: await self._store.delete(key) @property def supports_listing(self) -> bool: return self._store.supports_listing def list(self) -> AsyncIterator[str]: return self._store.list() def list_prefix(self, prefix: str) -> AsyncIterator[str]: return self._store.list_prefix(prefix) def list_dir(self, prefix: str) -> AsyncIterator[str]: return self._store.list_dir(prefix) async def delete_dir(self, prefix: str) -> None: return await self._store.delete_dir(prefix) def close(self) -> None: self._store.close() async def _get_many( self, requests: Iterable[tuple[str, BufferPrototype, ByteRequest | None]] ) -> AsyncGenerator[tuple[str, Buffer | None], None]: async for req in self._store._get_many(requests): yield req zarr-python-3.2.1/src/zarr/storage/_zip.py000066400000000000000000000223041517635743000205520ustar00rootroot00000000000000from __future__ import annotations import os import shutil import threading import time import zipfile from pathlib import Path from typing import TYPE_CHECKING, Any, Literal from zarr.abc.store import ( ByteRequest, OffsetByteRequest, RangeByteRequest, Store, SuffixByteRequest, ) from zarr.core.buffer import Buffer, BufferPrototype if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterable ZipStoreAccessModeLiteral = Literal["r", "w", "a"] class ZipStore(Store): """ Store using a ZIP file. Parameters ---------- path : str Location of file. mode : str, optional One of 'r' to read an existing file, 'w' to truncate and write a new file, 'a' to append to an existing file, or 'x' to exclusively create and write a new file. compression : int, optional Compression method to use when writing to the archive. allowZip64 : bool, optional If True (the default) will create ZIP files that use the ZIP64 extensions when the zipfile is larger than 2 GiB. If False will raise an exception when the ZIP file would require ZIP64 extensions. Attributes ---------- allowed_exceptions supports_writes supports_deletes supports_listing path compression allowZip64 """ supports_writes: bool = True supports_deletes: bool = False supports_listing: bool = True path: Path compression: int allowZip64: bool _zf: zipfile.ZipFile _lock: threading.RLock def __init__( self, path: Path | str, *, mode: ZipStoreAccessModeLiteral = "r", read_only: bool | None = None, compression: int = zipfile.ZIP_STORED, allowZip64: bool = True, ) -> None: if read_only is None: read_only = mode == "r" super().__init__(read_only=read_only) if isinstance(path, str): path = Path(path) assert isinstance(path, Path) self.path = path # root? self._zmode = mode self.compression = compression self.allowZip64 = allowZip64 def _sync_open(self) -> None: if self._is_open: raise ValueError("store is already open") self._lock = threading.RLock() self._zf = zipfile.ZipFile( self.path, mode=self._zmode, compression=self.compression, allowZip64=self.allowZip64, ) self._is_open = True async def _open(self) -> None: self._sync_open() def __getstate__(self) -> dict[str, Any]: # We need a copy to not modify the state of the original store state = self.__dict__.copy() for attr in ["_zf", "_lock"]: state.pop(attr, None) return state def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__ = state self._is_open = False self._sync_open() def close(self) -> None: # docstring inherited super().close() with self._lock: self._zf.close() async def clear(self) -> None: # docstring inherited with self._lock: self._check_writable() self._zf.close() os.remove(self.path) self._zf = zipfile.ZipFile( self.path, mode="w", compression=self.compression, allowZip64=self.allowZip64 ) def __str__(self) -> str: return f"zip://{self.path}" def __repr__(self) -> str: return f"ZipStore('{self}')" def __eq__(self, other: object) -> bool: return isinstance(other, type(self)) and self.path == other.path def _get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None, ) -> Buffer | None: if not self._is_open: self._sync_open() # docstring inherited try: with self._zf.open(key) as f: # will raise KeyError if byte_range is None: return prototype.buffer.from_bytes(f.read()) elif isinstance(byte_range, RangeByteRequest): f.seek(byte_range.start) return prototype.buffer.from_bytes(f.read(byte_range.end - f.tell())) size = f.seek(0, os.SEEK_END) if isinstance(byte_range, OffsetByteRequest): f.seek(byte_range.offset) elif isinstance(byte_range, SuffixByteRequest): f.seek(max(0, size - byte_range.suffix)) else: raise TypeError(f"Unexpected byte_range, got {byte_range}.") return prototype.buffer.from_bytes(f.read()) except KeyError: return None async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None, ) -> Buffer | None: # docstring inherited assert isinstance(key, str) with self._lock: return self._get(key, prototype=prototype, byte_range=byte_range) async def get_partial_values( self, prototype: BufferPrototype, key_ranges: Iterable[tuple[str, ByteRequest | None]], ) -> list[Buffer | None]: # docstring inherited out = [] with self._lock: for key, byte_range in key_ranges: out.append(self._get(key, prototype=prototype, byte_range=byte_range)) return out def _set(self, key: str, value: Buffer) -> None: if not self._is_open: self._sync_open() # generally, this should be called inside a lock keyinfo = zipfile.ZipInfo(filename=key, date_time=time.localtime(time.time())[:6]) keyinfo.compress_type = self.compression if keyinfo.filename[-1] == os.sep: keyinfo.external_attr = 0o40775 << 16 # drwxrwxr-x keyinfo.external_attr |= 0x10 # MS-DOS directory flag else: keyinfo.external_attr = 0o644 << 16 # ?rw-r--r-- self._zf.writestr(keyinfo, value.to_bytes()) async def set(self, key: str, value: Buffer) -> None: # docstring inherited self._check_writable() if not self._is_open: self._sync_open() assert isinstance(key, str) if not isinstance(value, Buffer): raise TypeError( f"ZipStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead." ) with self._lock: self._set(key, value) async def set_if_not_exists(self, key: str, value: Buffer) -> None: self._check_writable() with self._lock: members = self._zf.namelist() if key not in members: self._set(key, value) async def delete_dir(self, prefix: str) -> None: # only raise NotImplementedError if any keys are found self._check_writable() if prefix != "" and not prefix.endswith("/"): prefix += "/" async for _ in self.list_prefix(prefix): raise NotImplementedError async def delete(self, key: str) -> None: # docstring inherited # we choose to only raise NotImplementedError here if the key exists # this allows the array/group APIs to avoid the overhead of existence checks self._check_writable() if await self.exists(key): raise NotImplementedError async def exists(self, key: str) -> bool: # docstring inherited if not self._is_open: self._sync_open() with self._lock: try: self._zf.getinfo(key) except KeyError: return False else: return True async def list(self) -> AsyncIterator[str]: # docstring inherited if not self._is_open: self._sync_open() with self._lock: for key in self._zf.namelist(): yield key async def list_prefix(self, prefix: str) -> AsyncIterator[str]: # docstring inherited async for key in self.list(): if key.startswith(prefix): yield key async def list_dir(self, prefix: str) -> AsyncIterator[str]: # docstring inherited if not self._is_open: self._sync_open() prefix = prefix.rstrip("/") keys = self._zf.namelist() seen = set() if prefix == "": keys_unique = {k.split("/")[0] for k in keys} for key in keys_unique: if key not in seen: seen.add(key) yield key else: for key in keys: if key.startswith(f"{prefix}/") and key.strip("/") != prefix: k = key.removeprefix(f"{prefix}/").split("/")[0] if k not in seen: seen.add(k) yield k async def move(self, path: Path | str) -> None: """ Move the store to another path. """ if isinstance(path, str): path = Path(path) self.close() os.makedirs(path.parent, exist_ok=True) shutil.move(self.path, path) self.path = path await self._open() zarr-python-3.2.1/src/zarr/testing/000077500000000000000000000000001517635743000172475ustar00rootroot00000000000000zarr-python-3.2.1/src/zarr/testing/__init__.py000066400000000000000000000006641517635743000213660ustar00rootroot00000000000000import importlib.util import warnings from zarr.errors import ZarrUserWarning if importlib.util.find_spec("pytest") is not None: from zarr.testing.store import StoreTests else: warnings.warn( "pytest not installed, skipping test suite", category=ZarrUserWarning, stacklevel=2 ) from zarr.testing.utils import assert_bytes_equal # TODO: import public buffer tests? __all__ = ["StoreTests", "assert_bytes_equal"] zarr-python-3.2.1/src/zarr/testing/buffer.py000066400000000000000000000044321517635743000210750ustar00rootroot00000000000000# mypy: ignore-errors from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal import numpy as np import numpy.typing as npt from zarr.core.buffer import Buffer, BufferPrototype, cpu from zarr.storage import MemoryStore if TYPE_CHECKING: from collections.abc import Iterable from typing import Self __all__ = [ "NDBufferUsingTestNDArrayLike", "StoreExpectingTestBuffer", "TestBuffer", ] class TestNDArrayLike(np.ndarray): """An example of an ndarray-like class""" __test__ = False class TestBuffer(cpu.Buffer): """Example of a custom Buffer that handles ArrayLike""" __test__ = False class NDBufferUsingTestNDArrayLike(cpu.NDBuffer): """Example of a custom NDBuffer that handles MyNDArrayLike""" @classmethod def create( cls, *, shape: Iterable[int], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C", fill_value: Any | None = None, ) -> Self: """Overwrite `NDBuffer.create` to create a TestNDArrayLike instance""" ret = cls(TestNDArrayLike(shape=shape, dtype=dtype, order=order)) if fill_value is not None: ret.fill(fill_value) return ret @classmethod def empty( cls, shape: tuple[int, ...], dtype: npt.DTypeLike, order: Literal["C", "F"] = "C", ) -> Self: return super(cpu.NDBuffer, cls).empty(shape=shape, dtype=dtype, order=order) class StoreExpectingTestBuffer(MemoryStore): """Example of a custom Store that expect MyBuffer for all its non-metadata We assume that keys containing "json" is metadata """ async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None: if "json" not in key: assert isinstance(value, TestBuffer) await super().set(key, value, byte_range) async def get( self, key: str, prototype: BufferPrototype, byte_range: tuple[int, int | None] | None = None, ) -> Buffer | None: if "json" not in key: assert prototype.buffer is TestBuffer ret = await super().get(key=key, prototype=prototype, byte_range=byte_range) if ret is not None: assert isinstance(ret, prototype.buffer) return ret zarr-python-3.2.1/src/zarr/testing/conftest.py000066400000000000000000000007101517635743000214440ustar00rootroot00000000000000import pytest def pytest_configure(config: pytest.Config) -> None: # The tests in zarr.testing are intended to be run by downstream projects. # To allow those downstream projects to run with `--strict-markers`, we need # to register an entry point with pytest11 and register our "plugin" with it, # which just registers the markers used in zarr.testing config.addinivalue_line("markers", "gpu: mark a test as requiring CuPy and GPU") zarr-python-3.2.1/src/zarr/testing/stateful.py000066400000000000000000000613511517635743000214560ustar00rootroot00000000000000import builtins import functools from collections.abc import Callable from typing import Any, cast import hypothesis.extra.numpy as npst import hypothesis.strategies as st import numpy as np from hypothesis import assume, note from hypothesis.stateful import ( RuleBasedStateMachine, initialize, invariant, precondition, rule, ) from hypothesis.strategies import DataObject import zarr from zarr import Array from zarr.abc.store import Store from zarr.codecs.bytes import BytesCodec from zarr.core.buffer import Buffer, BufferPrototype, cpu, default_buffer_prototype from zarr.core.sync import SyncMixin from zarr.storage import LocalStore, MemoryStore from zarr.testing.strategies import ( arrays as zarr_arrays, ) from zarr.testing.strategies import ( basic_indices, chunk_paths, key_ranges, node_names, orthogonal_indices, ) from zarr.testing.strategies import keys as zarr_keys MAX_BINARY_SIZE = 100 def with_frequency[F: Callable[..., Any]](frequency: float) -> Callable[[F], F]: """This needs to be deterministic for hypothesis replaying""" def decorator(func: F) -> F: counter_attr = f"__{func.__name__}_counter" @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: return func(*args, **kwargs) @precondition def frequency_check(f: Any) -> Any: if not hasattr(f, counter_attr): setattr(f, counter_attr, 0) current_count = getattr(f, counter_attr) + 1 setattr(f, counter_attr, current_count) return (current_count * frequency) % 1.0 >= (1.0 - frequency) return cast(F, frequency_check(wrapper)) return decorator def split_prefix_name(path: str) -> tuple[str, str]: split = path.rsplit("/", maxsplit=1) if len(split) > 1: prefix, name = split else: prefix = "" (name,) = split return prefix, name class ZarrHierarchyStateMachine(SyncMixin, RuleBasedStateMachine): """ This state machine models operations that modify a zarr store's hierarchy. That is, user actions that modify arrays/groups as well as list operations. It is intended to be used by external stores, and compares their results to a MemoryStore that is assumed to be perfect. """ def __init__(self, store: Store) -> None: super().__init__() self.store = store self.model = MemoryStore() zarr.group(store=self.model) # Track state of the hierarchy, these should contain fully qualified paths self.all_groups: set[str] = set() self.all_arrays: set[str] = set() @initialize() def init_store(self) -> None: # This lets us reuse the fixture provided store. self._sync(self.store.clear()) zarr.group(store=self.store) def can_add(self, path: str) -> bool: return path not in self.all_groups and path not in self.all_arrays # -------------------- store operations ----------------------- @rule(name=node_names, data=st.data()) def add_group(self, name: str, data: DataObject) -> None: # Handle possible case-insensitive file systems (e.g. MacOS) if isinstance(self.store, LocalStore): name = name.lower() if self.all_groups: parent = data.draw(st.sampled_from(sorted(self.all_groups)), label="Group parent") else: parent = "" path = f"{parent}/{name}".lstrip("/") assume(self.can_add(path)) note(f"Adding group: path='{path}'") self.all_groups.add(path) zarr.group(store=self.store, path=path) zarr.group(store=self.model, path=path) @rule(data=st.data(), name=node_names) def add_array(self, data: DataObject, name: str) -> None: # Handle possible case-insensitive file systems (e.g. MacOS) if isinstance(self.store, LocalStore): name = name.lower() if self.all_groups: parent = data.draw(st.sampled_from(sorted(self.all_groups)), label="Array parent") else: parent = "" # TODO: support creating deeper paths # TODO: support overwriting potentially by just skipping `self.can_add` path = f"{parent}/{name}".lstrip("/") assume(self.can_add(path)) # Generate array on the model store using the arrays strategy a = data.draw( zarr_arrays( stores=st.just(self.model), paths=st.just(parent), array_names=st.just(name), zarr_formats=st.just(3), compressors=st.just(BytesCodec()), open_mode="a", ), label="generated array", ) note(f"Adding array: path='{path}' shape={a.shape} chunks={a.metadata.chunk_grid}") # Recreate the same array in the store under test from zarr.core.metadata.v3 import RectilinearChunkGridMetadata, RegularChunkGridMetadata chunk_grid = a.metadata.chunk_grid chunks_param: tuple[int, ...] | list[list[int]] if isinstance(chunk_grid, RectilinearChunkGridMetadata): chunks_param = [ list(dim) if isinstance(dim, tuple) else [dim] for dim in chunk_grid.chunk_shapes ] elif isinstance(chunk_grid, RegularChunkGridMetadata): chunks_param = chunk_grid.chunk_shape else: chunks_param = a.chunks root = zarr.open_group(store=self.store, mode="a") arr = root.create_array( path, shape=a.shape, chunks=chunks_param, dtype=a.dtype, fill_value=a.fill_value, dimension_names=a.metadata.dimension_names, # type: ignore[union-attr] compressors=None, ) arr[:] = a[:] self.all_arrays.add(path) @rule() @with_frequency(0.25) def clear(self) -> None: note("clearing") import zarr self._sync(self.store.clear()) self._sync(self.model.clear()) assert self._sync(self.store.is_empty("/")) assert self._sync(self.model.is_empty("/")) self.all_groups.clear() self.all_arrays.clear() zarr.group(store=self.store) zarr.group(store=self.model) # TODO: MemoryStore is broken? # assert not self._sync(self.store.is_empty("/")) # assert not self._sync(self.model.is_empty("/")) def draw_directory(self, data: DataObject) -> str: group_st = st.sampled_from(sorted(self.all_groups)) if self.all_groups else st.nothing() array_st = st.sampled_from(sorted(self.all_arrays)) if self.all_arrays else st.nothing() array_or_group = data.draw(st.one_of(group_st, array_st)) if data.draw(st.booleans()) and array_or_group in self.all_arrays: arr = zarr.open_array(path=array_or_group, store=self.model) path = data.draw( st.one_of( st.sampled_from([array_or_group]), chunk_paths(ndim=arr.ndim, numblocks=arr.cdata_shape).map( lambda x: f"{array_or_group}/c/" ), ) ) else: path = array_or_group return path @precondition(lambda self: bool(self.all_groups)) @rule(data=st.data()) def check_list_dir(self, data: DataObject) -> None: path = self.draw_directory(data) note(f"list_dir for {path=!r}") # Consider .list_dir("path/to/array") for an array with a single chunk. # The MemoryStore model will return `"c", "zarr.json"` only if the chunk exists # If that chunk was deleted, then `"c"` is not returned. # LocalStore will not have this behaviour :/ # There are similar consistency issues with delete_dir("/path/to/array/c/0/0") assume(not isinstance(self.store, LocalStore)) model_ls = sorted(self._sync_iter(self.model.list_dir(path))) store_ls = sorted(self._sync_iter(self.store.list_dir(path))) assert model_ls == store_ls, (model_ls, store_ls) @precondition(lambda self: bool(self.all_arrays)) @rule(data=st.data()) def delete_chunk(self, data: DataObject) -> None: array = data.draw(st.sampled_from(sorted(self.all_arrays))) arr = zarr.open_array(path=array, store=self.model) chunk_path = data.draw(chunk_paths(ndim=arr.ndim, numblocks=arr.cdata_shape, subset=False)) path = f"{array}/c/{chunk_path}" note(f"deleting chunk {path=!r}") self._sync(self.model.delete(path)) self._sync(self.store.delete(path)) @precondition(lambda self: bool(self.all_arrays)) @rule(data=st.data()) def check_array(self, data: DataObject) -> None: path = data.draw(st.sampled_from(sorted(self.all_arrays))) actual = zarr.open_array(self.store, path=path)[:] expected = zarr.open_array(self.model, path=path)[:] np.testing.assert_equal(actual, expected) @precondition(lambda self: bool(self.all_arrays)) @rule(data=st.data()) def overwrite_array_basic_indexing(self, data: DataObject) -> None: array = data.draw(st.sampled_from(sorted(self.all_arrays))) model_array = zarr.open_array(path=array, store=self.model) store_array = zarr.open_array(path=array, store=self.store) slicer = data.draw(basic_indices(shape=model_array.shape)) note(f"overwriting array with basic indexer: {slicer=}") new_data = data.draw( npst.arrays(shape=np.shape(model_array[slicer]), dtype=model_array.dtype) ) model_array[slicer] = new_data store_array[slicer] = new_data @precondition(lambda self: bool(self.all_arrays)) @rule(data=st.data()) def overwrite_array_orthogonal_indexing(self, data: DataObject) -> None: array = data.draw(st.sampled_from(sorted(self.all_arrays))) model_array = zarr.open_array(path=array, store=self.model) store_array = zarr.open_array(path=array, store=self.store) indexer, _ = data.draw(orthogonal_indices(shape=model_array.shape)) note(f"overwriting array orthogonal {indexer=}") new_data = data.draw( npst.arrays(shape=model_array.oindex[indexer].shape, dtype=model_array.dtype) # type: ignore[union-attr] ) model_array.oindex[indexer] = new_data store_array.oindex[indexer] = new_data @precondition(lambda self: bool(self.all_arrays)) @rule(data=st.data()) def resize_array(self, data: DataObject) -> None: array = data.draw(st.sampled_from(sorted(self.all_arrays))) model_array = zarr.open_array(path=array, store=self.model) store_array = zarr.open_array(path=array, store=self.store) ndim = model_array.ndim new_shape = tuple( 0 if oldsize == 0 else newsize for newsize, oldsize in zip( data.draw(npst.array_shapes(max_dims=ndim, min_dims=ndim, min_side=0)), model_array.shape, strict=True, ) ) note(f"resizing array from {model_array.shape} to {new_shape}") model_array.resize(new_shape) store_array.resize(new_shape) @precondition(lambda self: bool(self.all_arrays) or bool(self.all_groups)) @rule(data=st.data()) def delete_dir(self, data: DataObject) -> None: path = self.draw_directory(data) note(f"delete_dir with {path=!r}") self._sync(self.model.delete_dir(path)) self._sync(self.store.delete_dir(path)) matches = set() for node in self.all_groups | self.all_arrays: if node.startswith(path): matches.add(node) self.all_groups = self.all_groups - matches self.all_arrays = self.all_arrays - matches # @precondition(lambda self: bool(self.all_groups)) # @precondition(lambda self: bool(self.all_arrays)) # @rule(data=st.data()) # def move_array(self, data): # array_path = data.draw(st.sampled_from(self.all_arrays), label="Array move source") # to_group = data.draw(st.sampled_from(self.all_groups), label="Array move destination") # # fixme renaming to self? # array_name = os.path.basename(array_path) # assume(self.model.can_add(to_group, array_name)) # new_path = f"{to_group}/{array_name}".lstrip("/") # note(f"moving array '{array_path}' -> '{new_path}'") # self.model.rename(array_path, new_path) # self.repo.store.rename(array_path, new_path) # @precondition(lambda self: len(self.all_groups) >= 2) # @rule(data=st.data()) # def move_group(self, data): # from_group = data.draw(st.sampled_from(self.all_groups), label="Group move source") # to_group = data.draw(st.sampled_from(self.all_groups), label="Group move destination") # assume(not to_group.startswith(from_group)) # from_group_name = os.path.basename(from_group) # assume(self.model.can_add(to_group, from_group_name)) # # fixme renaming to self? # new_path = f"{to_group}/{from_group_name}".lstrip("/") # note(f"moving group '{from_group}' -> '{new_path}'") # self.model.rename(from_group, new_path) # self.repo.store.rename(from_group, new_path) @precondition(lambda self: self.store.supports_deletes) @precondition(lambda self: len(self.all_arrays) >= 1) @rule(data=st.data()) def delete_array_using_del(self, data: DataObject) -> None: array_path = data.draw( st.sampled_from(sorted(self.all_arrays)), label="Array deletion target" ) prefix, array_name = split_prefix_name(array_path) note(f"Deleting array '{array_path}' ({prefix=!r}, {array_name=!r}) using del") for store in [self.model, self.store]: group = zarr.open_group(path=prefix, store=store) group[array_name] # check that it exists del group[array_name] self.all_arrays.remove(array_path) @precondition(lambda self: self.store.supports_deletes) @precondition(lambda self: bool(self.all_groups)) @rule(data=st.data()) def delete_group_using_del(self, data: DataObject) -> None: group_path = data.draw( st.sampled_from(sorted(self.all_groups)), label="Group deletion target", ) prefix, group_name = split_prefix_name(group_path) note(f"Deleting group '{group_path=!r}', {prefix=!r}, {group_name=!r} using delete") members = zarr.open_group(store=self.model, path=group_path).members(max_depth=None) for _, obj in members: if isinstance(obj, Array): self.all_arrays.remove(obj.path) else: self.all_groups.remove(obj.path) for store in [self.store, self.model]: group = zarr.open_group(store=store, path=prefix) group[group_name] # check that it exists del group[group_name] self.all_groups.remove(group_path) # # --------------- assertions ----------------- # def check_group_arrays(self, group): # # note(f"Checking arrays of '{group}'") # g1 = self.model.get_group(group) # g2 = zarr.open_group(path=group, mode="r", store=self.repo.store) # model_arrays = sorted(g1.arrays(), key=itemgetter(0)) # our_arrays = sorted(g2.arrays(), key=itemgetter(0)) # for (n1, a1), (n2, a2) in zip_longest(model_arrays, our_arrays): # assert n1 == n2 # assert_array_equal(a1, a2) # def check_subgroups(self, group_path): # g1 = self.model.get_group(group_path) # g2 = zarr.open_group(path=group_path, mode="r", store=self.repo.store) # g1_children = [name for (name, _) in g1.groups()] # g2_children = [name for (name, _) in g2.groups()] # # note(f"Checking {len(g1_children)} subgroups of group '{group_path}'") # assert g1_children == g2_children # def check_list_prefix_from_group(self, group): # prefix = f"meta/root/{group}" # model_list = sorted(self.model.list_prefix(prefix)) # al_list = sorted(self.repo.store.list_prefix(prefix)) # # note(f"Checking {len(model_list)} keys under '{prefix}'") # assert model_list == al_list # prefix = f"data/root/{group}" # model_list = sorted(self.model.list_prefix(prefix)) # al_list = sorted(self.repo.store.list_prefix(prefix)) # # note(f"Checking {len(model_list)} keys under '{prefix}'") # assert model_list == al_list # @precondition(lambda self: self.model.is_persistent_session()) # @rule(data=st.data()) # def check_group_path(self, data): # t0 = time.time() # group = data.draw(st.sampled_from(self.all_groups)) # self.check_list_prefix_from_group(group) # self.check_subgroups(group) # self.check_group_arrays(group) # t1 = time.time() # note(f"Checks took {t1 - t0} sec.") @invariant() def check_list_prefix_from_root(self) -> None: model_list = self._sync_iter(self.model.list_prefix("")) store_list = self._sync_iter(self.store.list_prefix("")) note(f"Checking {len(model_list)} expected keys vs {len(store_list)} actual keys") assert sorted(model_list) == sorted(store_list), ( sorted(model_list), sorted(store_list), ) # check that our internal state matches that of the store and model assert all(f"{path}/zarr.json" in model_list for path in self.all_groups | self.all_arrays) assert all(f"{path}/zarr.json" in store_list for path in self.all_groups | self.all_arrays) class SyncStoreWrapper(zarr.core.sync.SyncMixin): def __init__(self, store: Store) -> None: """Synchronous Store wrapper This class holds synchronous methods that map to async methods of Store classes. The synchronous wrapper is needed because hypothesis' stateful testing infra does not support asyncio so we redefine sync versions of the Store API. https://github.com/HypothesisWorks/hypothesis/issues/3712#issuecomment-1668999041 """ self.store = store @property def read_only(self) -> bool: return self.store.read_only def set(self, key: str, data_buffer: Buffer) -> None: return self._sync(self.store.set(key, data_buffer)) def list(self) -> builtins.list[str]: return self._sync_iter(self.store.list()) def get(self, key: str, prototype: BufferPrototype) -> Buffer | None: return self._sync(self.store.get(key, prototype=prototype)) def get_partial_values( self, key_ranges: builtins.list[Any], prototype: BufferPrototype ) -> builtins.list[Buffer | None]: return self._sync(self.store.get_partial_values(prototype=prototype, key_ranges=key_ranges)) def delete(self, path: str) -> None: return self._sync(self.store.delete(path)) def is_empty(self, prefix: str) -> bool: return self._sync(self.store.is_empty(prefix=prefix)) def clear(self) -> None: return self._sync(self.store.clear()) def exists(self, key: str) -> bool: return self._sync(self.store.exists(key)) def list_dir(self, prefix: str) -> None: raise NotImplementedError def list_prefix(self, prefix: str) -> None: raise NotImplementedError @property def supports_listing(self) -> bool: return self.store.supports_listing @property def supports_writes(self) -> bool: return self.store.supports_writes @property def supports_deletes(self) -> bool: return self.store.supports_deletes class ZarrStoreStateMachine(RuleBasedStateMachine): """ " Zarr store state machine This is a subclass of a Hypothesis RuleBasedStateMachine. It is testing a framework to ensure that the state of a Zarr store matches an expected state after a set of random operations. It contains a store (currently, a Zarr MemoryStore) and a model, a simplified version of a zarr store (in this case, a dict). It also contains rules which represent actions that can be applied to a zarr store. Rules apply an action to both the store and the model, and invariants assert that the state of the model is equal to the state of the store. Hypothesis then generates sequences of rules, running invariants after each rule. It raises an error if a sequence produces discontinuity between state of the model and state of the store (ie. an invariant is violated). https://hypothesis.readthedocs.io/en/latest/stateful.html """ def __init__(self, store: Store) -> None: super().__init__() self.model: dict[str, Buffer] = {} self.store = SyncStoreWrapper(store) self.prototype = default_buffer_prototype() @initialize() def init_store(self) -> None: self.store.clear() @rule(key=zarr_keys(), data=st.binary(min_size=0, max_size=MAX_BINARY_SIZE)) def set(self, key: str, data: bytes) -> None: note(f"(set) Setting {key!r} with {data!r}") assert not self.store.read_only data_buf = cpu.Buffer.from_bytes(data) self.store.set(key, data_buf) self.model[key] = data_buf @precondition(lambda self: len(self.model.keys()) > 0) @rule(key=zarr_keys(), data=st.data()) def get(self, key: str, data: DataObject) -> None: key = data.draw( st.sampled_from(sorted(self.model.keys())) ) # hypothesis wants to sample from sorted list note("(get)") store_value = self.store.get(key, self.prototype) # to bytes here necessary because data_buf set to model in set() assert self.model[key] == store_value @rule(key=zarr_keys(), data=st.data()) def get_invalid_zarr_keys(self, key: str, data: DataObject) -> None: note("(get_invalid)") assume(key not in self.model) assert self.store.get(key, self.prototype) is None @precondition(lambda self: len(self.model.keys()) > 0) @rule(data=st.data()) def get_partial_values(self, data: DataObject) -> None: key_range = data.draw( key_ranges(keys=st.sampled_from(sorted(self.model.keys())), max_size=MAX_BINARY_SIZE) ) note(f"(get partial) {key_range=}") obs_maybe = self.store.get_partial_values(key_range, self.prototype) observed = [] for obs in obs_maybe: assert obs is not None observed.append(obs.to_bytes()) model_vals_ls = [] for key, byte_range in key_range: start = byte_range.start stop = byte_range.end model_vals_ls.append(self.model[key][start:stop]) assert all( obs == exp.to_bytes() for obs, exp in zip(observed, model_vals_ls, strict=True) ), ( observed, model_vals_ls, ) @precondition(lambda self: self.store.supports_deletes) @precondition(lambda self: len(self.model.keys()) > 0) @rule(data=st.data()) def delete(self, data: DataObject) -> None: key = data.draw(st.sampled_from(sorted(self.model.keys()))) note(f"(delete) Deleting {key=}") self.store.delete(key) del self.model[key] @rule() def clear(self) -> None: assert not self.store.read_only note("(clear)") self.store.clear() self.model.clear() assert self.store.is_empty("") assert len(self.model.keys()) == len(list(self.store.list())) == 0 @rule() # Local store can be non-empty when there are subdirectories but no files @precondition(lambda self: not isinstance(self.store.store, LocalStore)) def is_empty(self) -> None: note("(is_empty)") # make sure they either both are or both aren't empty (same state) assert self.store.is_empty("") == (not self.model) @rule(key=zarr_keys()) def exists(self, key: str) -> None: note("(exists)") assert self.store.exists(key) == (key in self.model) @invariant() def check_paths_equal(self) -> None: note("Checking that paths are equal") paths = sorted(self.store.list()) assert sorted(self.model.keys()) == paths @invariant() def check_vals_equal(self) -> None: note("Checking values equal") for key, val in self.model.items(): store_item = self.store.get(key, self.prototype) assert val == store_item @invariant() def check_num_zarr_keys_equal(self) -> None: note("check num zarr_keys equal") assert len(self.model) == len(list(self.store.list())) @invariant() def check_zarr_keys(self) -> None: keys = list(self.store.list()) if not keys: assert self.store.is_empty("") is True else: assert self.store.is_empty("") is False for key in keys: assert self.store.exists(key) is True note("checking keys / exists / empty") zarr-python-3.2.1/src/zarr/testing/store.py000066400000000000000000000655461517635743000207750ustar00rootroot00000000000000from __future__ import annotations import asyncio import json import pickle from abc import abstractmethod from typing import TYPE_CHECKING, Self from zarr.storage import WrapperStore if TYPE_CHECKING: from typing import Any from zarr.core.buffer.core import BufferPrototype import pytest from zarr.abc.store import ( ByteRequest, OffsetByteRequest, RangeByteRequest, Store, SuffixByteRequest, SupportsDeleteSync, SupportsGetSync, SupportsSetSync, ) from zarr.core.buffer import Buffer, default_buffer_prototype from zarr.core.sync import _collect_aiterator, sync from zarr.storage._utils import _normalize_byte_range_index from zarr.testing.utils import assert_bytes_equal __all__ = ["StoreTests"] class StoreTests[S: Store, B: Buffer]: store_cls: type[S] buffer_cls: type[B] @staticmethod def _require_get_sync(store: S) -> SupportsGetSync: """Skip unless *store* implements :class:`SupportsGetSync`.""" if not isinstance(store, SupportsGetSync): pytest.skip("store does not implement SupportsGetSync") return store # type: ignore[unreachable] @staticmethod def _require_set_sync(store: S) -> SupportsSetSync: """Skip unless *store* implements :class:`SupportsSetSync`.""" if not isinstance(store, SupportsSetSync): pytest.skip("store does not implement SupportsSetSync") return store # type: ignore[unreachable] @staticmethod def _require_delete_sync(store: S) -> SupportsDeleteSync: """Skip unless *store* implements :class:`SupportsDeleteSync`.""" if not isinstance(store, SupportsDeleteSync): pytest.skip("store does not implement SupportsDeleteSync") return store # type: ignore[unreachable] @abstractmethod async def set(self, store: S, key: str, value: Buffer) -> None: """ Insert a value into a storage backend, with a specific key. This should not use any store methods. Bypassing the store methods allows them to be tested. """ ... @abstractmethod async def get(self, store: S, key: str) -> Buffer: """ Retrieve a value from a storage backend, by key. This should not use any store methods. Bypassing the store methods allows them to be tested. """ ... @abstractmethod @pytest.fixture def store_kwargs(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Kwargs for instantiating a store""" ... @abstractmethod def test_store_repr(self, store: S) -> None: ... @abstractmethod def test_store_supports_writes(self, store: S) -> None: ... def test_store_supports_partial_writes(self, store: S) -> None: assert not store.supports_partial_writes @abstractmethod def test_store_supports_listing(self, store: S) -> None: ... @pytest.fixture def open_kwargs(self, store_kwargs: dict[str, Any]) -> dict[str, Any]: return store_kwargs @pytest.fixture async def store(self, open_kwargs: dict[str, Any]) -> Store: return await self.store_cls.open(**open_kwargs) @pytest.fixture async def store_not_open(self, store_kwargs: dict[str, Any]) -> Store: return self.store_cls(**store_kwargs) def test_store_type(self, store: S) -> None: assert isinstance(store, Store) assert isinstance(store, self.store_cls) def test_store_eq(self, store: S, store_kwargs: dict[str, Any]) -> None: # check self equality assert store == store # check store equality with same inputs # asserting this is important for being able to compare (de)serialized stores store2 = self.store_cls(**store_kwargs) assert store == store2 async def test_serializable_store(self, store: S) -> None: new_store: S = pickle.loads(pickle.dumps(store)) assert new_store == store assert new_store.read_only == store.read_only # quickly roundtrip data to a key to test that new store works data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") key = "foo" await store.set(key, data_buf) observed = await store.get(key, prototype=default_buffer_prototype()) assert_bytes_equal(observed, data_buf) def test_store_read_only(self, store: S) -> None: assert not store.read_only with pytest.raises(AttributeError): store.read_only = False # type: ignore[misc] @pytest.mark.parametrize("read_only", [True, False]) async def test_store_open_read_only(self, open_kwargs: dict[str, Any], read_only: bool) -> None: open_kwargs["read_only"] = read_only store = await self.store_cls.open(**open_kwargs) assert store._is_open assert store.read_only == read_only async def test_store_context_manager(self, open_kwargs: dict[str, Any]) -> None: # Test that the context manager closes the store with await self.store_cls.open(**open_kwargs) as store: assert store._is_open # Test trying to open an already open store with pytest.raises(ValueError, match="store is already open"): await store._open() assert not store._is_open async def test_read_only_store_raises(self, open_kwargs: dict[str, Any]) -> None: kwargs = {**open_kwargs, "read_only": True} store = await self.store_cls.open(**kwargs) assert store.read_only # set with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await store.set("foo", self.buffer_cls.from_bytes(b"bar")) # delete with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await store.delete("foo") async def test_with_read_only_store(self, open_kwargs: dict[str, Any]) -> None: kwargs = {**open_kwargs, "read_only": True} store = await self.store_cls.open(**kwargs) assert store.read_only # Test that you cannot write to a read-only store with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await store.set("foo", self.buffer_cls.from_bytes(b"bar")) # Check if the store implements with_read_only try: writer = store.with_read_only(read_only=False) except NotImplementedError: # Test that stores that do not implement with_read_only raise NotImplementedError with the correct message with pytest.raises( NotImplementedError, match=f"with_read_only is not implemented for the {type(store)} store type.", ): store.with_read_only(read_only=False) return # Test that you can write to a new store copy assert not writer._is_open assert not writer.read_only await writer.set("foo", self.buffer_cls.from_bytes(b"bar")) await writer.delete("foo") # Test that you cannot write to the original store assert store.read_only with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await store.set("foo", self.buffer_cls.from_bytes(b"bar")) with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await store.delete("foo") # Test that you cannot write to a read-only store copy reader = store.with_read_only(read_only=True) assert reader.read_only with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await reader.set("foo", self.buffer_cls.from_bytes(b"bar")) with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await reader.delete("foo") @pytest.mark.parametrize("key", ["c/0", "foo/c/0.0", "foo/0/0"]) @pytest.mark.parametrize( ("data", "byte_range"), [ (b"\x01\x02\x03\x04", None), (b"\x01\x02\x03\x04", RangeByteRequest(1, 4)), (b"\x01\x02\x03\x04", OffsetByteRequest(1)), (b"\x01\x02\x03\x04", SuffixByteRequest(1)), (b"", None), ], ) async def test_get(self, store: S, key: str, data: bytes, byte_range: ByteRequest) -> None: """ Ensure that data can be read from the store using the store.get method. """ data_buf = self.buffer_cls.from_bytes(data) await self.set(store, key, data_buf) observed = await store.get(key, prototype=default_buffer_prototype(), byte_range=byte_range) start, stop = _normalize_byte_range_index(data_buf, byte_range=byte_range) expected = data_buf[start:stop] assert_bytes_equal(observed, expected) async def test_get_not_open(self, store_not_open: S) -> None: """ Ensure that data can be read from the store that isn't yet open using the store.get method. """ assert not store_not_open._is_open data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") key = "c/0" await self.set(store_not_open, key, data_buf) observed = await store_not_open.get(key, prototype=default_buffer_prototype()) assert_bytes_equal(observed, data_buf) async def test_get_raises(self, store: S) -> None: """ Ensure that a ValueError is raise for invalid byte range syntax """ data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") await self.set(store, "c/0", data_buf) with pytest.raises((ValueError, TypeError), match=r"Unexpected byte_range, got.*"): await store.get("c/0", prototype=default_buffer_prototype(), byte_range=(0, 2)) # type: ignore[arg-type] async def test_get_many(self, store: S) -> None: """ Ensure that multiple keys can be retrieved at once with the _get_many method. """ keys = tuple(map(str, range(10))) values = tuple(f"{k}".encode() for k in keys) for k, v in zip(keys, values, strict=False): await self.set(store, k, self.buffer_cls.from_bytes(v)) observed_buffers = await _collect_aiterator( store._get_many( zip( keys, (default_buffer_prototype(),) * len(keys), (None,) * len(keys), strict=False, ) ) ) observed_kvs = sorted(((k, b.to_bytes()) for k, b in observed_buffers)) # type: ignore[union-attr] expected_kvs = sorted(((k, b) for k, b in zip(keys, values, strict=False))) assert observed_kvs == expected_kvs @pytest.mark.parametrize("key", ["c/0", "foo/c/0.0", "foo/0/0"]) @pytest.mark.parametrize("data", [b"\x01\x02\x03\x04", b""]) async def test_getsize(self, store: S, key: str, data: bytes) -> None: """ Test the result of store.getsize(). """ data_buf = self.buffer_cls.from_bytes(data) expected = len(data_buf) await self.set(store, key, data_buf) observed = await store.getsize(key) assert observed == expected async def test_getsize_prefix(self, store: S) -> None: """ Test the result of store.getsize_prefix(). """ data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") keys = ["c/0/0", "c/0/1", "c/1/0", "c/1/1"] keys_values = [(k, data_buf) for k in keys] await store._set_many(keys_values) expected = len(data_buf) * len(keys) observed = await store.getsize_prefix("c") assert observed == expected async def test_getsize_raises(self, store: S) -> None: """ Test that getsize() raise a FileNotFoundError if the key doesn't exist. """ with pytest.raises(FileNotFoundError): await store.getsize("c/1000") @pytest.mark.parametrize("key", ["zarr.json", "c/0", "foo/c/0.0", "foo/0/0"]) @pytest.mark.parametrize("data", [b"\x01\x02\x03\x04", b""]) async def test_set(self, store: S, key: str, data: bytes) -> None: """ Ensure that data can be written to the store using the store.set method. """ assert not store.read_only data_buf = self.buffer_cls.from_bytes(data) await store.set(key, data_buf) observed = await self.get(store, key) assert_bytes_equal(observed, data_buf) async def test_set_not_open(self, store_not_open: S) -> None: """ Ensure that data can be written to the store that's not yet open using the store.set method. """ assert not store_not_open._is_open data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") key = "c/0" await store_not_open.set(key, data_buf) observed = await self.get(store_not_open, key) assert_bytes_equal(observed, data_buf) async def test_set_many(self, store: S) -> None: """ Test that a dict of key : value pairs can be inserted into the store via the `_set_many` method. """ keys = ["zarr.json", "c/0", "foo/c/0.0", "foo/0/0"] data_buf = [self.buffer_cls.from_bytes(k.encode()) for k in keys] store_dict = dict(zip(keys, data_buf, strict=True)) await store._set_many(store_dict.items()) for k, v in store_dict.items(): assert (await self.get(store, k)).to_bytes() == v.to_bytes() @pytest.mark.parametrize( "key_ranges", [ [], [("zarr.json", RangeByteRequest(0, 2))], [("c/0", RangeByteRequest(0, 2)), ("zarr.json", None)], [ ("c/0/0", RangeByteRequest(0, 2)), ("c/0/1", SuffixByteRequest(2)), ("c/0/2", OffsetByteRequest(2)), ], ], ) async def test_get_partial_values( self, store: S, key_ranges: list[tuple[str, ByteRequest]] ) -> None: # put all of the data for key, _ in key_ranges: await self.set(store, key, self.buffer_cls.from_bytes(bytes(key, encoding="utf-8"))) # read back just part of it observed_maybe = await store.get_partial_values( prototype=default_buffer_prototype(), key_ranges=key_ranges ) observed: list[Buffer] = [] expected: list[Buffer] = [] for obs in observed_maybe: assert obs is not None observed.append(obs) for idx in range(len(observed)): key, byte_range = key_ranges[idx] result = await store.get( key, prototype=default_buffer_prototype(), byte_range=byte_range ) assert result is not None expected.append(result) assert all( obs.to_bytes() == exp.to_bytes() for obs, exp in zip(observed, expected, strict=True) ) async def test_exists(self, store: S) -> None: assert not await store.exists("foo") await store.set("foo/zarr.json", self.buffer_cls.from_bytes(b"bar")) assert await store.exists("foo/zarr.json") async def test_delete(self, store: S) -> None: if not store.supports_deletes: pytest.skip("store does not support deletes") await store.set("foo/zarr.json", self.buffer_cls.from_bytes(b"bar")) assert await store.exists("foo/zarr.json") await store.delete("foo/zarr.json") assert not await store.exists("foo/zarr.json") async def test_delete_dir(self, store: S) -> None: if not store.supports_deletes: pytest.skip("store does not support deletes") await store.set("zarr.json", self.buffer_cls.from_bytes(b"root")) await store.set("foo-bar/zarr.json", self.buffer_cls.from_bytes(b"root")) await store.set("foo/zarr.json", self.buffer_cls.from_bytes(b"bar")) await store.set("foo/c/0", self.buffer_cls.from_bytes(b"chunk")) await store.delete_dir("foo") assert await store.exists("zarr.json") assert await store.exists("foo-bar/zarr.json") assert not await store.exists("foo/zarr.json") assert not await store.exists("foo/c/0") async def test_delete_nonexistent_key_does_not_raise(self, store: S) -> None: if not store.supports_deletes: pytest.skip("store does not support deletes") await store.delete("nonexistent_key") async def test_is_empty(self, store: S) -> None: assert await store.is_empty("") await self.set( store, "foo/bar", self.buffer_cls.from_bytes(bytes("something", encoding="utf-8")) ) assert not await store.is_empty("") assert await store.is_empty("fo") assert not await store.is_empty("foo/") assert not await store.is_empty("foo") assert await store.is_empty("spam/") async def test_clear(self, store: S) -> None: await self.set( store, "key", self.buffer_cls.from_bytes(bytes("something", encoding="utf-8")) ) await store.clear() assert await store.is_empty("") async def test_list(self, store: S) -> None: assert await _collect_aiterator(store.list()) == () prefix = "foo" data = self.buffer_cls.from_bytes(b"") store_dict = { f"{prefix}/zarr.json": data, **{f"{prefix}/c/{idx}": data for idx in range(10)}, } await store._set_many(store_dict.items()) expected_sorted = sorted(store_dict.keys()) observed = await _collect_aiterator(store.list()) observed_sorted = sorted(observed) assert observed_sorted == expected_sorted async def test_list_prefix(self, store: S) -> None: """ Test that the `list_prefix` method works as intended. Given a prefix, it should return all the keys in storage that start with this prefix. """ prefixes = ("", "a/", "a/b/", "a/b/c/") data = self.buffer_cls.from_bytes(b"") fname = "zarr.json" store_dict = {p + fname: data for p in prefixes} await store._set_many(store_dict.items()) for prefix in prefixes: observed = tuple(sorted(await _collect_aiterator(store.list_prefix(prefix)))) expected: tuple[str, ...] = () for key in store_dict: if key.startswith(prefix): expected += (key,) expected = tuple(sorted(expected)) assert observed == expected async def test_list_empty_path(self, store: S) -> None: """ Verify that list and list_prefix work correctly when path is an empty string, i.e. no unwanted replacement occurs. """ data = self.buffer_cls.from_bytes(b"") store_dict = { "foo/bar/zarr.json": data, "foo/bar/c/1": data, "foo/baz/c/0": data, } await store._set_many(store_dict.items()) # Test list() observed_list = await _collect_aiterator(store.list()) observed_list_sorted = sorted(observed_list) expected_list_sorted = sorted(store_dict.keys()) assert observed_list_sorted == expected_list_sorted # Test list_prefix() with an empty prefix observed_prefix_empty = await _collect_aiterator(store.list_prefix("")) observed_prefix_empty_sorted = sorted(observed_prefix_empty) expected_prefix_empty_sorted = sorted(store_dict.keys()) assert observed_prefix_empty_sorted == expected_prefix_empty_sorted # Test list_prefix() with a non-empty prefix observed_prefix = await _collect_aiterator(store.list_prefix("foo/bar/")) observed_prefix_sorted = sorted(observed_prefix) expected_prefix_sorted = sorted(k for k in store_dict if k.startswith("foo/bar/")) assert observed_prefix_sorted == expected_prefix_sorted async def test_list_dir(self, store: S) -> None: roots_and_keys: list[tuple[str, dict[str, Buffer]]] = [ ( "foo", { "foo/zarr.json": self.buffer_cls.from_bytes(b"bar"), "foo/c/1": self.buffer_cls.from_bytes(b"\x01"), }, ), ( "foo/bar", { "foo/bar/foobar_first_child": self.buffer_cls.from_bytes(b"1"), "foo/bar/foobar_second_child/zarr.json": self.buffer_cls.from_bytes(b"2"), }, ), ] assert await _collect_aiterator(store.list_dir("")) == () for root, store_dict in roots_and_keys: assert await _collect_aiterator(store.list_dir(root)) == () await store._set_many(store_dict.items()) keys_observed = await _collect_aiterator(store.list_dir(root)) keys_expected = {k.removeprefix(f"{root}/").split("/")[0] for k in store_dict} assert sorted(keys_observed) == sorted(keys_expected) keys_observed = await _collect_aiterator(store.list_dir(f"{root}/")) assert sorted(keys_expected) == sorted(keys_observed) async def test_set_if_not_exists(self, store: S) -> None: key = "k" data_buf = self.buffer_cls.from_bytes(b"0000") await self.set(store, key, data_buf) new = self.buffer_cls.from_bytes(b"1111") await store.set_if_not_exists("k", new) # no error result = await store.get(key, default_buffer_prototype()) assert result == data_buf await store.set_if_not_exists("k2", new) # no error result = await store.get("k2", default_buffer_prototype()) assert result == new async def test_get_bytes(self, store: S) -> None: """ Test that the get_bytes method reads bytes. """ data = b"hello world" key = "zarr.json" await self.set(store, key, self.buffer_cls.from_bytes(data)) assert await store._get_bytes(key, prototype=default_buffer_prototype()) == data with pytest.raises(FileNotFoundError): await store._get_bytes("nonexistent_key", prototype=default_buffer_prototype()) def test_get_bytes_sync(self, store: S) -> None: """ Test that the get_bytes_sync method reads bytes. """ data = b"hello world" key = "zarr.json" sync(self.set(store, key, self.buffer_cls.from_bytes(data))) assert store._get_bytes_sync(key, prototype=default_buffer_prototype()) == data async def test_get_json(self, store: S) -> None: """ Test that the get_json method reads json. """ data = {"foo": "bar"} data_bytes = json.dumps(data).encode("utf-8") key = "zarr.json" await self.set(store, key, self.buffer_cls.from_bytes(data_bytes)) assert await store._get_json(key, prototype=default_buffer_prototype()) == data def test_get_json_sync(self, store: S) -> None: """ Test that the get_json method reads json. """ data = {"foo": "bar"} data_bytes = json.dumps(data).encode("utf-8") key = "zarr.json" sync(self.set(store, key, self.buffer_cls.from_bytes(data_bytes))) assert store._get_json_sync(key, prototype=default_buffer_prototype()) == data # ------------------------------------------------------------------- # Synchronous store methods (SupportsSyncStore protocol) # ------------------------------------------------------------------- def test_get_sync(self, store: S) -> None: getter = self._require_get_sync(store) data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") key = "sync_get" sync(self.set(store, key, data_buf)) result = getter.get_sync(key) assert result is not None assert_bytes_equal(result, data_buf) def test_get_sync_missing(self, store: S) -> None: getter = self._require_get_sync(store) result = getter.get_sync("nonexistent") assert result is None def test_set_sync(self, store: S) -> None: setter = self._require_set_sync(store) data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") key = "sync_set" setter.set_sync(key, data_buf) result = sync(self.get(store, key)) assert_bytes_equal(result, data_buf) def test_delete_sync(self, store: S) -> None: setter = self._require_set_sync(store) deleter = self._require_delete_sync(store) getter = self._require_get_sync(store) if not store.supports_deletes: pytest.skip("store does not support deletes") data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") key = "sync_delete" setter.set_sync(key, data_buf) deleter.delete_sync(key) result = getter.get_sync(key) assert result is None def test_delete_sync_missing(self, store: S) -> None: deleter = self._require_delete_sync(store) if not store.supports_deletes: pytest.skip("store does not support deletes") # should not raise deleter.delete_sync("nonexistent_sync") class LatencyStore(WrapperStore[Store]): """ A wrapper class that takes any store class in its constructor and adds latency to the `set` and `get` methods. This can be used for performance testing. """ get_latency: float set_latency: float def __init__(self, store: Store, *, get_latency: float = 0, set_latency: float = 0) -> None: self.get_latency = float(get_latency) self.set_latency = float(set_latency) self._store = store def _with_store(self, store: Store) -> Self: return type(self)(store, get_latency=self.get_latency, set_latency=self.set_latency) async def set(self, key: str, value: Buffer) -> None: """ Add latency to the ``set`` method. Calls ``asyncio.sleep(self.set_latency)`` before invoking the wrapped ``set`` method. Parameters ---------- key : str The key to set value : Buffer The value to set Returns ------- None """ await asyncio.sleep(self.set_latency) await self._store.set(key, value) async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> Buffer | None: """ Add latency to the ``get`` method. Calls ``asyncio.sleep(self.get_latency)`` before invoking the wrapped ``get`` method. Parameters ---------- key : str The key to get prototype : BufferPrototype The BufferPrototype to use. byte_range : ByteRequest, optional An optional byte range. Returns ------- buffer : Buffer or None """ await asyncio.sleep(self.get_latency) return await self._store.get(key, prototype=prototype, byte_range=byte_range) zarr-python-3.2.1/src/zarr/testing/strategies.py000066400000000000000000000556171517635743000220110ustar00rootroot00000000000000import math import sys from collections.abc import Callable, Mapping from typing import Any, Literal import hypothesis.extra.numpy as npst import hypothesis.strategies as st import numpy as np import numpy.typing as npt from hypothesis import event from hypothesis.strategies import SearchStrategy import zarr from zarr.abc.store import RangeByteRequest, Store from zarr.codecs.bytes import BytesCodec from zarr.core.array import Array from zarr.core.chunk_key_encodings import DefaultChunkKeyEncoding from zarr.core.common import JSON, AccessModeLiteral, ZarrFormat from zarr.core.dtype import get_data_type_from_native_dtype from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.core.metadata.v3 import RectilinearChunkGridMetadata, RegularChunkGridMetadata from zarr.core.sync import sync from zarr.storage import MemoryStore, StoreLike from zarr.storage._utils import _join_paths, normalize_path from zarr.types import AnyArray TrueOrFalse = Literal[True, False] # Copied from Xarray _attr_keys = st.text(st.characters(), min_size=1) _attr_values = st.recursive( st.none() | st.booleans() | st.text(st.characters(), max_size=5), lambda children: st.lists(children) | st.dictionaries(_attr_keys, children), max_leaves=3, ) @st.composite def keys(draw: st.DrawFn, *, max_num_nodes: int | None = None) -> str: return draw(st.lists(node_names, min_size=1, max_size=max_num_nodes).map("/".join)) @st.composite def paths(draw: st.DrawFn, *, max_num_nodes: int | None = None) -> str: return draw(st.just("/") | keys(max_num_nodes=max_num_nodes)) def dtypes() -> st.SearchStrategy[np.dtype[Any]]: return ( npst.boolean_dtypes() | npst.integer_dtypes(endianness="=") | npst.unsigned_integer_dtypes(endianness="=") | npst.floating_dtypes(endianness="=") | npst.complex_number_dtypes(endianness="=") | npst.byte_string_dtypes(endianness="=") | npst.unicode_string_dtypes(endianness="=") | npst.datetime64_dtypes(endianness="=") | npst.timedelta64_dtypes(endianness="=") ) def v3_dtypes() -> st.SearchStrategy[np.dtype[Any]]: return dtypes() def v2_dtypes() -> st.SearchStrategy[np.dtype[Any]]: return dtypes() def safe_unicode_for_dtype(dtype: np.dtype[np.str_]) -> st.SearchStrategy[str]: """Generate UTF-8-safe text constrained to max_len of dtype.""" # account for utf-32 encoding (i.e. 4 bytes/character) max_len = max(1, dtype.itemsize // 4) return st.text( alphabet=st.characters( exclude_categories=["Cs"], # Avoid *technically allowed* surrogates min_codepoint=32, ), min_size=1, max_size=max_len, ) def clear_store(x: Store) -> Store: sync(x.clear()) return x # From https://zarr-specs.readthedocs.io/en/latest/v3/core/v3.0.html#node-names # 1. must not be the empty string ("") # 2. must not include the character "/" # 3. must not be a string composed only of period characters, e.g. "." or ".." # 4. must not start with the reserved prefix "__" zarr_key_chars = st.sampled_from( ".-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" ) node_names = ( st.text(zarr_key_chars, min_size=1) .filter(lambda t: t not in (".", "..") and not t.startswith("__")) .filter(lambda name: name.lower() != "zarr.json") ) short_node_names = ( st.text(zarr_key_chars, max_size=3, min_size=1) .filter(lambda t: t not in (".", "..") and not t.startswith("__")) .filter(lambda name: name.lower() != "zarr.json") ) array_names = node_names attrs: st.SearchStrategy[Mapping[str, JSON] | None] = st.none() | st.dictionaries( _attr_keys, _attr_values ) # st.builds will only call a new store constructor for different keyword arguments # i.e. stores.examples() will always return the same object per Store class. # So we map a clear to reset the store. stores = st.builds(MemoryStore, st.just({})).map(clear_store) compressors = st.sampled_from([None, "default"]) zarr_formats: st.SearchStrategy[ZarrFormat] = st.sampled_from([3, 2]) # We de-prioritize arrays having dim sizes 0, 1, 2 array_shapes = npst.array_shapes(max_dims=4, min_side=3, max_side=5) | npst.array_shapes( max_dims=4, min_side=0 ) @st.composite def dimension_names(draw: st.DrawFn, *, ndim: int | None = None) -> list[None | str] | None: simple_text = st.text(zarr_key_chars, min_size=0) return draw(st.none() | st.lists(st.none() | simple_text, min_size=ndim, max_size=ndim)) # type: ignore[arg-type] @st.composite def array_metadata( draw: st.DrawFn, *, array_shapes: Callable[..., st.SearchStrategy[tuple[int, ...]]] = npst.array_shapes, zarr_formats: st.SearchStrategy[ZarrFormat] = zarr_formats, attributes: SearchStrategy[Mapping[str, JSON] | None] = attrs, ) -> ArrayV2Metadata | ArrayV3Metadata: zarr_format = draw(zarr_formats) # separator = draw(st.sampled_from(['/', '\\'])) shape = draw(array_shapes()) ndim = len(shape) np_dtype = draw(dtypes()) dtype = get_data_type_from_native_dtype(np_dtype) fill_value = draw(npst.from_dtype(np_dtype)) if zarr_format == 2: chunk_shape = draw(array_shapes(min_dims=ndim, max_dims=ndim, min_side=1)) return ArrayV2Metadata( shape=shape, chunks=chunk_shape, dtype=dtype, fill_value=fill_value, order=draw(st.sampled_from(["C", "F"])), attributes=draw(attributes), # type: ignore[arg-type] dimension_separator=draw(st.sampled_from([".", "/"])), filters=None, compressor=None, ) else: chunk_grid = draw(chunk_grids(shape=shape)) return ArrayV3Metadata( shape=shape, data_type=dtype, chunk_grid=chunk_grid, fill_value=fill_value, attributes=draw(attributes), # type: ignore[arg-type] dimension_names=draw(dimension_names(ndim=ndim)), chunk_key_encoding=DefaultChunkKeyEncoding(separator="/"), # FIXME codecs=[BytesCodec()], storage_transformers=(), ) @st.composite def numpy_arrays( draw: st.DrawFn, *, shapes: st.SearchStrategy[tuple[int, ...]] = array_shapes, dtype: np.dtype[Any] | None = None, ) -> npt.NDArray[Any]: """ Generate numpy arrays that can be saved in the provided Zarr format. """ if dtype is None: dtype = draw(dtypes()) if np.issubdtype(dtype, np.str_): safe_unicode_strings = safe_unicode_for_dtype(dtype) return draw(npst.arrays(dtype=dtype, shape=shapes, elements=safe_unicode_strings)) return draw(npst.arrays(dtype=dtype, shape=shapes)) @st.composite def chunk_shapes(draw: st.DrawFn, *, shape: tuple[int, ...]) -> tuple[int, ...]: # We want this strategy to shrink towards arrays with smaller number of chunks # 1. st.integers() shrinks towards smaller values. So we use that to generate number of chunks numchunks = draw( st.tuples( *[ st.integers(min_value=0 if size == 0 else 1, max_value=max(size, 1)) for size in shape ] ) ) # 2. and now generate the chunks tuple # Chunk sizes must be >= 1 per spec; for zero-extent dimensions use 1. chunks = tuple( max(1, size // nchunks) if nchunks > 0 else 1 for size, nchunks in zip(shape, numchunks, strict=True) ) for c in chunks: event("chunk size", c) if any((c != 0 and s % c != 0) for s, c in zip(shape, chunks, strict=True)): event("smaller last chunk") return chunks @st.composite def shard_shapes( draw: st.DrawFn, *, shape: tuple[int, ...], chunk_shape: tuple[int, ...] ) -> tuple[int, ...]: # We want this strategy to shrink towards arrays with smaller number of shards # shards must be an integral number of chunks assert all(c != 0 for c in chunk_shape) numchunks = tuple(s // c for s, c in zip(shape, chunk_shape, strict=True)) multiples = tuple(draw(st.integers(min_value=1, max_value=nc)) for nc in numchunks) return tuple(m * c for m, c in zip(multiples, chunk_shape, strict=True)) @st.composite def np_array_and_chunks( draw: st.DrawFn, *, arrays: st.SearchStrategy[npt.NDArray[Any]] = numpy_arrays(), # noqa: B008 ) -> tuple[np.ndarray[Any, Any], tuple[int, ...]]: """A hypothesis strategy to generate small sized random arrays. Returns: a tuple of the array and a suitable random chunking for it. """ array = draw(arrays) return (array, draw(chunk_shapes(shape=array.shape))) @st.composite def arrays( draw: st.DrawFn, *, shapes: st.SearchStrategy[tuple[int, ...]] = array_shapes, compressors: st.SearchStrategy = compressors, stores: st.SearchStrategy[StoreLike] = stores, paths: st.SearchStrategy[str] = paths(), # noqa: B008 array_names: st.SearchStrategy = array_names, arrays: st.SearchStrategy | None = None, attrs: st.SearchStrategy = attrs, zarr_formats: st.SearchStrategy = zarr_formats, open_mode: AccessModeLiteral = "w", ) -> AnyArray: store = draw(stores, label="store") path = draw(paths, label="array parent") name = draw(array_names, label="array name") attributes = draw(attrs, label="attributes") zarr_format = draw(zarr_formats, label="zarr format") if arrays is None: arrays = numpy_arrays(shapes=shapes) nparray = draw(arrays, label="array data") dim_names: None | list[str | None] = None # For v3 arrays, optionally use RectilinearChunkGridMetadata chunk_grid_meta: RegularChunkGridMetadata | RectilinearChunkGridMetadata | None = None shard_shape = None if zarr_format == 3: chunk_grid_meta = draw(chunk_grids(shape=nparray.shape), label="chunk grid") # Sharding is only supported with regular chunk grids, and has complex # divisibility constraints that don't play well with hypothesis shrinking. # Disabled for now — sharding should be tested separately. dim_names = draw(dimension_names(ndim=nparray.ndim), label="dimension names") else: dim_names = None # test that None works too. fill_value = draw(st.one_of([st.none(), npst.from_dtype(nparray.dtype)])) # compressor = draw(compressors) expected_attrs = {} if attributes is None else attributes array_path = _join_paths([path, name]) root = zarr.open_group(store, mode=open_mode, zarr_format=zarr_format) # Convert chunk grid metadata to a form create_array accepts: # - RegularChunkGridMetadata -> flat tuple of ints # - RectilinearChunkGridMetadata -> nested list of ints (triggers rectilinear path) # - v2 -> flat tuple of ints chunks_param: tuple[int, ...] | list[list[int]] if zarr_format == 3 and chunk_grid_meta is not None: if isinstance(chunk_grid_meta, RectilinearChunkGridMetadata): chunks_param = [ list(dim) if isinstance(dim, tuple) else [dim] for dim in chunk_grid_meta.chunk_shapes ] else: chunks_param = chunk_grid_meta.chunk_shape else: chunks_param = draw(chunk_shapes(shape=nparray.shape), label="chunk shape") a = root.create_array( array_path, shape=nparray.shape, chunks=chunks_param, shards=shard_shape, dtype=nparray.dtype, attributes=attributes, # compressor=compressor, # FIXME fill_value=fill_value, dimension_names=dim_names, ) assert isinstance(a, Array) if a.metadata.zarr_format == 3: assert a.fill_value is not None assert a.name is not None assert a.path == normalize_path(array_path) assert a.name == f"/{a.path}" assert isinstance(root[array_path], Array) assert nparray.shape == a.shape # Verify chunks — for rectilinear grids, .chunks raises if zarr_format == 3: if isinstance(a.metadata.chunk_grid, RectilinearChunkGridMetadata): assert shard_shape is None else: assert isinstance(a.metadata.chunk_grid, RegularChunkGridMetadata) assert a.metadata.chunk_grid.chunk_shape == a.chunks assert shard_shape == a.shards assert a.basename == name, (a.basename, name) assert dict(a.attrs) == expected_attrs a[:] = nparray return a @st.composite def simple_arrays( draw: st.DrawFn, *, shapes: st.SearchStrategy[tuple[int, ...]] = array_shapes, ) -> Any: return draw( arrays( shapes=shapes, paths=paths(max_num_nodes=2), array_names=short_node_names, attrs=st.none(), compressors=st.sampled_from([None, "default"]), ) ) @st.composite def rectilinear_chunks(draw: st.DrawFn, *, shape: tuple[int, ...]) -> list[list[int]]: """Generate valid rectilinear chunk shapes for a given array shape. Uses two modes per dimension: - "expanded": random divider points create arbitrary chunk sizes - "rle": uniform chunks with optional remainder, optionally shuffled Keeps max chunks per dimension <= 20 to avoid performance issues in property tests. With higher dimensions, the total chunk count grows multiplicatively. """ chunk_shapes: list[list[int]] = [] for size in shape: assert size > 0 if size > 1: mode = draw(st.sampled_from(["expanded", "rle"])) if mode == "expanded": event("rectilinear expanded") max_chunks = min(size - 1, 20) nchunks = draw(st.integers(min_value=1, max_value=max_chunks)) dividers = sorted( draw( st.lists( st.integers(min_value=1, max_value=size - 1), min_size=nchunks - 1, max_size=nchunks - 1, unique=True, ) ) ) chunk_shapes.append( [a - b for a, b in zip(dividers + [size], [0] + dividers, strict=False)] ) else: # RLE mode: uniform chunks with optional remainder max_chunk_size = min(size, 20) chunk_size = draw(st.integers(min_value=1, max_value=max_chunk_size)) n_full = size // chunk_size remainder = size % chunk_size chunks_list = [chunk_size] * n_full if remainder > 0: chunks_list.append(remainder) # Optionally shuffle to create non-contiguous duplicate patterns if draw(st.booleans()): event("rectilinear rle shuffled") chunks_list = draw(st.permutations(chunks_list)) else: event("rectilinear rle") chunk_shapes.append(list(chunks_list)) else: chunk_shapes.append([1]) return chunk_shapes @st.composite def chunk_grids( draw: st.DrawFn, *, shape: tuple[int, ...] ) -> RegularChunkGridMetadata | RectilinearChunkGridMetadata: """Generate either a RegularChunkGridMetadata or RectilinearChunkGridMetadata. This strategy depends on the global state of the config having rectilinear chunk grids enabled or not. This means that it may be a possible source of a hypothesis FlakyStrategy error due dependence on global state. However, in practice this seems unlikely to happen. This allows property tests to exercise both chunk grid types. """ # RectilinearChunkGridMetadata doesn't support zero-sized dimensions, # so use RegularChunkGridMetadata if any dimension is 0 if any(s == 0 for s in shape): event("using RegularChunkGridMetadata (zero-sized dimensions)") return RegularChunkGridMetadata(chunk_shape=draw(chunk_shapes(shape=shape))) if zarr.config.get("array.rectilinear_chunks") and draw(st.booleans()): chunks = draw(rectilinear_chunks(shape=shape)) event("using RectilinearChunkGridMetadata") return RectilinearChunkGridMetadata(chunk_shapes=tuple(tuple(dim) for dim in chunks)) else: event("using RegularChunkGridMetadata") return RegularChunkGridMetadata(chunk_shape=draw(chunk_shapes(shape=shape))) # Rectilinear arrays need min_side >= 1 so every dimension has at least one element _rectilinear_shapes = npst.array_shapes(max_dims=3, min_side=1, max_side=20) @st.composite def rectilinear_arrays( draw: st.DrawFn, *, shapes: st.SearchStrategy[tuple[int, ...]] = _rectilinear_shapes, ) -> Any: """Generate a zarr v3 array with rectilinear (variable) chunk grid.""" shape = draw(shapes) chunk_shapes = draw(rectilinear_chunks(shape=shape)) np_dtype = draw(dtypes()) nparray = draw(numpy_arrays(shapes=st.just(shape), dtype=np_dtype)) fill_value = draw(st.one_of([st.none(), npst.from_dtype(np_dtype)])) dim_names = draw(dimension_names(ndim=len(shape))) store = MemoryStore() with zarr.config.set({"array.rectilinear_chunks": True}): a = zarr.create_array( store=store, shape=shape, chunks=chunk_shapes, dtype=np_dtype, fill_value=fill_value, dimension_names=dim_names, ) a[:] = nparray return a def is_negative_slice(idx: Any) -> bool: return isinstance(idx, slice) and idx.step is not None and idx.step < 0 @st.composite def end_slices(draw: st.DrawFn, *, shape: tuple[int, ...]) -> Any: """ A strategy that slices ranges that include the last chunk. This is intended to stress-test handling of a possibly smaller last chunk. """ slicers = [] for size in shape: start = draw(st.integers(min_value=size // 2, max_value=size - 1)) length = draw(st.integers(min_value=0, max_value=size - start)) slicers.append(slice(start, start + length)) event("drawing end slice") return tuple(slicers) @st.composite def basic_indices( draw: st.DrawFn, *, shape: tuple[int, ...], min_dims: int = 0, max_dims: int | None = None, allow_newaxis: TrueOrFalse = False, allow_ellipsis: TrueOrFalse = True, ) -> Any: """Basic indices without unsupported negative slices.""" strategy = npst.basic_indices( shape=shape, min_dims=min_dims, max_dims=max_dims, allow_newaxis=allow_newaxis, allow_ellipsis=allow_ellipsis, ).filter( lambda idxr: ( not ( is_negative_slice(idxr) or (isinstance(idxr, tuple) and any(is_negative_slice(idx) for idx in idxr)) ) ) ) if math.prod(shape) >= 3: strategy = end_slices(shape=shape) | strategy return draw(strategy) @st.composite def orthogonal_indices( draw: st.DrawFn, *, shape: tuple[int, ...] ) -> tuple[tuple[np.ndarray[Any, Any], ...], tuple[np.ndarray[Any, Any], ...]]: """ Strategy that returns (1) a tuple of integer arrays used for orthogonal indexing of Zarr arrays. (2) a tuple of integer arrays that can be used for equivalent indexing of numpy arrays """ zindexer = [] npindexer = [] ndim = len(shape) for axis, size in enumerate(shape): if size != 0: strategy = npst.integer_array_indices( shape=(size,), result_shape=npst.array_shapes(min_side=1, max_side=size, max_dims=1) ) | basic_indices(min_dims=1, shape=(size,), allow_ellipsis=False) else: strategy = basic_indices(min_dims=1, shape=(size,), allow_ellipsis=False) val = draw( strategy # bare ints, slices .map(lambda x: (x,) if not isinstance(x, tuple) else x) # skip empty tuple .filter(bool) ) (idxr,) = val if isinstance(idxr, int): idxr = np.array([idxr]) zindexer.append(idxr) if isinstance(idxr, slice): idxr = np.arange(*idxr.indices(size)) elif isinstance(idxr, (tuple, int)): idxr = np.array(idxr) newshape = [1] * ndim newshape[axis] = idxr.size npindexer.append(idxr.reshape(newshape)) # casting the output of broadcast_arrays is needed for numpy < 2 return tuple(zindexer), tuple(np.broadcast_arrays(*npindexer)) def key_ranges( keys: SearchStrategy[str] = node_names, max_size: int = sys.maxsize ) -> SearchStrategy[list[tuple[str, RangeByteRequest]]]: """ Function to generate key_ranges strategy for get_partial_values() returns list strategy w/ form:: [(key, (range_start, range_end)), (key, (range_start, range_end)),...] """ def make_request(start: int, length: int) -> RangeByteRequest: return RangeByteRequest(start, end=min(start + length, max_size)) byte_ranges = st.builds( make_request, start=st.integers(min_value=0, max_value=max_size), length=st.integers(min_value=0, max_value=max_size), ) key_tuple = st.tuples(keys, byte_ranges) return st.lists(key_tuple, min_size=1, max_size=10) @st.composite def complex_rectilinear_arrays( draw: st.DrawFn, *, stores: st.SearchStrategy[StoreLike] = stores, paths: st.SearchStrategy[str] = paths(), # noqa: B008 array_names: st.SearchStrategy = array_names, attrs: st.SearchStrategy = attrs, ) -> tuple[npt.NDArray[Any], AnyArray]: """Generate a rectilinear array with many small chunks. The shape is derived from the chunk edges (5-10 chunks per dim, sizes 1-5), exercising higher chunk counts than ``rectilinear_arrays``. """ ndim = draw(st.integers(min_value=1, max_value=3)) nchunks = draw(st.integers(min_value=5, max_value=10)) dim_chunks = st.lists(st.integers(min_value=1, max_value=5), min_size=nchunks, max_size=nchunks) chunk_shapes = draw(st.lists(dim_chunks, min_size=ndim, max_size=ndim)) shape = tuple(sum(dim) for dim in chunk_shapes) nparray = draw(numpy_arrays(shapes=st.just(shape))) dim_names = draw(dimension_names(ndim=ndim)) fill_value = draw(st.one_of([st.none(), npst.from_dtype(nparray.dtype)])) attributes = draw(attrs) store = draw(stores, label="store") path = draw(paths, label="array parent") name = draw(array_names, label="array name") array_path = _join_paths([path, name]) root = zarr.open_group(store, mode="w", zarr_format=3) with zarr.config.set({"array.rectilinear_chunks": True}): a = root.create_array( array_path, shape=shape, chunks=chunk_shapes, dtype=nparray.dtype, fill_value=fill_value, dimension_names=dim_names, attributes=attributes, ) a[:] = nparray return nparray, a @st.composite def chunk_paths(draw: st.DrawFn, ndim: int, numblocks: tuple[int, ...], subset: bool = True) -> str: blockidx = draw( st.tuples(*tuple(st.integers(min_value=0, max_value=max(0, b - 1)) for b in numblocks)) ) subset_slicer = slice(draw(st.integers(min_value=0, max_value=ndim))) if subset else slice(None) return "/".join(map(str, blockidx[subset_slicer])) zarr-python-3.2.1/src/zarr/testing/utils.py000066400000000000000000000021101517635743000207530ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, cast import pytest from zarr.core.buffer import Buffer if TYPE_CHECKING: from zarr.core.common import BytesLike __all__ = ["assert_bytes_equal"] def assert_bytes_equal(b1: Buffer | BytesLike | None, b2: Buffer | BytesLike | None) -> None: """Help function to assert if two bytes-like or Buffers are equal Warnings -------- Always copies data, only use for testing and debugging """ if isinstance(b1, Buffer): b1 = b1.to_bytes() if isinstance(b2, Buffer): b2 = b2.to_bytes() assert b1 == b2 def has_cupy() -> bool: try: import cupy return cast("bool", cupy.cuda.runtime.getDeviceCount() > 0) except ImportError: return False except cupy.cuda.runtime.CUDARuntimeError: return False gpu_mark = pytest.mark.gpu skip_if_no_gpu = pytest.mark.skipif(not has_cupy(), reason="CuPy not installed or no GPU available") # Decorator for GPU tests def gpu_test[T](func: T) -> T: return cast(T, gpu_mark(skip_if_no_gpu(func))) zarr-python-3.2.1/src/zarr/types.py000066400000000000000000000011511517635743000173060ustar00rootroot00000000000000from typing import Any from zarr.core.array import Array, AsyncArray from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata type AnyAsyncArray = AsyncArray[Any] """A Zarr format 2 or 3 `AsyncArray`""" type AsyncArrayV2 = AsyncArray[ArrayV2Metadata] """A Zarr format 2 `AsyncArray`""" type AsyncArrayV3 = AsyncArray[ArrayV3Metadata] """A Zarr format 3 `AsyncArray`""" type AnyArray = Array[Any] """A Zarr format 2 or 3 `Array`""" type ArrayV2 = Array[ArrayV2Metadata] """A Zarr format 2 `Array`""" type ArrayV3 = Array[ArrayV3Metadata] """A Zarr format 3 `Array`""" zarr-python-3.2.1/tests/000077500000000000000000000000001517635743000151675ustar00rootroot00000000000000zarr-python-3.2.1/tests/__init__.py000066400000000000000000000000001517635743000172660ustar00rootroot00000000000000zarr-python-3.2.1/tests/benchmarks/000077500000000000000000000000001517635743000173045ustar00rootroot00000000000000zarr-python-3.2.1/tests/benchmarks/__init__.py000066400000000000000000000000001517635743000214030ustar00rootroot00000000000000zarr-python-3.2.1/tests/benchmarks/common.py000066400000000000000000000002621517635743000211460ustar00rootroot00000000000000from dataclasses import dataclass @dataclass(kw_only=True, frozen=True) class Layout: shape: tuple[int, ...] chunks: tuple[int, ...] shards: tuple[int, ...] | None zarr-python-3.2.1/tests/benchmarks/conftest.py000066400000000000000000000007441517635743000215100ustar00rootroot00000000000000"""Pytest configuration for benchmark tests.""" import pytest # Filter CodSpeed instrumentation warnings that can occur intermittently # when registering benchmark results. This is a known issue with the # CodSpeed walltime instrumentation hooks. # See: https://github.com/CodSpeedHQ/pytest-codspeed def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "filterwarnings", "ignore:Failed to set executed benchmark:RuntimeWarning", ) zarr-python-3.2.1/tests/benchmarks/test_e2e.py000066400000000000000000000046461517635743000214020ustar00rootroot00000000000000""" Benchmarks for end-to-end read/write performance of Zarr """ from __future__ import annotations from typing import TYPE_CHECKING from tests.benchmarks.common import Layout if TYPE_CHECKING: from pytest_benchmark.fixture import BenchmarkFixture from zarr.abc.store import Store from zarr.core.common import NamedConfig from operator import getitem, setitem from typing import Any, Literal import pytest from zarr import create_array CompressorName = Literal["gzip"] | None compressors: dict[CompressorName, NamedConfig[Any, Any] | None] = { None: None, "gzip": {"name": "gzip", "configuration": {"level": 1}}, } layouts: tuple[Layout, ...] = ( # No shards, just 1000 chunks Layout(shape=(1_000_000,), chunks=(1000,), shards=None), # 1:1 chunk:shard shape, should measure overhead of sharding Layout(shape=(1_000_000,), chunks=(1000,), shards=(1000,)), # One shard with all the chunks, should measure overhead of handling inner shard chunks Layout(shape=(1_000_000,), chunks=(100,), shards=(10000 * 100,)), ) @pytest.mark.parametrize("compression_name", [None, "gzip"]) @pytest.mark.parametrize("layout", layouts, ids=str) @pytest.mark.parametrize("store", ["memory", "local"], indirect=["store"]) def test_write_array( store: Store, layout: Layout, compression_name: CompressorName, benchmark: BenchmarkFixture ) -> None: """ Test the time required to fill an array with a single value """ arr = create_array( store, dtype="uint8", shape=layout.shape, chunks=layout.chunks, shards=layout.shards, compressors=compressors[compression_name], # type: ignore[arg-type] fill_value=0, ) benchmark(setitem, arr, Ellipsis, 1) @pytest.mark.parametrize("compression_name", [None, "gzip"]) @pytest.mark.parametrize("layout", layouts, ids=str) @pytest.mark.parametrize("store", ["memory", "local"], indirect=["store"]) def test_read_array( store: Store, layout: Layout, compression_name: CompressorName, benchmark: BenchmarkFixture ) -> None: """ Test the time required to fill an array with a single value """ arr = create_array( store, dtype="uint8", shape=layout.shape, chunks=layout.chunks, shards=layout.shards, compressors=compressors[compression_name], # type: ignore[arg-type] fill_value=0, ) arr[:] = 1 benchmark(getitem, arr, Ellipsis) zarr-python-3.2.1/tests/benchmarks/test_indexing.py000066400000000000000000000207441517635743000225310ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from pytest_benchmark.fixture import BenchmarkFixture from zarr.abc.store import Store from operator import getitem import pytest from zarr import create_array indexers = ( (0,) * 3, (slice(None),) * 3, (slice(0, None, 4),) * 3, (slice(10),) * 3, (slice(10, -10, 4),) * 3, (slice(None), slice(0, 3, 2), slice(0, 10)), ) shards = ( None, (50,) * 3, ) @pytest.mark.parametrize("store", ["memory", "memory_get_latency"], indirect=["store"]) @pytest.mark.parametrize("indexer", indexers, ids=str) @pytest.mark.parametrize("shards", shards, ids=str) def test_slice_indexing( store: Store, indexer: tuple[int | slice], shards: tuple[int, ...] | None, benchmark: BenchmarkFixture, ) -> None: data = create_array( store=store, shape=(105,) * 3, dtype="uint8", chunks=(10,) * 3, shards=shards, compressors=None, filters=None, fill_value=0, ) data[:] = 1 benchmark(getitem, data, indexer) # Benchmark for Morton order optimization with power-of-2 shards # Morton order is used internally by sharding codec for chunk iteration morton_shards = ( (16,) * 3, # With 2x2x2 chunks: 8x8x8 = 512 chunks per shard (32,) * 3, # With 2x2x2 chunks: 16x16x16 = 4096 chunks per shard ) @pytest.mark.parametrize("store", ["memory"], indirect=["store"]) @pytest.mark.parametrize("shards", morton_shards, ids=str) def test_sharded_morton_indexing( store: Store, shards: tuple[int, ...], benchmark: BenchmarkFixture, ) -> None: """Benchmark sharded array indexing with power-of-2 chunks per shard. This benchmark exercises the Morton order iteration path in the sharding codec, which benefits from the hypercube and vectorization optimizations. The Morton order cache is cleared before each iteration to measure the full computation cost. """ from zarr.core.indexing import _morton_order, _morton_order_keys # Create array where each shard contains many small chunks # e.g., shards=(32,32,32) with chunks=(2,2,2) means 16x16x16 = 4096 chunks per shard shape = tuple(s * 2 for s in shards) # 2 shards per dimension chunks = (2,) * 3 # Small chunks to maximize chunks per shard data = create_array( store=store, shape=shape, dtype="uint8", chunks=chunks, shards=shards, compressors=None, filters=None, fill_value=0, ) data[:] = 1 # Read a sub-shard region to exercise Morton order iteration indexer = (slice(shards[0]),) * 3 def read_with_cache_clear() -> None: _morton_order.cache_clear() _morton_order_keys.cache_clear() getitem(data, indexer) benchmark(read_with_cache_clear) # Benchmark with larger chunks_per_shard to make Morton order impact more visible large_morton_shards = ( (32,) * 3, # With 1x1x1 chunks: 32x32x32 = 32768 chunks per shard (power-of-2) (30,) * 3, # With 1x1x1 chunks: 30x30x30 = 27000 chunks per shard (non-power-of-2) (33,) * 3, # With 1x1x1 chunks: 33x33x33 = 35937 chunks per shard (near-miss: just above power-of-2) ) @pytest.mark.parametrize("store", ["memory"], indirect=["store"]) @pytest.mark.parametrize("shards", large_morton_shards, ids=str) def test_sharded_morton_indexing_large( store: Store, shards: tuple[int, ...], benchmark: BenchmarkFixture, ) -> None: """Benchmark sharded array indexing with large chunks_per_shard. Uses 1x1x1 chunks to maximize chunks_per_shard (32^3 = 32768), making the Morton order computation a more significant portion of total time. The Morton order cache is cleared before each iteration. """ from zarr.core.indexing import _morton_order, _morton_order_keys # 1x1x1 chunks means chunks_per_shard equals shard shape shape = tuple(s * 2 for s in shards) # 2 shards per dimension chunks = (1,) * 3 # 1x1x1 chunks: chunks_per_shard = shards data = create_array( store=store, shape=shape, dtype="uint8", chunks=chunks, shards=shards, compressors=None, filters=None, fill_value=0, ) data[:] = 1 # Read one full shard indexer = (slice(shards[0]),) * 3 def read_with_cache_clear() -> None: _morton_order.cache_clear() _morton_order_keys.cache_clear() getitem(data, indexer) benchmark(read_with_cache_clear) @pytest.mark.parametrize("store", ["memory"], indirect=["store"]) @pytest.mark.parametrize("shards", large_morton_shards, ids=str) def test_sharded_morton_single_chunk( store: Store, shards: tuple[int, ...], benchmark: BenchmarkFixture, ) -> None: """Benchmark reading a single chunk from a large shard. This isolates the Morton order computation overhead by minimizing I/O. Reading one chunk from a shard with 32^3 = 32768 chunks still requires computing the full Morton order, making the optimization impact clear. The Morton order cache is cleared before each iteration. """ from zarr.core.indexing import _morton_order, _morton_order_keys # 1x1x1 chunks means chunks_per_shard equals shard shape shape = tuple(s * 2 for s in shards) # 2 shards per dimension chunks = (1,) * 3 # 1x1x1 chunks: chunks_per_shard = shards data = create_array( store=store, shape=shape, dtype="uint8", chunks=chunks, shards=shards, compressors=None, filters=None, fill_value=0, ) data[:] = 1 # Read only a single chunk (1x1x1) from the shard indexer = (slice(1),) * 3 def read_with_cache_clear() -> None: _morton_order.cache_clear() _morton_order_keys.cache_clear() getitem(data, indexer) benchmark(read_with_cache_clear) # Benchmark for morton_order_iter directly (no I/O) morton_iter_shapes = ( (8, 8, 8), # 512 elements (power-of-2) (10, 10, 10), # 1000 elements (non-power-of-2) (16, 16, 16), # 4096 elements (power-of-2) (20, 20, 20), # 8000 elements (non-power-of-2) (32, 32, 32), # 32768 elements (power-of-2) (30, 30, 30), # 27000 elements (non-power-of-2) (33, 33, 33), # 35937 elements (near-miss: just above power-of-2, n_z=262144) ) @pytest.mark.parametrize("shape", morton_iter_shapes, ids=str) def test_morton_order_iter( shape: tuple[int, ...], benchmark: BenchmarkFixture, ) -> None: """Benchmark morton_order_iter directly without I/O. This isolates the Morton order computation to measure the optimization impact without array read/write overhead. The cache is cleared before each iteration. """ from zarr.core.indexing import _morton_order, _morton_order_keys, morton_order_iter def compute_morton_order() -> None: _morton_order.cache_clear() _morton_order_keys.cache_clear() # Consume the iterator to force computation list(morton_order_iter(shape)) benchmark(compute_morton_order) @pytest.mark.parametrize("store", ["memory"], indirect=["store"]) @pytest.mark.parametrize("shards", large_morton_shards, ids=str) def test_sharded_morton_write_single_chunk( store: Store, shards: tuple[int, ...], benchmark: BenchmarkFixture, ) -> None: """Benchmark writing a single chunk to a large shard. This is the clearest end-to-end demonstration of Morton order optimization. Writing a single chunk to a shard with 32^3 = 32768 chunks requires computing the full Morton order, but minimizes I/O overhead. Expected improvement: ~160ms (matching Morton computation speedup of ~178ms). The Morton order cache is cleared before each iteration. """ import numpy as np from zarr.core.indexing import _morton_order, _morton_order_keys # 1x1x1 chunks means chunks_per_shard equals shard shape shape = tuple(s * 2 for s in shards) # 2 shards per dimension chunks = (1,) * 3 # 1x1x1 chunks: chunks_per_shard = shards data = create_array( store=store, shape=shape, dtype="uint8", chunks=chunks, shards=shards, compressors=None, filters=None, fill_value=0, ) # Write data for a single chunk write_data = np.ones((1, 1, 1), dtype="uint8") indexer = (slice(1), slice(1), slice(1)) def write_with_cache_clear() -> None: _morton_order.cache_clear() _morton_order_keys.cache_clear() data[indexer] = write_data benchmark(write_with_cache_clear) zarr-python-3.2.1/tests/conftest.py000066400000000000000000000363721517635743000174010ustar00rootroot00000000000000from __future__ import annotations import math import os import pathlib import sys from collections.abc import Mapping, Sequence from dataclasses import dataclass, field from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt import pytest from hypothesis import HealthCheck, Verbosity, settings import zarr.registry from zarr import AsyncGroup, config from zarr.abc.store import Store from zarr.codecs.sharding import ShardingCodec, ShardingCodecIndexLocation from zarr.core.array import ( _parse_chunk_encoding_v2, _parse_chunk_encoding_v3, _parse_chunk_key_encoding, ) from zarr.core.chunk_grids import _auto_partition from zarr.core.common import ( JSON, DimensionNamesLike, MemoryOrder, ShapeLike, ZarrFormat, parse_shapelike, ) from zarr.core.config import config as zarr_config from zarr.core.dtype import ( get_data_type_from_native_dtype, ) from zarr.core.dtype.common import HasItemSize from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGridMetadata from zarr.core.sync import sync from zarr.storage import FsspecStore, LocalStore, MemoryStore, StorePath, ZipStore from zarr.testing.store import LatencyStore if TYPE_CHECKING: from collections.abc import Generator from typing import Any, Literal from _pytest.compat import LEGACY_PATH from zarr.abc.codec import Codec from zarr.core.array import CompressorsLike, FiltersLike, SerializerLike, ShardsLike from zarr.core.chunk_key_encodings import ( ChunkKeyEncoding, ChunkKeyEncodingLike, V2ChunkKeyEncoding, ) from zarr.core.dtype.wrapper import ZDType @dataclass class Expect[TIn, TOut]: """A test case with explicit input, expected output, and a human-readable id.""" input: TIn output: TOut id: str @dataclass class ExpectFail[TIn]: """A test case that should raise an exception.""" input: TIn exception: type[Exception] id: str msg: str async def parse_store( store: Literal["local", "memory", "fsspec", "zip", "memory_get_latency"], path: str ) -> LocalStore | MemoryStore | FsspecStore | ZipStore | LatencyStore: if store == "local": return await LocalStore.open(path) if store == "memory": return await MemoryStore.open() if store == "fsspec": return await FsspecStore.open(url=path) if store == "zip": return await ZipStore.open(f"{path}/zarr.zip", mode="w") if store == "memory_get_latency": return LatencyStore(MemoryStore(), get_latency=0.0001, set_latency=0) raise AssertionError @pytest.fixture(params=[str, pathlib.Path]) def path_type(request: pytest.FixtureRequest) -> Any: return request.param # todo: harmonize this with local_store fixture @pytest.fixture async def store_path(tmpdir: LEGACY_PATH) -> StorePath: store = await LocalStore.open(str(tmpdir)) return StorePath(store) @pytest.fixture async def local_store(tmpdir: LEGACY_PATH) -> LocalStore: return await LocalStore.open(str(tmpdir)) @pytest.fixture async def remote_store(url: str) -> FsspecStore: return await FsspecStore.open(url) @pytest.fixture async def memory_store() -> MemoryStore: return await MemoryStore.open() @pytest.fixture async def zip_store(tmpdir: LEGACY_PATH) -> ZipStore: return await ZipStore.open(str(tmpdir / "zarr.zip"), mode="w") @pytest.fixture async def store(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> Store: param = request.param return await parse_store(param, str(tmpdir)) @pytest.fixture async def store2(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> Store: """Fixture to create a second store for testing copy operations between stores""" param = request.param store2_path = tmpdir.mkdir("store2") return await parse_store(param, str(store2_path)) @pytest.fixture(params=["local", "memory", "zip"]) def sync_store(request: pytest.FixtureRequest, tmp_path: LEGACY_PATH) -> Store: result = sync(parse_store(request.param, str(tmp_path))) if not isinstance(result, Store): raise TypeError(f"Wrong store class returned by test fixture! got {result} instead") return result @dataclass class AsyncGroupRequest: zarr_format: ZarrFormat store: Literal["local", "fsspec", "memory", "zip"] attributes: dict[str, Any] = field(default_factory=dict) @pytest.fixture async def async_group(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> AsyncGroup: param: AsyncGroupRequest = request.param store = await parse_store(param.store, str(tmpdir)) return await AsyncGroup.from_store( store, attributes=param.attributes, zarr_format=param.zarr_format, overwrite=False, ) @pytest.fixture(params=["numpy", "cupy"]) def xp(request: pytest.FixtureRequest) -> Any: """Fixture to parametrize over numpy-like libraries""" if request.param == "cupy": request.node.add_marker(pytest.mark.gpu) return pytest.importorskip(request.param) @pytest.fixture(autouse=True) def reset_config() -> Generator[None, None, None]: config.reset() yield config.reset() @dataclass class ArrayRequest: shape: tuple[int, ...] dtype: str order: MemoryOrder @pytest.fixture def array_fixture(request: pytest.FixtureRequest) -> npt.NDArray[Any]: array_request: ArrayRequest = request.param return ( np.arange(np.prod(array_request.shape)) .reshape(array_request.shape, order=array_request.order) .astype(array_request.dtype) ) @pytest.fixture(params=(2, 3), ids=["zarr2", "zarr3"]) def zarr_format(request: pytest.FixtureRequest) -> ZarrFormat: if request.param == 2: return 2 elif request.param == 3: return 3 msg = f"Invalid zarr format requested. Got {request.param}, expected on of (2,3)." raise ValueError(msg) def _clear_registries() -> None: registries = zarr.registry._collect_entrypoints() for registry in registries: registry.lazy_load_list.clear() @pytest.fixture def set_path() -> Generator[None, None, None]: tests_dir = str(pathlib.Path(__file__).parent.absolute()) sys.path.append(tests_dir) _clear_registries() zarr.registry._collect_entrypoints() yield sys.path.remove(tests_dir) _clear_registries() zarr.registry._collect_entrypoints() config.reset() def pytest_addoption(parser: Any) -> None: parser.addoption( "--run-slow-hypothesis", action="store_true", default=False, help="run slow hypothesis tests", ) def pytest_collection_modifyitems(config: Any, items: Any) -> None: if config.getoption("--run-slow-hypothesis"): return skip_slow_hyp = pytest.mark.skip(reason="need --run-slow-hypothesis option to run") for item in items: if "slow_hypothesis" in item.keywords: item.add_marker(skip_slow_hyp) settings.register_profile( "default", parent=settings.get_profile("default"), max_examples=300, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], deadline=None, verbosity=Verbosity.verbose, ) settings.register_profile( "ci", parent=settings.get_profile("ci"), max_examples=300, derandomize=True, # more like regression testing deadline=None, suppress_health_check=[HealthCheck.filter_too_much, HealthCheck.too_slow], ) settings.register_profile( "nightly", max_examples=500, parent=settings.get_profile("ci"), derandomize=False, stateful_step_count=100, ) settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) # TODO: uncomment these overrides when we can get mypy to accept them """ @overload def create_array_metadata( *, shape: ShapeLike, dtype: npt.DTypeLike, chunks: tuple[int, ...] | Literal["auto"], shards: None, filters: FiltersLike, compressors: CompressorsLike, serializer: SerializerLike, fill_value: Any | None, order: MemoryOrder | None, zarr_format: Literal[2], attributes: dict[str, JSON] | None, chunk_key_encoding: ChunkKeyEncoding | ChunkKeyEncodingLike | None, dimension_names: None, ) -> ArrayV2Metadata: ... @overload def create_array_metadata( *, shape: ShapeLike, dtype: npt.DTypeLike, chunks: tuple[int, ...] | Literal["auto"], shards: ShardsLike | None, filters: FiltersLike, compressors: CompressorsLike, serializer: SerializerLike, fill_value: Any | None, order: None, zarr_format: Literal[3], attributes: dict[str, JSON] | None, chunk_key_encoding: ChunkKeyEncoding | ChunkKeyEncodingLike | None, dimension_names: Iterable[str] | None, ) -> ArrayV3Metadata: ... """ def create_array_metadata( *, shape: ShapeLike, dtype: npt.DTypeLike, chunks: tuple[int, ...] | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", serializer: SerializerLike = "auto", fill_value: Any = 0, order: MemoryOrder | None = None, zarr_format: ZarrFormat, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncoding | ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, ) -> ArrayV2Metadata | ArrayV3Metadata: """ Create array metadata """ dtype_parsed = get_data_type_from_native_dtype(dtype) shape_parsed = parse_shapelike(shape) chunk_key_encoding_parsed = _parse_chunk_key_encoding( chunk_key_encoding, zarr_format=zarr_format ) item_size = 1 if isinstance(dtype_parsed, HasItemSize): item_size = dtype_parsed.item_size shard_shape_parsed, chunk_shape_parsed = _auto_partition( array_shape=shape_parsed, shard_shape=shards, chunk_shape=chunks, item_size=item_size, ) if order is None: order_parsed = zarr_config.get("array.order") else: order_parsed = order chunks_out: tuple[int, ...] if zarr_format == 2: filters_parsed, compressor_parsed = _parse_chunk_encoding_v2( compressor=compressors, filters=filters, dtype=dtype_parsed ) chunk_key_encoding_parsed = cast("V2ChunkKeyEncoding", chunk_key_encoding_parsed) return ArrayV2Metadata( shape=shape_parsed, dtype=dtype_parsed, chunks=chunk_shape_parsed, order=order_parsed, dimension_separator=chunk_key_encoding_parsed.separator, fill_value=fill_value, compressor=compressor_parsed, filters=filters_parsed, attributes=attributes, ) elif zarr_format == 3: array_array, array_bytes, bytes_bytes = _parse_chunk_encoding_v3( compressors=compressors, filters=filters, serializer=serializer, dtype=dtype_parsed, ) sub_codecs: tuple[Codec, ...] = (*array_array, array_bytes, *bytes_bytes) codecs_out: tuple[Codec, ...] if shard_shape_parsed is not None: index_location = None if isinstance(shards, dict): index_location = ShardingCodecIndexLocation(shards.get("index_location", None)) if index_location is None: index_location = ShardingCodecIndexLocation.end sharding_codec = ShardingCodec( chunk_shape=chunk_shape_parsed, codecs=sub_codecs, index_location=index_location, ) sharding_codec.validate( shape=chunk_shape_parsed, dtype=dtype_parsed, chunk_grid=RegularChunkGridMetadata(chunk_shape=shard_shape_parsed), ) codecs_out = (sharding_codec,) chunks_out = shard_shape_parsed else: chunks_out = chunk_shape_parsed codecs_out = sub_codecs return ArrayV3Metadata( shape=shape_parsed, data_type=dtype_parsed, chunk_grid={"name": "regular", "configuration": {"chunk_shape": chunks_out}}, chunk_key_encoding=chunk_key_encoding_parsed, fill_value=fill_value, codecs=codecs_out, attributes=attributes, dimension_names=dimension_names, ) raise ValueError(f"Invalid Zarr format: {zarr_format}") # TODO: uncomment these overrides when we can get mypy to accept them """ @overload def meta_from_array( array: np.ndarray[Any, Any], chunks: tuple[int, ...] | Literal["auto"], shards: None, filters: FiltersLike, compressors: CompressorsLike, serializer: SerializerLike, fill_value: Any | None, order: MemoryOrder | None, zarr_format: Literal[2], attributes: dict[str, JSON] | None, chunk_key_encoding: ChunkKeyEncoding | ChunkKeyEncodingLike | None, dimension_names: Iterable[str] | None, ) -> ArrayV2Metadata: ... @overload def meta_from_array( array: np.ndarray[Any, Any], chunks: tuple[int, ...] | Literal["auto"], shards: ShardsLike | None, filters: FiltersLike, compressors: CompressorsLike, serializer: SerializerLike, fill_value: Any | None, order: None, zarr_format: Literal[3], attributes: dict[str, JSON] | None, chunk_key_encoding: ChunkKeyEncoding | ChunkKeyEncodingLike | None, dimension_names: Iterable[str] | None, ) -> ArrayV3Metadata: ... """ def meta_from_array( array: np.ndarray[Any, Any], *, chunks: tuple[int, ...] | Literal["auto"] = "auto", shards: ShardsLike | None = None, filters: FiltersLike = "auto", compressors: CompressorsLike = "auto", serializer: SerializerLike = "auto", fill_value: Any = 0, order: MemoryOrder | None = None, zarr_format: ZarrFormat = 3, attributes: dict[str, JSON] | None = None, chunk_key_encoding: ChunkKeyEncoding | ChunkKeyEncodingLike | None = None, dimension_names: DimensionNamesLike = None, ) -> ArrayV3Metadata | ArrayV2Metadata: """ Create array metadata from an array """ return create_array_metadata( shape=array.shape, dtype=array.dtype, chunks=chunks, shards=shards, filters=filters, compressors=compressors, serializer=serializer, fill_value=fill_value, order=order, zarr_format=zarr_format, attributes=attributes, chunk_key_encoding=chunk_key_encoding, dimension_names=dimension_names, ) def skip_object_dtype(dtype: ZDType[Any, Any]) -> None: if dtype.dtype_cls is type(np.dtype("O")): msg = ( f"{dtype} uses the numpy object data type, which is not a valid target for data " "type resolution" ) pytest.skip(msg) def nan_equal(a: object, b: object) -> bool: """ Convenience function for equality comparison between two values ``a`` and ``b``, that might both be NaN. Returns True if both ``a`` and ``b`` are NaN, otherwise returns a == b """ if math.isnan(a) and math.isnan(b): # type: ignore[arg-type] return True return a == b def deep_nan_equal(a: object, b: object) -> bool: if isinstance(a, Mapping) and isinstance(b, Mapping): return all(deep_nan_equal(a[k], b[k]) for k in a) if isinstance(a, Sequence) and isinstance(b, Sequence): return all(deep_nan_equal(a[i], b[i]) for i in range(len(a))) return nan_equal(a, b) zarr-python-3.2.1/tests/package_with_entrypoint-0.1.dist-info/000077500000000000000000000000001517635743000242775ustar00rootroot00000000000000zarr-python-3.2.1/tests/package_with_entrypoint-0.1.dist-info/entry_points.txt000066400000000000000000000012371517635743000276000ustar00rootroot00000000000000[zarr.codecs] test = package_with_entrypoint:TestEntrypointCodec [zarr.codecs.test] another_codec = package_with_entrypoint:TestEntrypointGroup.Codec [zarr] codec_pipeline = package_with_entrypoint:TestEntrypointCodecPipeline ndbuffer = package_with_entrypoint:TestEntrypointNDBuffer buffer = package_with_entrypoint:TestEntrypointBuffer [zarr.buffer] another_buffer = package_with_entrypoint:TestEntrypointGroup.Buffer [zarr.ndbuffer] another_ndbuffer = package_with_entrypoint:TestEntrypointGroup.NDBuffer [zarr.codec_pipeline] another_pipeline = package_with_entrypoint:TestEntrypointGroup.Pipeline [zarr.data_type] new_data_type = package_with_entrypoint:TestDataTypezarr-python-3.2.1/tests/package_with_entrypoint/000077500000000000000000000000001517635743000221105ustar00rootroot00000000000000zarr-python-3.2.1/tests/package_with_entrypoint/__init__.py000066400000000000000000000054341517635743000242270ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import numpy as np import numpy.typing as npt import zarr.core.buffer from zarr.abc.codec import ArrayBytesCodec, CodecInput, CodecPipeline from zarr.codecs import BytesCodec from zarr.core.buffer import Buffer, NDBuffer from zarr.core.dtype.common import DataTypeValidationError, DTypeJSON, DTypeSpec_V2 from zarr.core.dtype.npy.bool import Bool if TYPE_CHECKING: from collections.abc import Iterable from typing import Any, ClassVar, Literal, Self from zarr.core.array_spec import ArraySpec from zarr.core.common import ZarrFormat class TestEntrypointCodec(ArrayBytesCodec): is_fixed_size = True async def encode( self, chunks_and_specs: Iterable[tuple[CodecInput | None, ArraySpec]], ) -> Iterable[Buffer | None]: return [None] async def decode( self, chunks_and_specs: Iterable[tuple[CodecInput | None, ArraySpec]], ) -> npt.NDArray[Any]: return np.array(1) def compute_encoded_size(self, input_byte_length: int, chunk_spec: ArraySpec) -> int: return input_byte_length class TestEntrypointCodecPipeline(CodecPipeline): def __init__(self, batch_size: int = 1) -> None: pass async def encode( self, chunks_and_specs: Iterable[tuple[CodecInput | None, ArraySpec]] ) -> Iterable[Buffer | None]: return [None] async def decode( self, chunks_and_specs: Iterable[tuple[CodecInput | None, ArraySpec]] ) -> Iterable[NDBuffer | None]: return np.array(1) class TestEntrypointBuffer(Buffer): pass class TestEntrypointNDBuffer(NDBuffer): pass class TestEntrypointGroup: class Codec(BytesCodec): pass class Buffer(zarr.core.buffer.Buffer): pass class NDBuffer(zarr.core.buffer.NDBuffer): pass class Pipeline(CodecPipeline): pass class TestDataType(Bool): """ This is a "data type" that serializes to "test" """ _zarr_v3_name: ClassVar[Literal["test"]] = "test" # type: ignore[assignment] @classmethod def from_json(cls, data: DTypeJSON, *, zarr_format: ZarrFormat) -> Self: if zarr_format == 2 and data == {"name": cls._zarr_v3_name, "object_codec_id": None}: return cls() if zarr_format == 3 and data == cls._zarr_v3_name: return cls() raise DataTypeValidationError( f"Invalid JSON representation of {cls.__name__}. Got {data!r}" ) def to_json(self, zarr_format: ZarrFormat) -> str | DTypeSpec_V2: # type: ignore[override] if zarr_format == 2: return {"name": self._zarr_v3_name, "object_codec_id": None} if zarr_format == 3: return self._zarr_v3_name raise ValueError("zarr_format must be 2 or 3") zarr-python-3.2.1/tests/test_abc/000077500000000000000000000000001517635743000167535ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_abc/__init__.py000066400000000000000000000000001517635743000210520ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_abc/test_codec.py000066400000000000000000000005221517635743000214400ustar00rootroot00000000000000from __future__ import annotations from zarr.abc.codec import _check_codecjson_v2 def test_check_codecjson_v2_valid() -> None: """ Test that the _check_codecjson_v2 function works """ assert _check_codecjson_v2({"id": "gzip"}) assert not _check_codecjson_v2({"id": 10}) assert not _check_codecjson_v2([10, 11]) zarr-python-3.2.1/tests/test_api.py000066400000000000000000001525241517635743000173620ustar00rootroot00000000000000from __future__ import annotations import inspect import re from typing import TYPE_CHECKING, Any import zarr.codecs import zarr.storage from zarr.core.array import AsyncArray, init_array from zarr.storage import LocalStore, ZipStore from zarr.storage._common import StorePath if TYPE_CHECKING: from collections.abc import Callable from pathlib import Path from zarr.abc.store import Store from zarr.core.common import JSON, MemoryOrder, ZarrFormat from zarr.types import AnyArray import contextlib from typing import Literal import numpy as np import pytest from numpy.testing import assert_array_equal import zarr import zarr.api.asynchronous import zarr.api.synchronous import zarr.core.group from zarr import Array, Group from zarr.api.synchronous import ( create, create_array, create_group, from_array, group, load, open_group, save, save_array, save_group, ) from zarr.core.buffer import NDArrayLike from zarr.errors import ( ArrayNotFoundError, MetadataValidationError, ZarrDeprecationWarning, ZarrUserWarning, ) from zarr.storage import MemoryStore from zarr.storage._utils import normalize_path from zarr.testing.utils import gpu_test def test_create(memory_store: Store) -> None: store = memory_store # create array z = create(shape=100, store=store) assert isinstance(z, Array) assert z.shape == (100,) # create array, overwrite, specify chunk shape z = create(shape=200, chunk_shape=20, store=store, overwrite=True) assert isinstance(z, Array) assert z.shape == (200,) assert z.chunks == (20,) # create array, overwrite, specify chunk shape via chunks param z = create(shape=400, chunks=40, store=store, overwrite=True) assert isinstance(z, Array) assert z.shape == (400,) assert z.chunks == (40,) # create array with float shape with pytest.raises(TypeError): z = create(shape=(400.5, 100), store=store, overwrite=True) # type: ignore[arg-type] # create array with float chunk shape with pytest.raises(TypeError): z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type] @pytest.mark.parametrize( "func", [ zarr.api.asynchronous.zeros_like, zarr.api.asynchronous.ones_like, zarr.api.asynchronous.empty_like, zarr.api.asynchronous.full_like, zarr.api.asynchronous.open_like, ], ) @pytest.mark.parametrize("out_shape", ["keep", (10, 10)]) @pytest.mark.parametrize("out_chunks", ["keep", (10, 10)]) @pytest.mark.parametrize("out_dtype", ["keep", "int8"]) @pytest.mark.parametrize("out_fill", ["keep", 4]) async def test_array_like_creation( zarr_format: ZarrFormat, func: Callable[[Any], Any], out_shape: Literal["keep"] | tuple[int, ...], out_chunks: Literal["keep"] | tuple[int, ...], out_dtype: str, out_fill: Literal["keep"] | int, ) -> None: """ Test zeros_like, ones_like, empty_like, full_like, ensuring that we can override the shape, chunks, dtype and fill_value of the array-like object provided to these functions with appropriate keyword arguments """ ref_fill = 100 ref_arr = zarr.create_array( store={}, shape=(11, 12), dtype="uint8", chunks=(11, 12), zarr_format=zarr_format, fill_value=ref_fill, ) kwargs: dict[str, object] = {} if func is zarr.api.asynchronous.full_like: if out_fill == "keep": expect_fill = ref_fill else: expect_fill = out_fill kwargs["fill_value"] = expect_fill elif func is zarr.api.asynchronous.zeros_like: expect_fill = 0 elif func is zarr.api.asynchronous.ones_like: expect_fill = 1 elif func is zarr.api.asynchronous.empty_like: if out_fill == "keep": expect_fill = ref_fill else: kwargs["fill_value"] = out_fill expect_fill = out_fill elif func is zarr.api.asynchronous.open_like: # type: ignore[comparison-overlap] if out_fill == "keep": expect_fill = ref_fill else: kwargs["fill_value"] = out_fill expect_fill = out_fill kwargs["mode"] = "w" else: raise AssertionError if out_shape != "keep": kwargs["shape"] = out_shape expect_shape = out_shape else: expect_shape = ref_arr.shape if out_chunks != "keep": kwargs["chunks"] = out_chunks expect_chunks = out_chunks else: expect_chunks = ref_arr.chunks if out_dtype != "keep": kwargs["dtype"] = out_dtype expect_dtype = out_dtype else: expect_dtype = ref_arr.dtype # type: ignore[assignment] new_arr = await func(ref_arr, path="foo", zarr_format=zarr_format, **kwargs) # type: ignore[call-arg] assert new_arr.shape == expect_shape assert new_arr.chunks == expect_chunks assert new_arr.dtype == expect_dtype assert np.all(Array(new_arr)[:] == expect_fill) # TODO: parametrize over everything this function takes @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_create_array(store: Store, zarr_format: ZarrFormat) -> None: attrs: dict[str, JSON] = {"foo": 100} # explicit type annotation to avoid mypy error shape = (10, 10) path = "foo" data_val = 1 array_w = create_array( store, name=path, shape=shape, attributes=attrs, chunks=shape, dtype="uint8", zarr_format=zarr_format, ) array_w[:] = data_val assert array_w.shape == shape assert array_w.attrs == attrs assert np.array_equal(array_w[:], np.zeros(shape, dtype=array_w.dtype) + data_val) @pytest.mark.parametrize("write_empty_chunks", [True, False]) def test_write_empty_chunks_warns(write_empty_chunks: bool, zarr_format: ZarrFormat) -> None: """ Test that using the `write_empty_chunks` kwarg on array access will raise a warning. """ match = "The `write_empty_chunks` keyword argument .*" with pytest.warns(RuntimeWarning, match=match): _ = zarr.array( data=np.arange(10), shape=(10,), dtype="uint8", write_empty_chunks=write_empty_chunks, zarr_format=zarr_format, ) with pytest.warns(RuntimeWarning, match=match): _ = zarr.create( shape=(10,), dtype="uint8", write_empty_chunks=write_empty_chunks, zarr_format=zarr_format, ) @pytest.mark.parametrize("zarr_format", [2, 3]) def test_open_array_respects_write_empty_chunks_config(zarr_format: ZarrFormat) -> None: """Test that zarr.open() respects write_empty_chunks config.""" store = MemoryStore() _ = zarr.create( store=store, path="test_array", shape=(10,), chunks=(5,), dtype="f8", fill_value=0.0, zarr_format=zarr_format, ) arr2 = zarr.open(store=store, path="test_array", config={"write_empty_chunks": True}) assert isinstance(arr2, zarr.Array) assert arr2.async_array.config.write_empty_chunks is True arr2[0:5] = np.zeros(5) assert arr2.nchunks_initialized == 1 @pytest.mark.parametrize("path", ["foo", "/", "/foo", "///foo/bar"]) @pytest.mark.parametrize("node_type", ["array", "group"]) def test_open_normalized_path( memory_store: MemoryStore, path: str, node_type: Literal["array", "group"] ) -> None: node: Group | AnyArray if node_type == "group": node = group(store=memory_store, path=path) elif node_type == "array": node = create(store=memory_store, path=path, shape=(2,)) assert node.path == normalize_path(path) async def test_open_array(memory_store: MemoryStore, zarr_format: ZarrFormat) -> None: store = memory_store # open array, create if doesn't exist z = zarr.api.synchronous.open(store=store, shape=100, zarr_format=zarr_format) assert isinstance(z, Array) assert z.shape == (100,) # open array, overwrite # store._store_dict = {} store = MemoryStore() z = zarr.api.synchronous.open(store=store, shape=200, zarr_format=zarr_format) assert isinstance(z, Array) assert z.shape == (200,) # open array, read-only store_cls = type(store) ro_store = await store_cls.open(store_dict=store._store_dict, read_only=True) z = zarr.api.synchronous.open(store=ro_store, mode="r") assert isinstance(z, Array) assert z.shape == (200,) assert z.read_only # path not found with pytest.raises(FileNotFoundError): zarr.api.synchronous.open(store="doesnotexist", mode="r", zarr_format=zarr_format) def test_open_array_rectilinear_chunks(tmp_path: Path) -> None: """zarr.open with rectilinear (dask-style) chunks preserves the chunk grid.""" from zarr.core.metadata.v3 import RectilinearChunkGridMetadata chunks = ((3, 3, 4), (5, 5)) with zarr.config.set({"array.rectilinear_chunks": True}): z = zarr.open(store=tmp_path, shape=(10, 10), dtype="float64", chunks=chunks, mode="w") assert isinstance(z, Array) assert z.shape == (10, 10) assert isinstance(z.metadata.chunk_grid, RectilinearChunkGridMetadata) assert z.read_chunk_sizes == ((3, 3, 4), (5, 5)) @pytest.mark.asyncio async def test_async_array_open_array_not_found() -> None: """Test that AsyncArray.open raises ArrayNotFoundError when array doesn't exist""" store = MemoryStore() # Try to open an array that does not exist with pytest.raises(ArrayNotFoundError): await AsyncArray.open(store, zarr_format=2) def test_array_open_array_not_found_sync() -> None: """Test that Array.open raises ArrayNotFoundError when array doesn't exist""" store = MemoryStore() # Try to open an array that does not exist with pytest.raises(ArrayNotFoundError): Array.open(store) @pytest.mark.parametrize("store", ["memory", "local", "zip"], indirect=True) def test_v2_and_v3_exist_at_same_path(store: Store) -> None: zarr.create_array(store, shape=(10,), dtype="uint8", zarr_format=3) zarr.create_array(store, shape=(10,), dtype="uint8", zarr_format=2) msg = f"Both zarr.json (Zarr format 3) and .zarray (Zarr format 2) metadata objects exist at {store}. Zarr v3 will be used." with pytest.warns(ZarrUserWarning, match=re.escape(msg)): zarr.open(store=store) @pytest.mark.parametrize("store", ["memory"], indirect=True) async def test_create_group(store: Store, zarr_format: ZarrFormat) -> None: attrs = {"foo": 100} path = "node" node = create_group(store, path=path, attributes=attrs, zarr_format=zarr_format) assert isinstance(node, Group) assert node.attrs == attrs assert node.metadata.zarr_format == zarr_format async def test_open_group(memory_store: MemoryStore) -> None: store = memory_store # open group, create if doesn't exist g = open_group(store=store) g.create_group("foo") assert isinstance(g, Group) assert "foo" in g # open group, overwrite g = open_group(store=store, mode="w") assert isinstance(g, Group) assert "foo" not in g # open group, read-only store_cls = type(store) ro_store = await store_cls.open(store_dict=store._store_dict, read_only=True) g = open_group(store=ro_store, mode="r") assert isinstance(g, Group) assert g.read_only @pytest.mark.parametrize("zarr_format", [None, 2, 3]) async def test_open_group_unspecified_version(tmpdir: Path, zarr_format: ZarrFormat) -> None: """Regression test for https://github.com/zarr-developers/zarr-python/issues/2175""" # create a group with specified zarr format (could be 2, 3, or None) _ = await zarr.api.asynchronous.open_group( store=str(tmpdir), mode="w", zarr_format=zarr_format, attributes={"foo": "bar"} ) # now open that group without specifying the format g2 = await zarr.api.asynchronous.open_group(store=str(tmpdir), mode="r") assert g2.attrs == {"foo": "bar"} if zarr_format is not None: assert g2.metadata.zarr_format == zarr_format @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("n_args", [10, 1, 0]) @pytest.mark.parametrize("n_kwargs", [10, 1, 0]) @pytest.mark.parametrize("path", [None, "some_path"]) def test_save(store: Store, n_args: int, n_kwargs: int, path: None | str) -> None: data = np.arange(10) args = [np.arange(10) for _ in range(n_args)] kwargs = {f"arg_{i}": data for i in range(n_kwargs)} if n_kwargs == 0 and n_args == 0: with pytest.raises(ValueError): save(store, path=path) elif n_args == 1 and n_kwargs == 0: save(store, *args, path=path) array = zarr.api.synchronous.open(store, path=path) assert isinstance(array, Array) assert_array_equal(array[:], data) else: save(store, *args, path=path, **kwargs) # type: ignore[arg-type] group = zarr.api.synchronous.open(store, path=path) assert isinstance(group, Group) for array in group.array_values(): assert_array_equal(array[:], data) for k in kwargs: assert k in group assert group.nmembers() == n_args + n_kwargs def test_save_errors() -> None: with pytest.raises(ValueError): # no arrays provided save_group("data/group.zarr") with pytest.raises(TypeError): # no array provided save_array("data/group.zarr") # type: ignore[call-arg] with pytest.raises(ValueError): # no arrays provided save("data/group.zarr") a = np.arange(10) with pytest.raises(TypeError): # mode is no valid argument and would get handled as an array zarr.save("data/example.zarr", a, mode="w") def test_open_with_mode_r(tmp_path: Path) -> None: # 'r' means read only (must exist) with pytest.raises(FileNotFoundError): zarr.open(store=tmp_path, mode="r") z1 = zarr.ones(store=tmp_path, shape=(3, 3)) assert z1.fill_value == 1 z2 = zarr.open(store=tmp_path, mode="r") assert isinstance(z2, Array) assert z2.fill_value == 1 result = z2[:] assert isinstance(result, NDArrayLike) assert (result == 1).all() with pytest.raises(ValueError): z2[:] = 3 def test_open_with_mode_r_plus(tmp_path: Path) -> None: # 'r+' means read/write (must exist) new_store_path = tmp_path / "new_store.zarr" assert not new_store_path.exists(), "Test should operate on non-existent directory" with pytest.raises(FileNotFoundError): zarr.open(store=new_store_path, mode="r+") assert not new_store_path.exists(), "mode='r+' should not create directory" zarr.ones(store=tmp_path, shape=(3, 3)) z2 = zarr.open(store=tmp_path, mode="r+") assert isinstance(z2, Array) result = z2[:] assert isinstance(result, NDArrayLike) assert (result == 1).all() z2[:] = 3 async def test_open_with_mode_a(tmp_path: Path) -> None: # Open without shape argument should default to group g = zarr.open(store=tmp_path, mode="a") assert isinstance(g, Group) await g.store_path.delete() # 'a' means read/write (create if doesn't exist) arr = zarr.open(store=tmp_path, mode="a", shape=(3, 3)) assert isinstance(arr, Array) arr[...] = 1 z2 = zarr.open(store=tmp_path, mode="a") assert isinstance(z2, Array) result = z2[:] assert isinstance(result, NDArrayLike) assert (result == 1).all() z2[:] = 3 def test_open_with_mode_w(tmp_path: Path) -> None: # 'w' means create (overwrite if exists); arr = zarr.open(store=tmp_path, mode="w", shape=(3, 3)) assert isinstance(arr, Array) arr[...] = 3 z2 = zarr.open(store=tmp_path, mode="w", shape=(3, 3)) assert isinstance(z2, Array) result = z2[:] assert isinstance(result, NDArrayLike) assert not (result == 3).all() z2[:] = 3 def test_open_with_mode_w_minus(tmp_path: Path) -> None: # 'w-' means create (fail if exists) arr = zarr.open(store=tmp_path, mode="w-", shape=(3, 3)) assert isinstance(arr, Array) arr[...] = 1 with pytest.raises(FileExistsError): zarr.open(store=tmp_path, mode="w-") @pytest.mark.parametrize("order", ["C", "F", None]) @pytest.mark.parametrize("config", [{"order": "C"}, {"order": "F"}, {}], ids=["C", "F", "None"]) def test_array_order( order: MemoryOrder | None, config: dict[str, MemoryOrder | None], zarr_format: ZarrFormat ) -> None: """ Check that: - For v2, memory order is taken from the `order` keyword argument. - For v3, memory order is taken from `config`, and when order is passed a warning is raised - The numpy array returned has the expected order - For v2, the order metadata is set correctly """ default_order = zarr.config.get("array.order") ctx: contextlib.AbstractContextManager # type: ignore[type-arg] if zarr_format == 3: if order is None: ctx = contextlib.nullcontext() else: ctx = pytest.warns( RuntimeWarning, match="The `order` keyword argument has no effect for Zarr format 3 arrays", ) expected_order = config.get("order", default_order) if zarr_format == 2: ctx = contextlib.nullcontext() expected_order = order or config.get("order", default_order) with ctx: arr = zarr.ones(shape=(2, 2), order=order, zarr_format=zarr_format, config=config) assert arr.order == expected_order vals = np.asarray(arr) if expected_order == "C": assert vals.flags.c_contiguous elif expected_order == "F": assert vals.flags.f_contiguous else: raise AssertionError if zarr_format == 2: assert arr.metadata.zarr_format == 2 assert arr.metadata.order == expected_order async def test_init_order_warns() -> None: with pytest.warns( RuntimeWarning, match="The `order` keyword argument has no effect for Zarr format 3 arrays" ): await init_array( store_path=StorePath(store=MemoryStore()), shape=(1,), dtype="uint8", zarr_format=3, order="F", ) # def test_lazy_loader(): # foo = np.arange(100) # bar = np.arange(100, 0, -1) # store = "data/group.zarr" # save(store, foo=foo, bar=bar) # loader = load(store) # assert "foo" in loader # assert "bar" in loader # assert "baz" not in loader # assert len(loader) == 2 # assert sorted(loader) == ["bar", "foo"] # assert_array_equal(foo, loader["foo"]) # assert_array_equal(bar, loader["bar"]) # assert "LazyLoader: " in repr(loader) def test_load_array(sync_store: Store) -> None: store = sync_store foo = np.arange(100) bar = np.arange(100, 0, -1) save(store, foo=foo, bar=bar) # can also load arrays directly into a numpy array for array_name in ["foo", "bar"]: array = load(store, path=array_name) assert isinstance(array, np.ndarray) if array_name == "foo": assert_array_equal(foo, array) else: assert_array_equal(bar, array) @pytest.mark.parametrize("path", ["data", None]) @pytest.mark.parametrize("load_read_only", [True, False, None]) def test_load_zip(tmp_path: Path, path: str | None, load_read_only: bool | None) -> None: file = tmp_path / "test.zip" data = np.arange(100).reshape(10, 10) with ZipStore(file, mode="w", read_only=False) as zs: save(zs, data, path=path) with ZipStore(file, mode="r", read_only=load_read_only) as zs: result = zarr.load(store=zs, path=path) assert isinstance(result, np.ndarray) assert np.array_equal(result, data) with ZipStore(file, read_only=load_read_only) as zs: result = zarr.load(store=zs, path=path) assert isinstance(result, np.ndarray) assert np.array_equal(result, data) @pytest.mark.parametrize("path", ["data", None]) @pytest.mark.parametrize("load_read_only", [True, False]) def test_load_local(tmp_path: Path, path: str | None, load_read_only: bool) -> None: file = tmp_path / "test.zip" data = np.arange(100).reshape(10, 10) with LocalStore(file, read_only=False) as zs: save(zs, data, path=path) with LocalStore(file, read_only=load_read_only) as zs: result = zarr.load(store=zs, path=path) assert isinstance(result, np.ndarray) assert np.array_equal(result, data) def test_tree() -> None: g1 = zarr.group() g1.create_group("foo") g3 = g1.create_group("bar") g3.create_group("baz") g5 = g3.create_group("qux") g5.create_array("baz", shape=(100,), chunks=(10,), dtype="float64") with pytest.warns(ZarrDeprecationWarning, match=r"Group\.tree instead\."): # noqa: PT031 assert repr(zarr.tree(g1)) == repr(g1.tree()) assert str(zarr.tree(g1)) == str(g1.tree()) # @pytest.mark.parametrize("stores_from_path", [False, True]) # @pytest.mark.parametrize( # "with_chunk_store,listable", # [(False, True), (True, True), (False, False)], # ids=["default-listable", "with_chunk_store-listable", "default-unlistable"], # ) # def test_consolidate_metadata(with_chunk_store, listable, monkeypatch, stores_from_path): # # setup initial data # if stores_from_path: # store = tempfile.mkdtemp() # atexit.register(atexit_rmtree, store) # if with_chunk_store: # chunk_store = tempfile.mkdtemp() # atexit.register(atexit_rmtree, chunk_store) # else: # chunk_store = None # else: # store = MemoryStore() # chunk_store = MemoryStore() if with_chunk_store else None # path = None # z = group(store, chunk_store=chunk_store, path=path) # # Reload the actual store implementation in case str # store_to_copy = z.store # z.create_group("g1") # g2 = z.create_group("g2") # g2.attrs["hello"] = "world" # arr = g2.create_array("arr", shape=(20, 20), chunks=(5, 5), dtype="f8") # assert 16 == arr.nchunks # assert 0 == arr.nchunks_initialized # arr.attrs["data"] = 1 # arr[:] = 1.0 # assert 16 == arr.nchunks_initialized # if stores_from_path: # # get the actual store class for use with consolidate_metadata # store_class = z._store # else: # store_class = store # # perform consolidation # out = consolidate_metadata(store_class, path=path) # assert isinstance(out, Group) # assert ["g1", "g2"] == list(out) # if not stores_from_path: # assert isinstance(out._store, ConsolidatedMetadataStore) # assert ".zmetadata" in store # meta_keys = [ # ".zgroup", # "g1/.zgroup", # "g2/.zgroup", # "g2/.zattrs", # "g2/arr/.zarray", # "g2/arr/.zattrs", # ] # for key in meta_keys: # del store[key] # # https://github.com/zarr-developers/zarr-python/issues/993 # # Make sure we can still open consolidated on an unlistable store: # if not listable: # fs_memory = pytest.importorskip("fsspec.implementations.memory") # monkeypatch.setattr(fs_memory.MemoryFileSystem, "isdir", lambda x, y: False) # monkeypatch.delattr(fs_memory.MemoryFileSystem, "ls") # fs = fs_memory.MemoryFileSystem() # store_to_open = FSStore("", fs=fs) # # copy original store to new unlistable store # store_to_open.update(store_to_copy) # else: # store_to_open = store # # open consolidated # z2 = open_consolidated(store_to_open, chunk_store=chunk_store, path=path) # assert ["g1", "g2"] == list(z2) # assert "world" == z2.g2.attrs["hello"] # assert 1 == z2.g2.arr.attrs["data"] # assert (z2.g2.arr[:] == 1.0).all() # assert 16 == z2.g2.arr.nchunks # if listable: # assert 16 == z2.g2.arr.nchunks_initialized # else: # with pytest.raises(NotImplementedError): # _ = z2.g2.arr.nchunks_initialized # if stores_from_path: # # path string is note a BaseStore subclass so cannot be used to # # initialize a ConsolidatedMetadataStore. # with pytest.raises(ValueError): # cmd = ConsolidatedMetadataStore(store) # else: # # tests del/write on the store # cmd = ConsolidatedMetadataStore(store) # with pytest.raises(PermissionError): # del cmd[".zgroup"] # with pytest.raises(PermissionError): # cmd[".zgroup"] = None # # test getsize on the store # assert isinstance(getsize(cmd), Integral) # # test new metadata are not writeable # with pytest.raises(PermissionError): # z2.create_group("g3") # with pytest.raises(PermissionError): # z2.create_dataset("spam", shape=42, chunks=7, dtype="i4") # with pytest.raises(PermissionError): # del z2["g2"] # # test consolidated metadata are not writeable # with pytest.raises(PermissionError): # z2.g2.attrs["hello"] = "universe" # with pytest.raises(PermissionError): # z2.g2.arr.attrs["foo"] = "bar" # # test the data are writeable # z2.g2.arr[:] = 2 # assert (z2.g2.arr[:] == 2).all() # # test invalid modes # with pytest.raises(ValueError): # open_consolidated(store, chunk_store=chunk_store, mode="a", path=path) # with pytest.raises(ValueError): # open_consolidated(store, chunk_store=chunk_store, mode="w", path=path) # with pytest.raises(ValueError): # open_consolidated(store, chunk_store=chunk_store, mode="w-", path=path) # # make sure keyword arguments are passed through without error # open_consolidated( # store, # chunk_store=chunk_store, # path=path, # cache_attrs=True, # synchronizer=None, # ) # @pytest.mark.parametrize( # "options", # ( # {"dimension_separator": "/"}, # {"dimension_separator": "."}, # {"dimension_separator": None}, # ), # ) # def test_save_array_separator(tmpdir, options): # data = np.arange(6).reshape((3, 2)) # url = tmpdir.join("test.zarr") # save_array(url, data, **options) # class TestCopyStore(unittest.TestCase): # _version = 2 # def setUp(self): # source = dict() # source["foo"] = b"xxx" # source["bar/baz"] = b"yyy" # source["bar/qux"] = b"zzz" # self.source = source # def _get_dest_store(self): # return dict() # def test_no_paths(self): # source = self.source # dest = self._get_dest_store() # copy_store(source, dest) # assert len(source) == len(dest) # for key in source: # assert source[key] == dest[key] # def test_source_path(self): # source = self.source # # paths should be normalized # for source_path in "bar", "bar/", "/bar", "/bar/": # dest = self._get_dest_store() # copy_store(source, dest, source_path=source_path) # assert 2 == len(dest) # for key in source: # if key.startswith("bar/"): # dest_key = key.split("bar/")[1] # assert source[key] == dest[dest_key] # else: # assert key not in dest # def test_dest_path(self): # source = self.source # # paths should be normalized # for dest_path in "new", "new/", "/new", "/new/": # dest = self._get_dest_store() # copy_store(source, dest, dest_path=dest_path) # assert len(source) == len(dest) # for key in source: # if self._version == 3: # dest_key = f"{key[:10]}new/{key[10:]}" # else: # dest_key = f"new/{key}" # assert source[key] == dest[dest_key] # def test_source_dest_path(self): # source = self.source # # paths should be normalized # for source_path in "bar", "bar/", "/bar", "/bar/": # for dest_path in "new", "new/", "/new", "/new/": # dest = self._get_dest_store() # copy_store(source, dest, source_path=source_path, dest_path=dest_path) # assert 2 == len(dest) # for key in source: # if key.startswith("bar/"): # dest_key = "new/" + key.split("bar/")[1] # assert source[key] == dest[dest_key] # else: # assert key not in dest # assert (f"new/{key}") not in dest # def test_excludes_includes(self): # source = self.source # # single excludes # dest = self._get_dest_store() # excludes = "f.*" # copy_store(source, dest, excludes=excludes) # assert len(dest) == 2 # root = "" # assert "f{root}foo" not in dest # # multiple excludes # dest = self._get_dest_store() # excludes = "b.z", ".*x" # copy_store(source, dest, excludes=excludes) # assert len(dest) == 1 # assert f"{root}foo" in dest # assert f"{root}bar/baz" not in dest # assert f"{root}bar/qux" not in dest # # excludes and includes # dest = self._get_dest_store() # excludes = "b.*" # includes = ".*x" # copy_store(source, dest, excludes=excludes, includes=includes) # assert len(dest) == 2 # assert f"{root}foo" in dest # assert f"{root}bar/baz" not in dest # assert f"{root}bar/qux" in dest # def test_dry_run(self): # source = self.source # dest = self._get_dest_store() # copy_store(source, dest, dry_run=True) # assert 0 == len(dest) # def test_if_exists(self): # source = self.source # dest = self._get_dest_store() # root = "" # dest[f"{root}bar/baz"] = b"mmm" # # default ('raise') # with pytest.raises(CopyError): # copy_store(source, dest) # # explicit 'raise' # with pytest.raises(CopyError): # copy_store(source, dest, if_exists="raise") # # skip # copy_store(source, dest, if_exists="skip") # assert 3 == len(dest) # assert dest[f"{root}foo"] == b"xxx" # assert dest[f"{root}bar/baz"] == b"mmm" # assert dest[f"{root}bar/qux"] == b"zzz" # # replace # copy_store(source, dest, if_exists="replace") # assert 3 == len(dest) # assert dest[f"{root}foo"] == b"xxx" # assert dest[f"{root}bar/baz"] == b"yyy" # assert dest[f"{root}bar/qux"] == b"zzz" # # invalid option # with pytest.raises(ValueError): # copy_store(source, dest, if_exists="foobar") # def check_copied_array(original, copied, without_attrs=False, expect_props=None): # # setup # source_h5py = original.__module__.startswith("h5py.") # dest_h5py = copied.__module__.startswith("h5py.") # zarr_to_zarr = not (source_h5py or dest_h5py) # h5py_to_h5py = source_h5py and dest_h5py # zarr_to_h5py = not source_h5py and dest_h5py # h5py_to_zarr = source_h5py and not dest_h5py # if expect_props is None: # expect_props = dict() # else: # expect_props = expect_props.copy() # # common properties in zarr and h5py # for p in "dtype", "shape", "chunks": # expect_props.setdefault(p, getattr(original, p)) # # zarr-specific properties # if zarr_to_zarr: # for p in "compressor", "filters", "order", "fill_value": # expect_props.setdefault(p, getattr(original, p)) # # h5py-specific properties # if h5py_to_h5py: # for p in ( # "maxshape", # "compression", # "compression_opts", # "shuffle", # "scaleoffset", # "fletcher32", # "fillvalue", # ): # expect_props.setdefault(p, getattr(original, p)) # # common properties with some name differences # if h5py_to_zarr: # expect_props.setdefault("fill_value", original.fillvalue) # if zarr_to_h5py: # expect_props.setdefault("fillvalue", original.fill_value) # # compare properties # for k, v in expect_props.items(): # assert v == getattr(copied, k) # # compare data # assert_array_equal(original[:], copied[:]) # # compare attrs # if without_attrs: # for k in original.attrs.keys(): # assert k not in copied.attrs # else: # if dest_h5py and "filters" in original.attrs: # # special case in v3 (storing filters metadata under attributes) # # we explicitly do not copy this info over to HDF5 # original_attrs = original.attrs.asdict().copy() # original_attrs.pop("filters") # else: # original_attrs = original.attrs # assert sorted(original_attrs.items()) == sorted(copied.attrs.items()) # def check_copied_group(original, copied, without_attrs=False, expect_props=None, shallow=False): # # setup # if expect_props is None: # expect_props = dict() # else: # expect_props = expect_props.copy() # # compare children # for k, v in original.items(): # if hasattr(v, "shape"): # assert k in copied # check_copied_array(v, copied[k], without_attrs=without_attrs, expect_props=expect_props) # elif shallow: # assert k not in copied # else: # assert k in copied # check_copied_group( # v, # copied[k], # without_attrs=without_attrs, # shallow=shallow, # expect_props=expect_props, # ) # # compare attrs # if without_attrs: # for k in original.attrs.keys(): # assert k not in copied.attrs # else: # assert sorted(original.attrs.items()) == sorted(copied.attrs.items()) # def test_copy_all(): # """ # https://github.com/zarr-developers/zarr-python/issues/269 # copy_all used to not copy attributes as `.keys()` does not return hidden `.zattrs`. # """ # original_group = zarr.group(store=MemoryStore(), overwrite=True) # original_group.attrs["info"] = "group attrs" # original_subgroup = original_group.create_group("subgroup") # original_subgroup.attrs["info"] = "sub attrs" # destination_group = zarr.group(store=MemoryStore(), overwrite=True) # # copy from memory to directory store # copy_all( # original_group, # destination_group, # dry_run=False, # ) # assert "subgroup" in destination_group # assert destination_group.attrs["info"] == "group attrs" # assert destination_group.subgroup.attrs["info"] == "sub attrs" # class TestCopy: # @pytest.fixture(params=[False, True], ids=["zarr", "hdf5"]) # def source(self, request, tmpdir): # def prep_source(source): # foo = source.create_group("foo") # foo.attrs["experiment"] = "weird science" # baz = foo.create_dataset("bar/baz", data=np.arange(100), chunks=(50,)) # baz.attrs["units"] = "metres" # if request.param: # extra_kws = dict( # compression="gzip", # compression_opts=3, # fillvalue=84, # shuffle=True, # fletcher32=True, # ) # else: # extra_kws = dict(compressor=Zlib(3), order="F", fill_value=42, filters=[Adler32()]) # source.create_dataset( # "spam", # data=np.arange(100, 200).reshape(20, 5), # chunks=(10, 2), # dtype="i2", # **extra_kws, # ) # return source # if request.param: # h5py = pytest.importorskip("h5py") # fn = tmpdir.join("source.h5") # with h5py.File(str(fn), mode="w") as h5f: # yield prep_source(h5f) # else: # yield prep_source(group()) # @pytest.fixture(params=[False, True], ids=["zarr", "hdf5"]) # def dest(self, request, tmpdir): # if request.param: # h5py = pytest.importorskip("h5py") # fn = tmpdir.join("dest.h5") # with h5py.File(str(fn), mode="w") as h5f: # yield h5f # else: # yield group() # def test_copy_array(self, source, dest): # # copy array with default options # copy(source["foo/bar/baz"], dest) # check_copied_array(source["foo/bar/baz"], dest["baz"]) # copy(source["spam"], dest) # check_copied_array(source["spam"], dest["spam"]) # def test_copy_bad_dest(self, source, dest): # # try to copy to an array, dest must be a group # dest = dest.create_dataset("eggs", shape=(100,)) # with pytest.raises(ValueError): # copy(source["foo/bar/baz"], dest) # def test_copy_array_name(self, source, dest): # # copy array with name # copy(source["foo/bar/baz"], dest, name="qux") # assert "baz" not in dest # check_copied_array(source["foo/bar/baz"], dest["qux"]) # def test_copy_array_create_options(self, source, dest): # dest_h5py = dest.__module__.startswith("h5py.") # # copy array, provide creation options # compressor = Zlib(9) # create_kws = dict(chunks=(10,)) # if dest_h5py: # create_kws.update( # compression="gzip", compression_opts=9, shuffle=True, fletcher32=True, fillvalue=42 # ) # else: # create_kws.update(compressor=compressor, fill_value=42, order="F", filters=[Adler32()]) # copy(source["foo/bar/baz"], dest, without_attrs=True, **create_kws) # check_copied_array( # source["foo/bar/baz"], dest["baz"], without_attrs=True, expect_props=create_kws # ) # def test_copy_array_exists_array(self, source, dest): # # copy array, dest array in the way # dest.create_dataset("baz", shape=(10,)) # # raise # with pytest.raises(CopyError): # # should raise by default # copy(source["foo/bar/baz"], dest) # assert (10,) == dest["baz"].shape # with pytest.raises(CopyError): # copy(source["foo/bar/baz"], dest, if_exists="raise") # assert (10,) == dest["baz"].shape # # skip # copy(source["foo/bar/baz"], dest, if_exists="skip") # assert (10,) == dest["baz"].shape # # replace # copy(source["foo/bar/baz"], dest, if_exists="replace") # check_copied_array(source["foo/bar/baz"], dest["baz"]) # # invalid option # with pytest.raises(ValueError): # copy(source["foo/bar/baz"], dest, if_exists="foobar") # def test_copy_array_exists_group(self, source, dest): # # copy array, dest group in the way # dest.create_group("baz") # # raise # with pytest.raises(CopyError): # copy(source["foo/bar/baz"], dest) # assert not hasattr(dest["baz"], "shape") # with pytest.raises(CopyError): # copy(source["foo/bar/baz"], dest, if_exists="raise") # assert not hasattr(dest["baz"], "shape") # # skip # copy(source["foo/bar/baz"], dest, if_exists="skip") # assert not hasattr(dest["baz"], "shape") # # replace # copy(source["foo/bar/baz"], dest, if_exists="replace") # check_copied_array(source["foo/bar/baz"], dest["baz"]) # def test_copy_array_skip_initialized(self, source, dest): # dest_h5py = dest.__module__.startswith("h5py.") # dest.create_dataset("baz", shape=(100,), chunks=(10,), dtype="i8") # assert not np.all(source["foo/bar/baz"][:] == dest["baz"][:]) # if dest_h5py: # with pytest.raises(ValueError): # # not available with copy to h5py # copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") # else: # # copy array, dest array exists but not yet initialized # copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") # check_copied_array(source["foo/bar/baz"], dest["baz"]) # # copy array, dest array exists and initialized, will be skipped # dest["baz"][:] = np.arange(100, 200) # copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") # assert_array_equal(np.arange(100, 200), dest["baz"][:]) # assert not np.all(source["foo/bar/baz"][:] == dest["baz"][:]) # def test_copy_group(self, source, dest): # # copy group, default options # copy(source["foo"], dest) # check_copied_group(source["foo"], dest["foo"]) # def test_copy_group_no_name(self, source, dest): # with pytest.raises(TypeError): # # need a name if copy root # copy(source, dest) # copy(source, dest, name="root") # check_copied_group(source, dest["root"]) # def test_copy_group_options(self, source, dest): # # copy group, non-default options # copy(source["foo"], dest, name="qux", without_attrs=True) # assert "foo" not in dest # check_copied_group(source["foo"], dest["qux"], without_attrs=True) # def test_copy_group_shallow(self, source, dest): # # copy group, shallow # copy(source, dest, name="eggs", shallow=True) # check_copied_group(source, dest["eggs"], shallow=True) # def test_copy_group_exists_group(self, source, dest): # # copy group, dest groups exist # dest.create_group("foo/bar") # copy(source["foo"], dest) # check_copied_group(source["foo"], dest["foo"]) # def test_copy_group_exists_array(self, source, dest): # # copy group, dest array in the way # dest.create_dataset("foo/bar", shape=(10,)) # # raise # with pytest.raises(CopyError): # copy(source["foo"], dest) # assert dest["foo/bar"].shape == (10,) # with pytest.raises(CopyError): # copy(source["foo"], dest, if_exists="raise") # assert dest["foo/bar"].shape == (10,) # # skip # copy(source["foo"], dest, if_exists="skip") # assert dest["foo/bar"].shape == (10,) # # replace # copy(source["foo"], dest, if_exists="replace") # check_copied_group(source["foo"], dest["foo"]) # def test_copy_group_dry_run(self, source, dest): # # dry run, empty destination # n_copied, n_skipped, n_bytes_copied = copy( # source["foo"], dest, dry_run=True, return_stats=True # ) # assert 0 == len(dest) # assert 3 == n_copied # assert 0 == n_skipped # assert 0 == n_bytes_copied # # dry run, array exists in destination # baz = np.arange(100, 200) # dest.create_dataset("foo/bar/baz", data=baz) # assert not np.all(source["foo/bar/baz"][:] == dest["foo/bar/baz"][:]) # assert 1 == len(dest) # # raise # with pytest.raises(CopyError): # copy(source["foo"], dest, dry_run=True) # assert 1 == len(dest) # # skip # n_copied, n_skipped, n_bytes_copied = copy( # source["foo"], dest, dry_run=True, if_exists="skip", return_stats=True # ) # assert 1 == len(dest) # assert 2 == n_copied # assert 1 == n_skipped # assert 0 == n_bytes_copied # assert_array_equal(baz, dest["foo/bar/baz"]) # # replace # n_copied, n_skipped, n_bytes_copied = copy( # source["foo"], dest, dry_run=True, if_exists="replace", return_stats=True # ) # assert 1 == len(dest) # assert 3 == n_copied # assert 0 == n_skipped # assert 0 == n_bytes_copied # assert_array_equal(baz, dest["foo/bar/baz"]) # def test_logging(self, source, dest, tmpdir): # # callable log # copy(source["foo"], dest, dry_run=True, log=print) # # file name # fn = str(tmpdir.join("log_name")) # copy(source["foo"], dest, dry_run=True, log=fn) # # file # with tmpdir.join("log_file").open(mode="w") as f: # copy(source["foo"], dest, dry_run=True, log=f) # # bad option # with pytest.raises(TypeError): # copy(source["foo"], dest, dry_run=True, log=True) def test_open_falls_back_to_open_group() -> None: # https://github.com/zarr-developers/zarr-python/issues/2309 store = MemoryStore() zarr.open_group(store, attributes={"key": "value"}) group = zarr.open(store) assert isinstance(group, Group) assert group.attrs == {"key": "value"} async def test_open_falls_back_to_open_group_async(zarr_format: ZarrFormat) -> None: # https://github.com/zarr-developers/zarr-python/issues/2309 store = MemoryStore() await zarr.api.asynchronous.open_group( store, attributes={"key": "value"}, zarr_format=zarr_format ) group = await zarr.api.asynchronous.open(store=store) assert isinstance(group, zarr.core.group.AsyncGroup) assert group.metadata.zarr_format == zarr_format assert group.attrs == {"key": "value"} @pytest.mark.parametrize("mode", ["r", "r+", "w", "a"]) def test_open_modes_creates_group(tmp_path: Path, mode: str) -> None: # https://github.com/zarr-developers/zarr-python/issues/2490 zarr_dir = tmp_path / f"mode-{mode}-test.zarr" if mode in ["r", "r+"]: # Expect FileNotFoundError to be raised if 'r' or 'r+' mode with pytest.raises(FileNotFoundError): zarr.open(store=zarr_dir, mode=mode) # type: ignore[arg-type] else: group = zarr.open(store=zarr_dir, mode=mode) # type: ignore[arg-type] assert isinstance(group, Group) async def test_metadata_validation_error() -> None: with pytest.raises( MetadataValidationError, match="Invalid value for 'zarr_format'. Expected 2, 3, or None. Got '3.0'.", ): await zarr.api.asynchronous.open_group(zarr_format="3.0") # type: ignore[arg-type] with pytest.raises( MetadataValidationError, match="Invalid value for 'zarr_format'. Expected 2, 3, or None. Got '3.0'.", ): await zarr.api.asynchronous.open_array(shape=(1,), zarr_format="3.0") # type: ignore[arg-type] @pytest.mark.parametrize( "store", ["local", "memory", "zip"], indirect=True, ) def test_open_array_with_mode_r_plus(store: Store, zarr_format: ZarrFormat) -> None: # 'r+' means read/write (must exist) with pytest.raises(ArrayNotFoundError): zarr.open_array(store=store, mode="r+", zarr_format=zarr_format) zarr.ones(store=store, shape=(3, 3), zarr_format=zarr_format) z2 = zarr.open_array(store=store, mode="r+") assert isinstance(z2, Array) assert z2.metadata.zarr_format == zarr_format result = z2[:] assert isinstance(result, NDArrayLike) assert (result == 1).all() z2[:] = 3 @pytest.mark.parametrize( ("a_func", "b_func"), [ (zarr.api.asynchronous.create_array, zarr.api.synchronous.create_array), (zarr.api.asynchronous.save, zarr.api.synchronous.save), (zarr.api.asynchronous.save_array, zarr.api.synchronous.save_array), (zarr.api.asynchronous.save_group, zarr.api.synchronous.save_group), (zarr.api.asynchronous.open_group, zarr.api.synchronous.open_group), (zarr.api.asynchronous.create, zarr.api.synchronous.create), ], ) def test_consistent_signatures( a_func: Callable[[object], object], b_func: Callable[[object], object] ) -> None: """ Ensure that pairs of functions have the same signature """ base_sig = inspect.signature(a_func) test_sig = inspect.signature(b_func) wrong: dict[str, list[object]] = { "missing_from_test": [], "missing_from_base": [], "wrong_type": [], } for key, value in base_sig.parameters.items(): if key not in test_sig.parameters: wrong["missing_from_test"].append((key, value)) for key, value in test_sig.parameters.items(): if key not in base_sig.parameters: wrong["missing_from_base"].append((key, value)) if base_sig.parameters[key] != value: wrong["wrong_type"].append({key: {"test": value, "base": base_sig.parameters[key]}}) assert wrong["missing_from_base"] == [] assert wrong["missing_from_test"] == [] assert wrong["wrong_type"] == [] def test_api_exports() -> None: """ Test that the sync API and the async API export the same objects """ assert zarr.api.asynchronous.__all__ == zarr.api.synchronous.__all__ @gpu_test @pytest.mark.parametrize( "store", ["local", "memory", "zip"], indirect=True, ) @pytest.mark.parametrize("zarr_format", [None, 2, 3]) def test_gpu_basic(store: Store, zarr_format: ZarrFormat | None) -> None: import cupy as cp if zarr_format == 2: # Without this, the zstd codec attempts to convert the cupy # array to bytes. compressors = None else: compressors = "auto" with zarr.config.enable_gpu(): src = cp.random.uniform(size=(100, 100)) # allocate on the device z = zarr.create_array( store, name="a", shape=src.shape, chunks=(10, 10), dtype=src.dtype, overwrite=True, zarr_format=zarr_format, compressors=compressors, # type: ignore[arg-type] ) z[:10, :10] = src[:10, :10] result = z[:10, :10] # assert_array_equal doesn't check the type assert isinstance(result, type(src)) cp.testing.assert_array_equal(result, src[:10, :10]) def test_v2_without_compressor() -> None: # Make sure it's possible to set no compressor for v2 arrays arr = zarr.create(store={}, shape=(1), dtype="uint8", zarr_format=2, compressor=None) assert arr.compressors == () def test_v2_with_v3_compressor() -> None: # Check trying to create a v2 array with a v3 compressor fails with pytest.raises( ValueError, match="Cannot use a BytesBytesCodec as a compressor for zarr v2 arrays. Use a numcodecs codec directly instead.", ): zarr.create( store={}, shape=(1), dtype="uint8", zarr_format=2, compressor=zarr.codecs.BloscCodec() ) def add_empty_file(path: Path) -> Path: fpath = path / "a.txt" fpath.touch() return fpath @pytest.mark.parametrize("create_function", [create_array, from_array]) @pytest.mark.parametrize("overwrite", [True, False]) def test_no_overwrite_array(tmp_path: Path, create_function: Callable, overwrite: bool) -> None: # type:ignore[type-arg] store = zarr.storage.LocalStore(tmp_path) existing_fpath = add_empty_file(tmp_path) assert existing_fpath.exists() create_function(store=store, data=np.ones(shape=(1,)), overwrite=overwrite) if overwrite: assert not existing_fpath.exists() else: assert existing_fpath.exists() @pytest.mark.parametrize("create_function", [create_group, group]) @pytest.mark.parametrize("overwrite", [True, False]) def test_no_overwrite_group(tmp_path: Path, create_function: Callable, overwrite: bool) -> None: # type:ignore[type-arg] store = zarr.storage.LocalStore(tmp_path) existing_fpath = add_empty_file(tmp_path) assert existing_fpath.exists() create_function(store=store, overwrite=overwrite) if overwrite: assert not existing_fpath.exists() else: assert existing_fpath.exists() @pytest.mark.parametrize("open_func", [zarr.open, open_group]) @pytest.mark.parametrize("mode", ["r", "r+", "a", "w", "w-"]) def test_no_overwrite_open(tmp_path: Path, open_func: Callable, mode: str) -> None: # type:ignore[type-arg] store = zarr.storage.LocalStore(tmp_path) existing_fpath = add_empty_file(tmp_path) assert existing_fpath.exists() with contextlib.suppress(FileExistsError, FileNotFoundError, ZarrUserWarning): open_func(store=store, mode=mode) if mode == "w": assert not existing_fpath.exists() else: assert existing_fpath.exists() def test_no_overwrite_load(tmp_path: Path) -> None: store = zarr.storage.LocalStore(tmp_path) existing_fpath = add_empty_file(tmp_path) assert existing_fpath.exists() with contextlib.suppress(NotImplementedError): zarr.load(store) assert existing_fpath.exists() @pytest.mark.parametrize( "f", [ zarr.array, zarr.create, zarr.create_array, zarr.ones, zarr.ones_like, zarr.empty, zarr.empty_like, zarr.full, zarr.full_like, zarr.zeros, zarr.zeros_like, ], ) def test_auto_chunks(f: Callable[..., AnyArray]) -> None: # Make sure chunks are set automatically across the public API # TODO: test shards with this test too shape = (1000, 1000) dtype = np.uint8 kwargs = {"shape": shape, "dtype": dtype} array = np.zeros(shape, dtype=dtype) store = zarr.storage.MemoryStore() # ruff: disable[FURB171] if f in [zarr.full, zarr.full_like]: kwargs["fill_value"] = 0 if f in [zarr.array]: kwargs["data"] = array if f in [zarr.empty_like, zarr.full_like, zarr.empty_like, zarr.ones_like, zarr.zeros_like]: kwargs["a"] = array if f in [zarr.create_array]: kwargs["store"] = store # ruff: enable[FURB171] a = f(**kwargs) assert a.chunks == (500, 500) @pytest.mark.parametrize("kwarg_name", ["synchronizer", "chunk_store", "cache_attrs", "meta_array"]) def test_unimplemented_kwarg_warnings(kwarg_name: str) -> None: kwargs = {kwarg_name: 1} with pytest.warns(RuntimeWarning, match=".* is not yet implemented"): zarr.create(shape=(1,), **kwargs) # type: ignore[arg-type] zarr-python-3.2.1/tests/test_api/000077500000000000000000000000001517635743000167775ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_api/test_asynchronous.py000066400000000000000000000067651517635743000231610ustar00rootroot00000000000000from __future__ import annotations import json from dataclasses import dataclass from typing import TYPE_CHECKING import numpy as np import pytest from zarr import create_array from zarr.api.asynchronous import _get_shape_chunks, _like_args, group, open from zarr.core.buffer.core import default_buffer_prototype from zarr.core.group import AsyncGroup if TYPE_CHECKING: from pathlib import Path from typing import Any import numpy.typing as npt from zarr.core.array import AsyncArray from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.types import AnyArray @dataclass class WithShape: shape: tuple[int, ...] @dataclass class WithChunks(WithShape): chunks: tuple[int, ...] @dataclass class WithChunkLen(WithShape): chunklen: int @pytest.mark.parametrize( ("observed", "expected"), [ ({}, (None, None)), (WithShape(shape=(1, 2)), ((1, 2), None)), (WithChunks(shape=(1, 2), chunks=(1, 2)), ((1, 2), (1, 2))), (WithChunkLen(shape=(10, 10), chunklen=1), ((10, 10), (1, 10))), ], ) def test_get_shape_chunks( observed: object, expected: tuple[tuple[int, ...] | None, tuple[int, ...] | None] ) -> None: """ Test the _get_shape_chunks function """ assert _get_shape_chunks(observed) == expected @pytest.mark.parametrize( ("observed", "expected"), [ (np.arange(10, dtype=np.dtype("int64")), {"shape": (10,), "dtype": np.dtype("int64")}), (WithChunks(shape=(1, 2), chunks=(1, 2)), {"chunks": (1, 2), "shape": (1, 2)}), ( create_array( {}, chunks=(10,), shape=(100,), dtype="f8", compressors=None, filters=None, zarr_format=2, )._async_array, { "chunks": (10,), "shape": (100,), "dtype": np.dtype("f8"), "compressor": None, "filters": None, "order": "C", }, ), ], ) def test_like_args( observed: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AnyArray | npt.NDArray[Any], expected: object, ) -> None: """ Test the like_args function """ assert _like_args(observed) == expected async def test_open_no_array() -> None: """ Test that zarr.api.asynchronous.open attempts to open a group when no array is found, but shape was specified in kwargs. This behavior makes no sense but we should still test it. """ store = { "zarr.json": default_buffer_prototype().buffer.from_bytes( json.dumps({"zarr_format": 3, "node_type": "group"}).encode("utf-8") ) } with pytest.raises( TypeError, match=r"open_group\(\) got an unexpected keyword argument 'shape'" ): await open(store=store, shape=(1,)) async def test_open_group_new_path(tmp_path: Path) -> None: """ Test that zarr.api.asynchronous.group properly handles a string representation of a local file path that does not yet exist. See https://github.com/zarr-developers/zarr-python/issues/3406 """ # tmp_path exists, but tmp_path / "test.zarr" will not, which is important for this test path = tmp_path / "test.zarr" grp = await group(store=path, attributes={"a": 1}) assert isinstance(grp, AsyncGroup) # Calling group on an existing store should just open that store grp = await group(store=path) assert grp.attrs == {"a": 1} zarr-python-3.2.1/tests/test_api/test_synchronous.py000066400000000000000000000104631517635743000230060ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Final import pytest from numpydoc.docscrape import NumpyDocString import zarr from zarr.api import asynchronous, synchronous if TYPE_CHECKING: from collections.abc import Callable MATCHED_EXPORT_NAMES: Final[tuple[str, ...]] = tuple( sorted(set(synchronous.__all__) | set(asynchronous.__all__)) ) """A sorted tuple of names that are exported by both the sync and async APIs.""" MATCHED_CALLABLE_NAMES: Final[tuple[str, ...]] = tuple( x for x in MATCHED_EXPORT_NAMES if callable(getattr(synchronous, x)) ) """A sorted tuple of callable names that are exported by both the sync and async APIs.""" @pytest.mark.parametrize("callable_name", MATCHED_CALLABLE_NAMES) def test_docstrings_match(callable_name: str) -> None: """ Tests that the docstrings for the sync and async define identical parameters. """ callable_a = getattr(synchronous, callable_name) callable_b = getattr(asynchronous, callable_name) if callable_a.__doc__ is None: assert callable_b.__doc__ is None else: params_a = NumpyDocString(callable_a.__doc__)["Parameters"] params_b = NumpyDocString(callable_b.__doc__)["Parameters"] mismatch = [] for idx, (a, b) in enumerate(zip(params_a, params_b, strict=False)): if a != b: mismatch.append((idx, (a, b))) assert mismatch == [] @pytest.mark.parametrize( ("parameter_name", "array_creation_routines"), [ ( ("store", "path"), ( asynchronous.create_array, synchronous.create_array, asynchronous.create_group, synchronous.create_group, zarr.AsyncGroup.create_array, zarr.Group.create_array, ), ), ( ( "store", "path", ), ( asynchronous.create, synchronous.create, zarr.Group.create, ), ), ( ( ( "filters", "codecs", "compressors", "compressor", "chunks", "shape", "dtype", "shardsfill_value", ) ), ( asynchronous.create, synchronous.create, asynchronous.create_array, synchronous.create_array, zarr.AsyncGroup.create_array, zarr.Group.create_array, ), ), ], ids=str, ) def test_docstring_consistent_parameters( parameter_name: str, array_creation_routines: tuple[Callable[[Any], Any], ...] ) -> None: """ Tests that array and group creation routines document the same parameters consistently. This test inspects the docstrings of sets of callables and generates two dicts: - a dict where the keys are parameter descriptions and the values are the names of the routines with those descriptions - a dict where the keys are parameter types and the values are the names of the routines with those types If each dict has just 1 value, then the parameter description and type in the docstring must be identical across different routines. But if these dicts have multiple values, then there must be routines that use the same parameter but document it differently, which will trigger a test failure. """ descs: dict[tuple[str, ...], tuple[str, ...]] = {} types: dict[str, tuple[str, ...]] = {} for routine in array_creation_routines: key = f"{routine.__module__}.{routine.__qualname__}" docstring = NumpyDocString(routine.__doc__) param_dict = {d.name: d for d in docstring["Parameters"]} if parameter_name in param_dict: val = param_dict[parameter_name] if tuple(val.desc) in descs: descs[tuple(val.desc)] = descs[tuple(val.desc)] + (key,) else: descs[tuple(val.desc)] = (key,) if val.type in types: types[val.type] = types[val.type] + (key,) else: types[val.type] = (key,) assert len(descs) <= 1 assert len(types) <= 1 zarr-python-3.2.1/tests/test_array.py000066400000000000000000002412661517635743000177310ustar00rootroot00000000000000import dataclasses import inspect import json import math import multiprocessing as mp import pickle import re import sys from itertools import accumulate, starmap from typing import TYPE_CHECKING, Any, Literal from unittest import mock import numcodecs import numpy as np import numpy.typing as npt import pytest from packaging.version import Version import zarr.api.asynchronous import zarr.api.synchronous as sync_api from tests.conftest import skip_object_dtype from zarr import Array, Group from zarr.abc.store import Store from zarr.codecs import ( BytesCodec, GzipCodec, TransposeCodec, ZstdCodec, ) from zarr.core._info import ArrayInfo from zarr.core.array import ( AsyncArray, CompressorsLike, FiltersLike, _iter_chunk_coords, _iter_chunk_regions, _iter_shard_coords, _iter_shard_keys, _iter_shard_regions, _parse_chunk_encoding_v2, _parse_chunk_encoding_v3, _shards_initialized, create_array, default_filters_v2, default_serializer_v3, ) from zarr.core.array_spec import ArrayConfig, ArrayConfigParams from zarr.core.buffer import NDArrayLike, NDArrayLikeOrScalar, default_buffer_prototype from zarr.core.chunk_grids import _auto_partition from zarr.core.chunk_key_encodings import ChunkKeyEncodingParams from zarr.core.common import JSON, ZarrFormat, ceildiv from zarr.core.dtype import ( DateTime64, Float32, Float64, Int16, Structured, TimeDelta64, UInt8, VariableLengthBytes, VariableLengthUTF8, ZDType, parse_dtype, ) from zarr.core.dtype.common import ENDIANNESS_STR, EndiannessStr from zarr.core.dtype.npy.common import NUMPY_ENDIANNESS_STR, endianness_from_numpy_str from zarr.core.dtype.npy.string import UTF8Base from zarr.core.group import AsyncGroup from zarr.core.indexing import BasicIndexer, _iter_grid, _iter_regions from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.sync import sync from zarr.errors import ( ContainsArrayError, ContainsGroupError, ZarrUserWarning, ) from zarr.storage import LocalStore, MemoryStore, StorePath from zarr.storage._logging import LoggingStore from zarr.types import AnyArray, AnyAsyncArray from .test_dtype.conftest import zdtype_examples if TYPE_CHECKING: from zarr.abc.codec import CodecJSON_V3 @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.parametrize("overwrite", [True, False]) @pytest.mark.parametrize("extant_node", ["array", "group"]) def test_array_creation_existing_node( store: LocalStore | MemoryStore, zarr_format: ZarrFormat, overwrite: bool, extant_node: Literal["array", "group"], ) -> None: """ Check that an existing array or group is handled as expected during array creation. """ spath = StorePath(store) group = Group.from_store(spath, zarr_format=zarr_format) expected_exception: type[ContainsArrayError | ContainsGroupError] if extant_node == "array": expected_exception = ContainsArrayError _ = group.create_array("extant", shape=(10,), dtype="uint8") elif extant_node == "group": expected_exception = ContainsGroupError _ = group.create_group("extant") else: raise AssertionError new_shape = (2, 2) new_dtype = "float32" if overwrite: if not store.supports_deletes: pytest.skip("store does not support deletes") arr_new = zarr.create_array( spath / "extant", shape=new_shape, dtype=new_dtype, overwrite=overwrite, zarr_format=zarr_format, ) assert arr_new.shape == new_shape assert arr_new.dtype == new_dtype else: with pytest.raises(expected_exception): arr_new = zarr.create_array( spath / "extant", shape=new_shape, dtype=new_dtype, overwrite=overwrite, zarr_format=zarr_format, ) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_create_creates_parents( store: LocalStore | MemoryStore, zarr_format: ZarrFormat ) -> None: # prepare a root node, with some data set await zarr.api.asynchronous.open_group( store=store, path="a", zarr_format=zarr_format, attributes={"key": "value"} ) # create a child node with a couple intermediates await zarr.api.asynchronous.create( shape=(2, 2), store=store, path="a/b/c/d", zarr_format=zarr_format ) parts = ["a", "a/b", "a/b/c"] if zarr_format == 2: files = [".zattrs", ".zgroup"] else: files = ["zarr.json"] expected = [f"{part}/{file}" for file in files for part in parts] if zarr_format == 2: expected.extend([".zattrs", ".zgroup", "a/b/c/d/.zarray", "a/b/c/d/.zattrs"]) else: expected.extend(["zarr.json", "a/b/c/d/zarr.json"]) expected = sorted(expected) result = sorted([x async for x in store.list_prefix("")]) assert result == expected paths = ["a", "a/b", "a/b/c"] for path in paths: g = await zarr.api.asynchronous.open_group(store=store, path=path) assert isinstance(g, AsyncGroup) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) def test_array_name_properties_no_group( store: LocalStore | MemoryStore, zarr_format: ZarrFormat ) -> None: arr = zarr.create_array( store=store, shape=(100,), chunks=(10,), zarr_format=zarr_format, dtype=">i4" ) assert arr.path == "" assert arr.name == "/" assert arr.basename == "" @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) def test_array_name_properties_with_group( store: LocalStore | MemoryStore, zarr_format: ZarrFormat ) -> None: root = Group.from_store(store=store, zarr_format=zarr_format) foo = root.create_array("foo", shape=(100,), chunks=(10,), dtype="i4") assert foo.path == "foo" assert foo.name == "/foo" assert foo.basename == "foo" bar = root.create_group("bar") spam = bar.create_array("spam", shape=(100,), chunks=(10,), dtype="i4") assert spam.path == "bar/spam" assert spam.name == "/bar/spam" assert spam.basename == "spam" @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("specify_fill_value", [True, False]) @pytest.mark.parametrize( "zdtype", zdtype_examples, ids=tuple(str(type(v)) for v in zdtype_examples) ) def test_array_fill_value_default( store: MemoryStore, specify_fill_value: bool, zdtype: ZDType[Any, Any] ) -> None: """ Test that creating an array with the fill_value parameter set to None, or unspecified, results in the expected fill_value attribute of the array, i.e. the default value of the dtype """ shape = (10,) if specify_fill_value: arr = zarr.create_array( store=store, shape=shape, dtype=zdtype, zarr_format=3, chunks=shape, fill_value=None, ) else: arr = zarr.create_array(store=store, shape=shape, dtype=zdtype, zarr_format=3, chunks=shape) expected_fill_value = zdtype.default_scalar() if isinstance(expected_fill_value, np.datetime64 | np.timedelta64): if np.isnat(expected_fill_value): assert np.isnat(arr.fill_value) elif isinstance(expected_fill_value, np.floating | np.complexfloating): if np.isnan(expected_fill_value): assert np.isnan(arr.fill_value) else: assert arr.fill_value == expected_fill_value # A simpler check would be to ensure that arr.fill_value.dtype == arr.dtype # But for some numpy data types (namely, U), scalars might not have length. An empty string # scalar from a `>U4` array would have dtype `>U`, and arr.fill_value.dtype == arr.dtype will fail. assert type(arr.fill_value) is type(np.array([arr.fill_value], dtype=arr.dtype)[0]) @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize( ("dtype_str", "fill_value"), [("bool", True), ("uint8", 99), ("float32", -99.9), ("complex64", 3 + 4j)], ) def test_array_v3_fill_value(store: MemoryStore, fill_value: int, dtype_str: str) -> None: shape = (10,) arr = zarr.create_array( store=store, shape=shape, dtype=dtype_str, zarr_format=3, chunks=shape, fill_value=fill_value, ) assert arr.fill_value == np.dtype(dtype_str).type(fill_value) assert arr.fill_value.dtype == arr.dtype @pytest.mark.parametrize("store", ["memory"], indirect=True) async def test_array_v3_nan_fill_value(store: MemoryStore) -> None: shape = (10,) arr = zarr.create_array( store=store, shape=shape, dtype=np.float64, zarr_format=3, chunks=shape, fill_value=np.nan, ) arr[:] = np.nan assert np.isnan(arr.fill_value) assert arr.fill_value.dtype == arr.dtype # all fill value chunk is an empty chunk, and should not be written assert len([a async for a in store.list_prefix("/")]) == 0 @pytest.mark.parametrize("store", ["local"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_serializable_async_array( store: LocalStore | MemoryStore, zarr_format: ZarrFormat ) -> None: expected = await zarr.api.asynchronous.create_array( store=store, shape=(100,), chunks=(10,), zarr_format=zarr_format, dtype="i4" ) # await expected.setitems(list(range(100))) p = pickle.dumps(expected) actual = pickle.loads(p) assert actual == expected # np.testing.assert_array_equal(await actual.getitem(slice(None)), await expected.getitem(slice(None))) # TODO: uncomment the parts of this test that will be impacted by the config/prototype changes in flight @pytest.mark.parametrize("store", ["local"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) def test_serializable_sync_array(store: LocalStore, zarr_format: ZarrFormat) -> None: expected = zarr.create_array( store=store, shape=(100,), chunks=(10,), zarr_format=zarr_format, dtype="i4" ) expected[:] = list(range(100)) p = pickle.dumps(expected) actual = pickle.loads(p) assert actual == expected np.testing.assert_array_equal(actual[:], expected[:]) @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: """ Test that providing an actual storage transformer produces a warning and otherwise passes through """ metadata_dict: dict[str, JSON] if zarr_format == 3: metadata_dict = { "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(),), "fill_value": 0, "storage_transformers": ({"test": "should_raise"}), } else: metadata_dict = { "zarr_format": zarr_format, "shape": (10,), "chunks": (1,), "dtype": "|u1", "dimension_separator": ".", "codecs": (BytesCodec().to_dict(),), "fill_value": 0, "order": "C", "storage_transformers": ({"test": "should_raise"}), } 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) @pytest.mark.parametrize("test_cls", [AnyArray, AnyAsyncArray]) @pytest.mark.parametrize("nchunks", [2, 5, 10]) def test_nchunks(test_cls: type[AnyArray] | type[AnyAsyncArray], nchunks: int) -> None: """ Test that nchunks returns the number of chunks defined for the array. """ store = MemoryStore() shape = 100 arr = zarr.create_array(store, shape=(shape,), chunks=(ceildiv(shape, nchunks),), dtype="i4") expected = nchunks if test_cls == Array: observed = arr.nchunks else: observed = arr.async_array.nchunks assert observed == expected @pytest.mark.parametrize("test_cls", [Array, AsyncArray]) @pytest.mark.parametrize( ("shape", "shard_shape", "chunk_shape"), [((10,), None, (1,)), ((10,), (1,), (1,)), ((40,), (20,), (5,))], ) async def test_nchunks_initialized( test_cls: type[AnyArray] | type[AnyAsyncArray], shape: tuple[int, ...], shard_shape: tuple[int, ...] | None, chunk_shape: tuple[int, ...], ) -> None: """ Test that nchunks_initialized accurately returns the number of stored partitions. """ store = MemoryStore() if shard_shape is None: chunks_per_shard = 1 else: chunks_per_shard = np.prod(np.array(shard_shape) // np.array(chunk_shape)) arr = zarr.create_array(store, shape=shape, shards=shard_shape, chunks=chunk_shape, dtype="i1") # write chunks one at a time for idx, region in enumerate(arr._iter_shard_regions()): arr[region] = 1 expected = idx + 1 if test_cls == Array: observed = arr._nshards_initialized assert observed == arr.nchunks_initialized // chunks_per_shard else: observed = await arr.async_array._nshards_initialized() assert observed == await arr.async_array.nchunks_initialized() // chunks_per_shard assert observed == expected # delete chunks for idx, key in enumerate(arr._iter_shard_keys()): sync(arr.store_path.store.delete(key)) if test_cls == Array: observed = arr._nshards_initialized assert observed == arr.nchunks_initialized // chunks_per_shard else: observed = await arr.async_array._nshards_initialized() assert observed == await arr.async_array.nchunks_initialized() // chunks_per_shard expected = arr._nshards - idx - 1 assert observed == expected @pytest.mark.parametrize("path", ["", "foo"]) @pytest.mark.parametrize( ("shape", "shard_shape", "chunk_shape"), [((10,), None, (1,)), ((10,), (1,), (1,)), ((40,), (20,), (5,))], ) async def test_chunks_initialized( path: str, shape: tuple[int, ...], shard_shape: tuple[int, ...], chunk_shape: tuple[int, ...] ) -> None: """ Test that chunks_initialized accurately returns the keys of stored chunks. """ store = MemoryStore() arr = zarr.create_array( store, name=path, shape=shape, shards=shard_shape, chunks=chunk_shape, dtype="i1" ) chunks_accumulated = tuple( accumulate(tuple(tuple(v.split(" ")) for v in arr._iter_shard_keys())) ) for keys, region in zip(chunks_accumulated, arr._iter_shard_regions(), strict=False): arr[region] = 1 observed = sorted(await _shards_initialized(arr.async_array)) expected = sorted(keys) assert observed == expected def test_nbytes_stored() -> None: arr = zarr.create(shape=(100,), chunks=(10,), dtype="i4", codecs=[BytesCodec()]) result = arr.nbytes_stored() assert result == 502 # the size of the metadata document. This is a fragile test. arr[:50] = 1 result = arr.nbytes_stored() assert result == 702 # the size with 5 chunks filled. arr[50:] = 2 result = arr.nbytes_stored() assert result == 902 # the size with all chunks filled. async def test_nbytes_stored_async() -> None: arr = await zarr.api.asynchronous.create( shape=(100,), chunks=(10,), dtype="i4", codecs=[BytesCodec()] ) result = await arr.nbytes_stored() assert result == 502 # the size of the metadata document. This is a fragile test. await arr.setitem(slice(50), 1) result = await arr.nbytes_stored() assert result == 702 # the size with 5 chunks filled. await arr.setitem(slice(50, 100), 2) result = await arr.nbytes_stored() assert result == 902 # the size with all chunks filled. @pytest.mark.parametrize("zarr_format", [2, 3]) def test_update_attrs(zarr_format: ZarrFormat) -> None: # regression test for https://github.com/zarr-developers/zarr-python/issues/2328 store = MemoryStore() arr = zarr.create_array( store=store, shape=(5,), chunks=(5,), dtype="f8", zarr_format=zarr_format ) arr.attrs["foo"] = "bar" assert arr.attrs["foo"] == "bar" arr2 = zarr.open_array(store=store, zarr_format=zarr_format) assert arr2.attrs["foo"] == "bar" @pytest.mark.parametrize(("chunks", "shards"), [((2, 2), None), ((2, 2), (4, 4))]) class TestInfo: def test_info_v2(self, chunks: tuple[int, int], shards: tuple[int, int] | None) -> None: arr = zarr.create_array(store={}, shape=(8, 8), dtype="f8", chunks=chunks, zarr_format=2) result = arr.info expected = ArrayInfo( _zarr_format=2, _data_type=arr.async_array._zdtype, _fill_value=arr.fill_value, _shape=(8, 8), _chunk_shape=chunks, _shard_shape=None, _order="C", _read_only=False, _store_type="MemoryStore", _count_bytes=512, _compressors=(numcodecs.Zstd(),), ) assert result == expected def test_info_v3(self, chunks: tuple[int, int], shards: tuple[int, int] | None) -> None: arr = zarr.create_array(store={}, shape=(8, 8), dtype="f8", chunks=chunks, shards=shards) result = arr.info expected = ArrayInfo( _zarr_format=3, _data_type=arr.async_array._zdtype, _fill_value=arr.fill_value, _shape=(8, 8), _chunk_shape=chunks, _shard_shape=shards, _order="C", _read_only=False, _store_type="MemoryStore", _compressors=(ZstdCodec(),), _serializer=BytesCodec(), _count_bytes=512, ) assert result == expected def test_info_complete(self, chunks: tuple[int, int], shards: tuple[int, int] | None) -> None: arr = zarr.create_array( store={}, shape=(8, 8), dtype="f8", chunks=chunks, shards=shards, compressors=(), ) result = arr.info_complete() expected = ArrayInfo( _zarr_format=3, _data_type=arr.async_array._zdtype, _fill_value=arr.fill_value, _shape=(8, 8), _chunk_shape=chunks, _shard_shape=shards, _order="C", _read_only=False, _store_type="MemoryStore", _serializer=BytesCodec(), _count_bytes=512, _count_chunks_initialized=0, _count_bytes_stored=521 if shards is None else 982, # the metadata? ) assert result == expected arr[:4, :4] = 10 result = arr.info_complete() if shards is None: expected = dataclasses.replace( expected, _count_chunks_initialized=4, _count_bytes_stored=649 ) else: expected = dataclasses.replace( expected, _count_chunks_initialized=1, _count_bytes_stored=1178 ) assert result == expected async def test_info_v2_async( self, chunks: tuple[int, int], shards: tuple[int, int] | None ) -> None: arr = await zarr.api.asynchronous.create_array( store={}, shape=(8, 8), dtype="f8", chunks=chunks, zarr_format=2 ) result = arr.info expected = ArrayInfo( _zarr_format=2, _data_type=Float64(), _fill_value=arr.metadata.fill_value, _shape=(8, 8), _chunk_shape=(2, 2), _shard_shape=None, _order="C", _read_only=False, _store_type="MemoryStore", _count_bytes=512, _compressors=(numcodecs.Zstd(),), ) assert result == expected async def test_info_v3_async( self, chunks: tuple[int, int], shards: tuple[int, int] | None ) -> None: arr = await zarr.api.asynchronous.create_array( store={}, shape=(8, 8), dtype="f8", chunks=chunks, shards=shards, ) result = arr.info expected = ArrayInfo( _zarr_format=3, _data_type=arr._zdtype, _fill_value=arr.metadata.fill_value, _shape=(8, 8), _chunk_shape=chunks, _shard_shape=shards, _order="C", _read_only=False, _store_type="MemoryStore", _compressors=(ZstdCodec(),), _serializer=BytesCodec(), _count_bytes=512, ) assert result == expected async def test_info_complete_async( self, chunks: tuple[int, int], shards: tuple[int, int] | None ) -> None: arr = await zarr.api.asynchronous.create_array( store={}, dtype="f8", shape=(8, 8), chunks=chunks, shards=shards, compressors=None, ) result = await arr.info_complete() expected = ArrayInfo( _zarr_format=3, _data_type=arr._zdtype, _fill_value=arr.metadata.fill_value, _shape=(8, 8), _chunk_shape=chunks, _shard_shape=shards, _order="C", _read_only=False, _store_type="MemoryStore", _serializer=BytesCodec(), _count_bytes=512, _count_chunks_initialized=0, _count_bytes_stored=521 if shards is None else 982, # the metadata? ) assert result == expected await arr.setitem((slice(4), slice(4)), 10) result = await arr.info_complete() if shards is None: expected = dataclasses.replace( expected, _count_chunks_initialized=4, _count_bytes_stored=553 ) else: expected = dataclasses.replace( expected, _count_chunks_initialized=1, _count_bytes_stored=1178 ) @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_resize_1d(store: MemoryStore, zarr_format: ZarrFormat) -> None: z = zarr.create( shape=105, chunks=10, dtype="i4", fill_value=0, store=store, zarr_format=zarr_format ) a = np.arange(105, dtype="i4") z[:] = a result = z[:] assert isinstance(result, NDArrayLike) assert (105,) == z.shape assert (105,) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10,) == z.chunks np.testing.assert_array_equal(a, result) z.resize(205) result = z[:] assert isinstance(result, NDArrayLike) assert (205,) == z.shape assert (205,) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10,) == z.chunks np.testing.assert_array_equal(a, z[:105]) np.testing.assert_array_equal(np.zeros(100, dtype="i4"), z[105:]) z.resize(55) result = z[:] assert isinstance(result, NDArrayLike) assert (55,) == z.shape assert (55,) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10,) == z.chunks np.testing.assert_array_equal(a[:55], result) # via shape setter new_shape = (105,) z.shape = new_shape result = z[:] assert isinstance(result, NDArrayLike) assert new_shape == z.shape assert new_shape == result.shape @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_resize_2d(store: MemoryStore, zarr_format: ZarrFormat) -> None: z = zarr.create( shape=(105, 105), chunks=(10, 10), dtype="i4", fill_value=0, store=store, zarr_format=zarr_format, ) a = np.arange(105 * 105, dtype="i4").reshape((105, 105)) z[:] = a result = z[:] assert isinstance(result, NDArrayLike) assert (105, 105) == z.shape assert (105, 105) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10, 10) == z.chunks np.testing.assert_array_equal(a, result) z.resize((205, 205)) result = z[:] assert isinstance(result, NDArrayLike) assert (205, 205) == z.shape assert (205, 205) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10, 10) == z.chunks np.testing.assert_array_equal(a, z[:105, :105]) np.testing.assert_array_equal(np.zeros((100, 205), dtype="i4"), z[105:, :]) np.testing.assert_array_equal(np.zeros((205, 100), dtype="i4"), z[:, 105:]) z.resize((55, 55)) result = z[:] assert isinstance(result, NDArrayLike) assert (55, 55) == z.shape assert (55, 55) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10, 10) == z.chunks np.testing.assert_array_equal(a[:55, :55], result) z.resize((55, 1)) result = z[:] assert isinstance(result, NDArrayLike) assert (55, 1) == z.shape assert (55, 1) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10, 10) == z.chunks np.testing.assert_array_equal(a[:55, :1], result) z.resize((1, 55)) result = z[:] assert isinstance(result, NDArrayLike) assert (1, 55) == z.shape assert (1, 55) == result.shape assert np.dtype("i4") == z.dtype assert np.dtype("i4") == result.dtype assert (10, 10) == z.chunks np.testing.assert_array_equal(a[:1, :10], z[:, :10]) np.testing.assert_array_equal(np.zeros((1, 55 - 10), dtype="i4"), z[:, 10:55]) # via shape setter new_shape = (105, 105) z.shape = new_shape result = z[:] assert isinstance(result, NDArrayLike) assert new_shape == z.shape assert new_shape == result.shape @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_resize_growing_skips_chunk_enumeration( store: MemoryStore, zarr_format: ZarrFormat ) -> None: """Growing an array should not enumerate chunk coords for deletion (#3650 mitigation).""" z = zarr.create( shape=(10, 10), chunks=(5, 5), dtype="i4", fill_value=0, store=store, zarr_format=zarr_format, ) z[:] = np.ones((10, 10), dtype="i4") grid_cls = type(z._chunk_grid) # growth only - ensure no chunk coords are enumerated with mock.patch.object( grid_cls, "all_chunk_coords", wraps=z._chunk_grid.all_chunk_coords, ) as mock_coords: z.resize((20, 20)) mock_coords.assert_not_called() assert z.shape == (20, 20) np.testing.assert_array_equal(np.ones((10, 10), dtype="i4"), z[:10, :10]) np.testing.assert_array_equal(np.zeros((10, 10), dtype="i4"), z[10:, 10:]) # shrink - ensure no regression of behaviour with mock.patch.object( grid_cls, "all_chunk_coords", wraps=z._chunk_grid.all_chunk_coords, ) as mock_coords: z.resize((5, 5)) assert mock_coords.call_count > 0 assert z.shape == (5, 5) np.testing.assert_array_equal(np.ones((5, 5), dtype="i4"), z[:]) # mixed: grow dim 0, shrink dim 1 - ensure deletion path runs z2 = zarr.create( shape=(10, 10), chunks=(5, 5), dtype="i4", fill_value=0, store=store, zarr_format=zarr_format, overwrite=True, ) z2[:] = np.ones((10, 10), dtype="i4") with mock.patch.object( grid_cls, "all_chunk_coords", wraps=z2._chunk_grid.all_chunk_coords, ) as mock_coords: z2.resize((20, 5)) assert mock_coords.call_count > 0 assert z2.shape == (20, 5) np.testing.assert_array_equal(np.ones((10, 5), dtype="i4"), z2[:10, :]) np.testing.assert_array_equal(np.zeros((10, 5), dtype="i4"), z2[10:, :]) @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_append_1d(store: MemoryStore, zarr_format: ZarrFormat) -> None: a = np.arange(105) z = zarr.create(shape=a.shape, chunks=10, dtype=a.dtype, store=store, zarr_format=zarr_format) z[:] = a assert a.shape == z.shape assert a.dtype == z.dtype assert (10,) == z.chunks np.testing.assert_array_equal(a, z[:]) b = np.arange(105, 205) e = np.append(a, b) assert z.shape == (105,) z.append(b) assert e.shape == z.shape assert e.dtype == z.dtype assert (10,) == z.chunks np.testing.assert_array_equal(e, z[:]) # check append handles array-like c = [1, 2, 3] f = np.append(e, c) z.append(c) assert f.shape == z.shape assert f.dtype == z.dtype assert (10,) == z.chunks np.testing.assert_array_equal(f, z[:]) @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_append_2d(store: MemoryStore, zarr_format: ZarrFormat) -> None: a = np.arange(105 * 105, dtype="i4").reshape((105, 105)) z = zarr.create( shape=a.shape, chunks=(10, 10), dtype=a.dtype, store=store, zarr_format=zarr_format ) z[:] = a assert a.shape == z.shape assert a.dtype == z.dtype assert (10, 10) == z.chunks actual = z[:] np.testing.assert_array_equal(a, actual) b = np.arange(105 * 105, 2 * 105 * 105, dtype="i4").reshape((105, 105)) e = np.append(a, b, axis=0) z.append(b) assert e.shape == z.shape assert e.dtype == z.dtype assert (10, 10) == z.chunks actual = z[:] np.testing.assert_array_equal(e, actual) @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_append_2d_axis(store: MemoryStore, zarr_format: ZarrFormat) -> None: a = np.arange(105 * 105, dtype="i4").reshape((105, 105)) z = zarr.create( shape=a.shape, chunks=(10, 10), dtype=a.dtype, store=store, zarr_format=zarr_format ) z[:] = a assert a.shape == z.shape assert a.dtype == z.dtype assert (10, 10) == z.chunks np.testing.assert_array_equal(a, z[:]) b = np.arange(105 * 105, 2 * 105 * 105, dtype="i4").reshape((105, 105)) e = np.append(a, b, axis=1) z.append(b, axis=1) assert e.shape == z.shape assert e.dtype == z.dtype assert (10, 10) == z.chunks np.testing.assert_array_equal(e, z[:]) @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_append_bad_shape(store: MemoryStore, zarr_format: ZarrFormat) -> None: a = np.arange(100) z = zarr.create(shape=a.shape, chunks=10, dtype=a.dtype, store=store, zarr_format=zarr_format) z[:] = a b = a.reshape(10, 10) with pytest.raises(ValueError): z.append(b) @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("write_empty_chunks", [True, False]) @pytest.mark.parametrize("fill_value", [0, 5]) def test_write_empty_chunks_behavior( zarr_format: ZarrFormat, store: MemoryStore, write_empty_chunks: bool, fill_value: int ) -> None: """ Check that the write_empty_chunks value of the config is applied correctly. We expect that when write_empty_chunks is True, writing chunks equal to the fill value will result in those chunks appearing in the store. When write_empty_chunks is False, writing chunks that are equal to the fill value will result in those chunks not being present in the store. In particular, they should be deleted if they were already present. """ arr = zarr.create_array( store=store, shape=(2,), zarr_format=zarr_format, dtype="i4", fill_value=fill_value, chunks=(1,), config={"write_empty_chunks": write_empty_chunks}, ) assert arr.async_array.config.write_empty_chunks == write_empty_chunks # initialize the store with some non-fill value chunks arr[:] = fill_value + 1 assert arr._nshards_initialized == arr._nshards arr[:] = fill_value if not write_empty_chunks: assert arr._nshards_initialized == 0 else: assert arr._nshards_initialized == arr._nshards @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("fill_value", [0.0, -0.0]) @pytest.mark.parametrize("dtype", ["f4", "f2"]) def test_write_empty_chunks_negative_zero( zarr_format: ZarrFormat, store: MemoryStore, fill_value: float, dtype: str ) -> None: # regression test for https://github.com/zarr-developers/zarr-python/issues/3144 arr = zarr.create_array( store=store, shape=(2,), zarr_format=zarr_format, dtype=dtype, fill_value=fill_value, chunks=(1,), config={"write_empty_chunks": False}, ) assert arr.nchunks_initialized == 0 # initialize the with the negated fill value (-0.0 for +0.0, +0.0 for -0.0) arr[:] = -fill_value assert arr.nchunks_initialized == arr.nchunks @pytest.mark.parametrize( ("fill_value", "expected"), [ (np.nan * 1j, ["NaN", "NaN"]), (np.nan, ["NaN", 0.0]), (np.inf, ["Infinity", 0.0]), (np.inf * 1j, ["NaN", "Infinity"]), (-np.inf, ["-Infinity", 0.0]), (math.inf, ["Infinity", 0.0]), ], ) async def test_special_complex_fill_values_roundtrip(fill_value: Any, expected: list[Any]) -> None: store = MemoryStore() zarr.create_array(store=store, shape=(1,), dtype=np.complex64, fill_value=fill_value) content = await store.get("zarr.json", prototype=default_buffer_prototype()) assert content is not None actual = json.loads(content.to_bytes()) assert actual["fill_value"] == expected @pytest.mark.parametrize("shape", [(1,), (2, 3), (4, 5, 6)]) @pytest.mark.parametrize("dtype", ["uint8", "float32"]) @pytest.mark.parametrize("array_type", ["async", "sync"]) async def test_nbytes( shape: tuple[int, ...], dtype: str, array_type: Literal["async", "sync"] ) -> None: """ Test that the ``nbytes`` attribute of an Array or AsyncArray correctly reports the capacity of the chunks of that array. """ store = MemoryStore() arr = zarr.create_array(store=store, shape=shape, dtype=dtype, fill_value=0) if array_type == "async": assert arr.async_array.nbytes == np.prod(arr.shape) * arr.dtype.itemsize else: assert arr.nbytes == np.prod(arr.shape) * arr.dtype.itemsize @pytest.mark.parametrize( ("array_shape", "chunk_shape", "target_shard_size_bytes", "expected_shards"), [ pytest.param( (256, 256), (32, 32), 129 * 129, (128, 128), id="2d_chunking_max_byes_does_not_evenly_divide", ), pytest.param( (256, 256), (32, 32), 64 * 64, (64, 64), id="2d_chunking_max_byes_evenly_divides" ), pytest.param( (256, 256), (64, 32), 128 * 128, (128, 64), id="2d_non_square_chunking_max_byes_evenly_divides", ), pytest.param((256,), (2,), 255, (254,), id="max_bytes_just_below_array_shape"), pytest.param((256,), (2,), 256, (256,), id="max_bytes_equal_to_array_shape"), pytest.param((256,), (2,), 16, (16,), id="max_bytes_normal_val"), pytest.param((256,), (2,), 2, (2,), id="max_bytes_same_as_chunk"), pytest.param((256,), (2,), 1, (2,), id="max_bytes_less_than_chunk"), pytest.param((256,), (2,), None, (4,), id="use_default_auto_setting"), pytest.param((4,), (2,), None, (2,), id="small_array_shape_does_not_shard"), ], ) def test_auto_partition_auto_shards( array_shape: tuple[int, ...], chunk_shape: tuple[int, ...], target_shard_size_bytes: int | None, expected_shards: tuple[int, ...], ) -> None: """ Test that automatically picking a shard size returns a tuple of 2 * the chunk shape for any axis where there are 8 or more chunks. """ dtype = np.dtype("uint8") with pytest.warns( ZarrUserWarning, match="Automatic shard shape inference is experimental and may change without notice.", ): with zarr.config.set({"array.target_shard_size_bytes": target_shard_size_bytes}): auto_shards, _ = _auto_partition( array_shape=array_shape, chunk_shape=chunk_shape, shard_shape="auto", item_size=dtype.itemsize, ) assert auto_shards == expected_shards def test_auto_partition_auto_shards_with_auto_chunks_should_be_close_to_1MiB() -> None: """ Test that automatically picking a shard size and a chunk size gives roughly 1MiB chunks. """ with pytest.warns( ZarrUserWarning, match="Automatic shard shape inference is experimental and may change without notice.", ): with zarr.config.set({"array.target_shard_size_bytes": 10_000_000}): _, chunk_shape = _auto_partition( array_shape=(10_000_000,), chunk_shape="auto", shard_shape="auto", item_size=1, ) assert chunk_shape == (625000,) def test_chunks_and_shards() -> None: store = StorePath(MemoryStore()) shape = (100, 100) chunks = (5, 5) shards = (10, 10) arr_v3 = zarr.create_array(store=store / "v3", shape=shape, chunks=chunks, dtype="i4") assert arr_v3.chunks == chunks assert arr_v3.shards is None arr_v3_sharding = zarr.create_array( store=store / "v3_sharding", shape=shape, chunks=chunks, shards=shards, dtype="i4", ) assert arr_v3_sharding.chunks == chunks assert arr_v3_sharding.shards == shards arr_v2 = zarr.create_array( store=store / "v2", shape=shape, chunks=chunks, zarr_format=2, dtype="i4" ) assert arr_v2.chunks == chunks assert arr_v2.shards is None @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize( ("dtype", "fill_value_expected"), [(" None: a = zarr.create_array(store, shape=(5,), chunks=(5,), dtype=dtype) assert a.fill_value == fill_value_expected @pytest.mark.parametrize("store", ["memory"], indirect=True) class TestCreateArray: @staticmethod def test_chunks_and_shards(store: Store) -> None: spath = StorePath(store) shape = (100, 100) chunks = (5, 5) shards = (10, 10) arr_v3 = zarr.create_array(store=spath / "v3", shape=shape, chunks=chunks, dtype="i4") assert arr_v3.chunks == chunks assert arr_v3.shards is None arr_v3_sharding = zarr.create_array( store=spath / "v3_sharding", shape=shape, chunks=chunks, shards=shards, dtype="i4", ) assert arr_v3_sharding.chunks == chunks assert arr_v3_sharding.shards == shards arr_v2 = zarr.create_array( store=spath / "v2", shape=shape, chunks=chunks, zarr_format=2, dtype="i4" ) assert arr_v2.chunks == chunks assert arr_v2.shards is None @staticmethod @pytest.mark.parametrize("dtype", zdtype_examples) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_default_fill_value(dtype: ZDType[Any, Any], store: Store) -> None: """ Test that the fill value of an array is set to the default value for the dtype object """ a = zarr.create_array(store, shape=(5,), chunks=(5,), dtype=dtype) if isinstance(dtype, DateTime64 | TimeDelta64) and np.isnat(a.fill_value): assert np.isnat(dtype.default_scalar()) else: assert a.fill_value == dtype.default_scalar() @staticmethod # @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.parametrize("dtype", zdtype_examples) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_default_fill_value_None( dtype: ZDType[Any, Any], store: Store, zarr_format: ZarrFormat ) -> None: """ Test that the fill value of an array is set to the default value for an explicit None argument for Zarr Format 3, and to null for Zarr Format 2 """ a = zarr.create_array( store, shape=(5,), chunks=(5,), dtype=dtype, fill_value=None, zarr_format=zarr_format ) if zarr_format == 3: if isinstance(dtype, DateTime64 | TimeDelta64) and np.isnat(a.fill_value): assert np.isnat(dtype.default_scalar()) else: assert a.fill_value == dtype.default_scalar() elif zarr_format == 2: assert a.fill_value is None @staticmethod @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("dtype", zdtype_examples) def test_dtype_forms(dtype: ZDType[Any, Any], store: Store, zarr_format: ZarrFormat) -> None: """ Test that the same array is produced from a ZDType instance, a numpy dtype, or a numpy string """ skip_object_dtype(dtype) a = zarr.create_array( store, name="a", shape=(5,), chunks=(5,), dtype=dtype, zarr_format=zarr_format ) b = zarr.create_array( store, name="b", shape=(5,), chunks=(5,), dtype=dtype.to_native_dtype(), zarr_format=zarr_format, ) assert a.dtype == b.dtype # Structured dtypes do not have a numpy string representation that uniquely identifies them if not isinstance(dtype, Structured): if isinstance(dtype, VariableLengthUTF8): # in numpy 2.3, StringDType().str becomes the string 'StringDType()' which numpy # does not accept as a string representation of the dtype. c = zarr.create_array( store, name="c", shape=(5,), chunks=(5,), dtype=dtype.to_native_dtype().char, zarr_format=zarr_format, ) else: c = zarr.create_array( store, name="c", shape=(5,), chunks=(5,), dtype=dtype.to_native_dtype().str, zarr_format=zarr_format, ) assert a.dtype == c.dtype @staticmethod @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("dtype", zdtype_examples) def test_dtype_roundtrip( dtype: ZDType[Any, Any], store: Store, zarr_format: ZarrFormat ) -> None: """ Test that creating an array, then opening it, gets the same array. """ skip_object_dtype(dtype) a = zarr.create_array(store, shape=(5,), chunks=(5,), dtype=dtype, zarr_format=zarr_format) b = zarr.open_array(store) assert a.dtype == b.dtype @staticmethod @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("dtype", ["uint8", "float32", "U3", "S4", "V1"]) @pytest.mark.parametrize( "compressors", [ "auto", None, (), (ZstdCodec(level=3),), (ZstdCodec(level=3), GzipCodec(level=0)), ZstdCodec(level=3), {"name": "zstd", "configuration": {"level": 3}}, ({"name": "zstd", "configuration": {"level": 3}},), ], ) @pytest.mark.parametrize( "filters", [ "auto", None, (), ( TransposeCodec( order=[ 0, ] ), ), ( TransposeCodec( order=[ 0, ] ), TransposeCodec( order=[ 0, ] ), ), TransposeCodec( order=[ 0, ] ), {"name": "transpose", "configuration": {"order": [0]}}, ({"name": "transpose", "configuration": {"order": [0]}},), ], ) @pytest.mark.parametrize(("chunks", "shards"), [((6,), None), ((3,), (6,))]) async def test_v3_chunk_encoding( store: MemoryStore, compressors: CompressorsLike, filters: FiltersLike, dtype: str, chunks: tuple[int, ...], shards: tuple[int, ...] | None, ) -> None: """ Test various possibilities for the compressors and filters parameter to create_array """ arr = await create_array( store=store, dtype=dtype, shape=(12,), chunks=chunks, shards=shards, zarr_format=3, filters=filters, compressors=compressors, ) filters_expected, _, compressors_expected = _parse_chunk_encoding_v3( filters=filters, compressors=compressors, serializer="auto", dtype=arr._zdtype, ) assert arr.filters == filters_expected assert arr.compressors == compressors_expected @staticmethod @pytest.mark.parametrize("name", ["v2", "default", "invalid"]) @pytest.mark.parametrize("separator", [".", "/"]) async def test_chunk_key_encoding( name: str, separator: Literal[".", "/"], zarr_format: ZarrFormat, store: MemoryStore ) -> None: chunk_key_encoding = ChunkKeyEncodingParams(name=name, separator=separator) # type: ignore[typeddict-item] error_msg = "" if name == "invalid": error_msg = r'Unknown chunk key encoding: "Chunk key encoding \'invalid\' not found in registered chunk key encodings: \[.*\]."' if zarr_format == 2 and name == "default": error_msg = "Invalid chunk key encoding. For Zarr format 2 arrays, the `name` field of the chunk key encoding must be 'v2'." if error_msg: with pytest.raises(ValueError, match=error_msg): arr = await create_array( store=store, dtype="uint8", shape=(10,), chunks=(1,), zarr_format=zarr_format, chunk_key_encoding=chunk_key_encoding, ) else: arr = await create_array( store=store, dtype="uint8", shape=(10,), chunks=(1,), zarr_format=zarr_format, chunk_key_encoding=chunk_key_encoding, ) if isinstance(arr.metadata, ArrayV2Metadata): assert arr.metadata.dimension_separator == separator @staticmethod @pytest.mark.parametrize( ("kwargs", "error_msg"), [ ({"serializer": "bytes"}, "Zarr format 2 arrays do not support `serializer`."), ({"dimension_names": ["test"]}, "Zarr format 2 arrays do not support dimension names."), ], ) async def test_create_array_invalid_v2_arguments( kwargs: dict[str, Any], error_msg: str, store: MemoryStore ) -> None: with pytest.raises(ValueError, match=re.escape(error_msg)): await zarr.api.asynchronous.create_array( store=store, dtype="uint8", shape=(10,), chunks=(1,), zarr_format=2, **kwargs ) @staticmethod @pytest.mark.parametrize( ("kwargs", "error_msg"), [ ( {"dimension_names": ["test"]}, "dimension_names cannot be used for arrays with zarr_format 2.", ), ( {"chunk_key_encoding": {"name": "default", "separator": "/"}}, "chunk_key_encoding cannot be used for arrays with zarr_format 2. Use dimension_separator instead.", ), ( {"codecs": "bytes"}, "codecs cannot be used for arrays with zarr_format 2. Use filters and compressor instead.", ), ], ) async def test_create_invalid_v2_arguments( kwargs: dict[str, Any], error_msg: str, store: MemoryStore ) -> None: with pytest.raises(ValueError, match=re.escape(error_msg)): await zarr.api.asynchronous.create( store=store, dtype="uint8", shape=(10,), chunks=(1,), zarr_format=2, **kwargs ) @staticmethod @pytest.mark.parametrize( ("kwargs", "error_msg"), [ ( {"chunk_shape": (1,), "chunks": (2,)}, "Only one of chunk_shape or chunks can be provided.", ), ( {"dimension_separator": "/"}, "dimension_separator cannot be used for arrays with zarr_format 3. Use chunk_key_encoding instead.", ), ( {"filters": []}, "filters cannot be used for arrays with zarr_format 3. Use array-to-array codecs instead", ), ( {"compressor": "blosc"}, "compressor cannot be used for arrays with zarr_format 3. Use bytes-to-bytes codecs instead", ), ], ) async def test_invalid_v3_arguments( kwargs: dict[str, Any], error_msg: str, store: MemoryStore ) -> None: kwargs.setdefault("chunks", (1,)) with pytest.raises(ValueError, match=re.escape(error_msg)): zarr.create(store=store, dtype="uint8", shape=(10,), zarr_format=3, **kwargs) @staticmethod @pytest.mark.parametrize("dtype", ["uint8", "float32", "str", "U10", "S10", ">M8[10s]"]) @pytest.mark.parametrize( "compressors", [ "auto", None, numcodecs.Zstd(level=3), (), (numcodecs.Zstd(level=3),), ], ) @pytest.mark.parametrize( "filters", ["auto", None, numcodecs.GZip(level=1), (numcodecs.GZip(level=1),)] ) async def test_v2_chunk_encoding( store: MemoryStore, compressors: CompressorsLike, filters: FiltersLike, dtype: str ) -> None: if dtype == "str" and filters != "auto": pytest.skip("Only the auto filters are compatible with str dtype in this test.") arr: AsyncArray[ArrayV2Metadata] = await create_array( store=store, dtype=dtype, shape=(10,), zarr_format=2, compressors=compressors, filters=filters, ) filters_expected, compressor_expected = _parse_chunk_encoding_v2( filters=filters, compressor=compressors, dtype=parse_dtype(dtype, zarr_format=2) ) assert arr.metadata.zarr_format == 2 # guard for mypy assert arr.metadata.compressor == compressor_expected assert arr.metadata.filters == filters_expected # Normalize for property getters arr_compressors_expected = () if compressor_expected is None else (compressor_expected,) arr_filters_expected = () if filters_expected is None else filters_expected assert arr.compressors == arr_compressors_expected assert arr.filters == arr_filters_expected @staticmethod @pytest.mark.parametrize("dtype", [UInt8(), Float32(), VariableLengthUTF8()]) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") async def test_default_filters_compressors( store: MemoryStore, dtype: UInt8 | Float32 | VariableLengthUTF8, zarr_format: ZarrFormat ) -> None: """ Test that the default ``filters`` and ``compressors`` are used when ``create_array`` is invoked with ``filters`` and ``compressors`` unspecified. """ arr = await create_array( store=store, dtype=dtype, # type: ignore[arg-type] shape=(10,), zarr_format=zarr_format, ) sig = inspect.signature(create_array) if zarr_format == 3: expected_filters, expected_serializer, expected_compressors = _parse_chunk_encoding_v3( compressors=sig.parameters["compressors"].default, filters=sig.parameters["filters"].default, serializer=sig.parameters["serializer"].default, dtype=dtype, # type: ignore[arg-type] ) elif zarr_format == 2: default_filters, default_compressors = _parse_chunk_encoding_v2( compressor=sig.parameters["compressors"].default, filters=sig.parameters["filters"].default, dtype=dtype, # type: ignore[arg-type] ) if default_filters is None: expected_filters = () else: expected_filters = default_filters # type: ignore[assignment] if default_compressors is None: expected_compressors = () else: expected_compressors = (default_compressors,) # type: ignore[assignment] expected_serializer = None else: raise ValueError(f"Invalid zarr_format: {zarr_format}") assert arr.filters == expected_filters assert arr.serializer == expected_serializer assert arr.compressors == expected_compressors @staticmethod async def test_v2_no_shards(store: Store) -> None: """ Test that creating a Zarr v2 array with ``shard_shape`` set to a non-None value raises an error. """ msg = re.escape( "Zarr format 2 arrays can only be created with `shard_shape` set to `None`. Got `shard_shape=(5,)` instead." ) with pytest.raises(ValueError, match=msg): _ = await create_array( store=store, dtype="uint8", shape=(10,), shards=(5,), zarr_format=2, ) @staticmethod @pytest.mark.parametrize("impl", ["sync", "async"]) async def test_with_data(impl: Literal["sync", "async"], store: Store) -> None: """ Test that we can invoke ``create_array`` with a ``data`` parameter. """ data = np.arange(10) name = "foo" arr: AnyAsyncArray | AnyArray if impl == "sync": arr = sync_api.create_array(store, name=name, data=data) stored = arr[:] elif impl == "async": arr = await create_array(store, name=name, data=data, zarr_format=3) stored = await arr._get_selection( BasicIndexer(..., shape=arr.shape, chunk_grid=arr._chunk_grid), prototype=default_buffer_prototype(), ) else: raise ValueError(f"Invalid impl: {impl}") assert np.array_equal(stored, data) @staticmethod async def test_with_data_invalid_params(store: Store) -> None: """ Test that failing to specify data AND shape / dtype results in a ValueError """ with pytest.raises(ValueError, match="shape was not specified"): await create_array(store, data=None, shape=None, dtype=None) # we catch shape=None first, so specifying a dtype should raise the same exception as before with pytest.raises(ValueError, match="shape was not specified"): await create_array(store, data=None, shape=None, dtype="uint8") with pytest.raises(ValueError, match="dtype was not specified"): await create_array(store, data=None, shape=(10, 10)) @staticmethod async def test_data_ignored_params(store: Store) -> None: """ Test that specifying data AND shape AND dtype results in a ValueError """ data = np.arange(10) with pytest.raises( ValueError, match="The data parameter was used, but the shape parameter was also used." ): await create_array(store, data=data, shape=data.shape, dtype=None, overwrite=True) # we catch shape first, so specifying a dtype should raise the same warning as before with pytest.raises( ValueError, match="The data parameter was used, but the shape parameter was also used." ): await create_array(store, data=data, shape=data.shape, dtype=data.dtype, overwrite=True) with pytest.raises( ValueError, match="The data parameter was used, but the dtype parameter was also used." ): await create_array(store, data=data, shape=None, dtype=data.dtype, overwrite=True) @staticmethod @pytest.mark.parametrize("write_empty_chunks", [True, False]) async def test_write_empty_chunks_config(write_empty_chunks: bool, store: Store) -> None: """ Test that the value of write_empty_chunks is sensitive to the global config when not set explicitly """ with zarr.config.set({"array.write_empty_chunks": write_empty_chunks}): arr = await create_array(store, shape=(2, 2), dtype="i4") assert arr.config.write_empty_chunks == write_empty_chunks @staticmethod @pytest.mark.parametrize("path", [None, "", "/", "/foo", "foo", "foo/bar"]) async def test_name(store: Store, zarr_format: ZarrFormat, path: str | None) -> None: arr = await create_array( store, shape=(2, 2), dtype="i4", name=path, zarr_format=zarr_format ) if path is None: expected_path = "" elif path.startswith("/"): expected_path = path.lstrip("/") else: expected_path = path assert arr.path == expected_path assert arr.name == f"/{expected_path}" # test that implicit groups were created path_parts = expected_path.split("/") if len(path_parts) > 1: *parents, _ = ["", *accumulate(path_parts, lambda x, y: "/".join([x, y]))] # noqa: FLY002 for parent_path in parents: # this will raise if these groups were not created _ = await zarr.api.asynchronous.open_group( store=store, path=parent_path, zarr_format=zarr_format ) @staticmethod @pytest.mark.parametrize("endianness", ENDIANNESS_STR) def test_default_endianness( store: Store, zarr_format: ZarrFormat, endianness: EndiannessStr ) -> None: """ Test that that endianness is correctly set when creating an array when not specifying a serializer """ dtype = Int16(endianness=endianness) arr = zarr.create_array(store=store, shape=(1,), dtype=dtype, zarr_format=zarr_format) byte_order: str = arr[:].dtype.byteorder # type: ignore[union-attr] assert byte_order in NUMPY_ENDIANNESS_STR assert endianness_from_numpy_str(byte_order) == endianness # type: ignore[arg-type] @pytest.mark.parametrize("value", [1, 1.4, "a", b"a", np.array(1)]) @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_scalar_array(value: Any, zarr_format: ZarrFormat) -> None: arr = zarr.array(value, zarr_format=zarr_format) assert arr[...] == value assert arr.shape == () assert arr.ndim == 0 assert isinstance(arr[()], NDArrayLikeOrScalar) @pytest.mark.parametrize("store", ["local"], indirect=True) @pytest.mark.parametrize("store2", ["local"], indirect=["store2"]) @pytest.mark.parametrize("src_format", [2, 3]) @pytest.mark.parametrize("new_format", [2, 3, None]) async def test_creation_from_other_zarr_format( store: Store, store2: Store, src_format: ZarrFormat, new_format: ZarrFormat | None, ) -> None: if src_format == 2: src = zarr.create( (50, 50), chunks=(10, 10), store=store, zarr_format=src_format, dimension_separator="/" ) else: src = zarr.create( (50, 50), chunks=(10, 10), store=store, zarr_format=src_format, chunk_key_encoding=("default", "."), ) src[:] = np.arange(50 * 50).reshape((50, 50)) result = zarr.from_array( store=store2, data=src, zarr_format=new_format, ) np.testing.assert_array_equal(result[:], src[:]) assert result.fill_value == src.fill_value assert result.dtype == src.dtype assert result.chunks == src.chunks expected_format = src_format if new_format is None else new_format assert result.metadata.zarr_format == expected_format if src_format == new_format: assert result.metadata == src.metadata result2 = zarr.array( data=src, store=store2, overwrite=True, zarr_format=new_format, ) np.testing.assert_array_equal(result2[:], src[:]) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=True) @pytest.mark.parametrize("store2", ["local", "memory", "zip"], indirect=["store2"]) @pytest.mark.parametrize("src_chunks", [(40, 10), (11, 50)]) @pytest.mark.parametrize("new_chunks", [(40, 10), (11, 50)]) async def test_from_array( store: Store, store2: Store, src_chunks: tuple[int, int], new_chunks: tuple[int, int], zarr_format: ZarrFormat, ) -> None: src_fill_value = 2 src_dtype = np.dtype("uint8") src_attributes = None src = zarr.create( (100, 10), chunks=src_chunks, dtype=src_dtype, store=store, fill_value=src_fill_value, attributes=src_attributes, ) src[:] = np.arange(1000).reshape((100, 10)) new_fill_value = 3 new_attributes: dict[str, JSON] = {"foo": "bar"} result = zarr.from_array( data=src, store=store2, chunks=new_chunks, fill_value=new_fill_value, attributes=new_attributes, ) np.testing.assert_array_equal(result[:], src[:]) assert result.fill_value == new_fill_value assert result.dtype == src_dtype assert result.attrs == new_attributes assert result.chunks == new_chunks @pytest.mark.parametrize("store", ["local"], indirect=True) @pytest.mark.parametrize("chunks", ["keep", "auto"]) @pytest.mark.parametrize("write_data", [True, False]) @pytest.mark.parametrize( "src", [ np.arange(1000).reshape(10, 10, 10), zarr.ones((10, 10, 10)), 5, [1, 2, 3], [[1, 2, 3], [4, 5, 6]], ], ) # add other npt.ArrayLike? async def test_from_array_arraylike( store: Store, chunks: Literal["auto", "keep"] | tuple[int, int], write_data: bool, src: AnyArray | npt.ArrayLike, ) -> None: fill_value = 42 result = zarr.from_array( store, data=src, chunks=chunks, write_data=write_data, fill_value=fill_value ) if write_data: np.testing.assert_array_equal(result[...], np.array(src)) else: np.testing.assert_array_equal(result[...], np.full_like(src, fill_value)) def test_from_array_F_order() -> None: arr = zarr.create_array(store={}, data=np.array([1]), order="F", zarr_format=2) with pytest.warns( ZarrUserWarning, match="The existing order='F' of the source Zarr format 2 array will be ignored.", ): zarr.from_array(store={}, data=arr, zarr_format=3) async def test_orthogonal_set_total_slice() -> None: """Ensure that a whole chunk overwrite does not read chunks""" store = MemoryStore() array = zarr.create_array(store, shape=(20, 20), chunks=(1, 2), dtype=int, fill_value=-1) with mock.patch("zarr.storage.MemoryStore.get", side_effect=RuntimeError): array[0, slice(4, 10)] = np.arange(6) array = zarr.create_array( store, shape=(20, 21), chunks=(1, 2), dtype=int, fill_value=-1, overwrite=True ) with mock.patch("zarr.storage.MemoryStore.get", side_effect=RuntimeError): array[0, :] = np.arange(21) with mock.patch("zarr.storage.MemoryStore.get", side_effect=RuntimeError): array[:] = 1 @pytest.mark.skipif( Version(numcodecs.__version__) < Version("0.15.1"), reason="codec configuration is overwritten on older versions. GH2800", ) def test_roundtrip_numcodecs() -> None: store = MemoryStore() compressors = [ {"name": "numcodecs.shuffle", "configuration": {"elementsize": 2}}, {"name": "numcodecs.zlib", "configuration": {"level": 4}}, ] filters: list[CodecJSON_V3] = [ { "name": "numcodecs.fixedscaleoffset", "configuration": { "scale": 100.0, "offset": 0.0, "dtype": " Any: return arr[index] @pytest.mark.parametrize( "method", [ pytest.param( "fork", marks=pytest.mark.skipif( sys.platform in ("win32", "darwin"), reason="fork not supported on Windows or OSX" ), ), "spawn", pytest.param( "forkserver", marks=pytest.mark.skipif( sys.platform == "win32", reason="forkserver not supported on Windows" ), ), ], ) @pytest.mark.parametrize("store", ["local"], indirect=True) @pytest.mark.parametrize("shards", [None, (20,)]) def test_multiprocessing( store: Store, method: Literal["fork", "spawn", "forkserver"], shards: tuple[int, ...] | None ) -> None: """ Test that arrays can be pickled and indexed in child processes """ data = np.arange(100) chunks: Literal["auto"] | tuple[int, ...] if shards is None: chunks = "auto" else: chunks = (1,) arr = zarr.create_array(store=store, data=data, shards=shards, chunks=chunks) ctx = mp.get_context(method) with ctx.Pool() as pool: results = pool.starmap(_index_array, [(arr, slice(len(data)))]) assert all(np.array_equal(r, data) for r in results) def test_create_array_method_signature() -> None: """ Test that the signature of the ``AsyncGroup.create_array`` function has nearly the same signature as the ``create_array`` function. ``AsyncGroup.create_array`` should take all of the same keyword arguments as ``create_array`` except ``store``. """ base_sig = inspect.signature(create_array) meth_sig = inspect.signature(AsyncGroup.create_array) # ignore keyword arguments that are either missing or have different semantics when # create_array is invoked as a group method ignore_kwargs = {"zarr_format", "store", "name"} # TODO: make this test stronger. right now, it only checks that all the parameters in the # function signature are used in the method signature. we can be more strict and check that # the method signature uses no extra parameters. base_params = dict(filter(lambda kv: kv[0] not in ignore_kwargs, base_sig.parameters.items())) assert (set(base_params.items()) - set(meth_sig.parameters.items())) == set() async def test_sharding_coordinate_selection() -> None: store = MemoryStore() g = zarr.open_group(store, mode="w") arr = g.create_array( name="a", shape=(2, 3, 4), chunks=(1, 2, 2), overwrite=True, dtype=np.float32, shards=(2, 4, 4), ) arr[:] = np.arange(2 * 3 * 4).reshape((2, 3, 4)) result = arr[1, [0, 1]] # type: ignore[index] assert isinstance(result, NDArrayLike) assert (result == np.array([[12, 13, 14, 15], [16, 17, 18, 19]])).all() @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) def test_array_repr(store: Store) -> None: shape = (2, 3, 4) dtype = "uint8" arr = zarr.create_array(store, shape=shape, dtype=dtype) assert str(arr) == f"" class UnknownObjectDtype(UTF8Base[np.dtypes.ObjectDType]): object_codec_id = "unknown" # type: ignore[assignment] def to_native_dtype(self) -> np.dtypes.ObjectDType: """ Create a NumPy object dtype from this VariableLengthUTF8 ZDType. Returns ------- np.dtypes.ObjectDType The NumPy object dtype. """ return np.dtype("o") # type: ignore[return-value] @pytest.mark.parametrize( "dtype", [VariableLengthUTF8(), VariableLengthBytes(), UnknownObjectDtype()] ) def test_chunk_encoding_no_object_codec_errors(dtype: ZDType[Any, Any]) -> None: """ Test that a valuerror is raised when checking the chunk encoding for a v2 array with a data type that requires an object codec, but where no object codec is specified """ if isinstance(dtype, VariableLengthUTF8): codec_name = "the numcodecs.VLenUTF8 codec" elif isinstance(dtype, VariableLengthBytes): codec_name = "the numcodecs.VLenBytes codec" else: codec_name = f"an unknown object codec with id {dtype.object_codec_id!r}" # type: ignore[attr-defined] msg = ( f"Data type {dtype} requires {codec_name}, " "but no such codec was specified in the filters or compressor parameters for " "this array. " ) with pytest.raises(ValueError, match=re.escape(msg)): _parse_chunk_encoding_v2(filters=None, compressor=None, dtype=dtype) def test_unknown_object_codec_default_serializer_v3() -> None: """ Test that we get a valueerrror when trying to create the default serializer for a data type that requires an unknown object codec """ dtype = UnknownObjectDtype() msg = f"Data type {dtype} requires an unknown object codec: {dtype.object_codec_id!r}." with pytest.raises(ValueError, match=re.escape(msg)): default_serializer_v3(dtype) def test_unknown_object_codec_default_filters_v2() -> None: """ Test that we get a valueerrror when trying to create the default serializer for a data type that requires an unknown object codec """ dtype = UnknownObjectDtype() msg = f"Data type {dtype} requires an unknown object codec: {dtype.object_codec_id!r}." with pytest.raises(ValueError, match=re.escape(msg)): default_filters_v2(dtype) @pytest.mark.parametrize( ("array_shape", "shard_shape", "chunk_shape"), [ ((10,), None, (1,)), ((10,), (1,), (1,)), ((30, 10), None, (2, 5)), ((30, 10), (4, 10), (2, 5)), ], ) def test_chunk_grid_shape( array_shape: tuple[int, ...], shard_shape: tuple[int, ...] | None, chunk_shape: tuple[int, ...], zarr_format: ZarrFormat, ) -> None: """ Test that the shape of the chunk grid and the shard grid are correctly indicated """ if zarr_format == 2 and shard_shape is not None: with pytest.raises( ValueError, match="Zarr format 2 arrays can only be created with `shard_shape` set to `None`.", ): arr = zarr.create_array( {}, dtype="uint8", shape=array_shape, chunks=chunk_shape, shards=shard_shape, zarr_format=zarr_format, ) pytest.skip("Zarr format 2 arrays can only be created with `shard_shape` set to `None`.") else: arr = zarr.create_array( {}, dtype="uint8", shape=array_shape, chunks=chunk_shape, shards=shard_shape, zarr_format=zarr_format, ) chunk_grid_shape = tuple(starmap(ceildiv, zip(array_shape, chunk_shape, strict=True))) if shard_shape is None: _shard_shape = chunk_shape else: _shard_shape = shard_shape shard_grid_shape = tuple(starmap(ceildiv, zip(array_shape, _shard_shape, strict=True))) assert arr._chunk_grid_shape == chunk_grid_shape assert arr.cdata_shape == chunk_grid_shape assert arr.async_array.cdata_shape == chunk_grid_shape assert arr._shard_grid_shape == shard_grid_shape assert arr._nshards == np.prod(shard_grid_shape) @pytest.mark.parametrize( ("array_shape", "shard_shape", "chunk_shape"), [((10,), None, (1,)), ((30, 10), None, (2, 5))] ) def test_iter_chunk_coords( array_shape: tuple[int, ...], shard_shape: tuple[int, ...] | None, chunk_shape: tuple[int, ...], zarr_format: ZarrFormat, ) -> None: """ Test that we can use the various invocations of iter_chunk_coords to iterate over the coordinates of the origin of each chunk. """ arr = zarr.create_array( {}, dtype="uint8", shape=array_shape, chunks=chunk_shape, shards=shard_shape, zarr_format=zarr_format, ) expected = tuple(_iter_grid(arr._shard_grid_shape)) observed = tuple(_iter_chunk_coords(arr)) assert observed == expected assert observed == tuple(arr._iter_chunk_coords()) assert observed == tuple(arr.async_array._iter_chunk_coords()) @pytest.mark.parametrize( ("array_shape", "shard_shape", "chunk_shape"), [((10,), (1,), (1,)), ((10,), None, (1,)), ((30, 10), (10, 5), (2, 5))], ) def test_iter_shard_coords( array_shape: tuple[int, ...], shard_shape: tuple[int, ...] | None, chunk_shape: tuple[int, ...], zarr_format: ZarrFormat, ) -> None: """ Test that we can use the various invocations of iter_shard_coords to iterate over the coordinates of the origin of each shard. """ if zarr_format == 2 and shard_shape is not None: pytest.skip("Zarr format 2 does not support shard shape.") arr = zarr.create_array( {}, dtype="uint8", shape=array_shape, chunks=chunk_shape, shards=shard_shape, zarr_format=zarr_format, ) expected = tuple(_iter_grid(arr._shard_grid_shape)) observed = tuple(_iter_shard_coords(arr)) assert observed == expected assert observed == tuple(arr._iter_shard_coords()) assert observed == tuple(arr.async_array._iter_shard_coords()) @pytest.mark.parametrize( ("array_shape", "shard_shape", "chunk_shape"), [((10,), (1,), (1,)), ((10,), None, (1,)), ((30, 10), (10, 5), (2, 5))], ) def test_iter_shard_keys( array_shape: tuple[int, ...], shard_shape: tuple[int, ...] | None, chunk_shape: tuple[int, ...], zarr_format: ZarrFormat, ) -> None: """ Test that we can use the various invocations of iter_shard_keys to iterate over the stored keys of the shards of an array. """ if zarr_format == 2 and shard_shape is not None: pytest.skip("Zarr format 2 does not support shard shape.") arr = zarr.create_array( {}, dtype="uint8", shape=array_shape, chunks=chunk_shape, shards=shard_shape, zarr_format=zarr_format, ) expected = tuple( arr.metadata.encode_chunk_key(key) for key in _iter_grid(arr._shard_grid_shape) ) observed = tuple(_iter_shard_keys(arr)) assert observed == expected assert observed == tuple(arr._iter_shard_keys()) assert observed == tuple(arr.async_array._iter_shard_keys()) @pytest.mark.parametrize( ("array_shape", "shard_shape", "chunk_shape"), [((10,), None, (1,)), ((10,), (1,), (1,)), ((30, 10), (10, 5), (2, 5))], ) def test_iter_shard_regions( array_shape: tuple[int, ...], shard_shape: tuple[int, ...] | None, chunk_shape: tuple[int, ...], zarr_format: ZarrFormat, ) -> None: """ Test that we can use the various invocations of iter_shard_regions to iterate over the regions spanned by the shards of an array. """ if zarr_format == 2 and shard_shape is not None: pytest.skip("Zarr format 2 does not support shard shape.") arr = zarr.create_array( {}, dtype="uint8", shape=array_shape, chunks=chunk_shape, shards=shard_shape, zarr_format=zarr_format, ) if shard_shape is None: _shard_shape = chunk_shape else: _shard_shape = shard_shape expected = tuple(_iter_regions(arr.shape, _shard_shape)) observed = tuple(_iter_shard_regions(arr)) assert observed == expected assert observed == tuple(arr._iter_shard_regions()) assert observed == tuple(arr.async_array._iter_shard_regions()) @pytest.mark.parametrize( ("array_shape", "shard_shape", "chunk_shape"), [((10,), None, (1,)), ((30, 10), None, (2, 5))] ) def test_iter_chunk_regions( array_shape: tuple[int, ...], shard_shape: tuple[int, ...] | None, chunk_shape: tuple[int, ...], zarr_format: ZarrFormat, ) -> None: """ Test that we can use the various invocations of iter_chunk_regions to iterate over the regions spanned by the chunks of an array. """ arr = zarr.create_array( {}, dtype="uint8", shape=array_shape, chunks=chunk_shape, shards=shard_shape, zarr_format=zarr_format, ) expected = tuple(_iter_regions(arr.shape, chunk_shape)) observed = tuple(_iter_chunk_regions(arr)) assert observed == expected assert observed == tuple(arr._iter_chunk_regions()) assert observed == tuple(arr.async_array._iter_chunk_regions()) @pytest.mark.parametrize("num_shards", [1, 3]) @pytest.mark.parametrize("array_type", ["numpy", "zarr"]) def test_create_array_with_data_num_gets( num_shards: int, array_type: Literal["numpy", "zarr"] ) -> None: """ Test that creating an array with data only invokes a single get request per stored object """ store = LoggingStore(store=MemoryStore()) chunk_shape = (1,) shard_shape = (100,) shape = (shard_shape[0] * num_shards,) data: AnyArray | npt.NDArray[np.int64] if array_type == "numpy": data = np.zeros(shape[0], dtype="int64") else: data = zarr.zeros(shape, dtype="int64") zarr.create_array(store, data=data, chunks=chunk_shape, shards=shard_shape, fill_value=-1) # type: ignore[arg-type] # One get for the metadata; full-shard writes should not read shard payloads. assert store.counter["get"] == 1 @pytest.mark.parametrize( ("selection", "expected_gets"), [(slice(None), 0), (slice(1, 9), 1)], ) def test_shard_write_num_gets(selection: slice, expected_gets: int) -> None: """ Test that partial-shard writes read the existing data and full-shard writes don't. """ store = LoggingStore(store=MemoryStore()) arr = zarr.create_array( store, shape=(10,), chunks=(1,), shards=(10,), dtype="int64", fill_value=-1, ) arr[:] = 0 store.counter.clear() arr[selection] = 1 assert store.counter["get"] == expected_gets @pytest.mark.parametrize("config", [{}, {"write_empty_chunks": True}, {"order": "C"}]) def test_with_config(config: ArrayConfigParams) -> None: """ Test that `AsyncArray.with_config` and `Array.with_config` create a copy of the source array with a new runtime configuration. """ # the config we start with source_config: ArrayConfigParams = {"write_empty_chunks": False, "order": "F"} source_array = zarr.create_array({}, shape=(1,), dtype="uint8", config=source_config) new_async_array_config_dict = source_array._async_array.with_config(config).config.to_dict() new_array_config_dict = source_array.with_config(config).config.to_dict() for key in source_config: if key in config: assert new_async_array_config_dict[key] == config[key] # type: ignore[literal-required] assert new_array_config_dict[key] == config[key] # type: ignore[literal-required] else: assert new_async_array_config_dict[key] == source_config[key] # type: ignore[literal-required] assert new_array_config_dict[key] == source_config[key] # type: ignore[literal-required] def test_with_config_polymorphism() -> None: """ Test that `AsyncArray.with_config` and `Array.with_config` accept dicts and full array config objects. """ source_config: ArrayConfig = ArrayConfig.from_dict({"write_empty_chunks": False, "order": "F"}) source_config_dict = source_config.to_dict() arr = zarr.create_array({}, shape=(1,), dtype="uint8") arr_source_config = arr.with_config(source_config) arr_source_config_dict = arr.with_config(source_config_dict) assert arr_source_config.config == arr_source_config_dict.config zarr-python-3.2.1/tests/test_attributes.py000066400000000000000000000053331517635743000207720ustar00rootroot00000000000000import json from typing import TYPE_CHECKING, Any import numpy as np import pytest import zarr.core import zarr.core.attributes import zarr.storage from tests.conftest import deep_nan_equal from zarr.core.common import ZarrFormat if TYPE_CHECKING: from zarr.types import AnyArray @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.parametrize( "data", [{"inf": np.inf, "-inf": -np.inf, "nan": np.nan}, {"a": 3, "c": 4}] ) def test_put(data: dict[str, Any], zarr_format: ZarrFormat) -> None: store = zarr.storage.MemoryStore() attrs = zarr.core.attributes.Attributes(zarr.Group.from_store(store, zarr_format=zarr_format)) attrs.put(data) expected = json.loads(json.dumps(data, allow_nan=True)) assert deep_nan_equal(dict(attrs), expected) def test_asdict() -> None: store = zarr.storage.MemoryStore() attrs = zarr.core.attributes.Attributes( zarr.Group.from_store(store, attributes={"a": 1, "b": 2}) ) result = attrs.asdict() assert result == {"a": 1, "b": 2} def test_update_attributes_preserves_existing() -> None: """ Test that `update_attributes` only updates the specified attributes and preserves existing ones. """ store = zarr.storage.MemoryStore() z = zarr.create(10, store=store, overwrite=True) z.attrs["a"] = [] z.attrs["b"] = 3 assert dict(z.attrs) == {"a": [], "b": 3} z.update_attributes({"a": [3, 4], "c": 4}) assert dict(z.attrs) == {"a": [3, 4], "b": 3, "c": 4} def test_update_empty_attributes() -> None: """ Ensure updating when initial attributes are empty works. """ store = zarr.storage.MemoryStore() z = zarr.create(10, store=store, overwrite=True) assert dict(z.attrs) == {} z.update_attributes({"a": [3, 4], "c": 4}) assert dict(z.attrs) == {"a": [3, 4], "c": 4} def test_update_no_changes() -> None: """ Ensure updating when no new or modified attributes does not alter existing ones. """ store = zarr.storage.MemoryStore() z = zarr.create(10, store=store, overwrite=True) z.attrs["a"] = [] z.attrs["b"] = 3 z.update_attributes({}) assert dict(z.attrs) == {"a": [], "b": 3} @pytest.mark.parametrize("group", [True, False]) def test_del_works(group: bool) -> None: store = zarr.storage.MemoryStore() z: zarr.Group | AnyArray if group: z = zarr.create_group(store) else: z = zarr.create_array(store=store, shape=10, dtype=int) assert dict(z.attrs) == {} z.update_attributes({"a": [3, 4], "c": 4}) del z.attrs["a"] assert dict(z.attrs) == {"c": 4} z2: zarr.Group | AnyArray if group: z2 = zarr.open_group(store) else: z2 = zarr.open_array(store) assert dict(z2.attrs) == {"c": 4} zarr-python-3.2.1/tests/test_buffer.py000066400000000000000000000204201517635743000200470ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Literal import numpy as np import pytest import zarr from zarr.abc.buffer import ArrayLike, BufferPrototype, NDArrayLike from zarr.buffer import cpu, gpu from zarr.codecs.blosc import BloscCodec from zarr.codecs.crc32c_ import Crc32cCodec from zarr.codecs.gzip import GzipCodec from zarr.codecs.transpose import TransposeCodec from zarr.codecs.zstd import ZstdCodec from zarr.errors import ZarrUserWarning from zarr.storage import MemoryStore, StorePath from zarr.testing.buffer import ( NDBufferUsingTestNDArrayLike, StoreExpectingTestBuffer, TestBuffer, TestNDArrayLike, ) from zarr.testing.utils import gpu_mark, gpu_test, skip_if_no_gpu if TYPE_CHECKING: import types try: import cupy as cp except ImportError: cp = None import zarr.api.asynchronous if TYPE_CHECKING: import types def test_nd_array_like(xp: types.ModuleType) -> None: ary = xp.arange(10) assert isinstance(ary, ArrayLike) assert isinstance(ary, NDArrayLike) @pytest.mark.asyncio async def test_async_array_prototype() -> None: """Test the use of a custom buffer prototype""" expect = np.zeros((9, 9), dtype="uint16", order="F") a = await zarr.api.asynchronous.create_array( StorePath(StoreExpectingTestBuffer()) / "test_async_array_prototype", shape=expect.shape, chunks=(5, 5), dtype=expect.dtype, fill_value=0, ) expect[1:4, 3:6] = np.ones((3, 3)) my_prototype = BufferPrototype(buffer=TestBuffer, nd_buffer=NDBufferUsingTestNDArrayLike) await a.setitem( selection=(slice(1, 4), slice(3, 6)), value=np.ones((3, 3)), prototype=my_prototype, ) got = await a.getitem(selection=(slice(0, 9), slice(0, 9)), prototype=my_prototype) # ignoring a mypy error here that TestNDArrayLike doesn't meet the NDArrayLike protocol # The test passes, so it clearly does. assert isinstance(got, TestNDArrayLike) assert np.array_equal(expect, got) # type: ignore[unreachable] @gpu_test @pytest.mark.asyncio async def test_async_array_gpu_prototype() -> None: """Test the use of the GPU buffer prototype""" expect = cp.zeros((9, 9), dtype="uint16", order="F") a = await zarr.api.asynchronous.create_array( StorePath(MemoryStore()) / "test_async_array_gpu_prototype", shape=expect.shape, chunks=(5, 5), dtype=expect.dtype, fill_value=0, ) expect[1:4, 3:6] = cp.ones((3, 3)) await a.setitem( selection=(slice(1, 4), slice(3, 6)), value=cp.ones((3, 3)), prototype=gpu.buffer_prototype, ) got = await a.getitem(selection=(slice(0, 9), slice(0, 9)), prototype=gpu.buffer_prototype) assert isinstance(got, cp.ndarray) assert cp.array_equal(expect, got) @pytest.mark.asyncio async def test_codecs_use_of_prototype() -> None: expect = np.zeros((10, 10), dtype="uint16", order="F") a = await zarr.api.asynchronous.create_array( StorePath(StoreExpectingTestBuffer()) / "test_codecs_use_of_prototype", shape=expect.shape, chunks=(5, 5), dtype=expect.dtype, fill_value=0, compressors=[BloscCodec(), Crc32cCodec(), GzipCodec(), ZstdCodec()], filters=[TransposeCodec(order=(1, 0))], ) expect[:] = np.arange(100).reshape(10, 10) my_prototype = BufferPrototype(buffer=TestBuffer, nd_buffer=NDBufferUsingTestNDArrayLike) await a.setitem( selection=(slice(0, 10), slice(0, 10)), value=expect[:], prototype=my_prototype, ) got = await a.getitem(selection=(slice(0, 10), slice(0, 10)), prototype=my_prototype) # ignoring a mypy error here that TestNDArrayLike doesn't meet the NDArrayLike protocol # The test passes, so it clearly does. assert isinstance(got, TestNDArrayLike) assert np.array_equal(expect, got) # type: ignore[unreachable] @gpu_test @pytest.mark.asyncio async def test_codecs_use_of_gpu_prototype() -> None: expect = cp.zeros((10, 10), dtype="uint16", order="F") a = await zarr.api.asynchronous.create_array( StorePath(MemoryStore()) / "test_codecs_use_of_gpu_prototype", shape=expect.shape, chunks=(5, 5), dtype=expect.dtype, fill_value=0, compressors=[BloscCodec(), Crc32cCodec(), GzipCodec(), ZstdCodec()], filters=[TransposeCodec(order=(1, 0))], ) expect[:] = cp.arange(100).reshape(10, 10) msg = "Creating a zarr.buffer.gpu.Buffer with an array that does not support the __cuda_array_interface__ for zero-copy transfers, falling back to slow copy based path" with pytest.warns(ZarrUserWarning, match=msg): await a.setitem( selection=(slice(0, 10), slice(0, 10)), value=expect[:], prototype=gpu.buffer_prototype, ) with pytest.warns(ZarrUserWarning, match=msg): got = await a.getitem( selection=(slice(0, 10), slice(0, 10)), prototype=gpu.buffer_prototype ) assert isinstance(got, cp.ndarray) assert cp.array_equal(expect, got) @gpu_test @pytest.mark.asyncio async def test_sharding_use_of_gpu_prototype() -> None: with zarr.config.enable_gpu(): expect = cp.zeros((10, 10), dtype="uint16", order="F") a = await zarr.api.asynchronous.create_array( StorePath(MemoryStore()) / "test_codecs_use_of_gpu_prototype", shape=expect.shape, chunks=(5, 5), shards=(10, 10), dtype=expect.dtype, fill_value=0, ) expect[:] = cp.arange(100).reshape(10, 10) msg = "Creating a zarr.buffer.gpu.Buffer with an array that does not support the __cuda_array_interface__ for zero-copy transfers, falling back to slow copy based path" with pytest.warns(ZarrUserWarning, match=msg): await a.setitem( selection=(slice(0, 10), slice(0, 10)), value=expect[:], prototype=gpu.buffer_prototype, ) with pytest.warns(ZarrUserWarning, match=msg): got = await a.getitem( selection=(slice(0, 10), slice(0, 10)), prototype=gpu.buffer_prototype ) assert isinstance(got, cp.ndarray) assert cp.array_equal(expect, got) def test_numpy_buffer_prototype() -> None: buffer = cpu.buffer_prototype.buffer.create_zero_length() ndbuffer = cpu.buffer_prototype.nd_buffer.create(shape=(1, 2), dtype=np.dtype("int64")) assert isinstance(buffer.as_array_like(), np.ndarray) assert isinstance(ndbuffer.as_ndarray_like(), np.ndarray) with pytest.raises(ValueError, match="Buffer does not contain a single scalar value"): ndbuffer.as_scalar() @gpu_test def test_gpu_buffer_prototype() -> None: buffer = gpu.buffer_prototype.buffer.create_zero_length() ndbuffer = gpu.buffer_prototype.nd_buffer.create(shape=(1, 2), dtype=cp.dtype("int64")) assert isinstance(buffer.as_array_like(), cp.ndarray) assert isinstance(ndbuffer.as_ndarray_like(), cp.ndarray) with pytest.raises(ValueError, match="Buffer does not contain a single scalar value"): ndbuffer.as_scalar() # TODO: the same test for other buffer classes def test_cpu_buffer_as_scalar() -> None: buf = cpu.buffer_prototype.nd_buffer.create(shape=(), dtype="int64") assert buf.as_scalar() == buf.as_ndarray_like()[()] # type: ignore[index] @pytest.mark.parametrize( "prototype", [ cpu.buffer_prototype, pytest.param( gpu.buffer_prototype, marks=[gpu_mark, skip_if_no_gpu], ), BufferPrototype( buffer=cpu.Buffer, nd_buffer=NDBufferUsingTestNDArrayLike, ), ], ) @pytest.mark.parametrize( "shape", [ (1, 2), (1, 2, 3), ], ) @pytest.mark.parametrize("dtype", ["int32", "float64"]) @pytest.mark.parametrize("order", ["C", "F"]) def test_empty( prototype: BufferPrototype, shape: tuple[int, ...], dtype: str, order: Literal["C", "F"] ) -> None: buf = prototype.nd_buffer.empty(shape=shape, dtype=dtype, order=order) result = buf.as_ndarray_like() assert result.shape == shape assert result.dtype == dtype if order == "C": assert result.flags.c_contiguous # type: ignore[attr-defined] else: assert result.flags.f_contiguous # type: ignore[attr-defined] zarr-python-3.2.1/tests/test_chunk_grids.py000066400000000000000000000046111517635743000211020ustar00rootroot00000000000000from typing import Any import numpy as np import pytest from zarr.core.chunk_grids import _guess_chunks, normalize_chunks @pytest.mark.parametrize( "shape", [(0,), (0,) * 2, (1, 2, 0, 4, 5), (10, 0), (10,), (100,) * 3, (1000000,), (10000,) * 2] ) @pytest.mark.parametrize("itemsize", [1, 2, 4]) def test_guess_chunks(shape: tuple[int, ...], itemsize: int) -> None: chunks = _guess_chunks(shape, itemsize) chunk_size = np.prod(chunks) * itemsize assert isinstance(chunks, tuple) assert len(chunks) == len(shape) assert chunk_size < (64 * 1024 * 1024) # doesn't make any sense to allow chunks to have zero length dimension assert all(0 < c <= max(s, 1) for c, s in zip(chunks, shape, strict=False)) @pytest.mark.parametrize( ("chunks", "shape", "typesize", "expected"), [ ((10,), (100,), 1, (10,)), ([10], (100,), 1, (10,)), (10, (100,), 1, (10,)), ((10, 10), (100, 10), 1, (10, 10)), (10, (100, 10), 1, (10, 10)), ((10, None), (100, 10), 1, (10, 10)), (30, (100, 20, 10), 1, (30, 30, 30)), ((30,), (100, 20, 10), 1, (30, 20, 10)), ((30, None), (100, 20, 10), 1, (30, 20, 10)), ((30, None, None), (100, 20, 10), 1, (30, 20, 10)), ((30, 20, None), (100, 20, 10), 1, (30, 20, 10)), ((30, 20, 10), (100, 20, 10), 1, (30, 20, 10)), # dask-style chunks (uniform with optional smaller final chunk) (((100, 100, 100), (50, 50)), (300, 100), 1, (100, 50)), (((100, 100, 50),), (250,), 1, (100,)), (((100,),), (100,), 1, (100,)), # auto chunking (None, (100,), 1, (100,)), (-1, (100,), 1, (100,)), ((30, -1, None), (100, 20, 10), 1, (30, 20, 10)), ], ) def test_normalize_chunks( chunks: Any, shape: tuple[int, ...], typesize: int, expected: tuple[int, ...] ) -> None: assert expected == normalize_chunks(chunks, shape, typesize) def test_normalize_chunks_errors() -> None: with pytest.raises(ValueError): normalize_chunks("foo", (100,), 1) with pytest.raises(ValueError): normalize_chunks((100, 10), (100,), 1) # dask-style irregular chunks should raise with pytest.raises(ValueError, match="Irregular chunk sizes"): normalize_chunks(((10, 20, 30),), (60,), 1) with pytest.raises(ValueError, match="Irregular chunk sizes"): normalize_chunks(((100, 100), (10, 20)), (200, 30), 1) zarr-python-3.2.1/tests/test_cli/000077500000000000000000000000001517635743000167755ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_cli/conftest.py000066400000000000000000000104231517635743000211740ustar00rootroot00000000000000from pathlib import Path from typing import Any, Literal import pytest import zarr from zarr.abc.store import Store from zarr.core.common import ZarrFormat def create_nested_zarr( store: Store, attributes: dict[str, Any] | None = None, separator: Literal[".", "/"] = ".", zarr_format: ZarrFormat = 2, ) -> list[str]: """Create a zarr with nested groups / arrays for testing, returning the paths to all.""" if attributes is None: attributes = {"baz": 42, "qux": [1, 4, 7, 12]} # 3 levels of nested groups group_0 = zarr.create_group(store=store, zarr_format=zarr_format, attributes=attributes) group_1 = group_0.create_group(name="group_1", attributes=attributes) group_2 = group_1.create_group(name="group_2", attributes=attributes) paths = [group_0.path, group_1.path, group_2.path] # 1 array per group for i, group in enumerate([group_0, group_1, group_2]): array = group.create_array( name=f"array_{i}", shape=(10, 10), chunks=(5, 5), dtype="uint16", attributes=attributes, chunk_key_encoding={"name": "v2", "separator": separator}, ) array[:] = 1 paths.append(array.path) return paths @pytest.fixture def expected_paths() -> list[Path]: """Expected paths for create_nested_zarr, with no metadata files or chunks""" return [ Path("array_0"), Path("group_1"), Path("group_1/array_1"), Path("group_1/group_2"), Path("group_1/group_2/array_2"), ] @pytest.fixture def expected_chunks() -> list[Path]: """Expected chunks for create_nested_zarr""" return [ Path("array_0/0.0"), Path("array_0/0.1"), Path("array_0/1.0"), Path("array_0/1.1"), Path("group_1/array_1/0.0"), Path("group_1/array_1/0.1"), Path("group_1/array_1/1.0"), Path("group_1/array_1/1.1"), Path("group_1/group_2/array_2/0.0"), Path("group_1/group_2/array_2/0.1"), Path("group_1/group_2/array_2/1.0"), Path("group_1/group_2/array_2/1.1"), ] @pytest.fixture def expected_v3_metadata() -> list[Path]: """Expected v3 metadata for create_nested_zarr""" return sorted( [ Path("zarr.json"), Path("array_0/zarr.json"), Path("group_1/zarr.json"), Path("group_1/array_1/zarr.json"), Path("group_1/group_2/zarr.json"), Path("group_1/group_2/array_2/zarr.json"), ] ) @pytest.fixture def expected_v2_metadata() -> list[Path]: """Expected v2 metadata for create_nested_zarr""" return sorted( [ Path(".zgroup"), Path(".zattrs"), Path("array_0/.zarray"), Path("array_0/.zattrs"), Path("group_1/.zgroup"), Path("group_1/.zattrs"), Path("group_1/array_1/.zarray"), Path("group_1/array_1/.zattrs"), Path("group_1/group_2/.zgroup"), Path("group_1/group_2/.zattrs"), Path("group_1/group_2/array_2/.zarray"), Path("group_1/group_2/array_2/.zattrs"), ] ) @pytest.fixture def expected_paths_no_metadata( expected_paths: list[Path], expected_chunks: list[Path] ) -> list[Path]: return sorted(expected_paths + expected_chunks) @pytest.fixture def expected_paths_v3_metadata( expected_paths: list[Path], expected_chunks: list[Path], expected_v3_metadata: list[Path] ) -> list[Path]: return sorted(expected_paths + expected_chunks + expected_v3_metadata) @pytest.fixture def expected_paths_v3_metadata_no_chunks( expected_paths: list[Path], expected_v3_metadata: list[Path] ) -> list[Path]: return sorted(expected_paths + expected_v3_metadata) @pytest.fixture def expected_paths_v2_metadata( expected_paths: list[Path], expected_chunks: list[Path], expected_v2_metadata: list[Path] ) -> list[Path]: return sorted(expected_paths + expected_chunks + expected_v2_metadata) @pytest.fixture def expected_paths_v2_v3_metadata( expected_paths: list[Path], expected_chunks: list[Path], expected_v2_metadata: list[Path], expected_v3_metadata: list[Path], ) -> list[Path]: return sorted(expected_paths + expected_chunks + expected_v2_metadata + expected_v3_metadata) zarr-python-3.2.1/tests/test_cli/test_migrate_v3.py000066400000000000000000000543051517635743000224550ustar00rootroot00000000000000import lzma from pathlib import Path from typing import Literal, cast import numcodecs import numcodecs.abc import numpy as np import pytest import zarr from tests.test_cli.conftest import create_nested_zarr from zarr.abc.codec import Codec from zarr.codecs.blosc import BloscCodec from zarr.codecs.bytes import BytesCodec from zarr.codecs.gzip import GzipCodec from zarr.codecs.numcodecs import LZMA, Delta from zarr.codecs.transpose import TransposeCodec from zarr.codecs.zstd import ZstdCodec from zarr.core.chunk_key_encodings import V2ChunkKeyEncoding from zarr.core.common import JSON, ZarrFormat from zarr.core.dtype.npy.int import UInt8, UInt16 from zarr.core.group import Group, GroupMetadata from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.storage._local import LocalStore from zarr.types import AnyArray typer_testing = pytest.importorskip( "typer.testing", reason="optional cli dependencies aren't installed" ) cli = pytest.importorskip("zarr._cli.cli", reason="optional cli dependencies aren't installed") runner = typer_testing.CliRunner() def test_migrate_array(local_store: LocalStore) -> None: shape = (10, 10) chunks = (10, 10) dtype = "uint16" compressors = numcodecs.Blosc(cname="zstd", clevel=3, shuffle=1) fill_value = 2 attributes = cast(dict[str, JSON], {"baz": 42, "qux": [1, 4, 7, 12]}) zarr.create_array( store=local_store, shape=shape, chunks=chunks, dtype=dtype, compressors=compressors, zarr_format=2, fill_value=fill_value, attributes=attributes, ) result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 assert (local_store.root / "zarr.json").exists() zarr_array = zarr.open(local_store.root, zarr_format=3) expected_metadata = ArrayV3Metadata( shape=shape, data_type=UInt16(endianness="little"), chunk_grid={"name": "regular", "configuration": {"chunk_shape": chunks}}, chunk_key_encoding=V2ChunkKeyEncoding(separator="."), fill_value=fill_value, codecs=( BytesCodec(endian="little"), BloscCodec(typesize=2, cname="zstd", clevel=3, shuffle="shuffle", blocksize=0), ), attributes=attributes, dimension_names=None, storage_transformers=None, ) assert zarr_array.metadata == expected_metadata def test_migrate_group(local_store: LocalStore) -> None: attributes = {"baz": 42, "qux": [1, 4, 7, 12]} zarr.create_group(store=local_store, zarr_format=2, attributes=attributes) result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 assert (local_store.root / "zarr.json").exists() zarr_array = zarr.open(local_store.root, zarr_format=3) expected_metadata = GroupMetadata( attributes=attributes, zarr_format=3, consolidated_metadata=None ) assert zarr_array.metadata == expected_metadata @pytest.mark.parametrize("separator", [".", "/"]) def test_migrate_nested_groups_and_arrays_in_place( local_store: LocalStore, separator: str, expected_v3_metadata: list[Path] ) -> None: """Test that zarr.json are made at the correct points in a hierarchy of groups and arrays (including when there are additional dirs due to using a / separator)""" attributes = {"baz": 42, "qux": [1, 4, 7, 12]} paths = create_nested_zarr(local_store, attributes=attributes, separator=separator) result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 zarr_json_paths = sorted(local_store.root.rglob("zarr.json")) expected_zarr_json_paths = [local_store.root / p for p in expected_v3_metadata] assert zarr_json_paths == expected_zarr_json_paths # Check converted zarr can be opened + metadata accessed at all levels zarr_array = zarr.open(local_store.root, zarr_format=3) for path in paths: zarr_v3 = cast(AnyArray | Group, zarr_array[path]) metadata = zarr_v3.metadata assert metadata.zarr_format == 3 assert metadata.attributes == attributes @pytest.mark.parametrize("separator", [".", "/"]) async def test_migrate_nested_groups_and_arrays_separate_location( tmp_path: Path, separator: str, expected_v2_metadata: list[Path], expected_v3_metadata: list[Path], ) -> None: """Test that zarr.json are made at the correct paths, when saving to a separate output location.""" input_zarr_path = tmp_path / "input.zarr" output_zarr_path = tmp_path / "output.zarr" local_store = await LocalStore.open(str(input_zarr_path)) create_nested_zarr(local_store, separator=separator) result = runner.invoke(cli.app, ["migrate", "v3", str(input_zarr_path), str(output_zarr_path)]) assert result.exit_code == 0 # Files in input zarr should be unchanged i.e. still v2 only zarr_json_paths = sorted(input_zarr_path.rglob("zarr.json")) assert len(zarr_json_paths) == 0 paths = [ path for path in input_zarr_path.rglob("*") if path.stem in [".zarray", ".zgroup", ".zattrs"] ] expected_paths = [input_zarr_path / p for p in expected_v2_metadata] assert sorted(paths) == expected_paths # Files in output zarr should only contain v3 metadata zarr_json_paths = sorted(output_zarr_path.rglob("zarr.json")) expected_zarr_json_paths = [output_zarr_path / p for p in expected_v3_metadata] assert zarr_json_paths == expected_zarr_json_paths def test_remove_v2_metadata_option_in_place( local_store: LocalStore, expected_paths_v3_metadata: list[Path] ) -> None: create_nested_zarr(local_store) # convert v2 metadata to v3, then remove v2 metadata result = runner.invoke( cli.app, ["migrate", "v3", str(local_store.root), "--remove-v2-metadata"] ) assert result.exit_code == 0 paths = sorted(local_store.root.rglob("*")) expected_paths = [local_store.root / p for p in expected_paths_v3_metadata] assert paths == expected_paths async def test_remove_v2_metadata_option_separate_location( tmp_path: Path, expected_paths_v2_metadata: list[Path], expected_paths_v3_metadata_no_chunks: list[Path], ) -> None: """Check that when using --remove-v2-metadata with a separate output location, no v2 metadata is removed from the input location.""" input_zarr_path = tmp_path / "input.zarr" output_zarr_path = tmp_path / "output.zarr" local_store = await LocalStore.open(str(input_zarr_path)) create_nested_zarr(local_store) result = runner.invoke( cli.app, ["migrate", "v3", str(input_zarr_path), str(output_zarr_path), "--remove-v2-metadata"], ) assert result.exit_code == 0 # input image should be unchanged paths = sorted(input_zarr_path.rglob("*")) expected_paths = [input_zarr_path / p for p in expected_paths_v2_metadata] assert paths == expected_paths # output image should be only v3 metadata paths = sorted(output_zarr_path.rglob("*")) expected_paths = [output_zarr_path / p for p in expected_paths_v3_metadata_no_chunks] assert paths == expected_paths def test_overwrite_option_in_place( local_store: LocalStore, expected_paths_v2_v3_metadata: list[Path] ) -> None: create_nested_zarr(local_store) # add v3 metadata in place result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 # check that v3 metadata can be overwritten with --overwrite result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root), "--overwrite"]) assert result.exit_code == 0 paths = sorted(local_store.root.rglob("*")) expected_paths = [local_store.root / p for p in expected_paths_v2_v3_metadata] assert paths == expected_paths async def test_overwrite_option_separate_location( tmp_path: Path, expected_paths_v2_metadata: list[Path], expected_paths_v3_metadata_no_chunks: list[Path], ) -> None: input_zarr_path = tmp_path / "input.zarr" output_zarr_path = tmp_path / "output.zarr" local_store = await LocalStore.open(str(input_zarr_path)) create_nested_zarr(local_store) # create v3 metadata at output_zarr_path result = runner.invoke( cli.app, ["migrate", "v3", str(input_zarr_path), str(output_zarr_path)], ) assert result.exit_code == 0 # re-run with --overwrite option result = runner.invoke( cli.app, ["migrate", "v3", str(input_zarr_path), str(output_zarr_path), "--overwrite", "--force"], ) assert result.exit_code == 0 # original image should be un-changed paths = sorted(input_zarr_path.rglob("*")) expected_paths = [input_zarr_path / p for p in expected_paths_v2_metadata] assert paths == expected_paths # output image is only v3 metadata paths = sorted(output_zarr_path.rglob("*")) expected_paths = [output_zarr_path / p for p in expected_paths_v3_metadata_no_chunks] assert paths == expected_paths @pytest.mark.parametrize("separator", [".", "/"]) def test_migrate_sub_group( local_store: LocalStore, separator: str, expected_v3_metadata: list[Path] ) -> None: """Test that only arrays/groups within group_1 are converted (+ no other files in store)""" create_nested_zarr(local_store, separator=separator) group_path = local_store.root / "group_1" result = runner.invoke(cli.app, ["migrate", "v3", str(group_path)]) assert result.exit_code == 0 zarr_json_paths = sorted(local_store.root.rglob("zarr.json")) expected_zarr_json_paths = [ local_store.root / p for p in expected_v3_metadata if group_path in (local_store.root / p).parents ] assert zarr_json_paths == expected_zarr_json_paths @pytest.mark.parametrize( ("compressor_v2", "compressor_v3"), [ ( numcodecs.Blosc(cname="zstd", clevel=3, shuffle=1), BloscCodec(typesize=2, cname="zstd", clevel=3, shuffle="shuffle", blocksize=0), ), (numcodecs.Zstd(level=3), ZstdCodec(level=3)), (numcodecs.GZip(level=3), GzipCodec(level=3)), ], ids=["blosc", "zstd", "gzip"], ) def test_migrate_compressor( local_store: LocalStore, compressor_v2: numcodecs.abc.Codec, compressor_v3: Codec ) -> None: zarr_array = zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), dtype="uint16", compressors=compressor_v2, zarr_format=2, fill_value=0, ) zarr_array[:] = 1 result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 assert (local_store.root / "zarr.json").exists() zarr_array = zarr.open_array(local_store.root, zarr_format=3) metadata = zarr_array.metadata assert metadata.zarr_format == 3 assert metadata.codecs == ( BytesCodec(endian="little"), compressor_v3, ) assert np.all(zarr_array[:] == 1) def test_migrate_numcodecs_compressor(local_store: LocalStore) -> None: """Test migration of a numcodecs compressor without a zarr.codecs equivalent.""" lzma_settings = { "format": lzma.FORMAT_RAW, "check": -1, "preset": None, "filters": [ {"id": lzma.FILTER_DELTA, "dist": 4}, {"id": lzma.FILTER_LZMA2, "preset": 1}, ], } zarr_array = zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), dtype="uint16", compressors=numcodecs.LZMA.from_config(lzma_settings), zarr_format=2, fill_value=0, ) zarr_array[:] = 1 result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 assert (local_store.root / "zarr.json").exists() zarr_array = zarr.open_array(local_store.root, zarr_format=3) metadata = zarr_array.metadata assert metadata.zarr_format == 3 assert metadata.codecs == ( BytesCodec(endian="little"), LZMA( format=lzma_settings["format"], check=lzma_settings["check"], preset=lzma_settings["preset"], filters=lzma_settings["filters"], ), ) assert np.all(zarr_array[:] == 1) def test_migrate_filter(local_store: LocalStore) -> None: filter_v2 = numcodecs.Delta(dtype=" None: zarr_array = zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), dtype="uint16", compressors=None, zarr_format=2, fill_value=0, order=order, ) zarr_array[:] = 1 result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 assert (local_store.root / "zarr.json").exists() zarr_array = zarr.open_array(local_store.root, zarr_format=3) metadata = zarr_array.metadata assert metadata.zarr_format == 3 assert metadata.codecs == expected_codecs assert np.all(zarr_array[:] == 1) @pytest.mark.parametrize( ("dtype", "expected_data_type", "expected_codecs"), [ ("uint8", UInt8(), (BytesCodec(endian=None),)), ("uint16", UInt16(), (BytesCodec(endian="little"),)), ], ids=["single_byte", "multi_byte"], ) def test_migrate_endian( local_store: LocalStore, dtype: str, expected_data_type: UInt8 | UInt16, expected_codecs: tuple[Codec], ) -> None: zarr_array = zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), dtype=dtype, compressors=None, zarr_format=2, fill_value=0, ) zarr_array[:] = 1 result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 assert (local_store.root / "zarr.json").exists() zarr_array = zarr.open_array(local_store.root, zarr_format=3) metadata = zarr_array.metadata assert metadata.zarr_format == 3 assert metadata.data_type == expected_data_type assert metadata.codecs == expected_codecs assert np.all(zarr_array[:] == 1) @pytest.mark.parametrize("node_type", ["array", "group"]) def test_migrate_v3(local_store: LocalStore, node_type: str) -> None: """Attempting to convert a v3 array/group should always fail""" if node_type == "array": zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), zarr_format=3, dtype="uint16" ) else: zarr.create_group(store=local_store, zarr_format=3) result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 1 assert isinstance(result.exception, TypeError) assert str(result.exception) == "Only arrays / groups with zarr v2 metadata can be converted" def test_migrate_consolidated_metadata(local_store: LocalStore) -> None: """Attempting to convert a group with consolidated metadata should always fail""" group = zarr.create_group(store=local_store, zarr_format=2) group.create_array(shape=(1,), name="a", dtype="uint8") zarr.consolidate_metadata(local_store) result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 1 assert isinstance(result.exception, NotImplementedError) assert str(result.exception) == "Migration of consolidated metadata isn't supported." def test_migrate_unknown_codec(local_store: LocalStore) -> None: """Attempting to convert a codec without a v3 equivalent should always fail""" zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), dtype="uint16", filters=[numcodecs.Categorize(labels=["a", "b"], dtype=object)], zarr_format=2, fill_value=0, ) result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 1 assert isinstance(result.exception, ValueError) assert ( str(result.exception) == "Couldn't find corresponding zarr.codecs.numcodecs codec for categorize" ) def test_migrate_incorrect_filter(local_store: LocalStore) -> None: """Attempting to convert a filter (which is the wrong type of codec) should always fail""" zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), dtype="uint16", filters=[numcodecs.Zstd(level=3)], zarr_format=2, fill_value=0, ) result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 1 assert isinstance(result.exception, TypeError) assert ( str(result.exception) == "Filter is not an ArrayArrayCodec" ) def test_migrate_incorrect_compressor(local_store: LocalStore) -> None: """Attempting to convert a compressor (which is the wrong type of codec) should always fail""" zarr.create_array( store=local_store, shape=(10, 10), chunks=(10, 10), dtype="uint16", compressors=numcodecs.Delta(dtype=" is not a BytesBytesCodec" ) @pytest.mark.parametrize("zarr_format", [2, 3]) def test_remove_metadata_fails_without_force( local_store: LocalStore, zarr_format: ZarrFormat ) -> None: """Test removing metadata (when no alternate metadata is present) fails without --force.""" create_nested_zarr(local_store, zarr_format=zarr_format) result = runner.invoke(cli.app, ["remove-metadata", f"v{zarr_format}", str(local_store.root)]) assert result.exit_code == 1 assert isinstance(result.exception, ValueError) assert str(result.exception).startswith(f"Cannot remove v{zarr_format} metadata at file") @pytest.mark.parametrize("zarr_format", [2, 3]) def test_remove_metadata_succeeds_with_force( local_store: LocalStore, zarr_format: ZarrFormat, expected_paths_no_metadata: list[Path] ) -> None: """Test removing metadata (when no alternate metadata is present) succeeds with --force.""" create_nested_zarr(local_store, zarr_format=zarr_format) result = runner.invoke( cli.app, ["remove-metadata", f"v{zarr_format}", str(local_store.root), "--force"] ) assert result.exit_code == 0 paths = sorted(local_store.root.rglob("*")) expected_paths = [local_store.root / p for p in expected_paths_no_metadata] assert paths == expected_paths def test_remove_metadata_sub_group( local_store: LocalStore, expected_paths_no_metadata: list[Path] ) -> None: """Test only v2 metadata within group_1 is removed and rest remains un-changed.""" create_nested_zarr(local_store) result = runner.invoke( cli.app, ["remove-metadata", "v2", str(local_store.root / "group_1"), "--force"] ) assert result.exit_code == 0 # check all metadata files inside group_1 are removed (.zattrs / .zgroup / .zarray should remain only inside the top # group) paths = sorted(local_store.root.rglob("*")) expected_paths = [local_store.root / p for p in expected_paths_no_metadata] expected_paths.append(local_store.root / ".zattrs") expected_paths.append(local_store.root / ".zgroup") expected_paths.append(local_store.root / "array_0" / ".zarray") expected_paths.append(local_store.root / "array_0" / ".zattrs") assert paths == sorted(expected_paths) @pytest.mark.parametrize( ("zarr_format", "expected_output_paths"), [("v2", "expected_paths_v3_metadata"), ("v3", "expected_paths_v2_metadata")], ) def test_remove_metadata_after_conversion( local_store: LocalStore, request: pytest.FixtureRequest, zarr_format: str, expected_output_paths: str, ) -> None: """Test all v2/v3 metadata can be removed after metadata conversion (all groups / arrays / metadata of other versions should remain as-is)""" create_nested_zarr(local_store) # convert v2 metadata to v3 (so now both v2 and v3 metadata present!), then remove either the v2 or v3 metadata result = runner.invoke(cli.app, ["migrate", "v3", str(local_store.root)]) assert result.exit_code == 0 result = runner.invoke(cli.app, ["remove-metadata", zarr_format, str(local_store.root)]) assert result.exit_code == 0 paths = sorted(local_store.root.rglob("*")) expected_paths = request.getfixturevalue(expected_output_paths) expected_paths = [local_store.root / p for p in expected_paths] assert paths == expected_paths @pytest.mark.parametrize("cli_command", ["migrate", "remove-metadata"]) def test_dry_run( local_store: LocalStore, cli_command: str, expected_paths_v2_metadata: list[Path] ) -> None: """Test that all files are un-changed after a dry run""" create_nested_zarr(local_store) if cli_command == "migrate": result = runner.invoke( cli.app, ["migrate", "v3", str(local_store.root), "--overwrite", "--force", "--dry-run"] ) else: result = runner.invoke( cli.app, ["remove-metadata", "v2", str(local_store.root), "--force", "--dry-run"] ) assert result.exit_code == 0 paths = sorted(local_store.root.rglob("*")) expected_paths = [local_store.root / p for p in expected_paths_v2_metadata] assert paths == expected_paths zarr-python-3.2.1/tests/test_codec_entrypoints.py000066400000000000000000000023011517635743000223270ustar00rootroot00000000000000import pytest import zarr.registry from zarr import config @pytest.mark.usefixtures("set_path") @pytest.mark.parametrize("codec_name", ["TestEntrypointCodec", "TestEntrypointGroup.Codec"]) def test_entrypoint_codec(codec_name: str) -> None: config.set({"codecs.test": f"package_with_entrypoint.{codec_name}"}) cls_test = zarr.registry.get_codec_class("test") assert cls_test.__qualname__ == codec_name @pytest.mark.usefixtures("set_path") def test_entrypoint_pipeline() -> None: config.set({"codec_pipeline.path": "package_with_entrypoint.TestEntrypointCodecPipeline"}) cls = zarr.registry.get_pipeline_class() assert cls.__name__ == "TestEntrypointCodecPipeline" @pytest.mark.usefixtures("set_path") @pytest.mark.parametrize("buffer_name", ["TestEntrypointBuffer", "TestEntrypointGroup.Buffer"]) def test_entrypoint_buffer(buffer_name: str) -> None: config.set( { "buffer": f"package_with_entrypoint.{buffer_name}", "ndbuffer": "package_with_entrypoint.TestEntrypointNDBuffer", } ) assert zarr.registry.get_buffer_class().__qualname__ == buffer_name assert zarr.registry.get_ndbuffer_class().__name__ == "TestEntrypointNDBuffer" zarr-python-3.2.1/tests/test_codec_pipeline.py000066400000000000000000000100001517635743000215310ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest import zarr from zarr.codecs import BytesCodec, CastValue from zarr.core.array import _get_chunk_spec from zarr.core.buffer.core import default_buffer_prototype from zarr.core.indexing import BasicIndexer from zarr.storage import MemoryStore @pytest.mark.parametrize( ("write_slice", "read_slice", "expected_statuses"), [ # Write all chunks, read all — all present (slice(None), slice(None), ("present", "present", "present")), # Write first chunk only, read all — first present, rest missing (slice(0, 2), slice(None), ("present", "missing", "missing")), # Write nothing, read all — all missing (None, slice(None), ("missing", "missing", "missing")), ], ) async def test_read_returns_get_results( write_slice: slice | None, read_slice: slice, expected_statuses: tuple[str, ...], ) -> None: """ Test that CodecPipeline.read returns a tuple of GetResult with correct statuses. """ store = MemoryStore() arr = zarr.open_array(store, mode="w", shape=(6,), chunks=(2,), dtype="int64", fill_value=-1) if write_slice is not None: arr[write_slice] = 0 async_arr = arr._async_array pipeline = async_arr.codec_pipeline metadata = async_arr.metadata prototype = default_buffer_prototype() config = async_arr.config indexer = BasicIndexer( read_slice, shape=metadata.shape, chunk_grid=async_arr._chunk_grid, ) out_buffer = prototype.nd_buffer.empty( shape=indexer.shape, dtype=metadata.dtype.to_native_dtype(), order=config.order, ) results = await pipeline.read( [ ( async_arr.store_path / metadata.encode_chunk_key(chunk_coords), _get_chunk_spec(metadata, async_arr._chunk_grid, chunk_coords, config, prototype), chunk_selection, out_selection, is_complete_chunk, ) for chunk_coords, chunk_selection, out_selection, is_complete_chunk in indexer ], out_buffer, drop_axes=indexer.drop_axes, ) assert len(results) == len(expected_statuses) for result, expected_status in zip(results, expected_statuses, strict=True): assert result["status"] == expected_status try: import cast_value_rs # noqa: F401 _HAS_CAST_VALUE_RS = True except ModuleNotFoundError: _HAS_CAST_VALUE_RS = False requires_cast_value_rs = pytest.mark.skipif( not _HAS_CAST_VALUE_RS, reason="cast-value-rs not installed" ) @requires_cast_value_rs @pytest.mark.parametrize( ("source_dtype", "target_dtype"), [ # Source is single-byte (no endianness); target is multi-byte (has endianness). # Without the fix, BytesCodec.evolve_from_array_spec sees the source dtype, # strips its `endian` to None, and then chokes when the chunk_spec dtype # gets transformed to the multi-byte target before bytes-decoding. ("int8", "int16"), ("uint8", "int32"), ("int8", "float32"), # Source is multi-byte; target is single-byte (the reverse direction also # exercises the spec-threading logic). ("int16", "int8"), ], ) def test_codec_pipeline_threads_dtype_through_evolve(source_dtype: str, target_dtype: str) -> None: """Regression for #3937: each codec must be evolved against the spec it will see at runtime, not the original array spec. cast_value transforms the dtype between AA codecs and the array->bytes serializer.""" arr = zarr.create_array( store={}, shape=(4,), chunks=(4,), dtype=source_dtype, fill_value=0, filters=[CastValue(data_type=target_dtype)], serializer=BytesCodec(endian="little"), compressors=[], zarr_format=3, overwrite=True, ) arr[:] = np.asarray([0, 1, 2, 3], dtype=source_dtype) np.testing.assert_array_equal(arr[:], np.asarray([0, 1, 2, 3], dtype=source_dtype)) zarr-python-3.2.1/tests/test_codecs/000077500000000000000000000000001517635743000174665ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_codecs/__init__.py000066400000000000000000000000001517635743000215650ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_codecs/conftest.py000066400000000000000000000006331517635743000216670ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass @dataclass(frozen=True) class Expect[TIn, TOut]: """Model an input and an expected output value for a test case.""" input: TIn expected: TOut @dataclass(frozen=True) class ExpectErr[TIn]: """Model an input and an expected error message for a test case.""" input: TIn msg: str exception_cls: type[Exception] zarr-python-3.2.1/tests/test_codecs/test_blosc.py000066400000000000000000000113371517635743000222060ustar00rootroot00000000000000import json import numcodecs import numpy as np import pytest from packaging.version import Version import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.codecs import BloscCodec from zarr.codecs.blosc import BloscShuffle, Shuffle from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import UInt16, get_data_type_from_native_dtype from zarr.storage import MemoryStore, StorePath @pytest.mark.parametrize("dtype", ["uint8", "uint16"]) async def test_blosc_evolve(dtype: str) -> None: typesize = np.dtype(dtype).itemsize path = "blosc_evolve" store = MemoryStore() spath = StorePath(store, path) zarr.create_array( spath, shape=(16, 16), chunks=(16, 16), dtype=dtype, fill_value=0, compressors=BloscCodec(), ) buf = await store.get(f"{path}/zarr.json", prototype=default_buffer_prototype()) assert buf is not None zarr_json = json.loads(buf.to_bytes()) blosc_configuration_json = zarr_json["codecs"][1]["configuration"] assert blosc_configuration_json["typesize"] == typesize if typesize == 1: assert blosc_configuration_json["shuffle"] == "bitshuffle" else: assert blosc_configuration_json["shuffle"] == "shuffle" path2 = "blosc_evolve_sharding" spath2 = StorePath(store, path2) zarr.create_array( spath2, shape=(16, 16), chunks=(16, 16), shards=(16, 16), dtype=dtype, fill_value=0, compressors=BloscCodec(), ) buf = await store.get(f"{path2}/zarr.json", prototype=default_buffer_prototype()) assert buf is not None zarr_json = json.loads(buf.to_bytes()) blosc_configuration_json = zarr_json["codecs"][0]["configuration"]["codecs"][1]["configuration"] assert blosc_configuration_json["typesize"] == typesize if typesize == 1: assert blosc_configuration_json["shuffle"] == "bitshuffle" else: assert blosc_configuration_json["shuffle"] == "shuffle" @pytest.mark.parametrize("shuffle", [None, "bitshuffle", BloscShuffle.shuffle]) @pytest.mark.parametrize("typesize", [None, 1, 2]) def test_tunable_attrs_param(shuffle: None | Shuffle | BloscShuffle, typesize: None | int) -> None: """ Test that the tunable_attrs parameter is set as expected when creating a BloscCodec, """ codec = BloscCodec(typesize=typesize, shuffle=shuffle) if shuffle is None: assert codec.shuffle == BloscShuffle.bitshuffle # default shuffle assert "shuffle" in codec._tunable_attrs if typesize is None: assert codec.typesize == 1 # default typesize assert "typesize" in codec._tunable_attrs new_dtype = UInt16() array_spec = ArraySpec( shape=(1,), dtype=new_dtype, fill_value=1, prototype=default_buffer_prototype(), config={}, # type: ignore[arg-type] ) evolved_codec = codec.evolve_from_array_spec(array_spec=array_spec) if typesize is None: assert evolved_codec.typesize == new_dtype.item_size else: assert evolved_codec.typesize == codec.typesize if shuffle is None: assert evolved_codec.shuffle == BloscShuffle.shuffle else: assert evolved_codec.shuffle == codec.shuffle async def test_typesize() -> None: a = np.arange(1000000, dtype=np.uint64) codecs = [zarr.codecs.BytesCodec(), zarr.codecs.BloscCodec()] z = zarr.array(a, chunks=(10000), codecs=codecs) data = await z.store.get("c/0", prototype=default_buffer_prototype()) assert data is not None bytes = data.to_bytes() size = len(bytes) msg = f"Blosc size mismatch. First 10 bytes: {bytes[:20]!r} and last 10 bytes: {bytes[-20:]!r}" if Version(numcodecs.__version__) >= Version("0.16.0"): expected_size = 402 assert size == expected_size, msg else: expected_size = 10216 assert size == expected_size, msg def test_blosc_codec_supports_sync() -> None: assert isinstance(BloscCodec(), SupportsSyncCodec) def test_blosc_codec_sync_roundtrip() -> None: codec = BloscCodec(typesize=8) arr = np.arange(100, dtype="float64") zdtype = get_data_type_from_native_dtype(arr.dtype) spec = ArraySpec( shape=arr.shape, dtype=zdtype, fill_value=zdtype.cast_scalar(0), config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) buf = default_buffer_prototype().buffer.from_array_like(arr.view("B")) encoded = codec._encode_sync(buf, spec) assert encoded is not None decoded = codec._decode_sync(encoded, spec) result = np.frombuffer(decoded.as_numpy_array(), dtype="float64") np.testing.assert_array_equal(arr, result) zarr-python-3.2.1/tests/test_codecs/test_cast_value.py000066400000000000000000000345351517635743000232370ustar00rootroot00000000000000from __future__ import annotations from typing import Any import numpy as np import pytest import zarr from tests.test_codecs.conftest import Expect, ExpectErr from zarr.codecs.cast_value import CastValue try: import cast_value_rs # noqa: F401 _HAS_CAST_VALUE_RS = True except ModuleNotFoundError: _HAS_CAST_VALUE_RS = False requires_cast_value_rs = pytest.mark.skipif( not _HAS_CAST_VALUE_RS, reason="cast-value-rs not installed" ) # --------------------------------------------------------------------------- # Serialization # --------------------------------------------------------------------------- @pytest.mark.parametrize( "case", [ Expect( input=CastValue(data_type="uint8"), expected={"name": "cast_value", "configuration": {"data_type": "uint8"}}, ), Expect( input=CastValue( data_type="uint8", rounding="towards-zero", out_of_range="clamp", scalar_map={"encode": [("NaN", 0)]}, ), expected={ "name": "cast_value", "configuration": { "data_type": "uint8", "rounding": "towards-zero", "out_of_range": "clamp", "scalar_map": {"encode": [("NaN", 0)]}, }, }, ), ], ids=["minimal", "full"], ) def test_to_dict(case: Expect[CastValue, dict[str, Any]]) -> None: """to_dict produces the expected JSON structure.""" assert case.input.to_dict() == case.expected @pytest.mark.parametrize( "case", [ Expect( input={"name": "cast_value", "configuration": {"data_type": "float32"}}, expected=("float32", "nearest-even", None), ), Expect( input={ "name": "cast_value", "configuration": { "data_type": "int16", "rounding": "towards-zero", "out_of_range": "clamp", }, }, expected=("int16", "towards-zero", "clamp"), ), ], ids=["defaults", "explicit"], ) def test_from_dict(case: Expect[dict[str, Any], tuple[str, str, str | None]]) -> None: """from_dict deserializes configuration with correct values and defaults.""" codec = CastValue.from_dict(case.input) dtype_name, rounding, out_of_range = case.expected assert codec.dtype.to_native_dtype() == np.dtype(dtype_name) assert codec.rounding == rounding assert codec.out_of_range == out_of_range @pytest.mark.parametrize( "codec", [ CastValue(data_type="int16", rounding="towards-zero", out_of_range="clamp"), CastValue( data_type="uint8", out_of_range="clamp", scalar_map={"encode": [("NaN", 0)], "decode": [(0, "NaN")]}, ), ], ids=["no-scalar-map", "with-scalar-map"], ) def test_serialization_roundtrip(codec: CastValue) -> None: """to_dict followed by from_dict produces an equal codec.""" restored = CastValue.from_dict(codec.to_dict()) assert codec == restored # --------------------------------------------------------------------------- # Construction # --------------------------------------------------------------------------- def test_construction_accepts_zdtype_object() -> None: """data_type can be a ZDType instance, not just a JSON string.""" from zarr.core.dtype import UInt8 codec = CastValue(data_type=UInt8()) assert codec.dtype.to_native_dtype() == np.dtype("uint8") def test_construction_rejects_invalid_target_dtype() -> None: """Construction rejects target dtypes not in PERMITTED_DATA_TYPE_NAMES.""" with pytest.raises(ValueError, match="Invalid target data type"): CastValue(data_type="complex64") # --------------------------------------------------------------------------- # Validation # --------------------------------------------------------------------------- @pytest.mark.parametrize( "case", [ ExpectErr( input={"dtype": "complex128", "target": "float64"}, msg="only supports integer and floating-point", exception_cls=ValueError, ), ExpectErr( input={"dtype": "int32", "target": "float64", "out_of_range": "wrap"}, msg="only valid for integer", exception_cls=ValueError, ), ], ids=["complex-source", "wrap-float-target"], ) def test_validation_rejects_invalid(case: ExpectErr[dict[str, Any]]) -> None: """Invalid dtype or out_of_range combinations are rejected at array creation.""" with pytest.raises(case.exception_cls, match=case.msg): zarr.create_array( store={}, shape=(10,), dtype=case.input["dtype"], chunks=(10,), filters=[ CastValue( data_type=case.input["target"], out_of_range=case.input.get("out_of_range"), ) ], compressors=None, fill_value=0, ) @requires_cast_value_rs @pytest.mark.parametrize( ("source_dtype", "target_dtype"), [ ("float16", "int8"), ("float32", "int32"), ("float64", "int64"), ("int32", "uint8"), ], ) def test_validation_accepts_wrap_with_integer_target(source_dtype: str, target_dtype: str) -> None: """Regression for #3936: `out_of_range="wrap"` is permitted when the cast TARGET (not the source array dtype) is an integer type.""" zarr.create_array( store={}, shape=(1,), dtype=source_dtype, chunks=(1,), filters=[CastValue(data_type=target_dtype, out_of_range="wrap")], compressors=None, fill_value=0, ) def test_zero_itemsize_raises() -> None: """Variable-length dtypes (itemsize=0) are rejected by compute_encoded_size.""" from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype.npy.string import VariableLengthUTF8 codec = CastValue(data_type="uint8") spec = ArraySpec( shape=(10,), dtype=VariableLengthUTF8(), # type: ignore[arg-type] fill_value="", config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) with pytest.raises(ValueError, match="fixed-size integer and floating-point data types"): codec.compute_encoded_size(100, spec) # --------------------------------------------------------------------------- # Encode / decode # --------------------------------------------------------------------------- @requires_cast_value_rs @pytest.mark.parametrize( "case", [ Expect(input=("float64", "float32"), expected=np.arange(50, dtype="float64")), Expect(input=("float32", "float64"), expected=np.arange(50, dtype="float32")), Expect(input=("int32", "int64"), expected=np.arange(50, dtype="int32")), Expect(input=("int64", "int16"), expected=np.arange(50, dtype="int64")), Expect(input=("float64", "int32"), expected=np.arange(50, dtype="float64")), Expect(input=("int32", "float64"), expected=np.arange(50, dtype="int32")), ], ids=["f64→f32", "f32→f64", "i32→i64", "i64→i16", "f64→i32", "i32→f64"], ) def test_encode_decode_roundtrip( case: Expect[tuple[str, str], np.ndarray[Any, np.dtype[Any]]], ) -> None: """Small integer data survives encode → decode for each dtype pair.""" import zarr source_dtype, target_dtype = case.input arr = zarr.create_array( store={}, shape=(50,), dtype=source_dtype, chunks=(50,), filters=[CastValue(data_type=target_dtype)], compressors=None, fill_value=0, ) arr[:] = case.expected np.testing.assert_array_equal(arr[:], case.expected) @requires_cast_value_rs @pytest.mark.parametrize( "case", [ Expect( input=np.array([1.7, -1.7, 2.5, -2.5], dtype="float64"), expected=np.array([1, -1, 2, -2], dtype="float64"), ), ], ids=["towards-zero"], ) def test_float_to_int_rounding( case: Expect[np.ndarray[Any, np.dtype[Any]], np.ndarray[Any, np.dtype[Any]]], ) -> None: """Fractional float values are truncated towards zero when cast to int32.""" import zarr arr = zarr.create_array( store={}, shape=case.input.shape, dtype=case.input.dtype, chunks=case.input.shape, filters=[CastValue(data_type="int32", rounding="towards-zero", out_of_range="clamp")], compressors=None, fill_value=0, ) arr[:] = case.input np.testing.assert_array_equal(arr[:], case.expected) @requires_cast_value_rs @pytest.mark.parametrize( "case", [ Expect( input=np.array([0, 200, -200], dtype="int32"), expected=np.array([0, 127, -128], dtype="int32"), ), ], ids=["int32→int8"], ) def test_out_of_range_clamp( case: Expect[np.ndarray[Any, np.dtype[Any]], np.ndarray[Any, np.dtype[Any]]], ) -> None: """Values outside the int8 range are clamped to [-128, 127].""" import zarr arr = zarr.create_array( store={}, shape=case.input.shape, dtype=case.input.dtype, chunks=case.input.shape, filters=[CastValue(data_type="int8", out_of_range="clamp")], compressors=None, fill_value=0, ) arr[:] = case.input np.testing.assert_array_equal(arr[:], case.expected) def test_compute_encoded_size() -> None: """compute_encoded_size correctly scales byte length by itemsize ratio.""" from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import get_data_type_from_json codec = CastValue(data_type="int16") spec = ArraySpec( shape=(10,), dtype=get_data_type_from_json("float64", zarr_format=3), fill_value=0, config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) # 10 float64 elements = 80 bytes -> 10 int16 elements = 20 bytes assert codec.compute_encoded_size(80, spec) == 20 @requires_cast_value_rs def test_scalar_map_encode_decode_roundtrip() -> None: """Scalar map entries are applied during encode and decode.""" import zarr data = np.array([1.0, float("nan"), 3.0], dtype="float64") arr = zarr.create_array( store={}, shape=data.shape, dtype="float64", chunks=data.shape, filters=[ CastValue( data_type="int32", rounding="nearest-even", out_of_range="clamp", scalar_map={"encode": [("NaN", -999)], "decode": [(-999, "NaN")]}, ), ], compressors=None, fill_value=1, ) arr[:] = data result = np.asarray(arr[:]) np.testing.assert_equal(result[0], 1.0) np.testing.assert_equal(result[2], 3.0) assert np.isnan(result[1]) @pytest.mark.parametrize( "case", [ ExpectErr( input={ "dtype": "int32", "target": "int8", "scalar_map": {"encode": [("NaN", 0)]}, }, msg="not representable in dtype int32", exception_cls=ValueError, ), ExpectErr( input={ "dtype": "int32", "target": "float64", "scalar_map": {"decode": [(0, "NaN")]}, }, msg="not representable in dtype int32", exception_cls=ValueError, ), ExpectErr( input={ "dtype": "float64", "target": "int8", "scalar_map": {"encode": [("NaN", 999)]}, }, msg="not representable in dtype int8", exception_cls=ValueError, ), ExpectErr( input={ "dtype": "float64", "target": "int8", "scalar_map": {"encode": [("NaN", 1.5)]}, }, msg="not representable in dtype int8", exception_cls=ValueError, ), ], ids=[ "nan-key-for-int-source", "nan-value-for-int-decode-target", "encode-value-out-of-range", "encode-value-not-integer", ], ) def test_scalar_map_validation_rejects_invalid(case: ExpectErr[dict[str, Any]]) -> None: """Invalid scalar_map entries are rejected at array creation.""" import zarr with pytest.raises(case.exception_cls, match=case.msg): zarr.create_array( store={}, shape=(10,), dtype=case.input["dtype"], chunks=(10,), filters=[ CastValue( data_type=case.input["target"], out_of_range="clamp", scalar_map=case.input["scalar_map"], ) ], compressors=None, fill_value=0, ) @requires_cast_value_rs def test_combined_with_scale_offset() -> None: """scale_offset followed by cast_value compresses float64 into int16 and round-trips.""" import zarr from zarr.codecs.scale_offset import ScaleOffset arr = zarr.create_array( store={}, shape=(100,), dtype="float64", chunks=(100,), filters=[ ScaleOffset(offset=0, scale=10), CastValue(data_type="int16", rounding="nearest-even", out_of_range="clamp"), ], compressors=None, fill_value=0, ) data = np.arange(100, dtype="float64") * 0.1 arr[:] = data result = arr[:] np.testing.assert_array_almost_equal(result, data, decimal=1) # type: ignore[arg-type] @pytest.mark.parametrize( "case", [ Expect( input={"encode": [("NaN", 0)]}, expected={"encode": {"NaN": 0}}, ), Expect( input={"encode": [("NaN", 0)], "decode": [(0, "NaN")]}, expected={"encode": {"NaN": 0}, "decode": {0: "NaN"}}, ), Expect( input={"encode": {"NaN": 0}}, expected={"encode": {"NaN": 0}}, ), ], ids=["encode-only", "both-directions", "already-normalized"], ) def test_parse_scalar_map(case: Expect[Any, Any]) -> None: from zarr.codecs.cast_value import parse_scalar_map assert parse_scalar_map(case.input) == case.expected zarr-python-3.2.1/tests/test_codecs/test_codecs.py000066400000000000000000000306541517635743000223470ustar00rootroot00000000000000from __future__ import annotations import json from dataclasses import dataclass from typing import TYPE_CHECKING, Any import numpy as np import pytest import zarr import zarr.api import zarr.api.asynchronous from zarr import Array, AsyncArray, config from zarr.codecs import ( BytesCodec, GzipCodec, ShardingCodec, TransposeCodec, ) from zarr.core.buffer import default_buffer_prototype from zarr.core.indexing import BasicSelection, decode_morton, morton_order_iter from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.dtype import UInt8 from zarr.errors import ZarrUserWarning from zarr.storage import StorePath if TYPE_CHECKING: from zarr.abc.codec import Codec from zarr.abc.store import Store from zarr.core.buffer.core import NDArrayLikeOrScalar from zarr.core.common import MemoryOrder from zarr.types import AnyAsyncArray @dataclass(frozen=True) class _AsyncArrayProxy: array: AnyAsyncArray def __getitem__(self, selection: BasicSelection) -> _AsyncArraySelectionProxy: return _AsyncArraySelectionProxy(self.array, selection) @dataclass(frozen=True) class _AsyncArraySelectionProxy: array: AnyAsyncArray selection: BasicSelection async def get(self) -> NDArrayLikeOrScalar: return await self.array.getitem(self.selection) async def set(self, value: np.ndarray[Any, Any]) -> None: return await self.array.setitem(self.selection, value) def order_from_dim(order: MemoryOrder, ndim: int) -> tuple[int, ...]: if order == "F": return tuple(ndim - x - 1 for x in range(ndim)) else: return tuple(range(ndim)) def test_sharding_pickle() -> None: """ Test that sharding codecs can be pickled """ @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize("input_order", ["F", "C"]) @pytest.mark.parametrize("store_order", ["F", "C"]) @pytest.mark.parametrize("runtime_write_order", ["F", "C"]) @pytest.mark.parametrize("runtime_read_order", ["F", "C"]) @pytest.mark.parametrize("with_sharding", [True, False]) async def test_order( store: Store, input_order: MemoryOrder, store_order: MemoryOrder, runtime_write_order: MemoryOrder, runtime_read_order: MemoryOrder, with_sharding: bool, ) -> None: data = np.arange(0, 256, dtype="uint16").reshape((32, 8), order=input_order) path = "order" spath = StorePath(store, path=path) a = await zarr.api.asynchronous.create_array( spath, shape=data.shape, chunks=(16, 8) if with_sharding else (32, 8), shards=(32, 8) if with_sharding else None, dtype=data.dtype, fill_value=0, chunk_key_encoding={"name": "v2", "separator": "."}, filters=[TransposeCodec(order=order_from_dim(store_order, data.ndim))], config={"order": runtime_write_order}, ) await _AsyncArrayProxy(a)[:, :].set(data) read_data = await _AsyncArrayProxy(a)[:, :].get() assert np.array_equal(data, read_data) with config.set({"array.order": runtime_read_order}): a = await AsyncArray.open( spath, ) read_data = await _AsyncArrayProxy(a)[:, :].get() assert np.array_equal(data, read_data) assert isinstance(read_data, np.ndarray) if runtime_read_order == "F": assert read_data.flags["F_CONTIGUOUS"] assert not read_data.flags["C_CONTIGUOUS"] else: assert not read_data.flags["F_CONTIGUOUS"] assert read_data.flags["C_CONTIGUOUS"] @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize("input_order", ["F", "C"]) @pytest.mark.parametrize("runtime_write_order", ["F", "C"]) @pytest.mark.parametrize("runtime_read_order", ["F", "C"]) @pytest.mark.parametrize("with_sharding", [True, False]) def test_order_implicit( store: Store, input_order: MemoryOrder, runtime_write_order: MemoryOrder, runtime_read_order: MemoryOrder, with_sharding: bool, ) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16), order=input_order) path = "order_implicit" spath = StorePath(store, path) with config.set({"array.order": runtime_write_order}): a = zarr.create_array( spath, shape=data.shape, chunks=(8, 8) if with_sharding else (16, 16), shards=(16, 16) if with_sharding else None, dtype=data.dtype, fill_value=0, ) a[:, :] = data with config.set({"array.order": runtime_read_order}): a = Array.open(spath) read_data = a[:, :] assert np.array_equal(data, read_data) assert isinstance(read_data, np.ndarray) if runtime_read_order == "F": assert read_data.flags["F_CONTIGUOUS"] assert not read_data.flags["C_CONTIGUOUS"] else: assert not read_data.flags["F_CONTIGUOUS"] assert read_data.flags["C_CONTIGUOUS"] @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_open(store: Store) -> None: spath = StorePath(store) a = zarr.create_array( spath, shape=(16, 16), chunks=(16, 16), dtype="int32", fill_value=0, ) b = Array.open(spath) assert a.metadata == b.metadata def test_morton_exact_order() -> None: """Test exact morton ordering for power-of-2 shapes.""" assert list(morton_order_iter((2, 2))) == [(0, 0), (1, 0), (0, 1), (1, 1)] assert list(morton_order_iter((2, 2, 2))) == [ (0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0), (0, 0, 1), (1, 0, 1), (0, 1, 1), (1, 1, 1), ] assert list(morton_order_iter((2, 2, 2, 2))) == [ (0, 0, 0, 0), (1, 0, 0, 0), (0, 1, 0, 0), (1, 1, 0, 0), (0, 0, 1, 0), (1, 0, 1, 0), (0, 1, 1, 0), (1, 1, 1, 0), (0, 0, 0, 1), (1, 0, 0, 1), (0, 1, 0, 1), (1, 1, 0, 1), (0, 0, 1, 1), (1, 0, 1, 1), (0, 1, 1, 1), (1, 1, 1, 1), ] @pytest.mark.parametrize( "shape", [ (2, 2, 2), (5, 2), (2, 5), (2, 9, 2), (3, 2, 12), (2, 5, 1), (4, 3, 6, 2, 7), (3, 2, 1, 6, 4, 5, 2), (1,), (1, 1), (5, 1, 3), (1, 4, 1, 2), (5, 5, 5), # triggers argsort strategy (n_z/n_total > 4) ], ) def test_morton_is_permutation(shape: tuple[int, ...]) -> None: """Test that morton_order_iter produces every valid coordinate exactly once.""" import itertools from zarr.core.common import product order = list(morton_order_iter(shape)) expected_len = product(shape) # completeness: every valid coordinate is present assert len(order) == expected_len # no duplicates assert len(set(order)) == expected_len # all coordinates are within bounds assert all(all(c < s for c, s in zip(coord, shape, strict=True)) for coord in order) # the set of coordinates equals the full cartesian product assert set(order) == set(itertools.product(*(range(s) for s in shape))) @pytest.mark.parametrize( "shape", [ (2, 2), (4, 4), (2, 2, 2), (4, 4, 4), (2, 2, 2, 2), ], ) def test_morton_ordering(shape: tuple[int, ...]) -> None: """Test that the iteration order matches consecutive decode_morton outputs. For power-of-2 shapes, every decode_morton output is in-bounds, so the ordering should be exactly decode_morton(0), decode_morton(1), ... """ order = list(morton_order_iter(shape)) for i, coord in enumerate(order): assert coord == decode_morton(i, shape) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_write_partial_chunks(store: Store) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16)) spath = StorePath(store) a = zarr.create_array( spath, shape=data.shape, chunks=(20, 20), dtype=data.dtype, fill_value=1, ) a[0:16, 0:16] = data assert np.array_equal(a[0:16, 0:16], data) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) async def test_delete_empty_chunks(store: Store) -> None: data = np.ones((16, 16)) path = "delete_empty_chunks" spath = StorePath(store, path) a = await zarr.api.asynchronous.create_array( spath, shape=data.shape, chunks=(32, 32), dtype=data.dtype, fill_value=1, ) await _AsyncArrayProxy(a)[:16, :16].set(np.zeros((16, 16))) await _AsyncArrayProxy(a)[:16, :16].set(data) assert np.array_equal(await _AsyncArrayProxy(a)[:16, :16].get(), data) assert await store.get(f"{path}/c0/0", prototype=default_buffer_prototype()) is None @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) async def test_dimension_names(store: Store) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16)) path = "dimension_names" spath = StorePath(store, path) await zarr.api.asynchronous.create_array( spath, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, dimension_names=("x", "y"), ) assert isinstance( meta := (await zarr.api.asynchronous.open_array(store=spath)).metadata, ArrayV3Metadata ) assert meta.dimension_names == ( "x", "y", ) path2 = "dimension_names2" spath2 = StorePath(store, path2) await zarr.api.asynchronous.create_array( spath2, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, ) assert isinstance(meta := (await AsyncArray.open(spath2)).metadata, ArrayV3Metadata) assert meta.dimension_names is None zarr_json_buffer = await store.get(f"{path2}/zarr.json", prototype=default_buffer_prototype()) assert zarr_json_buffer is not None assert "dimension_names" not in json.loads(zarr_json_buffer.to_bytes()) @pytest.mark.parametrize( "codecs", [ (BytesCodec(), TransposeCodec(order=order_from_dim("F", 2))), (TransposeCodec(order=order_from_dim("F", 2)),), ], ) def test_invalid_metadata(codecs: tuple[Codec, ...]) -> None: shape = (16,) chunks = (16,) data_type = UInt8() with pytest.raises(ValueError, match="The `order` tuple must have as many entries"): ArrayV3Metadata( shape=shape, chunk_grid={"name": "regular", "configuration": {"chunk_shape": chunks}}, chunk_key_encoding={"name": "default", "configuration": {"separator": "/"}}, fill_value=0, data_type=data_type, codecs=codecs, attributes={}, dimension_names=None, ) def test_invalid_metadata_create_array() -> None: with pytest.warns( ZarrUserWarning, match="codec disables partial reads and writes, which may lead to inefficient performance", ): zarr.create_array( {}, shape=(16, 16), chunks=(16, 16), dtype=np.dtype("uint8"), fill_value=0, serializer=ShardingCodec(chunk_shape=(8, 8)), compressors=[ GzipCodec(), ], ) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) async def test_resize(store: Store) -> None: data = np.zeros((16, 18), dtype="uint16") path = "resize" spath = StorePath(store, path) a = await zarr.api.asynchronous.create_array( spath, shape=data.shape, chunks=(10, 10), dtype=data.dtype, chunk_key_encoding={"name": "v2", "separator": "."}, fill_value=1, ) await _AsyncArrayProxy(a)[:16, :18].set(data) assert await store.get(f"{path}/1.1", prototype=default_buffer_prototype()) is not None assert await store.get(f"{path}/0.0", prototype=default_buffer_prototype()) is not None assert await store.get(f"{path}/0.1", prototype=default_buffer_prototype()) is not None assert await store.get(f"{path}/1.0", prototype=default_buffer_prototype()) is not None await a.resize((10, 12)) assert a.metadata.shape == (10, 12) assert a.shape == (10, 12) assert await store.get(f"{path}/0.0", prototype=default_buffer_prototype()) is not None assert await store.get(f"{path}/0.1", prototype=default_buffer_prototype()) is not None assert await store.get(f"{path}/1.0", prototype=default_buffer_prototype()) is None assert await store.get(f"{path}/1.1", prototype=default_buffer_prototype()) is None zarr-python-3.2.1/tests/test_codecs/test_crc32c.py000066400000000000000000000021611517635743000221560ustar00rootroot00000000000000from __future__ import annotations import numpy as np from zarr.abc.codec import SupportsSyncCodec from zarr.codecs.crc32c_ import Crc32cCodec from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import get_data_type_from_native_dtype def test_crc32c_codec_supports_sync() -> None: assert isinstance(Crc32cCodec(), SupportsSyncCodec) def test_crc32c_codec_sync_roundtrip() -> None: codec = Crc32cCodec() arr = np.arange(100, dtype="float64") zdtype = get_data_type_from_native_dtype(arr.dtype) spec = ArraySpec( shape=arr.shape, dtype=zdtype, fill_value=zdtype.cast_scalar(0), config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) buf = default_buffer_prototype().buffer.from_array_like(arr.view("B")) encoded = codec._encode_sync(buf, spec) assert encoded is not None decoded = codec._decode_sync(encoded, spec) result = np.frombuffer(decoded.as_numpy_array(), dtype="float64") np.testing.assert_array_equal(arr, result) zarr-python-3.2.1/tests/test_codecs/test_endian.py000066400000000000000000000060541517635743000223420ustar00rootroot00000000000000from typing import Literal import numpy as np import pytest import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.abc.store import Store from zarr.codecs import BytesCodec from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import NDBuffer, default_buffer_prototype from zarr.core.dtype import get_data_type_from_native_dtype from zarr.storage import StorePath from .test_codecs import _AsyncArrayProxy @pytest.mark.filterwarnings("ignore:The endianness of the requested serializer") @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize("endian", ["big", "little"]) async def test_endian(store: Store, endian: Literal["big", "little"]) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16)) path = "endian" spath = StorePath(store, path) a = await zarr.api.asynchronous.create_array( spath, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, chunk_key_encoding={"name": "v2", "separator": "."}, serializer=BytesCodec(endian=endian), ) await _AsyncArrayProxy(a)[:, :].set(data) readback_data = await _AsyncArrayProxy(a)[:, :].get() assert np.array_equal(data, readback_data) def test_bytes_codec_supports_sync() -> None: assert isinstance(BytesCodec(), SupportsSyncCodec) def test_bytes_codec_sync_roundtrip() -> None: codec = BytesCodec() arr = np.arange(100, dtype="float64") zdtype = get_data_type_from_native_dtype(arr.dtype) spec = ArraySpec( shape=arr.shape, dtype=zdtype, fill_value=zdtype.cast_scalar(0), config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) nd_buf: NDBuffer = default_buffer_prototype().nd_buffer.from_numpy_array(arr) codec = codec.evolve_from_array_spec(spec) encoded = codec._encode_sync(nd_buf, spec) assert encoded is not None decoded = codec._decode_sync(encoded, spec) np.testing.assert_array_equal(arr, decoded.as_numpy_array()) @pytest.mark.filterwarnings("ignore:The endianness of the requested serializer") @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize("dtype_input_endian", [">u2", "u2", " None: data = np.arange(0, 256, dtype=dtype_input_endian).reshape((16, 16)) path = "endian" spath = StorePath(store, path) a = await zarr.api.asynchronous.create_array( spath, shape=data.shape, chunks=(16, 16), dtype="uint16", fill_value=0, chunk_key_encoding={"name": "v2", "separator": "."}, serializer=BytesCodec(endian=dtype_store_endian), ) await _AsyncArrayProxy(a)[:, :].set(data) readback_data = await _AsyncArrayProxy(a)[:, :].get() assert np.array_equal(data, readback_data) zarr-python-3.2.1/tests/test_codecs/test_gzip.py000066400000000000000000000031161517635743000220510ustar00rootroot00000000000000import numpy as np import pytest import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.abc.store import Store from zarr.codecs import GzipCodec from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import get_data_type_from_native_dtype from zarr.storage import StorePath @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_gzip(store: Store) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16)) a = zarr.create_array( StorePath(store), shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, compressors=GzipCodec(), ) a[:, :] = data assert np.array_equal(data, a[:, :]) def test_gzip_codec_supports_sync() -> None: assert isinstance(GzipCodec(), SupportsSyncCodec) def test_gzip_codec_sync_roundtrip() -> None: codec = GzipCodec(level=1) arr = np.arange(100, dtype="float64") zdtype = get_data_type_from_native_dtype(arr.dtype) spec = ArraySpec( shape=arr.shape, dtype=zdtype, fill_value=zdtype.cast_scalar(0), config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) buf = default_buffer_prototype().buffer.from_array_like(arr.view("B")) encoded = codec._encode_sync(buf, spec) assert encoded is not None decoded = codec._decode_sync(encoded, spec) result = np.frombuffer(decoded.as_numpy_array(), dtype="float64") np.testing.assert_array_equal(arr, result) zarr-python-3.2.1/tests/test_codecs/test_numcodecs.py000066400000000000000000000236371517635743000230720ustar00rootroot00000000000000from __future__ import annotations import contextlib import pickle from typing import TYPE_CHECKING, Any import numpy as np import pytest from numcodecs import GZip try: from numcodecs.errors import UnknownCodecError except ImportError: # Older versions of numcodecs don't have a separate errors module UnknownCodecError = ValueError from zarr import config, create_array, open_array from zarr.abc.numcodec import _is_numcodec, _is_numcodec_cls from zarr.codecs import numcodecs as _numcodecs from zarr.registry import get_codec_class, get_numcodec if TYPE_CHECKING: from collections.abc import Iterator @contextlib.contextmanager def codec_conf() -> Iterator[Any]: base_conf = config.get("codecs") new_conf = { "numcodecs.bz2": "zarr.codecs.numcodecs.BZ2", "numcodecs.crc32": "zarr.codecs.numcodecs.CRC32", "numcodecs.crc32c": "zarr.codecs.numcodecs.CRC32C", "numcodecs.lz4": "zarr.codecs.numcodecs.LZ4", "numcodecs.lzma": "zarr.codecs.numcodecs.LZMA", "numcodecs.zfpy": "zarr.codecs.numcodecs.ZFPY", "numcodecs.adler32": "zarr.codecs.numcodecs.Adler32", "numcodecs.astype": "zarr.codecs.numcodecs.AsType", "numcodecs.bitround": "zarr.codecs.numcodecs.BitRound", "numcodecs.blosc": "zarr.codecs.numcodecs.Blosc", "numcodecs.delta": "zarr.codecs.numcodecs.Delta", "numcodecs.fixedscaleoffset": "zarr.codecs.numcodecs.FixedScaleOffset", "numcodecs.fletcher32": "zarr.codecs.numcodecs.Fletcher32", "numcodecs.gzip": "zarr.codecs.numcodecs.GZip", "numcodecs.jenkinslookup3": "zarr.codecs.numcodecs.JenkinsLookup3", "numcodecs.pcodec": "zarr.codecs.numcodecs.PCodec", "numcodecs.packbits": "zarr.codecs.numcodecs.PackBits", "numcodecs.shuffle": "zarr.codecs.numcodecs.Shuffle", "numcodecs.quantize": "zarr.codecs.numcodecs.Quantize", "numcodecs.zlib": "zarr.codecs.numcodecs.Zlib", "numcodecs.zstd": "zarr.codecs.numcodecs.Zstd", } yield config.set({"codecs": new_conf | base_conf}) if TYPE_CHECKING: from zarr.core.common import JSON def test_get_numcodec() -> None: assert get_numcodec({"id": "gzip", "level": 2}) == GZip(level=2) # type: ignore[typeddict-unknown-key] def test_is_numcodec() -> None: """ Test the _is_numcodec function """ assert _is_numcodec(GZip()) def test_is_numcodec_cls() -> None: """ Test the _is_numcodec_cls function """ assert _is_numcodec_cls(GZip) ALL_CODECS = tuple( filter( lambda v: issubclass(v, _numcodecs._NumcodecsCodec) and hasattr(v, "codec_name"), tuple(getattr(_numcodecs, cls_name) for cls_name in _numcodecs.__all__), ) ) @pytest.mark.parametrize("codec_cls", ALL_CODECS) def test_get_codec_class(codec_cls: type[_numcodecs._NumcodecsCodec]) -> None: assert get_codec_class(codec_cls.codec_name) == codec_cls # type: ignore[comparison-overlap] @pytest.mark.parametrize("codec_class", ALL_CODECS) def test_docstring(codec_class: type[_numcodecs._NumcodecsCodec]) -> None: """ Test that the docstring for the zarr.numcodecs codecs references the wrapped numcodecs class. """ assert "See [numcodecs." in codec_class.__doc__ # type: ignore[operator] @pytest.mark.parametrize( "codec_class", [ _numcodecs.Blosc, _numcodecs.LZ4, _numcodecs.Zstd, _numcodecs.Zlib, _numcodecs.GZip, _numcodecs.BZ2, _numcodecs.LZMA, _numcodecs.Shuffle, ], ) def test_generic_compressor(codec_class: type[_numcodecs._NumcodecsBytesBytesCodec]) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16)) a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, compressors=[codec_class()], ) a[:, :] = data.copy() np.testing.assert_array_equal(data, a[:, :]) @pytest.mark.parametrize( ("codec_class", "codec_config"), [ (_numcodecs.Delta, {"dtype": "float32"}), (_numcodecs.FixedScaleOffset, {"offset": 0, "scale": 25.5}), (_numcodecs.FixedScaleOffset, {"offset": 0, "scale": 51, "astype": "uint16"}), (_numcodecs.AsType, {"encode_dtype": "float32", "decode_dtype": "float32"}), ], ids=[ "delta", "fixedscaleoffset", "fixedscaleoffset2", "astype", ], ) def test_generic_filter( codec_class: type[_numcodecs._NumcodecsArrayArrayCodec], codec_config: dict[str, JSON], ) -> None: data = np.linspace(0, 10, 256, dtype="float32").reshape((16, 16)) a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, filters=[ codec_class(**codec_config), ], ) a[:, :] = data.copy() with codec_conf(): b = open_array(a.store, mode="r") np.testing.assert_array_equal(data, b[:, :]) def test_generic_filter_bitround() -> None: data = np.linspace(0, 1, 256, dtype="float32").reshape((16, 16)) a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, filters=[_numcodecs.BitRound(keepbits=3)], ) a[:, :] = data.copy() b = open_array(a.store, mode="r") assert np.allclose(data, b[:, :], atol=0.1) def test_generic_filter_quantize() -> None: data = np.linspace(0, 10, 256, dtype="float32").reshape((16, 16)) a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, filters=[_numcodecs.Quantize(digits=3)], ) a[:, :] = data.copy() b = open_array(a.store, mode="r") assert np.allclose(data, b[:, :], atol=0.001) def test_generic_filter_packbits() -> None: data = np.zeros((16, 16), dtype="bool") data[0:4, :] = True a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, filters=[_numcodecs.PackBits()], ) a[:, :] = data.copy() b = open_array(a.store, mode="r") np.testing.assert_array_equal(data, b[:, :]) with pytest.raises(ValueError, match=".*requires bool dtype.*"): create_array( {}, shape=data.shape, chunks=(16, 16), dtype="uint32", fill_value=0, filters=[_numcodecs.PackBits()], ) @pytest.mark.parametrize( "codec_class", [ _numcodecs.CRC32, _numcodecs.CRC32C, _numcodecs.Adler32, _numcodecs.Fletcher32, _numcodecs.JenkinsLookup3, ], ) def test_generic_checksum(codec_class: type[_numcodecs._NumcodecsBytesBytesCodec]) -> None: # Check if the codec is available in numcodecs try: codec_class()._codec # noqa: B018 except UnknownCodecError as e: # pragma: no cover pytest.skip(f"{codec_class.codec_name} is not available in numcodecs: {e}") data = np.linspace(0, 10, 256, dtype="float32").reshape((16, 16)) a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, compressors=[codec_class()], ) a[:, :] = data.copy() with codec_conf(): b = open_array(a.store, mode="r") np.testing.assert_array_equal(data, b[:, :]) @pytest.mark.parametrize("codec_class", [_numcodecs.PCodec, _numcodecs.ZFPY]) def test_generic_bytes_codec(codec_class: type[_numcodecs._NumcodecsArrayBytesCodec]) -> None: try: codec_class()._codec # noqa: B018 except ValueError as e: # pragma: no cover if "codec not available" in str(e): pytest.xfail(f"{codec_class.codec_name} is not available: {e}") else: raise except ImportError as e: # pragma: no cover pytest.xfail(f"{codec_class.codec_name} is not available: {e}") data = np.arange(0, 256, dtype="float32").reshape((16, 16)) a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, serializer=codec_class(), ) a[:, :] = data.copy() np.testing.assert_array_equal(data, a[:, :]) def test_delta_astype() -> None: data = np.linspace(0, 10, 256, dtype="i8").reshape((16, 16)) a = create_array( {}, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, filters=[ _numcodecs.Delta(dtype="i8", astype="i2"), ], ) a[:, :] = data.copy() with codec_conf(): b = open_array(a.store, mode="r") np.testing.assert_array_equal(data, b[:, :]) def test_repr() -> None: codec = _numcodecs.LZ4(level=5) assert repr(codec) == "LZ4(codec_name='numcodecs.lz4', codec_config={'level': 5})" def test_to_dict() -> None: codec = _numcodecs.LZ4(level=5) assert codec.to_dict() == {"name": "numcodecs.lz4", "configuration": {"level": 5}} @pytest.mark.parametrize( "codec_cls", [ _numcodecs.Blosc, _numcodecs.LZ4, _numcodecs.Zstd, _numcodecs.Zlib, _numcodecs.GZip, _numcodecs.BZ2, _numcodecs.LZMA, _numcodecs.Shuffle, _numcodecs.BitRound, _numcodecs.Delta, _numcodecs.FixedScaleOffset, _numcodecs.Quantize, _numcodecs.PackBits, _numcodecs.AsType, _numcodecs.CRC32, _numcodecs.CRC32C, _numcodecs.Adler32, _numcodecs.Fletcher32, _numcodecs.JenkinsLookup3, _numcodecs.PCodec, _numcodecs.ZFPY, ], ) def test_codecs_pickleable(codec_cls: type[_numcodecs._NumcodecsCodec]) -> None: # Check if the codec is available in numcodecs try: codec = codec_cls() except UnknownCodecError as e: # pragma: no cover pytest.skip(f"{codec_cls.codec_name} is not available in numcodecs: {e}") expected = codec p = pickle.dumps(codec) actual = pickle.loads(p) assert actual == expected zarr-python-3.2.1/tests/test_codecs/test_scale_offset.py000066400000000000000000000370321517635743000235410ustar00rootroot00000000000000from __future__ import annotations from typing import Any import numpy as np import pytest import zarr from tests.test_codecs.conftest import Expect, ExpectErr from zarr.codecs.scale_offset import ( ScaleOffset, _decode, _decode_fits_natively, _encode, ) from zarr.core.buffer.core import default_buffer_prototype from zarr.storage._memory import MemoryStore # --------------------------------------------------------------------------- # Serialization # --------------------------------------------------------------------------- @pytest.mark.parametrize( "case", [ Expect(input=ScaleOffset(), expected={"name": "scale_offset"}), Expect( input=ScaleOffset(offset=5), expected={"name": "scale_offset", "configuration": {"offset": 5}}, ), Expect( input=ScaleOffset(scale=0.1), expected={"name": "scale_offset", "configuration": {"scale": 0.1}}, ), Expect( input=ScaleOffset(offset=5, scale=0.1), expected={"name": "scale_offset", "configuration": {"offset": 5, "scale": 0.1}}, ), ], ids=["default", "offset-only", "scale-only", "both"], ) def test_to_dict(case: Expect[ScaleOffset, dict[str, Any]]) -> None: """to_dict produces the expected JSON structure.""" assert case.input.to_dict() == case.expected @pytest.mark.parametrize( "case", [ Expect(input={"name": "scale_offset"}, expected=(0, 1)), Expect( input={"name": "scale_offset", "configuration": {"offset": 3, "scale": 2}}, expected=(3, 2), ), ], ids=["no-config", "with-config"], ) def test_from_dict(case: Expect[dict[str, Any], tuple[int | float, int | float]]) -> None: """from_dict deserializes configuration with correct values and defaults.""" codec = ScaleOffset.from_dict(case.input) expected_offset, expected_scale = case.expected assert codec.offset == expected_offset assert codec.scale == expected_scale def test_serialization_roundtrip() -> None: """to_dict followed by from_dict produces an equal codec.""" original = ScaleOffset(offset=7, scale=0.5) restored = ScaleOffset.from_dict(original.to_dict()) assert original == restored # --------------------------------------------------------------------------- # Construction # --------------------------------------------------------------------------- @pytest.mark.parametrize( "case", [ ExpectErr( input={"offset": [1, 2]}, msg="offset must be a number or string", exception_cls=TypeError, ), ExpectErr( input={"scale": [1, 2]}, msg="scale must be a number or string", exception_cls=TypeError ), ], ids=["list-offset", "list-scale"], ) def test_construction_rejects_non_numeric(case: ExpectErr[dict[str, Any]]) -> None: """Non-numeric offset or scale is rejected at construction time.""" with pytest.raises(case.exception_cls, match=case.msg): ScaleOffset(**case.input) @pytest.mark.parametrize( "case", [ Expect(input={"offset": 5, "scale": 2}, expected=(5, 2)), Expect(input={"offset": 0.5, "scale": 0.1}, expected=(0.5, 0.1)), ], ids=["int", "float"], ) def test_construction_accepts_numeric( case: Expect[dict[str, Any], tuple[int | float, int | float]], ) -> None: """Integer and float values are accepted for both parameters.""" codec = ScaleOffset(**case.input) assert codec.offset == case.expected[0] assert codec.scale == case.expected[1] # --------------------------------------------------------------------------- # Encode / decode # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("dtype", "offset", "scale"), [ ("float64", 10.0, 0.1), ("float32", 5.0, 2.0), ("int32", 0, 1), ], ids=["float64", "float32", "int32-identity"], ) def test_encode_decode_roundtrip(dtype: str, offset: float, scale: float) -> None: """Data survives encode → decode.""" arr = zarr.create_array( store={}, shape=(100,), dtype=dtype, chunks=(100,), filters=[ScaleOffset(offset=offset, scale=scale)], compressors=None, fill_value=0, ) data = np.arange(100, dtype=dtype) arr[:] = data np.testing.assert_array_almost_equal(arr[:], data) # type: ignore[arg-type] def test_fill_value_transformed() -> None: """Fill value is transformed through the encode formula and read back correctly.""" arr = zarr.create_array( store={}, shape=(10,), dtype="float64", chunks=(10,), filters=[ScaleOffset(offset=5, scale=2)], compressors=None, fill_value=10.0, ) # fill_value=10.0, encode: (10 - 5) * 2 = 10.0 stored # Reading back without writing should return the original fill value np.testing.assert_array_equal(arr[:], np.full(10, 10.0)) def test_identity_is_noop() -> None: """Default codec (offset=0, scale=1) is a no-op.""" import zarr arr = zarr.create_array( store={}, shape=(50,), dtype="float64", chunks=(50,), filters=[ScaleOffset()], compressors=None, fill_value=0, ) data = np.arange(50, dtype="float64") arr[:] = data np.testing.assert_array_equal(arr[:], data) def test_rejects_complex_dtype() -> None: """Complex dtypes are rejected at array creation time.""" with pytest.raises(ValueError, match="only supports integer and floating-point"): zarr.create_array( store={}, shape=(10,), dtype="complex128", chunks=(10,), filters=[ScaleOffset(offset=1, scale=2)], compressors=None, fill_value=0, ) def test_uint64_large_value_roundtrip() -> None: """uint64 values above 2**63 must survive encode+decode (spec requires uint64 support).""" arr = zarr.create_array( store={}, shape=(3,), dtype="uint64", chunks=(3,), filters=[ScaleOffset(offset=0, scale=1)], compressors=None, fill_value=0, ) # Value above int64.max (2**63 - 1) — would wrap if we used int64 as wide dtype. data = np.array([0, 2**63, 2**64 - 1], dtype="uint64") arr[:] = data np.testing.assert_array_equal(arr[:], data) def test_float_nan_inf_preserved() -> None: """NaN and Inf are representable in float dtypes per IEEE 754 and must pass through.""" arr = np.array([1.0, np.nan, np.inf, -np.inf], dtype="float64") encoded = _encode(arr, np.float64(0.0), np.float64(2.0)) np.testing.assert_array_equal(encoded[[0]], np.array([2.0])) assert np.isnan(encoded[1]) assert encoded[2] == np.inf assert encoded[3] == -np.inf decoded = _decode(encoded, np.float64(0.0), np.float64(2.0), scale_repr=2.0) np.testing.assert_array_equal(decoded[[0]], np.array([1.0])) assert np.isnan(decoded[1]) def test_uint64_encode_rejects_underflow() -> None: """uint64 underflow during encode raises rather than silently wrapping.""" arr = zarr.create_array( store={}, shape=(3,), dtype="uint64", chunks=(3,), filters=[ScaleOffset(offset=100, scale=1)], compressors=None, fill_value=100, ) with pytest.raises(ValueError, match="outside the range of dtype uint64"): arr[:] = np.array([100, 50, 200], dtype="uint64") def test_rejects_zero_scale() -> None: """scale=0 is rejected (destroys data and breaks decode division).""" with pytest.raises(ValueError, match="scale must be non-zero"): zarr.create_array( store={}, shape=(10,), dtype="int32", chunks=(10,), filters=[ScaleOffset(offset=0, scale=0)], compressors=None, fill_value=0, ) @pytest.mark.parametrize( "case", [ ExpectErr( input={"dtype": "int32", "offset": 1.5, "scale": 1}, msg="offset value 1.5 is not representable", exception_cls=ValueError, ), ExpectErr( input={"dtype": "int32", "offset": 0, "scale": 0.5}, msg="scale value 0.5 is not representable", exception_cls=ValueError, ), ExpectErr( input={"dtype": "int16", "offset": "NaN", "scale": 1}, msg="offset value 'NaN' is not representable", exception_cls=ValueError, ), ], ids=["float-offset-for-int", "float-scale-for-int", "nan-offset-for-int"], ) def test_rejects_unrepresentable_scale_offset(case: ExpectErr[dict[str, Any]]) -> None: """Scale/offset values that can't be represented in the array dtype are rejected.""" with pytest.raises(case.exception_cls, match=case.msg): zarr.create_array( store={}, shape=(10,), dtype=case.input["dtype"], chunks=(10,), filters=[ScaleOffset(offset=case.input["offset"], scale=case.input["scale"])], compressors=None, fill_value=0, ) def test_dtype_preservation() -> None: """Integer scale/offset arithmetic preserves the array dtype when division is exact.""" arr = zarr.create_array( store={}, shape=(10,), dtype="int8", chunks=(10,), filters=[ScaleOffset(offset=1, scale=2)], compressors=None, fill_value=0, ) data = np.arange(10, dtype="int8") arr[:] = data # encode=(x-1)*2 is always divisible by scale=2, so decode is exact np.testing.assert_array_equal(arr[:], data) async def test_integer_decode_rejects_non_exact_division() -> None: """Decoding an integer array raises when the stored value isn't divisible by scale.""" store = MemoryStore() arr = zarr.create_array( store=store, shape=(3,), dtype="int8", chunks=(3,), filters=[ScaleOffset(offset=0, scale=2)], compressors=None, fill_value=0, ) # Write raw encoded bytes directly so we can inject a value that isn't divisible by scale. # Array layout: int8 [2, 3, 4]; 3 % 2 != 0, so decode must fail. buf = default_buffer_prototype().buffer.from_bytes(np.array([2, 3, 4], dtype="int8").tobytes()) await arr.store_path.store.set("c/0", buf) with pytest.raises(ValueError, match="non-zero remainder"): arr[:] def test_encode_rejects_signed_integer_overflow() -> None: """Encoding raises when (value - offset) * scale exceeds the target integer range.""" arr = zarr.create_array( store={}, shape=(3,), dtype="int8", chunks=(3,), filters=[ScaleOffset(offset=0, scale=100)], compressors=None, fill_value=0, ) # 2 * 100 = 200, outside int8 range [-128, 127] with pytest.raises(ValueError, match="outside the range of dtype int8"): arr[:] = np.array([0, 1, 2], dtype="int8") def test_encode_rejects_unsigned_integer_underflow() -> None: """Encoding raises when value - offset underflows an unsigned dtype.""" arr = zarr.create_array( store={}, shape=(3,), dtype="uint8", chunks=(3,), filters=[ScaleOffset(offset=10, scale=1)], compressors=None, fill_value=10, ) # 5 - 10 = -5, outside uint8 range [0, 255] with pytest.raises(ValueError, match="outside the range of dtype uint8"): arr[:] = np.array([10, 5, 20], dtype="uint8") def test_float32_dtype_preserved() -> None: """float32 arrays survive encode+decode without being promoted to float64.""" arr = np.arange(100, dtype="float32") offset = np.float32(5.0) scale = np.float32(0.25) encoded = _encode(arr, offset, scale) assert encoded.dtype == np.dtype("float32") decoded = _decode(encoded, offset, scale, scale_repr=0.25) assert decoded.dtype == np.dtype("float32") def test_float_encode_rejects_wider_scalar() -> None: """A float64 scalar passed with a float32 array must not silently widen the result.""" arr = np.arange(10, dtype="float32") # A numpy float64 scalar (not a Python float — NEP 50 exempts those) mixed with a # float32 ndarray promotes to float64. The codec must reject that. with pytest.raises(ValueError, match="changed dtype from float32 to float64"): _encode(arr, np.float64(5.0), np.float64(0.25)) def test_float_decode_rejects_wider_scalar() -> None: """A float64 scalar passed with a float32 array must not silently widen on decode.""" arr = np.arange(10, dtype="float32") with pytest.raises(ValueError, match="changed dtype from float32 to float64"): _decode(arr, np.float64(5.0), np.float64(0.25), scale_repr=0.25) async def test_decode_rejects_integer_overflow_on_offset_add() -> None: """Decoding raises when quotient + offset overflows the target integer dtype.""" store = MemoryStore() arr = zarr.create_array( store=store, shape=(3,), dtype="int8", chunks=(3,), filters=[ScaleOffset(offset=100, scale=1)], compressors=None, fill_value=0, ) # encoded=100 → decoded = 100/1 + 100 = 200, outside int8 range buf = default_buffer_prototype().buffer.from_bytes( np.array([0, 50, 100], dtype="int8").tobytes() ) await arr.store_path.store.set("c/0", buf) with pytest.raises(ValueError, match="outside the range of dtype int8"): arr[:] def test_decode_fits_natively_negative_scale() -> None: """_decode_fits_natively handles negative scale by swapping bounds.""" # For a negative scale, x // scale flips the relationship between min/max. # The function should use info.max // scale as the lower bound and info.min // scale # as the upper bound. dtype = np.dtype("int16") # scale=-2 inverts; offset=0 means range is just q_lo..q_hi assert _decode_fits_natively(dtype, offset=0, scale=-2) is True # An offset that pushes the range out of bounds returns False assert _decode_fits_natively(dtype, offset=100000, scale=-2) is False async def test_decode_int_widened_path() -> None: """When _decode_fits_natively returns False, decode falls through to the widened path.""" # For uint32 with offset near max, q_hi + offset can exceed uint32 if computed in target dtype. # The widened path uses int64 arithmetic and range-checks the result. # We bypass encode by writing raw bytes directly to the store. store = MemoryStore() arr = zarr.create_array( store=store, shape=(3,), dtype="uint32", chunks=(3,), # offset large enough that _decode_fits_natively returns False filters=[ScaleOffset(offset=2**31, scale=1)], compressors=None, # fill_value must be >= offset to avoid uint32 underflow during encode fill_value=2**31, ) # Encoded values that, when added to offset, stay within uint32 buf = default_buffer_prototype().buffer.from_bytes( np.array([0, 100, 1000], dtype="uint32").tobytes() ) await arr.store_path.store.set("c/0", buf) expected = np.array([2**31, 2**31 + 100, 2**31 + 1000], dtype="uint32") np.testing.assert_array_equal(arr[:], expected) def test_compute_encoded_size() -> None: """compute_encoded_size returns the input byte length unchanged (codec is fixed-size).""" codec = ScaleOffset(offset=0, scale=1) # The chunk_spec argument is unused; pass any sentinel assert codec.compute_encoded_size(input_byte_length=100, _chunk_spec=None) == 100 # type: ignore[arg-type] assert codec.compute_encoded_size(input_byte_length=0, _chunk_spec=None) == 0 # type: ignore[arg-type] zarr-python-3.2.1/tests/test_codecs/test_sharding.py000066400000000000000000000417231517635743000227050ustar00rootroot00000000000000import pickle from typing import Any import numpy as np import numpy.typing as npt import pytest import zarr import zarr.api import zarr.api.asynchronous from zarr import Array from zarr.abc.store import Store from zarr.codecs import ( BloscCodec, ShardingCodec, ShardingCodecIndexLocation, TransposeCodec, ) from zarr.core.buffer import NDArrayLike, default_buffer_prototype from zarr.storage import StorePath, ZipStore from ..conftest import ArrayRequest from .test_codecs import _AsyncArrayProxy, order_from_dim @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("index_location", ["start", "end"]) @pytest.mark.parametrize( "array_fixture", [ ArrayRequest(shape=(128,) * 1, dtype="uint8", order="C"), ArrayRequest(shape=(128,) * 2, dtype="uint8", order="C"), ArrayRequest(shape=(128,) * 3, dtype="uint16", order="F"), ], indirect=["array_fixture"], ) @pytest.mark.parametrize("offset", [0, 10]) def test_sharding( store: Store, array_fixture: npt.NDArray[Any], index_location: ShardingCodecIndexLocation, offset: int, ) -> None: """ Test that we can create an array with a sharding codec, write data to that array, and get the same data out via indexing. """ data = array_fixture spath = StorePath(store) arr = zarr.create_array( spath, shape=tuple(s + offset for s in data.shape), chunks=(32,) * data.ndim, shards={"shape": (64,) * data.ndim, "index_location": index_location}, dtype=data.dtype, fill_value=6, filters=[TransposeCodec(order=order_from_dim("F", data.ndim))], compressors=BloscCodec(cname="lz4"), ) write_region = tuple(slice(offset, None) for dim in range(data.ndim)) arr[write_region] = data if offset > 0: empty_region = tuple(slice(0, offset) for dim in range(data.ndim)) assert np.all(arr[empty_region] == arr.metadata.fill_value) read_data = arr[write_region] assert isinstance(read_data, NDArrayLike) assert data.shape == read_data.shape assert np.array_equal(data, read_data) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("index_location", ["start", "end"]) @pytest.mark.parametrize("offset", [0, 10]) def test_sharding_scalar( store: Store, index_location: ShardingCodecIndexLocation, offset: int, ) -> None: """ Test that we can create an array with a sharding codec, write data to that array, and get the same data out via indexing. """ spath = StorePath(store) arr = zarr.create_array( spath, shape=(128, 128), chunks=(32, 32), shards={"shape": (64, 64), "index_location": index_location}, dtype="uint8", fill_value=6, filters=[TransposeCodec(order=order_from_dim("F", 2))], compressors=BloscCodec(cname="lz4"), ) arr[:16, :16] = 10 # intentionally write partial chunks read_data = arr[:16, :16] np.testing.assert_array_equal(read_data, 10) @pytest.mark.parametrize("index_location", ["start", "end"]) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize( "array_fixture", [ ArrayRequest(shape=(128,) * 3, dtype="uint16", order="F"), ], indirect=["array_fixture"], ) def test_sharding_partial( store: Store, array_fixture: npt.NDArray[Any], index_location: ShardingCodecIndexLocation ) -> None: data = array_fixture spath = StorePath(store) a = zarr.create_array( spath, shape=tuple(a + 10 for a in data.shape), chunks=(32, 32, 32), shards={"shape": (64, 64, 64), "index_location": index_location}, compressors=BloscCodec(cname="lz4"), filters=[TransposeCodec(order=order_from_dim("F", data.ndim))], dtype=data.dtype, fill_value=0, ) a[10:, 10:, 10:] = data read_data = a[0:10, 0:10, 0:10] assert np.all(read_data == 0) read_data = a[10:, 10:, 10:] assert isinstance(read_data, NDArrayLike) assert data.shape == read_data.shape assert np.array_equal(data, read_data) @pytest.mark.parametrize("index_location", ["start", "end"]) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize( "array_fixture", [ ArrayRequest(shape=(128,) * 3, dtype="uint16", order="F"), ], indirect=["array_fixture"], ) def test_sharding_partial_readwrite( store: Store, array_fixture: npt.NDArray[Any], index_location: ShardingCodecIndexLocation ) -> None: data = array_fixture spath = StorePath(store) a = zarr.create_array( spath, shape=data.shape, chunks=(1, data.shape[1], data.shape[2]), shards={"shape": data.shape, "index_location": index_location}, dtype=data.dtype, fill_value=0, filters=None, compressors=None, ) a[:] = data for x in range(data.shape[0]): read_data = a[x, :, :] assert np.array_equal(data[x], read_data) @pytest.mark.parametrize( "array_fixture", [ ArrayRequest(shape=(128,) * 3, dtype="uint16", order="F"), ], indirect=["array_fixture"], ) @pytest.mark.parametrize("index_location", ["start", "end"]) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) def test_sharding_partial_read( store: Store, array_fixture: npt.NDArray[Any], index_location: ShardingCodecIndexLocation ) -> None: data = array_fixture spath = StorePath(store) a = zarr.create_array( spath, shape=tuple(a + 10 for a in data.shape), chunks=(32, 32, 32), shards={"shape": (64, 64, 64), "index_location": index_location}, compressors=BloscCodec(cname="lz4"), filters=[TransposeCodec(order=order_from_dim("F", data.ndim))], dtype=data.dtype, fill_value=1, ) read_data = a[0:10, 0:10, 0:10] assert np.all(read_data == 1) @pytest.mark.parametrize( "array_fixture", [ ArrayRequest(shape=(128,) * 3, dtype="uint16", order="F"), ], indirect=["array_fixture"], ) @pytest.mark.parametrize("index_location", ["start", "end"]) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) def test_sharding_partial_overwrite( store: Store, array_fixture: npt.NDArray[Any], index_location: ShardingCodecIndexLocation ) -> None: data = array_fixture[:10, :10, :10] spath = StorePath(store) a = zarr.create_array( spath, shape=tuple(a + 10 for a in data.shape), chunks=(32, 32, 32), shards={"shape": (64, 64, 64), "index_location": index_location}, compressors=BloscCodec(cname="lz4"), filters=[TransposeCodec(order=order_from_dim("F", data.ndim))], dtype=data.dtype, fill_value=1, ) a[:10, :10, :10] = data read_data = a[0:10, 0:10, 0:10] assert np.array_equal(data, read_data) data += 10 if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): a[:10, :10, :10] = data else: a[:10, :10, :10] = data read_data = a[0:10, 0:10, 0:10] assert np.array_equal(data, read_data) # Zip storage raises a warning about a duplicate name, which we ignore. @pytest.mark.filterwarnings("ignore:Duplicate name.*:UserWarning") @pytest.mark.parametrize( "array_fixture", [ ArrayRequest(shape=(127, 128, 129), dtype="uint16", order="F"), ], indirect=True, ) @pytest.mark.parametrize( "outer_index_location", ["start", "end"], ) @pytest.mark.parametrize( "inner_index_location", ["start", "end"], ) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) def test_nested_sharding( store: Store, array_fixture: npt.NDArray[Any], outer_index_location: ShardingCodecIndexLocation, inner_index_location: ShardingCodecIndexLocation, ) -> None: data = array_fixture spath = StorePath(store) # compressors=None ensures no BytesBytesCodec is added, which keeps # supports_partial_decode=True and exercises the partial decode path a = zarr.create_array( spath, data=data, chunks=(64,) * data.ndim, compressors=None, serializer=ShardingCodec( chunk_shape=(32,) * data.ndim, codecs=[ ShardingCodec(chunk_shape=(16,) * data.ndim, index_location=inner_index_location) ], index_location=outer_index_location, ), ) a[:] = data read_data = a[0 : data.shape[0], 0 : data.shape[1], 0 : data.shape[2]] assert isinstance(read_data, NDArrayLike) assert data.shape == read_data.shape assert np.array_equal(data, read_data) @pytest.mark.parametrize( "array_fixture", [ ArrayRequest(shape=(128,) * 3, dtype="uint16", order="F"), ], indirect=["array_fixture"], ) @pytest.mark.parametrize( "outer_index_location", ["start", "end"], ) @pytest.mark.parametrize( "inner_index_location", ["start", "end"], ) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) def test_nested_sharding_create_array( store: Store, array_fixture: npt.NDArray[Any], outer_index_location: ShardingCodecIndexLocation, inner_index_location: ShardingCodecIndexLocation, ) -> None: data = array_fixture spath = StorePath(store) a = zarr.create_array( spath, shape=data.shape, chunks=(32, 32, 32), dtype=data.dtype, fill_value=0, serializer=ShardingCodec( chunk_shape=(32, 32, 32), codecs=[ShardingCodec(chunk_shape=(16, 16, 16), index_location=inner_index_location)], index_location=outer_index_location, ), filters=None, compressors=None, ) a[:] = data read_data = a[:] assert np.array_equal(data, read_data) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) def test_open_sharding(store: Store) -> None: path = "open_sharding" spath = StorePath(store, path) a = zarr.create_array( spath, shape=(16, 16), chunks=(8, 8), shards=(16, 16), filters=[TransposeCodec(order=order_from_dim("F", 2))], compressors=BloscCodec(), dtype="int32", fill_value=0, ) b = Array.open(spath) assert a.metadata == b.metadata @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) def test_write_partial_sharded_chunks(store: Store) -> None: data = np.arange(0, 16 * 16, dtype="uint16").reshape((16, 16)) spath = StorePath(store) a = zarr.create_array( spath, shape=(40, 40), chunks=(10, 10), shards=(20, 20), dtype=data.dtype, compressors=BloscCodec(), fill_value=1, ) a[0:16, 0:16] = data assert np.array_equal(a[0:16, 0:16], data) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) async def test_delete_empty_shards(store: Store) -> None: if not store.supports_deletes: pytest.skip("store does not support deletes") path = "delete_empty_shards" spath = StorePath(store, path) a = await zarr.api.asynchronous.create_array( spath, shape=(16, 16), chunks=(8, 8), shards=(8, 16), dtype="uint16", compressors=None, fill_value=1, ) print(a.metadata.to_dict()) await _AsyncArrayProxy(a)[:, :].set(np.zeros((16, 16))) await _AsyncArrayProxy(a)[8:, :].set(np.ones((8, 16))) await _AsyncArrayProxy(a)[:, 8:].set(np.ones((16, 8))) # chunk (0, 0) is full # chunks (0, 1), (1, 0), (1, 1) are empty # shard (0, 0) is half-full # shard (1, 0) is empty data = np.ones((16, 16), dtype="uint16") data[:8, :8] = 0 assert np.array_equal(data, await _AsyncArrayProxy(a)[:, :].get()) assert await store.get(f"{path}/c/1/0", prototype=default_buffer_prototype()) is None chunk_bytes = await store.get(f"{path}/c/0/0", prototype=default_buffer_prototype()) assert chunk_bytes is not None assert len(chunk_bytes) == 16 * 2 + 8 * 8 * 2 + 4 def test_pickle() -> None: codec = ShardingCodec(chunk_shape=(8, 8)) assert pickle.loads(pickle.dumps(codec)) == codec @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize( "index_location", [ShardingCodecIndexLocation.start, ShardingCodecIndexLocation.end] ) async def test_sharding_with_empty_inner_chunk( store: Store, index_location: ShardingCodecIndexLocation ) -> None: data = np.arange(0, 16 * 16, dtype="uint32").reshape((16, 16)) fill_value = 1 path = f"sharding_with_empty_inner_chunk_{index_location}" spath = StorePath(store, path) a = await zarr.api.asynchronous.create_array( spath, shape=(16, 16), chunks=(4, 4), shards={"shape": (8, 8), "index_location": index_location}, dtype="uint32", fill_value=fill_value, ) data[:4, :4] = fill_value await a.setitem(..., data) print("read data") data_read = await a.getitem(...) assert np.array_equal(data_read, data) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize( "index_location", [ShardingCodecIndexLocation.start, ShardingCodecIndexLocation.end], ) @pytest.mark.parametrize("chunks_per_shard", [(5, 2), (2, 5), (5, 5)]) async def test_sharding_with_chunks_per_shard( store: Store, index_location: ShardingCodecIndexLocation, chunks_per_shard: tuple[int] ) -> None: chunk_shape = (2, 1) shape = tuple(x * y for x, y in zip(chunks_per_shard, chunk_shape, strict=False)) data = np.ones(np.prod(shape), dtype="int32").reshape(shape) fill_value = 42 path = f"test_sharding_with_chunks_per_shard_{index_location}" spath = StorePath(store, path) a = zarr.create_array( spath, shape=shape, chunks=chunk_shape, shards={"shape": shape, "index_location": index_location}, dtype="int32", fill_value=fill_value, ) a[...] = data data_read = a[...] assert np.array_equal(data_read, data) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_invalid_metadata(store: Store) -> None: spath1 = StorePath(store, "invalid_inner_chunk_shape") with pytest.raises(ValueError): zarr.create_array( spath1, shape=(16, 16), shards=(16, 16), chunks=(8,), dtype=np.dtype("uint8"), fill_value=0, ) spath2 = StorePath(store, "invalid_inner_chunk_shape") with pytest.raises(ValueError): zarr.create_array( spath2, shape=(16, 16), shards=(16, 16), chunks=(8, 7), dtype=np.dtype("uint8"), fill_value=0, ) def test_invalid_shard_shape() -> None: with pytest.raises( ValueError, match=( f"Chunk edge length {16} in dimension {0} is not " f"divisible by the shard's inner chunk size {9}\\." ), ): zarr.create_array( {}, shape=(16, 16), shards=(16, 16), chunks=(9,), dtype=np.dtype("uint8"), fill_value=0, ) @pytest.mark.parametrize("store", ["local"], indirect=["store"]) def test_sharding_mixed_integer_list_indexing(store: Store) -> None: """Regression test for https://github.com/zarr-developers/zarr-python/issues/3691. Mixed integer/list indexing on sharded arrays should return the same shape and data as on equivalent chunked arrays. """ import numpy as np data = np.arange(200 * 100 * 10, dtype=np.uint8).reshape(200, 100, 10) chunked = zarr.create_array( store, name="chunked", shape=(200, 100, 10), dtype=np.uint8, chunks=(200, 100, 1), overwrite=True, ) chunked[:, :, :] = data sharded = zarr.create_array( store, name="sharded", shape=(200, 100, 10), dtype=np.uint8, chunks=(200, 100, 1), shards=(200, 100, 10), overwrite=True, ) sharded[:, :, :] = data # Mixed integer + list indexing c = chunked[0:10, 0, [0, 1]] # type: ignore[index] s = sharded[0:10, 0, [0, 1]] # type: ignore[index] assert c.shape == s.shape == (10, 2), ( # type: ignore[union-attr] f"Expected (10, 2), got chunked={c.shape}, sharded={s.shape}" # type: ignore[union-attr] ) np.testing.assert_array_equal(c, s) # Multiple integer axes c2 = chunked[0, 0, [0, 1, 2]] # type: ignore[index] s2 = sharded[0, 0, [0, 1, 2]] # type: ignore[index] assert c2.shape == s2.shape == (3,) # type: ignore[union-attr] np.testing.assert_array_equal(c2, s2) # Slice + integer + slice c3 = chunked[0:5, 1, 0:3] s3 = sharded[0:5, 1, 0:3] assert c3.shape == s3.shape == (5, 3) # type: ignore[union-attr] np.testing.assert_array_equal(c3, s3) zarr-python-3.2.1/tests/test_codecs/test_transpose.py000066400000000000000000000106611517635743000231210ustar00rootroot00000000000000import numpy as np import pytest import zarr from zarr import AsyncArray, config from zarr.abc.codec import SupportsSyncCodec from zarr.abc.store import Store from zarr.codecs import TransposeCodec from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import NDBuffer, default_buffer_prototype from zarr.core.common import MemoryOrder from zarr.core.dtype import get_data_type_from_native_dtype from zarr.storage import StorePath from .test_codecs import _AsyncArrayProxy @pytest.mark.parametrize("input_order", ["F", "C"]) @pytest.mark.parametrize("runtime_write_order", ["F", "C"]) @pytest.mark.parametrize("runtime_read_order", ["F", "C"]) @pytest.mark.parametrize("with_sharding", [True, False]) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) async def test_transpose( store: Store, input_order: MemoryOrder, runtime_write_order: MemoryOrder, runtime_read_order: MemoryOrder, with_sharding: bool, ) -> None: data = np.arange(0, 256, dtype="uint16").reshape((1, 32, 8), order=input_order) spath = StorePath(store, path="transpose") with config.set({"array.order": runtime_write_order}): a = await zarr.api.asynchronous.create_array( spath, shape=data.shape, chunks=(1, 16, 8) if with_sharding else (1, 32, 8), shards=(1, 32, 8) if with_sharding else None, dtype=data.dtype, fill_value=0, chunk_key_encoding={"name": "v2", "separator": "."}, filters=[TransposeCodec(order=(2, 1, 0))], ) await _AsyncArrayProxy(a)[:, :].set(data) read_data = await _AsyncArrayProxy(a)[:, :].get() assert np.array_equal(data, read_data) with config.set({"array.order": runtime_read_order}): a = await AsyncArray.open( spath, ) read_data = await _AsyncArrayProxy(a)[:, :].get() assert np.array_equal(data, read_data) assert isinstance(read_data, np.ndarray) if runtime_read_order == "F": assert read_data.flags["F_CONTIGUOUS"] assert not read_data.flags["C_CONTIGUOUS"] else: assert not read_data.flags["F_CONTIGUOUS"] assert read_data.flags["C_CONTIGUOUS"] @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize("order", [[1, 2, 0], [1, 2, 3, 0], [3, 2, 4, 0, 1]]) def test_transpose_non_self_inverse(store: Store, order: list[int]) -> None: shape = [i + 3 for i in range(len(order))] data = np.arange(0, np.prod(shape), dtype="uint16").reshape(shape) spath = StorePath(store, "transpose_non_self_inverse") a = zarr.create_array( spath, shape=data.shape, chunks=data.shape, dtype=data.dtype, fill_value=0, filters=[TransposeCodec(order=order)], ) a[:, :] = data read_data = a[:, :] assert np.array_equal(data, read_data) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_transpose_invalid( store: Store, ) -> None: data = np.arange(0, 256, dtype="uint16").reshape((1, 32, 8)) spath = StorePath(store, "transpose_invalid") for order in [(1, 0), (3, 2, 1), (3, 3, 1), "F", "C"]: with pytest.raises((ValueError, TypeError)): zarr.create_array( spath, shape=data.shape, chunks=(1, 32, 8), dtype=data.dtype, fill_value=0, chunk_key_encoding={"name": "v2", "separator": "."}, filters=[TransposeCodec(order=order)], # type: ignore[arg-type] ) def test_transpose_codec_supports_sync() -> None: assert isinstance(TransposeCodec(order=(0, 1)), SupportsSyncCodec) def test_transpose_codec_sync_roundtrip() -> None: codec = TransposeCodec(order=(1, 0)) arr = np.arange(12, dtype="float64").reshape(3, 4) zdtype = get_data_type_from_native_dtype(arr.dtype) spec = ArraySpec( shape=arr.shape, dtype=zdtype, fill_value=zdtype.cast_scalar(0), config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) nd_buf: NDBuffer = default_buffer_prototype().nd_buffer.from_numpy_array(arr) encoded = codec._encode_sync(nd_buf, spec) assert encoded is not None resolved_spec = codec.resolve_metadata(spec) decoded = codec._decode_sync(encoded, resolved_spec) np.testing.assert_array_equal(arr, decoded.as_numpy_array()) zarr-python-3.2.1/tests/test_codecs/test_vlen.py000066400000000000000000000051041517635743000220430ustar00rootroot00000000000000from typing import Any import numpy as np import pytest import zarr from zarr import Array from zarr.abc.codec import Codec, SupportsSyncCodec from zarr.abc.store import Store from zarr.codecs import ZstdCodec from zarr.codecs.vlen_utf8 import VLenBytesCodec, VLenUTF8Codec from zarr.core.dtype import get_data_type_from_native_dtype from zarr.core.dtype.npy.string import _NUMPY_SUPPORTS_VLEN_STRING from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.storage import StorePath numpy_str_dtypes: list[type | str | None] = [None, str, "str", np.dtypes.StrDType, "S", "U"] expected_array_string_dtype: np.dtype[Any] if _NUMPY_SUPPORTS_VLEN_STRING: numpy_str_dtypes.append(np.dtypes.StringDType) expected_array_string_dtype = np.dtypes.StringDType() else: expected_array_string_dtype = np.dtype("O") @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("store", ["memory", "local"], indirect=["store"]) @pytest.mark.parametrize("dtype", numpy_str_dtypes) @pytest.mark.parametrize("as_object_array", [False, True]) @pytest.mark.parametrize("compressor", [None, ZstdCodec()]) def test_vlen_string( store: Store, dtype: np.dtype[Any] | None, as_object_array: bool, compressor: Codec | None ) -> None: strings = ["hello", "world", "this", "is", "a", "test"] data = np.array(strings, dtype=dtype).reshape((2, 3)) sp = StorePath(store, path="string") a = zarr.create_array( sp, shape=data.shape, chunks=data.shape, dtype=data.dtype, fill_value="", compressors=compressor, # type: ignore[arg-type] ) assert isinstance(a.metadata, ArrayV3Metadata) # needed for mypy # should also work if input array is an object array, provided we explicitly specified # a stringlike dtype when creating the Array if as_object_array: data_obj = data.astype("O") a[:, :] = data_obj else: a[:, :] = data assert np.array_equal(data, a[:, :]) assert a.metadata.data_type == get_data_type_from_native_dtype(data.dtype) assert a.dtype == data.dtype # test round trip b = Array.open(sp) assert isinstance(b.metadata, ArrayV3Metadata) # needed for mypy assert np.array_equal(data, b[:, :]) assert b.metadata.data_type == get_data_type_from_native_dtype(data.dtype) assert a.dtype == data.dtype def test_vlen_utf8_codec_supports_sync() -> None: assert isinstance(VLenUTF8Codec(), SupportsSyncCodec) def test_vlen_bytes_codec_supports_sync() -> None: assert isinstance(VLenBytesCodec(), SupportsSyncCodec) zarr-python-3.2.1/tests/test_codecs/test_zstd.py000066400000000000000000000032711517635743000220660ustar00rootroot00000000000000import numpy as np import pytest import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.abc.store import Store from zarr.codecs import ZstdCodec from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import get_data_type_from_native_dtype from zarr.storage import StorePath @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize("checksum", [True, False]) def test_zstd(store: Store, checksum: bool) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16)) a = zarr.create_array( StorePath(store, path="zstd"), shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, compressors=ZstdCodec(level=0, checksum=checksum), ) a[:, :] = data assert np.array_equal(data, a[:, :]) def test_zstd_codec_supports_sync() -> None: assert isinstance(ZstdCodec(), SupportsSyncCodec) def test_zstd_codec_sync_roundtrip() -> None: codec = ZstdCodec(level=1) arr = np.arange(100, dtype="float64") zdtype = get_data_type_from_native_dtype(arr.dtype) spec = ArraySpec( shape=arr.shape, dtype=zdtype, fill_value=zdtype.cast_scalar(0), config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) buf = default_buffer_prototype().buffer.from_array_like(arr.view("B")) encoded = codec._encode_sync(buf, spec) assert encoded is not None decoded = codec._decode_sync(encoded, spec) result = np.frombuffer(decoded.as_numpy_array(), dtype="float64") np.testing.assert_array_equal(arr, result) zarr-python-3.2.1/tests/test_common.py000066400000000000000000000100321517635743000200640ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterable from typing import TYPE_CHECKING, get_args import numpy as np import pytest from zarr.core.common import ( ANY_ACCESS_MODE, AccessModeLiteral, parse_name, parse_shapelike, product, ) from zarr.core.config import parse_indexing_order if TYPE_CHECKING: from typing import Any, Literal @pytest.mark.parametrize("data", [(0, 0, 0, 0), (1, 3, 4, 5, 6), (2, 4)]) def test_product(data: tuple[int, ...]) -> None: assert product(data) == np.prod(data) def test_access_modes() -> None: """ Test that the access modes type and variable for run-time checking are equivalent. """ assert set(ANY_ACCESS_MODE) == set(get_args(AccessModeLiteral)) # todo: test def test_concurrent_map() -> None: ... # todo: test def test_to_thread() -> None: ... # todo: test def test_enum_names() -> None: ... # todo: test def test_parse_enum() -> None: ... @pytest.mark.parametrize("data", [("foo", "bar"), (10, 11)]) def test_parse_name_invalid(data: tuple[Any, Any]) -> None: observed, expected = data if isinstance(observed, str): with pytest.raises(ValueError, match=f"Expected '{expected}'. Got {observed} instead."): parse_name(observed, expected) else: with pytest.raises( TypeError, match=f"Expected a string, got an instance of {type(observed)}." ): parse_name(observed, expected) @pytest.mark.parametrize("data", [("foo", "foo"), ("10", "10")]) def test_parse_name_valid(data: tuple[Any, Any]) -> None: observed, expected = data assert parse_name(observed, expected) == observed @pytest.mark.parametrize("data", [0, 1, "hello", "f"]) def test_parse_indexing_order_invalid(data: Any) -> None: with pytest.raises(ValueError, match="Expected one of"): parse_indexing_order(data) @pytest.mark.parametrize("data", ["C", "F"]) def parse_indexing_order_valid(data: Literal["C", "F"]) -> None: assert parse_indexing_order(data) == data @pytest.mark.parametrize("data", [lambda v: v, slice(None)]) def test_parse_shapelike_invalid_single_type(data: Any) -> None: """ Test that we get the expected error message when passing in a value that is not an integer or an iterable of integers. """ with pytest.raises(TypeError, match="Expected an integer or an iterable of integers."): parse_shapelike(data) def test_parse_shapelike_invalid_single_value() -> None: """ Test that we get the expected error message when passing in a negative integer. """ with pytest.raises(ValueError, match="Expected a non-negative integer."): parse_shapelike(-1) @pytest.mark.parametrize("data", ["shape", ("0", 1, 2, 3), {"0": "0"}, ((1, 2), (2, 2)), (4.0, 2)]) def test_parse_shapelike_invalid_iterable_types(data: Any) -> None: """ Test that we get the expected error message when passing in an iterable containing non-integer elements """ with pytest.raises(TypeError, match="Expected an iterable of integers"): parse_shapelike(data) @pytest.mark.parametrize("data", [(1, 2, 3, -1), (-10,)]) def test_parse_shapelike_invalid_iterable_values(data: Any) -> None: """ Test that we get the expected error message when passing in an iterable containing negative integers """ with pytest.raises(ValueError, match="Expected all values to be non-negative."): parse_shapelike(data) @pytest.mark.parametrize( "data", [range(10), [0, 1, 2, np.uint64(3)], (3, 4, 5), (), 1, np.uint8(1)] ) def test_parse_shapelike_valid(data: Iterable[int] | int) -> None: if isinstance(data, Iterable): expected = tuple(data) else: expected = (data,) assert parse_shapelike(data) == expected # todo: more dtypes @pytest.mark.parametrize("data", [("uint8", np.uint8), ("float64", np.float64)]) def parse_dtype(data: tuple[str, np.dtype[Any]]) -> None: unparsed, parsed = data assert parse_dtype(unparsed) == parsed # todo: figure out what it means to test this def test_parse_fill_value() -> None: ... zarr-python-3.2.1/tests/test_config.py000066400000000000000000000373631517635743000200610ustar00rootroot00000000000000import os from collections.abc import Iterable from typing import Any from unittest import mock from unittest.mock import Mock import numpy as np import pytest import zarr from zarr import zeros from zarr.abc.codec import CodecPipeline from zarr.abc.store import ByteSetter, Store from zarr.codecs import ( BloscCodec, BytesCodec, Crc32cCodec, ShardingCodec, ) from zarr.core.array_spec import ArraySpec from zarr.core.buffer import NDBuffer from zarr.core.buffer.core import Buffer from zarr.core.codec_pipeline import BatchedCodecPipeline from zarr.core.config import BadConfigError, config from zarr.core.indexing import SelectorTuple from zarr.errors import ChunkNotFoundError, ZarrUserWarning from zarr.registry import ( fully_qualified_name, get_buffer_class, get_codec_class, get_ndbuffer_class, get_pipeline_class, register_buffer, register_codec, register_ndbuffer, register_pipeline, ) from zarr.testing.buffer import ( NDBufferUsingTestNDArrayLike, StoreExpectingTestBuffer, TestBuffer, TestNDArrayLike, ) def test_config_defaults_set() -> None: # regression test for available defaults assert ( config.defaults == [ { "default_zarr_format": 3, "array": { "order": "C", "write_empty_chunks": False, "read_missing_chunks": True, "target_shard_size_bytes": None, "rectilinear_chunks": False, }, "async": {"concurrency": 10, "timeout": None}, "threading": {"max_workers": None}, "json_indent": 2, "codec_pipeline": { "path": "zarr.core.codec_pipeline.BatchedCodecPipeline", "batch_size": 1, }, "codecs": { "blosc": "zarr.codecs.blosc.BloscCodec", "gzip": "zarr.codecs.gzip.GzipCodec", "zstd": "zarr.codecs.zstd.ZstdCodec", "bytes": "zarr.codecs.bytes.BytesCodec", "endian": "zarr.codecs.bytes.BytesCodec", # compatibility with earlier versions of ZEP1 "crc32c": "zarr.codecs.crc32c_.Crc32cCodec", "sharding_indexed": "zarr.codecs.sharding.ShardingCodec", "transpose": "zarr.codecs.transpose.TransposeCodec", "vlen-utf8": "zarr.codecs.vlen_utf8.VLenUTF8Codec", "vlen-bytes": "zarr.codecs.vlen_utf8.VLenBytesCodec", "numcodecs.bz2": "zarr.codecs.numcodecs.BZ2", "numcodecs.crc32": "zarr.codecs.numcodecs.CRC32", "numcodecs.crc32c": "zarr.codecs.numcodecs.CRC32C", "numcodecs.lz4": "zarr.codecs.numcodecs.LZ4", "numcodecs.lzma": "zarr.codecs.numcodecs.LZMA", "numcodecs.zfpy": "zarr.codecs.numcodecs.ZFPY", "numcodecs.adler32": "zarr.codecs.numcodecs.Adler32", "numcodecs.astype": "zarr.codecs.numcodecs.AsType", "numcodecs.bitround": "zarr.codecs.numcodecs.BitRound", "numcodecs.blosc": "zarr.codecs.numcodecs.Blosc", "numcodecs.delta": "zarr.codecs.numcodecs.Delta", "numcodecs.fixedscaleoffset": "zarr.codecs.numcodecs.FixedScaleOffset", "numcodecs.fletcher32": "zarr.codecs.numcodecs.Fletcher32", "numcodecs.gzip": "zarr.codecs.numcodecs.GZip", "numcodecs.jenkins_lookup3": "zarr.codecs.numcodecs.JenkinsLookup3", "numcodecs.pcodec": "zarr.codecs.numcodecs.PCodec", "numcodecs.packbits": "zarr.codecs.numcodecs.PackBits", "numcodecs.shuffle": "zarr.codecs.numcodecs.Shuffle", "numcodecs.quantize": "zarr.codecs.numcodecs.Quantize", "numcodecs.zlib": "zarr.codecs.numcodecs.Zlib", "numcodecs.zstd": "zarr.codecs.numcodecs.Zstd", }, "buffer": "zarr.buffer.cpu.Buffer", "ndbuffer": "zarr.buffer.cpu.NDBuffer", } ] ) assert config.get("array.order") == "C" assert config.get("async.concurrency") == 10 assert config.get("async.timeout") is None assert config.get("codec_pipeline.batch_size") == 1 assert config.get("json_indent") == 2 @pytest.mark.parametrize( ("key", "old_val", "new_val"), [("array.order", "C", "F"), ("async.concurrency", 10, 128), ("json_indent", 2, 0)], ) def test_config_defaults_can_be_overridden(key: str, old_val: Any, new_val: Any) -> None: assert config.get(key) == old_val with config.set({key: new_val}): assert config.get(key) == new_val def test_fully_qualified_name() -> None: class MockClass: pass assert ( fully_qualified_name(MockClass) == "tests.test_config.test_fully_qualified_name..MockClass" ) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_config_codec_pipeline_class(store: Store) -> None: # has default value assert get_pipeline_class().__name__ != "" config.set({"codec_pipeline.name": "zarr.core.codec_pipeline.BatchedCodecPipeline"}) assert get_pipeline_class() == zarr.core.codec_pipeline.BatchedCodecPipeline _mock = Mock() class MockCodecPipeline(BatchedCodecPipeline): async def write( self, batch_info: Iterable[tuple[ByteSetter, ArraySpec, SelectorTuple, SelectorTuple, bool]], value: NDBuffer, drop_axes: tuple[int, ...] = (), ) -> None: _mock.call() register_pipeline(MockCodecPipeline) config.set({"codec_pipeline.path": fully_qualified_name(MockCodecPipeline)}) assert get_pipeline_class() == MockCodecPipeline # test if codec is used arr = zarr.create_array( store=store, shape=(100,), chunks=(10,), zarr_format=3, dtype="i4", ) arr[:] = range(100) _mock.call.assert_called() config.set({"codec_pipeline.path": "wrong_name"}) with pytest.raises(BadConfigError): get_pipeline_class() class MockEnvCodecPipeline(CodecPipeline): pass register_pipeline(MockEnvCodecPipeline) # type: ignore[type-abstract] with mock.patch.dict( os.environ, {"ZARR_CODEC_PIPELINE__PATH": fully_qualified_name(MockEnvCodecPipeline)} ): assert get_pipeline_class(reload_config=True) == MockEnvCodecPipeline @pytest.mark.filterwarnings("error") @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_config_codec_implementation(store: Store) -> None: # has default value assert fully_qualified_name(get_codec_class("blosc")) == config.defaults[0]["codecs"]["blosc"] _mock = Mock() class MockBloscCodec(BloscCodec): async def _encode_single(self, chunk_bytes: Buffer, chunk_spec: ArraySpec) -> Buffer | None: _mock.call() return None register_codec("blosc", MockBloscCodec) with config.set({"codecs.blosc": fully_qualified_name(MockBloscCodec)}): assert get_codec_class("blosc") == MockBloscCodec # test if codec is used arr = zarr.create_array( store=store, shape=(100,), chunks=(10,), zarr_format=3, dtype="i4", compressors=[{"name": "blosc", "configuration": {}}], ) arr[:] = range(100) _mock.call.assert_called() # test set codec with environment variable class NewBloscCodec(BloscCodec): pass register_codec("blosc", NewBloscCodec) with mock.patch.dict(os.environ, {"ZARR_CODECS__BLOSC": fully_qualified_name(NewBloscCodec)}): assert get_codec_class("blosc", reload_config=True) == NewBloscCodec @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_config_ndbuffer_implementation(store: Store) -> None: # set custom ndbuffer with TestNDArrayLike implementation register_ndbuffer(NDBufferUsingTestNDArrayLike) with config.set({"ndbuffer": fully_qualified_name(NDBufferUsingTestNDArrayLike)}): assert get_ndbuffer_class() == NDBufferUsingTestNDArrayLike arr = zarr.create_array( store=store, shape=(100,), chunks=(10,), zarr_format=3, dtype="i4", ) got = arr[:] assert isinstance(got, TestNDArrayLike) def test_config_buffer_implementation() -> None: # has default value assert config.defaults[0]["buffer"] == "zarr.buffer.cpu.Buffer" arr = zeros(shape=(100,), store=StoreExpectingTestBuffer()) # AssertionError of StoreExpectingTestBuffer when not using my buffer with pytest.raises(AssertionError): arr[:] = np.arange(100) register_buffer(TestBuffer) with config.set({"buffer": fully_qualified_name(TestBuffer)}): assert get_buffer_class() == TestBuffer # no error using TestBuffer data = np.arange(100) arr[:] = np.arange(100) assert np.array_equal(arr[:], data) data2d = np.arange(1000).reshape(100, 10) arr_sharding = zeros( shape=(100, 10), store=StoreExpectingTestBuffer(), codecs=[ShardingCodec(chunk_shape=(10, 10))], ) arr_sharding[:] = data2d assert np.array_equal(arr_sharding[:], data2d) arr_Crc32c = zeros( shape=(100, 10), store=StoreExpectingTestBuffer(), codecs=[BytesCodec(), Crc32cCodec()], ) arr_Crc32c[:] = data2d assert np.array_equal(arr_Crc32c[:], data2d) def test_config_buffer_backwards_compatibility() -> None: # This should warn once zarr.core is private # https://github.com/zarr-developers/zarr-python/issues/2621 with zarr.config.set( {"buffer": "zarr.core.buffer.cpu.Buffer", "ndbuffer": "zarr.core.buffer.cpu.NDBuffer"} ): get_buffer_class() get_ndbuffer_class() @pytest.mark.gpu def test_config_buffer_backwards_compatibility_gpu() -> None: # This should warn once zarr.core is private # https://github.com/zarr-developers/zarr-python/issues/2621 with zarr.config.set( {"buffer": "zarr.core.buffer.gpu.Buffer", "ndbuffer": "zarr.core.buffer.gpu.NDBuffer"} ): get_buffer_class() get_ndbuffer_class() @pytest.mark.filterwarnings("error") def test_warning_on_missing_codec_config() -> None: class NewCodec(BytesCodec): pass class NewCodec2(BytesCodec): pass # error if codec is not registered with pytest.raises(KeyError): get_codec_class("missing_codec") # no warning if only one implementation is available register_codec("new_codec", NewCodec) get_codec_class("new_codec") # warning because multiple implementations are available but none is selected in the config register_codec("new_codec", NewCodec2) with pytest.warns( ZarrUserWarning, match="not configured in config. Selecting any implementation" ): get_codec_class("new_codec") # no warning if multiple implementations are available and one is selected in the config with config.set({"codecs.new_codec": fully_qualified_name(NewCodec)}): get_codec_class("new_codec") @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize( "kwargs", [ {"shards": (4, 4)}, {"compressors": None}, ], ids=["partial_decode", "full_decode"], ) def test_config_read_missing_chunks(store: Store, kwargs: dict[str, Any]) -> None: arr = zarr.create_array( store=store, shape=(4, 4), chunks=(2, 2), dtype="int32", fill_value=42, **kwargs, ) # default behavior: missing chunks are filled with the fill value result = zarr.open_array(store)[:] assert np.array_equal(result, np.full((4, 4), 42, dtype="int32")) # with read_missing_chunks=False, reading missing chunks raises an error with config.set({"array.read_missing_chunks": False}): with pytest.raises(ChunkNotFoundError): zarr.open_array(store)[:] # after writing data, all chunks exist and no error is raised arr[:] = np.arange(16, dtype="int32").reshape(4, 4) with config.set({"array.read_missing_chunks": False}): result = zarr.open_array(store)[:] assert np.array_equal(result, np.arange(16, dtype="int32").reshape(4, 4)) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_config_read_missing_chunks_sharded_inner(store: Store) -> None: """Because the shard index and inner chunks should be stored together in a single storage object (read: a file or blob), we delegate to the shard index the responsibility of determining what chunks should be present. Thus, `read_missing_chunks` raises an error only if the entire *shard* is missing. Missing inner chunks are filled with the array's fill value and do not raise an error, even if `read_missing_chunks=False` at the array level. """ arr = zarr.create_array( store=store, shape=(8, 4), chunks=(2, 2), shards=(4, 4), dtype="int32", fill_value=42, ) # write only one inner chunk in the first shard, leaving the second shard empty arr[0:2, 0:2] = np.ones((2, 2), dtype="int32") with config.set({"array.read_missing_chunks": False}): a = zarr.open_array(store) # first shard exists: missing inner chunks are filled, no error result = a[:4] expected = np.full((4, 4), 42, dtype="int32") expected[0:2, 0:2] = 1 assert np.array_equal(result, expected) # second shard is entirely missing: raises an error with pytest.raises(ChunkNotFoundError): a[4:] @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_config_read_missing_chunks_write_empty_chunks(store: Store) -> None: """write_empty_chunks=False drops chunks equal to fill_value, which then appear missing to read_missing_chunks=False.""" arr = zarr.create_array( store=store, shape=(4,), chunks=(2,), dtype="int32", fill_value=0, config={"write_empty_chunks": False, "read_missing_chunks": False}, ) # write non-fill-value data: chunks are stored arr[:] = [1, 2, 3, 4] assert np.array_equal(arr[:], [1, 2, 3, 4]) # overwrite with fill_value: chunks are dropped by write_empty_chunks=False arr[:] = 0 with pytest.raises(ChunkNotFoundError): arr[:] # with write_empty_chunks=True, chunks are kept and no error is raised with config.set({"array.write_empty_chunks": True}): arr = zarr.open_array(store) arr[:] = 0 assert np.array_equal(arr[:], [0, 0, 0, 0]) @pytest.mark.parametrize( "key", [ "array.v2_default_compressor.numeric", "array.v2_default_compressor.string", "array.v2_default_compressor.bytes", "array.v2_default_filters.string", "array.v2_default_filters.bytes", "array.v3_default_filters.numeric", "array.v3_default_filters.raw", "array.v3_default_filters.bytes", "array.v3_default_serializer.numeric", "array.v3_default_serializer.string", "array.v3_default_serializer.bytes", "array.v3_default_compressors.string", "array.v3_default_compressors.bytes", "array.v3_default_compressors", ], ) def test_deprecated_config(key: str) -> None: """ Test that a valuerror is raised when setting the default chunk encoding for a given data type category """ with pytest.raises(ValueError): with zarr.config.set({key: "foo"}): pass zarr-python-3.2.1/tests/test_docs.py000066400000000000000000000101321517635743000175250ustar00rootroot00000000000000""" Tests for executable code blocks in markdown documentation. This module uses pytest-examples to validate that all Python code examples with exec="true" in the documentation execute successfully. """ from __future__ import annotations from collections import defaultdict from pathlib import Path import pytest pytest.importorskip("pytest_examples") from pytest_examples import CodeExample, EvalExample, find_examples # Find all markdown files with executable code blocks DOCS_ROOT = Path(__file__).parent.parent / "docs" SOURCES_ROOT = Path(__file__).parent.parent / "src" / "zarr" def find_markdown_files_with_exec() -> list[Path]: """Find all markdown files containing exec="true" code blocks.""" markdown_files = [] for md_file in DOCS_ROOT.rglob("*.md"): try: content = md_file.read_text(encoding="utf-8") if 'exec="true"' in content: markdown_files.append(md_file) except Exception: # Skip files that can't be read continue return sorted(markdown_files) def group_examples_by_session() -> list[tuple[str, str]]: """ Group examples by their session and file, maintaining order. Returns a list of session_key tuples where session_key is (file_path, session_name). """ all_examples = list(find_examples(DOCS_ROOT)) # Group by file and session sessions = defaultdict(list) for example in all_examples: settings = example.prefix_settings() if settings.get("exec") != "true": continue # Use file path and session name as key file_path = example.path session_name = settings.get("session", "_default") session_key = (str(file_path), session_name) sessions[session_key].append(example) # Return sorted list of session keys for consistent test ordering return sorted(sessions.keys(), key=lambda x: (x[0], x[1])) def name_example(path: str, session: str) -> str: """Generate a readable name for a test case from file path and session.""" return f"{Path(path).relative_to(DOCS_ROOT)}:{session}" # Get all example sessions @pytest.mark.parametrize( "session_key", group_examples_by_session(), ids=lambda v: name_example(v[0], v[1]) ) def test_documentation_examples( session_key: tuple[str, str], eval_example: EvalExample, ) -> None: """ Test that all exec="true" code examples in documentation execute successfully. This test groups examples by session (file + session name) and runs them sequentially in the same execution context, allowing code to build on previous examples. This test uses pytest-examples to: - Find all code examples with exec="true" in markdown files - Group them by session - Execute them in order within the same context - Verify no exceptions are raised """ file_path, session_name = session_key # Get examples for this session all_examples = list(find_examples(DOCS_ROOT)) examples = [] for example in all_examples: settings = example.prefix_settings() if settings.get("exec") != "true": continue if str(example.path) == file_path and settings.get("session", "_default") == session_name: examples.append(example) # Run all examples in this session sequentially, preserving state module_globals: dict[str, object] = {} for example in examples: # TODO: uncomment this line when we are ready to fix output checks # result = eval_example.run_print_check(example, module_globals=module_globals) result = eval_example.run(example, module_globals=module_globals) # Update globals with the results from this execution module_globals.update(result) @pytest.mark.parametrize("example", find_examples(str(SOURCES_ROOT)), ids=str) def test_docstrings(example: CodeExample, eval_example: EvalExample) -> None: """Test our docstring examples.""" if example.path.name == "config.py" and "your.module" in example.source: pytest.skip("Skip testing docstring example that assumes nonexistent module.") eval_example.run_print_check(example) zarr-python-3.2.1/tests/test_dtype/000077500000000000000000000000001517635743000173535ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_dtype/__init__.py000066400000000000000000000000001517635743000214520ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_dtype/conftest.py000066400000000000000000000050211517635743000215500ustar00rootroot00000000000000# Generate a collection of zdtype instances for use in testing. import warnings from typing import Any import numpy as np from zarr.core.dtype import data_type_registry from zarr.core.dtype.common import HasLength from zarr.core.dtype.npy.structured import Struct from zarr.core.dtype.npy.time import DateTime64, TimeDelta64 from zarr.core.dtype.wrapper import ZDType zdtype_examples: tuple[ZDType[Any, Any], ...] = () for wrapper_cls in data_type_registry.contents.values(): if wrapper_cls is Struct: with warnings.catch_warnings(): warnings.simplefilter("ignore") zdtype_examples += ( wrapper_cls.from_native_dtype(np.dtype([("a", np.float64), ("b", np.int8)])), ) elif issubclass(wrapper_cls, HasLength): zdtype_examples += (wrapper_cls(length=1),) elif issubclass(wrapper_cls, DateTime64 | TimeDelta64): zdtype_examples += (wrapper_cls(unit="s", scale_factor=10),) else: zdtype_examples += (wrapper_cls(),) def pytest_generate_tests(metafunc: Any) -> None: """ This is a pytest hook to parametrize class-scoped fixtures. This hook allows us to define class-scoped fixtures as class attributes and then generate the parametrize calls for pytest. This allows the fixtures to be reused across multiple tests within the same class. For example, if you had a regular pytest class like this: class TestClass: @pytest.mark.parametrize("param_a", [1, 2, 3]) def test_method(self, param_a): ... Child classes inheriting from ``TestClass`` would not be able to override the ``param_a`` fixture this implementation of ``pytest_generate_tests`` allows you to define class-scoped fixtures as class attributes, which allows the following to work: class TestExample: param_a = [1, 2, 3] def test_example(self, param_a): ... # this class will have its test_example method parametrized with the values of TestB.param_a class TestB(TestExample): param_a = [1, 2, 100, 10] """ # Iterate over all the fixtures defined in the class # and parametrize them with the values defined in the class # This allows us to define class-scoped fixtures as class attributes # and then generate the parametrize calls for pytest for fixture_name in metafunc.fixturenames: if hasattr(metafunc.cls, fixture_name): params = getattr(metafunc.cls, fixture_name) metafunc.parametrize(fixture_name, params, scope="class", ids=str) zarr-python-3.2.1/tests/test_dtype/test_npy/000077500000000000000000000000001517635743000212205ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_dtype/test_npy/test_bool.py000066400000000000000000000020501517635743000235610ustar00rootroot00000000000000from __future__ import annotations import numpy as np from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.bool import Bool class TestBool(BaseTestZDType): test_cls = Bool valid_dtype = (np.dtype(np.bool_),) invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype(np.uint16), ) valid_json_v2 = ({"name": "|b1", "object_codec_id": None},) valid_json_v3 = ("bool",) invalid_json_v2 = ( "|b1", "bool", "|f8", ) invalid_json_v3 = ( "|b1", "|f8", {"name": "bool", "configuration": {"endianness": "little"}}, ) scalar_v2_params = ((Bool(), True), (Bool(), False)) scalar_v3_params = ((Bool(), True), (Bool(), False)) cast_value_params = ( (Bool(), "true", np.True_), (Bool(), True, np.True_), (Bool(), False, np.False_), (Bool(), np.True_, np.True_), (Bool(), np.False_, np.False_), ) invalid_scalar_params = (None,) item_size_params = (Bool(),) zarr-python-3.2.1/tests/test_dtype/test_npy/test_bytes.py000066400000000000000000000126131517635743000237620ustar00rootroot00000000000000import numpy as np import pytest from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.bytes import NullTerminatedBytes, RawBytes, VariableLengthBytes from zarr.errors import UnstableSpecificationWarning class TestNullTerminatedBytes(BaseTestZDType): test_cls = NullTerminatedBytes valid_dtype = (np.dtype("|S10"), np.dtype("|S4")) invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype("|U10"), ) valid_json_v2 = ( {"name": "|S1", "object_codec_id": None}, {"name": "|S2", "object_codec_id": None}, {"name": "|S4", "object_codec_id": None}, ) valid_json_v3 = ({"name": "null_terminated_bytes", "configuration": {"length_bytes": 10}},) invalid_json_v2 = ( "|S", "|U10", "|f8", {"name": "|S4", "object_codec_id": "vlen-bytes"}, ) invalid_json_v3 = ( {"name": "fixed_length_ascii", "configuration": {"length_bits": 0}}, {"name": "numpy.fixed_length_ascii", "configuration": {"length_bits": "invalid"}}, ) scalar_v2_params = ( (NullTerminatedBytes(length=1), "MA=="), (NullTerminatedBytes(length=2), "YWI="), (NullTerminatedBytes(length=4), "YWJjZA=="), ) scalar_v3_params = ( (NullTerminatedBytes(length=1), "MA=="), (NullTerminatedBytes(length=2), "YWI="), (NullTerminatedBytes(length=4), "YWJjZA=="), ) cast_value_params = ( (NullTerminatedBytes(length=1), "", np.bytes_("")), (NullTerminatedBytes(length=2), "ab", np.bytes_("ab")), (NullTerminatedBytes(length=4), "abcdefg", np.bytes_("abcd")), ) invalid_scalar_params = ((NullTerminatedBytes(length=1), 1.0),) item_size_params = ( NullTerminatedBytes(length=1), NullTerminatedBytes(length=4), NullTerminatedBytes(length=10), ) class TestRawBytes(BaseTestZDType): test_cls = RawBytes valid_dtype = (np.dtype("|V10"),) invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype("|S10"), ) valid_json_v2 = ({"name": "|V10", "object_codec_id": None},) valid_json_v3 = ( {"name": "raw_bytes", "configuration": {"length_bytes": 1}}, {"name": "raw_bytes", "configuration": {"length_bytes": 8}}, ) invalid_json_v2 = ( "|V", "|S10", "|f8", ) invalid_json_v3 = ( {"name": "r10"}, {"name": "r-80"}, ) scalar_v2_params = ( (RawBytes(length=1), "AA=="), (RawBytes(length=2), "YWI="), (RawBytes(length=4), "YWJjZA=="), ) scalar_v3_params = ( (RawBytes(length=1), "AA=="), (RawBytes(length=2), "YWI="), (RawBytes(length=4), "YWJjZA=="), ) cast_value_params = ( (RawBytes(length=1), b"\x00", np.void(b"\x00")), (RawBytes(length=2), b"ab", np.void(b"ab")), (RawBytes(length=4), b"abcd", np.void(b"abcd")), ) invalid_scalar_params = ((RawBytes(length=1), 1.0),) item_size_params = ( RawBytes(length=1), RawBytes(length=4), RawBytes(length=10), ) class TestVariableLengthBytes(BaseTestZDType): test_cls = VariableLengthBytes valid_dtype = (np.dtype("|O"),) invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype("|U10"), ) valid_json_v2 = ({"name": "|O", "object_codec_id": "vlen-bytes"},) valid_json_v3 = ("variable_length_bytes",) invalid_json_v2 = ( "|S", "|U10", "|f8", ) invalid_json_v3 = ( {"name": "fixed_length_ascii", "configuration": {"length_bits": 0}}, {"name": "numpy.fixed_length_ascii", "configuration": {"length_bits": "invalid"}}, ) scalar_v2_params = ( (VariableLengthBytes(), ""), (VariableLengthBytes(), "YWI="), (VariableLengthBytes(), "YWJjZA=="), ) scalar_v3_params = ( (VariableLengthBytes(), ""), (VariableLengthBytes(), "YWI="), (VariableLengthBytes(), "YWJjZA=="), ) cast_value_params = ( (VariableLengthBytes(), "", b""), (VariableLengthBytes(), "ab", b"ab"), (VariableLengthBytes(), "abcdefg", b"abcdefg"), ) invalid_scalar_params = ((VariableLengthBytes(), 1.0),) item_size_params = (VariableLengthBytes(),) def test_vlen_bytes_alias() -> None: """Test that "bytes" is an accepted alias for "variable_length_bytes" in JSON metadata""" a = VariableLengthBytes.from_json("bytes", zarr_format=3) b = VariableLengthBytes.from_json("variable_length_bytes", zarr_format=3) assert a == b @pytest.mark.parametrize( "zdtype", [NullTerminatedBytes(length=10), RawBytes(length=10), VariableLengthBytes()] ) def test_unstable_dtype_warning( zdtype: NullTerminatedBytes | RawBytes | VariableLengthBytes, ) -> None: """ Test that we get a warning when serializing a dtype without a zarr v3 spec to json when zarr_format is 3 """ with pytest.warns(UnstableSpecificationWarning): zdtype.to_json(zarr_format=3) @pytest.mark.parametrize("zdtype_cls", [NullTerminatedBytes, RawBytes]) def test_invalid_size(zdtype_cls: type[NullTerminatedBytes] | type[RawBytes]) -> None: """ Test that it's impossible to create a data type that has no length """ length = 0 msg = f"length must be >= 1, got {length}." with pytest.raises(ValueError, match=msg): zdtype_cls(length=length) zarr-python-3.2.1/tests/test_dtype/test_npy/test_common.py000066400000000000000000000275311517635743000241310ustar00rootroot00000000000000from __future__ import annotations import base64 import re import sys from typing import TYPE_CHECKING, Any, get_args import numpy as np import pytest from tests.conftest import nan_equal from zarr.core.dtype.common import ENDIANNESS_STR, JSONFloatV2, SpecialFloatStrings from zarr.core.dtype.npy.common import ( NumpyEndiannessStr, bytes_from_json, bytes_to_json, check_json_bool, check_json_complex_float_v2, check_json_complex_float_v3, check_json_float_v2, check_json_float_v3, check_json_int, check_json_intish_float, check_json_str, complex_float_to_json_v2, complex_float_to_json_v3, endianness_from_numpy_str, endianness_to_numpy_str, float_from_json_v2, float_from_json_v3, float_to_json_v2, float_to_json_v3, ) if TYPE_CHECKING: from zarr.core.common import JSON, ZarrFormat json_float_v2_roundtrip_cases: tuple[tuple[JSONFloatV2, float | np.floating[Any]], ...] = ( ("Infinity", float("inf")), ("Infinity", np.inf), ("-Infinity", float("-inf")), ("-Infinity", -np.inf), ("NaN", float("nan")), ("NaN", np.nan), (1.0, 1.0), ) json_float_v3_cases = json_float_v2_roundtrip_cases @pytest.mark.parametrize( ("data", "expected"), [(">", "big"), ("<", "little"), ("=", sys.byteorder), ("|", None), ("err", "")], ) def test_endianness_from_numpy_str(data: str, expected: str | None) -> None: """ Test that endianness_from_numpy_str correctly converts a numpy str literal to a human-readable literal value. This test also checks that an invalid string input raises a ``ValueError`` """ if data in get_args(NumpyEndiannessStr): assert endianness_from_numpy_str(data) == expected # type: ignore[arg-type] else: msg = f"Invalid endianness: {data!r}. Expected one of {get_args(NumpyEndiannessStr)}" with pytest.raises(ValueError, match=re.escape(msg)): endianness_from_numpy_str(data) # type: ignore[arg-type] @pytest.mark.parametrize( ("data", "expected"), [("big", ">"), ("little", "<"), (None, "|"), ("err", "")], ) def test_endianness_to_numpy_str(data: str | None, expected: str) -> None: """ Test that endianness_to_numpy_str correctly converts a human-readable literal value to a numpy str literal. This test also checks that an invalid string input raises a ``ValueError`` """ if data in ENDIANNESS_STR: assert endianness_to_numpy_str(data) == expected # type: ignore[arg-type] else: msg = f"Invalid endianness: {data!r}. Expected one of {ENDIANNESS_STR}" with pytest.raises(ValueError, match=re.escape(msg)): endianness_to_numpy_str(data) # type: ignore[arg-type] @pytest.mark.parametrize( ("data", "expected"), json_float_v2_roundtrip_cases + (("SHOULD_ERR", ""),) ) def test_float_from_json_v2(data: JSONFloatV2 | str, expected: float | str) -> None: """ Test that float_from_json_v2 correctly converts a JSON string representation of a float to a float. This test also checks that an invalid string input raises a ``ValueError`` """ if data != "SHOULD_ERR": assert nan_equal(float_from_json_v2(data), expected) # type: ignore[arg-type] else: msg = f"could not convert string to float: {data!r}" with pytest.raises(ValueError, match=msg): float_from_json_v2(data) # type: ignore[arg-type] @pytest.mark.parametrize( ("data", "expected"), json_float_v3_cases + (("SHOULD_ERR", ""), ("0x", "")) ) def test_float_from_json_v3(data: JSONFloatV2 | str, expected: float | str) -> None: """ Test that float_from_json_v3 correctly converts a JSON string representation of a float to a float. This test also checks that an invalid string input raises a ``ValueError`` """ if data == "SHOULD_ERR": msg = ( f"Invalid float value: {data!r}. Expected a string starting with the hex prefix" " '0x', or one of 'NaN', 'Infinity', or '-Infinity'." ) with pytest.raises(ValueError, match=msg): float_from_json_v3(data) elif data == "0x": msg = ( f"Invalid hexadecimal float value: {data!r}. " "Expected the '0x' prefix to be followed by 4, 8, or 16 numeral characters" ) with pytest.raises(ValueError, match=msg): float_from_json_v3(data) else: assert nan_equal(float_from_json_v3(data), expected) # note the order of parameters relative to the order of the parametrized variable. @pytest.mark.parametrize(("expected", "data"), json_float_v2_roundtrip_cases) def test_float_to_json_v2(data: float | np.floating[Any], expected: JSONFloatV2) -> None: """ Test that floats are JSON-encoded properly for zarr v2 """ observed = float_to_json_v2(data) assert observed == expected # note the order of parameters relative to the order of the parametrized variable. @pytest.mark.parametrize(("expected", "data"), json_float_v3_cases) def test_float_to_json_v3(data: float | np.floating[Any], expected: JSONFloatV2) -> None: """ Test that floats are JSON-encoded properly for zarr v3 """ observed = float_to_json_v3(data) assert observed == expected def test_bytes_from_json(zarr_format: ZarrFormat) -> None: """ Test that a string is interpreted as base64-encoded bytes using the ascii alphabet. This test takes zarr_format as a parameter but doesn't actually do anything with it, because at present there is no zarr-format-specific logic in the code being tested, but such logic may exist in the future. """ data = "\00" assert bytes_from_json(data, zarr_format=zarr_format) == base64.b64decode(data.encode("ascii")) def test_bytes_to_json(zarr_format: ZarrFormat) -> None: """ Test that bytes are encoded with base64 using the ascii alphabet. This test takes zarr_format as a parameter but doesn't actually do anything with it, because at present there is no zarr-format-specific logic in the code being tested, but such logic may exist in the future. """ data = b"asdas" assert bytes_to_json(data, zarr_format=zarr_format) == base64.b64encode(data).decode("ascii") # note the order of parameters relative to the order of the parametrized variable. @pytest.mark.parametrize(("json_expected", "float_data"), json_float_v2_roundtrip_cases) def test_complex_to_json_v2( float_data: float | np.floating[Any], json_expected: JSONFloatV2 ) -> None: """ Test that complex numbers are correctly converted to JSON in v2 format. This use the same test input as the float tests, but the conversion is tested for complex numbers with real and imaginary parts equal to the float values provided in the test cases. """ cplx = complex(float_data, float_data) cplx_npy = np.complex128(cplx) assert complex_float_to_json_v2(cplx) == (json_expected, json_expected) assert complex_float_to_json_v2(cplx_npy) == (json_expected, json_expected) # note the order of parameters relative to the order of the parametrized variable. @pytest.mark.parametrize(("json_expected", "float_data"), json_float_v3_cases) def test_complex_to_json_v3( float_data: float | np.floating[Any], json_expected: JSONFloatV2 ) -> None: """ Test that complex numbers are correctly converted to JSON in v3 format. This use the same test input as the float tests, but the conversion is tested for complex numbers with real and imaginary parts equal to the float values provided in the test cases. """ cplx = complex(float_data, float_data) cplx_npy = np.complex128(cplx) assert complex_float_to_json_v3(cplx) == (json_expected, json_expected) assert complex_float_to_json_v3(cplx_npy) == (json_expected, json_expected) @pytest.mark.parametrize(("json_expected", "float_data"), json_float_v3_cases) def test_complex_float_to_json( float_data: float | np.floating[Any], json_expected: JSONFloatV2, zarr_format: ZarrFormat ) -> None: """ Test that complex numbers are correctly converted to JSON in v2 or v3 formats, depending on the ``zarr_format`` keyword argument. This use the same test input as the float tests, but the conversion is tested for complex numbers with real and imaginary parts equal to the float values provided in the test cases. """ cplx = complex(float_data, float_data) cplx_npy = np.complex128(cplx) if zarr_format == 2: assert complex_float_to_json_v2(cplx) == (json_expected, json_expected) assert complex_float_to_json_v2(cplx_npy) == ( json_expected, json_expected, ) elif zarr_format == 3: assert complex_float_to_json_v3(cplx) == (json_expected, json_expected) assert complex_float_to_json_v3(cplx_npy) == ( json_expected, json_expected, ) else: raise ValueError("zarr_format must be 2 or 3") # pragma: no cover check_json_float_cases = get_args(SpecialFloatStrings) + (1.0, 2) @pytest.mark.parametrize("data", check_json_float_cases) def test_check_json_float_v2_valid(data: JSONFloatV2 | int) -> None: assert check_json_float_v2(data) def test_check_json_float_v2_invalid() -> None: assert not check_json_float_v2("invalid") @pytest.mark.parametrize("data", check_json_float_cases) def test_check_json_float_v3_valid(data: JSONFloatV2 | int) -> None: assert check_json_float_v3(data) def test_check_json_float_v3_invalid() -> None: assert not check_json_float_v3("invalid") check_json_complex_float_true_cases: tuple[list[JSONFloatV2], ...] = ( [0.0, 1.0], [0.0, 1.0], [-1.0, "NaN"], ["Infinity", 1.0], ["Infinity", "NaN"], ) check_json_complex_float_false_cases: tuple[object, ...] = ( 0.0, "foo", [0.0], [1.0, 2.0, 3.0], [1.0, "_infinity_"], {"hello": 1.0}, ) @pytest.mark.parametrize("data", check_json_complex_float_true_cases) def test_check_json_complex_float_v2_true(data: JSON) -> None: assert check_json_complex_float_v2(data) @pytest.mark.parametrize("data", check_json_complex_float_false_cases) def test_check_json_complex_float_v2_false(data: JSON) -> None: assert not check_json_complex_float_v2(data) @pytest.mark.parametrize("data", check_json_complex_float_true_cases) def test_check_json_complex_float_v3_true(data: JSON) -> None: assert check_json_complex_float_v3(data) @pytest.mark.parametrize("data", check_json_complex_float_false_cases) def test_check_json_complex_float_v3_false(data: JSON) -> None: assert not check_json_complex_float_v3(data) @pytest.mark.parametrize("data", check_json_complex_float_true_cases) def test_check_json_complex_float_true(data: JSON, zarr_format: ZarrFormat) -> None: if zarr_format == 2: assert check_json_complex_float_v2(data) elif zarr_format == 3: assert check_json_complex_float_v3(data) else: raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover @pytest.mark.parametrize("data", check_json_complex_float_false_cases) def test_check_json_complex_float_false(data: JSON, zarr_format: ZarrFormat) -> None: if zarr_format == 2: assert not check_json_complex_float_v2(data) elif zarr_format == 3: assert not check_json_complex_float_v3(data) else: raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover def test_check_json_int() -> None: assert check_json_int(0) assert not check_json_int(1.0) def test_check_json_intish_float() -> None: assert check_json_intish_float(0.0) assert check_json_intish_float(1.0) assert not check_json_intish_float("0") assert not check_json_intish_float(1.1) def test_check_json_str() -> None: assert check_json_str("0") assert not check_json_str(1.0) def test_check_json_bool() -> None: assert check_json_bool(True) assert check_json_bool(False) assert not check_json_bool(1.0) assert not check_json_bool("True") zarr-python-3.2.1/tests/test_dtype/test_npy/test_complex.py000066400000000000000000000057411517635743000243070ustar00rootroot00000000000000from __future__ import annotations import math import numpy as np from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.complex import Complex64, Complex128 class _BaseTestFloat(BaseTestZDType): def scalar_equals(self, scalar1: object, scalar2: object) -> bool: if np.isnan(scalar1) and np.isnan(scalar2): # type: ignore[call-overload] return True return super().scalar_equals(scalar1, scalar2) class TestComplex64(_BaseTestFloat): test_cls = Complex64 valid_dtype = (np.dtype(">c8"), np.dtype("c8", "object_codec_id": None}, {"name": "c16"), np.dtype("c16", "object_codec_id": None}, {"name": " bool: if np.isnan(scalar1) and np.isnan(scalar2): # type: ignore[call-overload] return True return super().scalar_equals(scalar1, scalar2) hex_string_params: tuple[tuple[str, float], ...] = () def test_hex_encoding(self, hex_string_params: tuple[str, float]) -> None: """ Test that hexadecimal strings can be read as NaN values """ hex_string, expected = hex_string_params zdtype = self.test_cls() observed = zdtype.from_json_scalar(hex_string, zarr_format=3) assert self.scalar_equals(observed, expected) class TestFloat16(_BaseTestFloat): test_cls = Float16 valid_dtype = (np.dtype(">f2"), np.dtype("f2", "object_codec_id": None}, {"name": "f4"), np.dtype("f4", "object_codec_id": None}, {"name": "f8"), np.dtype("f8", "object_codec_id": None}, {"name": " None: """Test the check_json_floatish_str function.""" from zarr.core.dtype.npy.common import check_json_floatish_str # Test valid string floats assert check_json_floatish_str("3.14") assert check_json_floatish_str("0.0") assert check_json_floatish_str("-2.5") assert check_json_floatish_str("1.0") # Test invalid cases assert not check_json_floatish_str("not_a_number") assert not check_json_floatish_str("") assert not check_json_floatish_str(3.14) # actual float, not string assert not check_json_floatish_str(42) # int assert not check_json_floatish_str(None) # Test that special cases still work via float() conversion # (these will be handled by existing functions first in practice) assert check_json_floatish_str("NaN") assert check_json_floatish_str("Infinity") assert check_json_floatish_str("-Infinity") def test_string_float_from_json_scalar() -> None: """Test that string representations of floats can be parsed by from_json_scalar.""" # Test with Float32 dtype_instance = Float32() result = dtype_instance.from_json_scalar("3.14", zarr_format=3) assert abs(result - np.float32(3.14)) < 1e-6 assert isinstance(result, np.float32) # Test other cases result = dtype_instance.from_json_scalar("0.0", zarr_format=3) assert result == np.float32(0.0) result = dtype_instance.from_json_scalar("-2.5", zarr_format=3) assert result == np.float32(-2.5) # Test that it works for v2 format too result = dtype_instance.from_json_scalar("1.5", zarr_format=2) assert result == np.float32(1.5) zarr-python-3.2.1/tests/test_dtype/test_npy/test_int.py000066400000000000000000000231401517635743000234230ustar00rootroot00000000000000from __future__ import annotations import numpy as np from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.int import Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 class TestInt8(BaseTestZDType): test_cls = Int8 scalar_type = np.int8 valid_dtype = (np.dtype(np.int8),) invalid_dtype = ( np.dtype(np.int16), np.dtype(np.uint16), np.dtype(np.float64), ) valid_json_v2 = ({"name": "|i1", "object_codec_id": None},) valid_json_v3 = ("int8",) invalid_json_v2 = ( ">i1", "int8", "|f8", ) invalid_json_v3 = ( "|i1", "|f8", {"name": "int8", "configuration": {"endianness": "little"}}, ) scalar_v2_params = ((Int8(), 1), (Int8(), -1), (Int8(), 1.0)) scalar_v3_params = ((Int8(), 1), (Int8(), -1)) cast_value_params = ( (Int8(), 1, np.int8(1)), (Int8(), -1, np.int8(-1)), ) invalid_scalar_params = ((Int8(), {"set!"}), (Int8(), ("tuple",))) item_size_params = (Int8(),) class TestInt16(BaseTestZDType): test_cls = Int16 scalar_type = np.int16 valid_dtype = (np.dtype(">i2"), np.dtype("i2", "object_codec_id": None}, {"name": "i4"), np.dtype("i4", "object_codec_id": None}, {"name": "i8"), np.dtype("i8", "object_codec_id": None}, {"name": "u2"), np.dtype("u2", "object_codec_id": None}, {"name": "u4"), np.dtype("u4", "object_codec_id": None}, {"name": "u8"), np.dtype("u8", "object_codec_id": None}, {"name": " None: """Test the check_json_intish_str function.""" from zarr.core.dtype.npy.common import check_json_intish_str # Test valid string integers assert check_json_intish_str("0") assert check_json_intish_str("42") assert check_json_intish_str("-5") assert check_json_intish_str("123") # Test invalid cases assert not check_json_intish_str("3.14") assert not check_json_intish_str("not_a_number") assert not check_json_intish_str("") assert not check_json_intish_str(42) # actual int, not string assert not check_json_intish_str(3.14) # float assert not check_json_intish_str(None) def test_string_integer_from_json_scalar() -> None: """Test that string representations of integers can be parsed by from_json_scalar.""" # Test the specific reproducer case dtype_instance = Int32() result = dtype_instance.from_json_scalar("0", zarr_format=3) assert result == np.int32(0) assert isinstance(result, np.int32) # Test other cases result = dtype_instance.from_json_scalar("42", zarr_format=3) assert result == np.int32(42) result = dtype_instance.from_json_scalar("-5", zarr_format=3) assert result == np.int32(-5) # Test that it works for v2 format too result = dtype_instance.from_json_scalar("123", zarr_format=2) assert result == np.int32(123) zarr-python-3.2.1/tests/test_dtype/test_npy/test_string.py000066400000000000000000000113261517635743000241420ustar00rootroot00000000000000from __future__ import annotations import numpy as np import pytest from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype import FixedLengthUTF32 from zarr.core.dtype.npy.string import _NUMPY_SUPPORTS_VLEN_STRING, VariableLengthUTF8 from zarr.errors import UnstableSpecificationWarning if _NUMPY_SUPPORTS_VLEN_STRING: class TestVariableLengthString(BaseTestZDType): test_cls = VariableLengthUTF8 # type: ignore[assignment] valid_dtype = (np.dtypes.StringDType(),) # type: ignore[assignment] invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype("|S10"), ) valid_json_v2 = ({"name": "|O", "object_codec_id": "vlen-utf8"},) valid_json_v3 = ("string",) invalid_json_v2 = ( "|S10", "|f8", "invalid", ) invalid_json_v3 = ( {"name": "variable_length_utf8", "configuration": {"invalid_key": "value"}}, {"name": "invalid_name"}, ) scalar_v2_params = ((VariableLengthUTF8(), ""), (VariableLengthUTF8(), "hi")) scalar_v3_params = ( (VariableLengthUTF8(), ""), (VariableLengthUTF8(), "hi"), ) cast_value_params = ( (VariableLengthUTF8(), "", np.str_("")), (VariableLengthUTF8(), "hi", np.str_("hi")), ) # anything can become a string invalid_scalar_params = (None,) item_size_params = (VariableLengthUTF8(),) else: class TestVariableLengthString(BaseTestZDType): # type: ignore[no-redef] test_cls = VariableLengthUTF8 # type: ignore[assignment] valid_dtype = (np.dtype("O"),) invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype("|S10"), ) valid_json_v2 = ({"name": "|O", "object_codec_id": "vlen-utf8"},) valid_json_v3 = ("string",) invalid_json_v2 = ( "|S10", "|f8", "invalid", ) invalid_json_v3 = ( {"name": "numpy.variable_length_utf8", "configuration": {"invalid_key": "value"}}, {"name": "invalid_name"}, ) scalar_v2_params = ((VariableLengthUTF8(), ""), (VariableLengthUTF8(), "hi")) scalar_v3_params = ( (VariableLengthUTF8(), ""), (VariableLengthUTF8(), "hi"), ) cast_value_params = ( (VariableLengthUTF8(), "", np.str_("")), (VariableLengthUTF8(), "hi", np.str_("hi")), ) # anything can become a string invalid_scalar_params = (None,) item_size_params = (VariableLengthUTF8(),) class TestFixedLengthUTF32(BaseTestZDType): test_cls = FixedLengthUTF32 valid_dtype = (np.dtype(">U10"), np.dtype("U10", "object_codec_id": None}, {"name": " None: """ Test that we get a warning when serializing a dtype without a zarr v3 spec to json when zarr_format is 3 """ with pytest.warns(UnstableSpecificationWarning): zdtype.to_json(zarr_format=3) def test_invalid_size() -> None: """ Test that it's impossible to create a data type that has no length """ length = 0 msg = f"length must be >= 1, got {length}." with pytest.raises(ValueError, match=msg): FixedLengthUTF32(length=length) zarr-python-3.2.1/tests/test_dtype/test_npy/test_structured.py000066400000000000000000000217341517635743000250440ustar00rootroot00000000000000from __future__ import annotations from typing import Any import numpy as np import pytest from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype import ( Float16, Float64, Int32, Int64, Struct, Structured, UInt8, ) class TestStruct(BaseTestZDType): """Test the canonical 'struct' dtype format.""" test_cls = Struct valid_dtype = ( np.dtype([("field1", np.int32), ("field2", np.float64)]), np.dtype([("field1", np.int64), ("field2", np.int32)]), ) invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype("|S10"), ) valid_json_v2 = ( {"name": [["field1", ">i4"], ["field2", ">f8"]], "object_codec_id": None}, {"name": [["field1", ">i8"], ["field2", ">i4"]], "object_codec_id": None}, ) valid_json_v3 = ( { "name": "struct", "configuration": { "fields": [ {"name": "field1", "data_type": "int32"}, {"name": "field2", "data_type": "float64"}, ] }, }, { "name": "struct", "configuration": { "fields": [ { "name": "field1", "data_type": { "name": "numpy.datetime64", "configuration": {"unit": "s", "scale_factor": 1}, }, }, { "name": "field2", "data_type": { "name": "fixed_length_utf32", "configuration": {"length_bytes": 32}, }, }, ] }, }, ) invalid_json_v2 = ( [("field1", "|i1"), ("field2", "|f8")], [("field1", "|S10"), ("field2", "|f8")], ) invalid_json_v3 = ( { "name": "struct", "configuration": { "fields": [ ("field1", {"name": "int32", "configuration": {"endianness": "invalid"}}), ("field2", {"name": "float64", "configuration": {"endianness": "big"}}), ] }, }, {"name": "invalid_name"}, ) scalar_v2_params = ( (Struct(fields=(("field1", Int32()), ("field2", Float64()))), "AQAAAAAAAAAAAPA/"), (Struct(fields=(("field1", Float16()), ("field2", Int32()))), "AQAAAAAA"), ) scalar_v3_params = ( ( Struct(fields=(("field1", Int32()), ("field2", Float64()))), {"field1": 1, "field2": 1.0}, ), (Struct(fields=(("field1", Int64()), ("field2", Int32()))), {"field1": 1, "field2": 1}), ) cast_value_params = ( ( Struct(fields=(("field1", Int32()), ("field2", Float64()))), (1, 2.0), np.array((1, 2.0), dtype=[("field1", np.int32), ("field2", np.float64)]), ), ( Struct(fields=(("field1", Int64()), ("field2", Int32()))), (3, 4.5), np.array((3, 4.5), dtype=[("field1", np.int64), ("field2", np.int32)]), ), ) item_size_params = ( Struct(fields=(("field1", Int32()), ("field2", Float64()))), Struct(fields=(("field1", Int64()), ("field2", Int32()))), ) invalid_scalar_params = ( (Struct(fields=(("field1", Int32()), ("field2", Float64()))), "i am a string"), (Struct(fields=(("field1", Int32()), ("field2", Float64()))), {"type": "dict"}), ) def scalar_equals(self, scalar1: Any, scalar2: Any) -> bool: if hasattr(scalar1, "shape") and hasattr(scalar2, "shape"): return np.array_equal(scalar1, scalar2) return super().scalar_equals(scalar1, scalar2) class TestStructured: """Test the legacy 'structured' dtype format.""" def test_invalid_size(self) -> None: """Test that it's impossible to create a data type that has no fields.""" fields = () msg = f"must have at least one field. Got {fields!r}" with pytest.raises(ValueError, match=msg): Structured(fields=fields) def test_structured_legacy_name_with_tuple_format(self) -> None: """Test that the legacy 'structured' name with tuple field format is accepted.""" json_v3 = { "name": "structured", "configuration": { "fields": [ ["field1", "int32"], ["field2", "float64"], ] }, } dtype = Structured.from_json(json_v3, zarr_format=3) assert dtype.fields[0][0] == "field1" assert dtype.fields[1][0] == "field2" @pytest.mark.filterwarnings("ignore::zarr.errors.UnstableSpecificationWarning") def test_structured_writes_tuple_format(self) -> None: """Test that 'structured' writes the tuple field format.""" dtype = Structured(fields=(("field1", Int32()), ("field2", Float64()))) json_v3 = dtype.to_json(zarr_format=3) assert json_v3["name"] == "structured" assert json_v3["configuration"]["fields"][0] == ["field1", "int32"] def test_invalid_size() -> None: """Test that it's impossible to create a data type that has no fields.""" fields = () msg = f"must have at least one field. Got {fields!r}" with pytest.raises(ValueError, match=msg): Struct(fields=fields) @pytest.mark.filterwarnings("ignore::zarr.errors.UnstableSpecificationWarning") def test_struct_name_is_primary() -> None: """Test that 'struct' is the primary name written to JSON.""" dtype = Struct(fields=(("field1", Int32()), ("field2", Float64()))) json_v3 = dtype.to_json(zarr_format=3) assert json_v3["name"] == "struct" def test_struct_reads_legacy_tuple_format() -> None: """Test that 'struct' dtype reads the legacy tuple field format.""" json_v3 = { "name": "struct", "configuration": { "fields": [ ["field1", "int32"], ["field2", "float64"], ] }, } dtype = Struct.from_json(json_v3, zarr_format=3) assert isinstance(dtype, Struct) assert dtype.fields[0][0] == "field1" assert dtype.fields[1][0] == "field2" def test_struct_reads_canonical_object_format() -> None: """Test that 'struct' dtype reads the new object field format.""" json_v3 = { "name": "struct", "configuration": { "fields": [ {"name": "field1", "data_type": "int32"}, {"name": "field2", "data_type": "float64"}, ] }, } dtype = Struct.from_json(json_v3, zarr_format=3) assert isinstance(dtype, Struct) assert dtype.fields[0][0] == "field1" assert dtype.fields[1][0] == "field2" def test_fill_value_dict_form() -> None: """Test that dict form fill values are properly parsed.""" dtype = Struct(fields=(("x", Int32()), ("y", Float64()))) fill_value = dtype.from_json_scalar({"x": 42, "y": 3.14}, zarr_format=3) assert fill_value["x"] == 42 assert fill_value["y"] == 3.14 def test_fill_value_dict_form_missing_fields() -> None: """Test that missing fields in dict form fill values use defaults.""" dtype = Struct(fields=(("x", Int32()), ("y", Float64()))) fill_value = dtype.from_json_scalar({"x": 42}, zarr_format=3) assert fill_value["x"] == 42 assert fill_value["y"] == 0.0 def test_fill_value_legacy_base64() -> None: """Test that legacy base64-encoded fill values are still readable.""" dtype = Struct(fields=(("field1", Int32()), ("field2", Float64()))) fill_value = dtype.from_json_scalar("AQAAAAAAAAAAAPA/", zarr_format=3) assert fill_value["field1"] == 1 assert fill_value["field2"] == 1.0 def test_fill_value_to_json_dict_form() -> None: """Test that fill values are serialized as dict form.""" dtype = Struct(fields=(("x", Int32()), ("y", Float64()))) scalar = np.array((42, 3.14), dtype=[("x", np.int32), ("y", np.float64)])[()] json_val = dtype.to_json_scalar(scalar, zarr_format=3) assert isinstance(json_val, dict) assert json_val["x"] == 42 assert json_val["y"] == 3.14 def test_has_multi_byte_fields_true() -> None: """Test that has_multi_byte_fields returns True for dtypes with multi-byte fields.""" dtype = Struct(fields=(("field1", Int32()), ("field2", Float64()))) assert dtype.has_multi_byte_fields() is True def test_has_multi_byte_fields_false() -> None: """Test that has_multi_byte_fields returns False for dtypes with only single-byte fields.""" dtype = Struct(fields=(("field1", UInt8()), ("field2", UInt8()))) assert dtype.has_multi_byte_fields() is False def test_struct_from_native_dtype() -> None: """Test that Struct can be created from native numpy dtype.""" dtype = np.dtype([("field1", np.int32), ("field2", np.float64)]) struct = Struct.from_native_dtype(dtype) assert struct.fields[0][0] == "field1" assert struct.fields[1][0] == "field2" zarr-python-3.2.1/tests/test_dtype/test_npy/test_time.py000066400000000000000000000142221517635743000235700ustar00rootroot00000000000000from __future__ import annotations import re from typing import get_args import numpy as np import pytest from tests.test_dtype.test_wrapper import BaseTestZDType from zarr.core.dtype.npy.common import DateTimeUnit from zarr.core.dtype.npy.time import DateTime64, TimeDelta64, datetime_from_int class _TestTimeBase(BaseTestZDType): def json_scalar_equals(self, scalar1: object, scalar2: object) -> bool: # This method gets overridden here to support the equivalency between NaT and # -9223372036854775808 fill values nat_scalars = (-9223372036854775808, "NaT") if scalar1 in nat_scalars and scalar2 in nat_scalars: return True return scalar1 == scalar2 def scalar_equals(self, scalar1: object, scalar2: object) -> bool: if np.isnan(scalar1) and np.isnan(scalar2): # type: ignore[call-overload] return True return super().scalar_equals(scalar1, scalar2) class TestDateTime64(_TestTimeBase): test_cls = DateTime64 valid_dtype = (np.dtype("datetime64[10ns]"), np.dtype("datetime64[us]"), np.dtype("datetime64")) invalid_dtype = ( np.dtype(np.int8), np.dtype(np.float64), np.dtype("timedelta64[ns]"), ) valid_json_v2 = ( {"name": ">M8", "object_codec_id": None}, {"name": ">M8[s]", "object_codec_id": None}, {"name": "m8", "object_codec_id": None}, {"name": ">m8[s]", "object_codec_id": None}, {"name": " None: """ Test that an invalid unit raises a ValueError. """ unit = "invalid" msg = f"unit must be one of ('Y', 'M', 'W', 'D', 'h', 'm', 's', 'ms', 'us', 'μs', 'ns', 'ps', 'fs', 'as', 'generic'), got {unit!r}." with pytest.raises(ValueError, match=re.escape(msg)): DateTime64(unit=unit) # type: ignore[arg-type] with pytest.raises(ValueError, match=re.escape(msg)): TimeDelta64(unit=unit) # type: ignore[arg-type] def test_time_scale_factor_too_low() -> None: """ Test that an invalid unit raises a ValueError. """ scale_factor = 0 msg = f"scale_factor must be > 0, got {scale_factor}." with pytest.raises(ValueError, match=msg): DateTime64(scale_factor=scale_factor) with pytest.raises(ValueError, match=msg): TimeDelta64(scale_factor=scale_factor) def test_default_is_NaT() -> None: np.testing.assert_equal( TimeDelta64(unit="ns", scale_factor=1).default_scalar(), np.timedelta64("NaT", "ns") ) def test_time_scale_factor_too_high() -> None: """ Test that an invalid unit raises a ValueError. """ scale_factor = 2**31 msg = f"scale_factor must be < 2147483648, got {scale_factor}." with pytest.raises(ValueError, match=msg): DateTime64(scale_factor=scale_factor) with pytest.raises(ValueError, match=msg): TimeDelta64(scale_factor=scale_factor) @pytest.mark.parametrize("unit", get_args(DateTimeUnit)) @pytest.mark.parametrize("scale_factor", [1, 10]) @pytest.mark.parametrize("value", [0, 1, 10]) def test_datetime_from_int(unit: DateTimeUnit, scale_factor: int, value: int) -> None: """ Test datetime_from_int. """ expected = np.int64(value).view(f"datetime64[{scale_factor}{unit}]") assert datetime_from_int(value, unit=unit, scale_factor=scale_factor) == expected zarr-python-3.2.1/tests/test_dtype/test_wrapper.py000066400000000000000000000176671517635743000224650ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING, Any, ClassVar import pytest from zarr.core.dtype.common import DTypeSpec_V2, DTypeSpec_V3, HasItemSize if TYPE_CHECKING: from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType """ class _TestZDTypeSchema: # subclasses define the URL for the schema, if available schema_url: ClassVar[str] = "" @pytest.fixture(scope="class") def get_schema(self) -> object: response = requests.get(self.schema_url) response.raise_for_status() return json_schema.loads(response.text) def test_schema(self, schema: json_schema.Schema) -> None: assert schema.is_valid(self.test_cls.to_json(zarr_format=2)) """ class BaseTestZDType: """ A base class for testing ZDType subclasses. This class works in conjunction with the custom pytest collection function ``pytest_generate_tests`` defined in conftest.py, which applies the following procedure when generating tests: At test generation time, for each test fixture referenced by a method on this class pytest will look for an attribute with the same name as that fixture. Pytest will assume that this class attribute is a tuple of values to be used for generating a parametrized test fixture. This means that child classes can, by using different values for these class attributes, have customized test parametrization. Attributes ---------- test_cls : type[ZDType[TBaseDType, TBaseScalar]] The ZDType subclass being tested. scalar_type : ClassVar[type[TBaseScalar]] The expected scalar type for the ZDType. valid_dtype : ClassVar[tuple[TBaseDType, ...]] A tuple of valid numpy dtypes for the ZDType. invalid_dtype : ClassVar[tuple[TBaseDType, ...]] A tuple of invalid numpy dtypes for the ZDType. valid_json_v2 : ClassVar[tuple[str | dict[str, object] | list[object], ...]] A tuple of valid JSON representations for Zarr format version 2. invalid_json_v2 : ClassVar[tuple[str | dict[str, object] | list[object], ...]] A tuple of invalid JSON representations for Zarr format version 2. valid_json_v3 : ClassVar[tuple[str | dict[str, object], ...]] A tuple of valid JSON representations for Zarr format version 3. invalid_json_v3 : ClassVar[tuple[str | dict[str, object], ...]] A tuple of invalid JSON representations for Zarr format version 3. cast_value_params : ClassVar[tuple[tuple[Any, Any, Any], ...]] A tuple of (dtype, value, expected) tuples for testing ZDType.cast_value. scalar_v2_params : ClassVar[tuple[Any, ...]] A tuple of (dtype, scalar json) tuples for testing ZDType.from_json_scalar / ZDType.to_json_scalar for zarr v2 scalar_v3_params : ClassVar[tuple[Any, ...]] A tuple of (dtype, scalar json) tuples for testing ZDType.from_json_scalar / ZDType.to_json_scalar for zarr v3 invalid_scalar_params : ClassVar[tuple[Any, ...]] A tuple of (dtype, value) tuples, where each value is expected to fail ZDType.cast_value. item_size_params : ClassVar[tuple[Any, ...]] A tuple of (dtype, expected) tuples for testing ZDType.item_size """ test_cls: type[ZDType[TBaseDType, TBaseScalar]] scalar_type: ClassVar[type[TBaseScalar]] valid_dtype: ClassVar[tuple[TBaseDType, ...]] = () invalid_dtype: ClassVar[tuple[TBaseDType, ...]] = () valid_json_v2: ClassVar[tuple[DTypeSpec_V2, ...]] = () invalid_json_v2: ClassVar[tuple[str | dict[str, object] | list[object], ...]] = () valid_json_v3: ClassVar[tuple[DTypeSpec_V3, ...]] = () invalid_json_v3: ClassVar[tuple[str | dict[str, object], ...]] = () # for testing scalar round-trip serialization, we need a tuple of (data type json, scalar json) # pairs. the first element of the pair is used to create a dtype instance, and the second # element is the json serialization of the scalar that we want to round-trip. scalar_v2_params: ClassVar[tuple[tuple[ZDType[Any, Any], Any], ...]] = () scalar_v3_params: ClassVar[tuple[tuple[Any, Any], ...]] = () cast_value_params: ClassVar[tuple[tuple[ZDType[Any, Any], Any, Any], ...]] = () # Some data types, like bool and string, can consume any python object as a scalar. # So we allow passing None in to this test to indicate that it should be skipped. invalid_scalar_params: ClassVar[tuple[tuple[ZDType[Any, Any], Any], ...] | tuple[None]] = () item_size_params: ClassVar[tuple[ZDType[Any, Any], ...]] = () def json_scalar_equals(self, scalar1: object, scalar2: object) -> bool: # An equality check for json-encoded scalars. This defaults to regular equality, # but some classes may need to override this for special cases return scalar1 == scalar2 def scalar_equals(self, scalar1: object, scalar2: object) -> bool: # An equality check for scalars. This defaults to regular equality, # but some classes may need to override this for special cases return scalar1 == scalar2 def test_check_dtype_valid(self, valid_dtype: TBaseDType) -> None: assert self.test_cls._check_native_dtype(valid_dtype) def test_check_dtype_invalid(self, invalid_dtype: object) -> None: assert not self.test_cls._check_native_dtype(invalid_dtype) # type: ignore[arg-type] def test_from_dtype_roundtrip(self, valid_dtype: Any) -> None: zdtype = self.test_cls.from_native_dtype(valid_dtype) assert zdtype.to_native_dtype() == valid_dtype def test_from_json_roundtrip_v2(self, valid_json_v2: DTypeSpec_V2) -> None: zdtype = self.test_cls.from_json(valid_json_v2, zarr_format=2) assert zdtype.to_json(zarr_format=2) == valid_json_v2 @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_from_json_roundtrip_v3(self, valid_json_v3: DTypeSpec_V3) -> None: zdtype = self.test_cls.from_json(valid_json_v3, zarr_format=3) assert zdtype.to_json(zarr_format=3) == valid_json_v3 def test_scalar_roundtrip_v2(self, scalar_v2_params: tuple[ZDType[Any, Any], Any]) -> None: zdtype, scalar_json = scalar_v2_params scalar = zdtype.from_json_scalar(scalar_json, zarr_format=2) assert self.json_scalar_equals(scalar_json, zdtype.to_json_scalar(scalar, zarr_format=2)) def test_scalar_roundtrip_v3(self, scalar_v3_params: tuple[ZDType[Any, Any], Any]) -> None: zdtype, scalar_json = scalar_v3_params scalar = zdtype.from_json_scalar(scalar_json, zarr_format=3) assert self.json_scalar_equals(scalar_json, zdtype.to_json_scalar(scalar, zarr_format=3)) def test_cast_value(self, cast_value_params: tuple[ZDType[Any, Any], Any, Any]) -> None: zdtype, value, expected = cast_value_params observed = zdtype.cast_scalar(value) assert self.scalar_equals(expected, observed) # check that casting is idempotent assert self.scalar_equals(zdtype.cast_scalar(observed), observed) def test_invalid_scalar( self, invalid_scalar_params: tuple[ZDType[Any, Any], Any] | None ) -> None: if invalid_scalar_params is None: pytest.skip(f"No test data provided for {self}.{__name__}") zdtype, data = invalid_scalar_params msg = ( f"Cannot convert object {data!r} with type {type(data)} to a scalar compatible with the " f"data type {zdtype}." ) with pytest.raises(TypeError, match=re.escape(msg)): zdtype.cast_scalar(data) def test_item_size(self, item_size_params: ZDType[Any, Any]) -> None: """ Test that the item_size attribute matches the numpy dtype itemsize attribute, for dtypes with a fixed scalar size. """ if isinstance(item_size_params, HasItemSize): assert item_size_params.item_size == item_size_params.to_native_dtype().itemsize else: pytest.skip(f"Data type {item_size_params} does not implement HasItemSize") zarr-python-3.2.1/tests/test_dtype_registry.py000066400000000000000000000213041517635743000216550ustar00rootroot00000000000000from __future__ import annotations import re from typing import TYPE_CHECKING, Any, Literal, get_args import numpy as np import pytest from tests.conftest import skip_object_dtype from zarr.core.dtype import ( AnyDType, DataTypeRegistry, TBaseDType, TBaseScalar, get_data_type_from_json, ) from zarr.core.dtype.common import unpack_dtype_json from zarr.core.dtype.npy.string import _NUMPY_SUPPORTS_VLEN_STRING from zarr.dtype import ( # type: ignore[attr-defined] Bool, FixedLengthUTF32, VariableLengthUTF8, ZDType, data_type_registry, parse_data_type, parse_dtype, ) if TYPE_CHECKING: from zarr.core.common import ZarrFormat from .test_dtype.conftest import zdtype_examples @pytest.fixture def data_type_registry_fixture() -> DataTypeRegistry: return DataTypeRegistry() class TestRegistry: @staticmethod def test_register(data_type_registry_fixture: DataTypeRegistry) -> None: """ Test that registering a dtype in a data type registry works. """ data_type_registry_fixture.register(Bool._zarr_v3_name, Bool) assert data_type_registry_fixture.get(Bool._zarr_v3_name) == Bool assert isinstance(data_type_registry_fixture.match_dtype(np.dtype("bool")), Bool) @staticmethod def test_override(data_type_registry_fixture: DataTypeRegistry) -> None: """ Test that registering a new dtype with the same name works (overriding the previous one). """ data_type_registry_fixture.register(Bool._zarr_v3_name, Bool) class NewBool(Bool): def default_scalar(self) -> np.bool_: return np.True_ data_type_registry_fixture.register(NewBool._zarr_v3_name, NewBool) assert isinstance(data_type_registry_fixture.match_dtype(np.dtype("bool")), NewBool) @staticmethod @pytest.mark.parametrize( ("wrapper_cls", "dtype_str"), [(Bool, "bool"), (FixedLengthUTF32, "|U4")] ) def test_match_dtype( data_type_registry_fixture: DataTypeRegistry, wrapper_cls: type[ZDType[TBaseDType, TBaseScalar]], dtype_str: str, ) -> None: """ Test that match_dtype resolves a numpy dtype into an instance of the correspond wrapper for that dtype. """ data_type_registry_fixture.register(wrapper_cls._zarr_v3_name, wrapper_cls) assert isinstance(data_type_registry_fixture.match_dtype(np.dtype(dtype_str)), wrapper_cls) @pytest.mark.skipif(not _NUMPY_SUPPORTS_VLEN_STRING, reason="requires numpy with T dtype") @staticmethod def test_match_dtype_string_na_object_error( data_type_registry_fixture: DataTypeRegistry, ) -> None: data_type_registry_fixture.register(VariableLengthUTF8._zarr_v3_name, VariableLengthUTF8) # type: ignore[arg-type] dtype: np.dtype[Any] = np.dtypes.StringDType(na_object=None) # type: ignore[call-arg] with pytest.raises(ValueError, match=r"Zarr data type resolution from StringDType.*failed"): data_type_registry_fixture.match_dtype(dtype) @staticmethod def test_unregistered_dtype(data_type_registry_fixture: DataTypeRegistry) -> None: """ Test that match_dtype raises an error if the dtype is not registered. """ outside_dtype_name = "int8" outside_dtype = np.dtype(outside_dtype_name) msg = f"No Zarr data type found that matches dtype '{outside_dtype!r}'" with pytest.raises(ValueError, match=re.escape(msg)): data_type_registry_fixture.match_dtype(outside_dtype) with pytest.raises(KeyError): data_type_registry_fixture.get(outside_dtype_name) @staticmethod @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("zdtype", zdtype_examples) def test_registered_dtypes_match_dtype(zdtype: ZDType[TBaseDType, TBaseScalar]) -> None: """ Test that the registered dtypes can be retrieved from the registry. """ skip_object_dtype(zdtype) assert data_type_registry.match_dtype(zdtype.to_native_dtype()) == zdtype @staticmethod @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("zdtype", zdtype_examples) def test_registered_dtypes_match_json( zdtype: ZDType[TBaseDType, TBaseScalar], zarr_format: ZarrFormat ) -> None: assert ( data_type_registry.match_json( zdtype.to_json(zarr_format=zarr_format), zarr_format=zarr_format ) == zdtype ) @staticmethod @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("zdtype", zdtype_examples) def test_match_dtype_unique( zdtype: ZDType[Any, Any], data_type_registry_fixture: DataTypeRegistry, zarr_format: ZarrFormat, ) -> None: """ Test that the match_dtype method uniquely specifies a registered data type. We create a local registry that excludes the data type class being tested, and ensure that an instance of the wrapped data type fails to match anything in the registry """ skip_object_dtype(zdtype) for _cls in get_args(AnyDType): if _cls is not type(zdtype): data_type_registry_fixture.register(_cls._zarr_v3_name, _cls) dtype_instance = zdtype.to_native_dtype() msg = f"No Zarr data type found that matches dtype '{dtype_instance!r}'" with pytest.raises(ValueError, match=re.escape(msg)): data_type_registry_fixture.match_dtype(dtype_instance) instance_dict = zdtype.to_json(zarr_format=zarr_format) msg = f"No Zarr data type found that matches {instance_dict!r}" with pytest.raises(ValueError, match=re.escape(msg)): data_type_registry_fixture.match_json(instance_dict, zarr_format=zarr_format) @pytest.mark.usefixtures("set_path") def test_entrypoint_dtype(zarr_format: ZarrFormat) -> None: from package_with_entrypoint import TestDataType data_type_registry._lazy_load() instance = TestDataType() dtype_json = instance.to_json(zarr_format=zarr_format) assert get_data_type_from_json(dtype_json, zarr_format=zarr_format) == instance data_type_registry.unregister(TestDataType._zarr_v3_name) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize("data_type", zdtype_examples, ids=str) @pytest.mark.parametrize("json_style", [(2, "internal"), (2, "metadata"), (3, None)], ids=str) @pytest.mark.parametrize( "dtype_parser_func", [parse_dtype, parse_data_type], ids=["parse_dtype", "parse_data_type"] ) def test_parse_data_type( data_type: ZDType[Any, Any], json_style: tuple[ZarrFormat, None | Literal["internal", "metadata"]], dtype_parser_func: Any, ) -> None: """ Test the parsing of data types into ZDType instances. This function tests the ability of `dtype_parser_func` to correctly interpret and parse data type specifications into `ZDType` instances according to the specified Zarr format and JSON style. Parameters ---------- data_type : ZDType[Any, Any] The data type to be tested for parsing. json_style : tuple[ZarrFormat, None or Literal["internal", "metadata"]] A tuple specifying the Zarr format version and the JSON style for Zarr V2 2. For Zarr V2 there are 2 JSON styles: "internal", and "metadata". The internal style takes the form {"name": , "object_codec_id": }, while the metadata style is just . dtype_parser_func : Any The function to be tested for parsing the data type. This is necessary for compatibility reasons, as we support multiple functions that perform the same data type parsing operation. """ zarr_format, style = json_style dtype_spec: Any if zarr_format == 2: dtype_spec = data_type.to_json(zarr_format=zarr_format) if style == "internal": pass elif style == "metadata": dtype_spec = unpack_dtype_json(dtype_spec) else: raise ValueError(f"Invalid zarr v2 json style: {style}") else: dtype_spec = data_type.to_json(zarr_format=zarr_format) if dtype_spec == "|O": # The object data type on its own is ambiguous and should fail to resolve. msg = "Zarr data type resolution from object failed." with pytest.raises(ValueError, match=msg): dtype_parser_func(dtype_spec, zarr_format=zarr_format) else: observed = dtype_parser_func(dtype_spec, zarr_format=zarr_format) assert observed == data_type zarr-python-3.2.1/tests/test_errors.py000066400000000000000000000051101517635743000201110ustar00rootroot00000000000000"""Test errors""" from zarr.errors import ( ArrayNotFoundError, ContainsArrayAndGroupError, ContainsArrayError, ContainsGroupError, GroupNotFoundError, MetadataValidationError, NodeTypeValidationError, ) def test_group_not_found_error() -> None: """ Test that calling GroupNotFoundError with multiple arguments returns a formatted string. This is deprecated behavior. """ err = GroupNotFoundError("store", "path") assert str(err) == "No group found in store 'store' at path 'path'" def test_array_not_found_error() -> None: """ Test that calling ArrayNotFoundError with multiple arguments returns a formatted string. This is deprecated behavior. """ err = ArrayNotFoundError("store", "path") assert str(err) == "No array found in store 'store' at path 'path'" def test_metadata_validation_error() -> None: """ Test that calling MetadataValidationError with multiple arguments returns a formatted string. This is deprecated behavior. """ err = MetadataValidationError("a", "b", "c") assert str(err) == "Invalid value for 'a'. Expected 'b'. Got 'c'." def test_contains_group_error() -> None: """ Test that calling ContainsGroupError with multiple arguments returns a formatted string. This is deprecated behavior. """ err = ContainsGroupError("store", "path") assert str(err) == "A group exists in store 'store' at path 'path'." def test_contains_array_error() -> None: """ Test that calling ContainsArrayError with multiple arguments returns a formatted string. This is deprecated behavior. """ err = ContainsArrayError("store", "path") assert str(err) == "An array exists in store 'store' at path 'path'." def test_contains_array_and_group_error() -> None: """ Test that calling ContainsArrayAndGroupError with multiple arguments returns a formatted string. This is deprecated behavior. """ err = ContainsArrayAndGroupError("store", "path") assert str(err) == ( "Array and group metadata documents (.zarray and .zgroup) were both found in store 'store' " "at path 'path'. Only one of these files may be present in a given directory / prefix. " "Remove the .zarray file, or the .zgroup file, or both." ) def test_node_type_validation_error() -> None: """ Test that calling NodeTypeValidationError with multiple arguments returns a formatted string. This is deprecated behavior. """ err = NodeTypeValidationError("a", "b", "c") assert str(err) == "Invalid value for 'a'. Expected 'b'. Got 'c'." zarr-python-3.2.1/tests/test_examples.py000066400000000000000000000057601517635743000204260ustar00rootroot00000000000000from __future__ import annotations import re import subprocess import sys from pathlib import Path from typing import Final import pytest import tomlkit from packaging.requirements import Requirement examples_dir = "examples" script_paths = tuple(Path(examples_dir).rglob("*.py")) PEP_723_REGEX: Final = r"(?m)^# /// (?P[a-zA-Z0-9-]+)$\s(?P(^#(| .*)$\s)+)^# ///$" # This is the absolute path to the local Zarr installation. Moving this test to a different directory will break it. ZARR_PROJECT_PATH = Path(".").absolute() def set_dep(script: str, dependency: str) -> str: """ Set a dependency in a PEP-723 script header. If the package is already in the list, it will be replaced. If the package is not already in the list, it will be added. Source code modified from https://packaging.python.org/en/latest/specifications/inline-script-metadata/#reference-implementation """ match = re.search(PEP_723_REGEX, script) if match is None: raise ValueError(f"PEP-723 header not found in {script}") content = "".join( line[2:] if line.startswith("# ") else line[1:] for line in match.group("content").splitlines(keepends=True) ) config = tomlkit.parse(content) for idx, dep in enumerate(tuple(config["dependencies"])): if Requirement(dep).name == Requirement(dependency).name: config["dependencies"][idx] = dependency new_content = "".join( f"# {line}" if line.strip() else f"#{line}" for line in tomlkit.dumps(config).splitlines(keepends=True) ) start, end = match.span("content") return script[:start] + new_content + script[end:] def resave_script(source_path: Path, dest_path: Path) -> None: """ Read a script from source_path and save it to dest_path after inserting the absolute path to the local Zarr project directory in the PEP-723 header. """ source_text = source_path.read_text() dest_text = set_dep(source_text, f"zarr @ file:///{ZARR_PROJECT_PATH}") dest_path.write_text(dest_text) def test_script_paths() -> None: """ Test that our test fixture is working properly and collecting script paths. """ assert len(script_paths) > 0 @pytest.mark.skipif( sys.platform == "win32", reason="This test fails for unknown reasons on Windows in CI." ) @pytest.mark.parametrize("script_path", script_paths) def test_scripts_can_run(script_path: Path, tmp_path: Path) -> None: dest_path = tmp_path / script_path.name # We resave the script after inserting the absolute path to the local Zarr project directory, # and then test its behavior. # This allows the example to be useful to users who don't have Zarr installed, but also testable. resave_script(script_path, dest_path) result = subprocess.run( ["uv", "run", "--refresh", str(dest_path)], capture_output=True, text=True ) assert result.returncode == 0, ( f"Script at {script_path} failed to run. Output: {result.stdout} Error: {result.stderr}" ) zarr-python-3.2.1/tests/test_experimental/000077500000000000000000000000001517635743000207235ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_experimental/test_cache_store.py000066400000000000000000001223351517635743000246210ustar00rootroot00000000000000""" Tests for the dual-store cache implementation. """ import asyncio import time import pytest from zarr.abc.store import RangeByteRequest, Store, SuffixByteRequest from zarr.core.buffer.core import default_buffer_prototype from zarr.core.buffer.cpu import Buffer as CPUBuffer from zarr.experimental.cache_store import CacheStore from zarr.storage import MemoryStore class TestCacheStore: """Test the dual-store cache implementation.""" @pytest.fixture def source_store(self) -> MemoryStore: """Create a source store with some test data.""" return MemoryStore() @pytest.fixture def cache_store(self) -> MemoryStore: """Create an empty cache store.""" return MemoryStore() @pytest.fixture def cached_store(self, source_store: Store, cache_store: Store) -> CacheStore: """Create a cached store instance.""" return CacheStore(source_store, cache_store=cache_store) async def test_with_read_only_round_trip(self) -> None: """ Ensure that CacheStore.with_read_only returns another CacheStore with the requested read_only state, shares cache state, and does not change the original store's read_only flag. """ source = MemoryStore() cache = MemoryStore() # Start from a read-only underlying store source_ro = source.with_read_only(read_only=True) cached_ro = CacheStore(store=source_ro, cache_store=cache) assert cached_ro.read_only buf = CPUBuffer.from_bytes(b"0123") # Cannot write through the read-only cache store with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await cached_ro.set("foo", buf) # Create a writable cache store from the read-only one writer = cached_ro.with_read_only(read_only=False) assert isinstance(writer, CacheStore) assert not writer.read_only # Cache configuration and state are shared assert writer._cache is cached_ro._cache assert writer._state is cached_ro._state assert writer._state.key_insert_times is cached_ro._state.key_insert_times # Writes via the writable cache store succeed and are cached await writer.set("foo", buf) out = await writer.get("foo", default_buffer_prototype()) assert out is not None assert out.to_bytes() == buf.to_bytes() # The original cache store remains read-only assert cached_ro.read_only with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await cached_ro.set("bar", buf) # Creating a read-only copy from the writable cache store works and is enforced reader = writer.with_read_only(read_only=True) assert isinstance(reader, CacheStore) assert reader.read_only with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await reader.set("baz", buf) async def test_basic_caching(self, cached_store: CacheStore, source_store: Store) -> None: """Test basic cache functionality.""" # Store some data test_data = CPUBuffer.from_bytes(b"test data") await cached_store.set("test_key", test_data) # Verify it's in both stores assert await source_store.exists("test_key") assert await cached_store._cache.exists("test_key") # Retrieve and verify caching works result = await cached_store.get("test_key", default_buffer_prototype()) assert result is not None assert result.to_bytes() == b"test data" async def test_cache_miss_and_population( self, cached_store: CacheStore, source_store: Store ) -> None: """Test cache miss and subsequent population.""" # Put data directly in source store (bypassing cache) test_data = CPUBuffer.from_bytes(b"source data") await source_store.set("source_key", test_data) # First access should miss cache but populate it result = await cached_store.get("source_key", default_buffer_prototype()) assert result is not None assert result.to_bytes() == b"source data" # Verify data is now in cache assert await cached_store._cache.exists("source_key") async def test_cache_expiration(self) -> None: """Test cache expiration based on max_age_seconds.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( source_store, cache_store=cache_store, max_age_seconds=1, # 1 second expiration ) # Store data test_data = CPUBuffer.from_bytes(b"expiring data") await cached_store.set("expire_key", test_data) # Should be fresh initially (if _is_key_fresh method exists) if hasattr(cached_store, "_is_key_fresh"): assert cached_store._is_key_fresh("expire_key") # Wait for expiration await asyncio.sleep(1.1) # Should now be stale assert not cached_store._is_key_fresh("expire_key") else: # Skip freshness check if method doesn't exist await asyncio.sleep(1.1) # Just verify the data is still accessible result = await cached_store.get("expire_key", default_buffer_prototype()) assert result is not None async def test_cache_set_data_false(self, source_store: Store, cache_store: Store) -> None: """Test behavior when cache_set_data=False.""" cached_store = CacheStore(source_store, cache_store=cache_store, cache_set_data=False) test_data = CPUBuffer.from_bytes(b"no cache data") await cached_store.set("no_cache_key", test_data) # Data should be in source but not cache assert await source_store.exists("no_cache_key") assert not await cache_store.exists("no_cache_key") async def test_delete_removes_from_both_stores(self, cached_store: CacheStore) -> None: """Test that delete removes from both source and cache.""" test_data = CPUBuffer.from_bytes(b"delete me") await cached_store.set("delete_key", test_data) # Verify in both stores assert await cached_store._store.exists("delete_key") assert await cached_store._cache.exists("delete_key") # Delete await cached_store.delete("delete_key") # Verify removed from both assert not await cached_store._store.exists("delete_key") assert not await cached_store._cache.exists("delete_key") async def test_exists_checks_source_store( self, cached_store: CacheStore, source_store: Store ) -> None: """Test that exists() checks the source store (source of truth).""" # Put data directly in source test_data = CPUBuffer.from_bytes(b"exists test") await source_store.set("exists_key", test_data) # Should exist even though not in cache assert await cached_store.exists("exists_key") async def test_list_operations(self, cached_store: CacheStore, source_store: Store) -> None: """Test listing operations delegate to source store.""" # Add some test data test_data = CPUBuffer.from_bytes(b"list test") await cached_store.set("list/item1", test_data) await cached_store.set("list/item2", test_data) await cached_store.set("other/item3", test_data) # Test list_dir list_items = [key async for key in cached_store.list_dir("list/")] assert len(list_items) >= 2 # Should include our items # Test list_prefix prefix_items = [key async for key in cached_store.list_prefix("list/")] assert len(prefix_items) >= 2 async def test_stale_cache_refresh(self) -> None: """Test that stale cache entries are refreshed from source.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_age_seconds=1) # Store initial data old_data = CPUBuffer.from_bytes(b"old data") await cached_store.set("refresh_key", old_data) # Wait for expiration await asyncio.sleep(1.1) # Update source store directly (simulating external update) new_data = CPUBuffer.from_bytes(b"new data") await source_store.set("refresh_key", new_data) # Access should refresh from source when cache is stale result = await cached_store.get("refresh_key", default_buffer_prototype()) assert result is not None assert result.to_bytes() == b"new data" async def test_infinity_max_age(self, cached_store: CacheStore) -> None: """Test that 'infinity' max_age means cache never expires.""" # Skip test if _is_key_fresh method doesn't exist if not hasattr(cached_store, "_is_key_fresh"): pytest.skip("_is_key_fresh method not implemented") test_data = CPUBuffer.from_bytes(b"eternal data") await cached_store.set("eternal_key", test_data) # Should always be fresh assert cached_store._is_key_fresh("eternal_key") # Even after time passes await asyncio.sleep(0.1) assert cached_store._is_key_fresh("eternal_key") async def test_cache_returns_cached_data_for_performance( self, cached_store: CacheStore, source_store: Store ) -> None: """Test that cache returns cached data for performance, even if not in source.""" # Put data in cache but not source (simulates orphaned cache entry) test_data = CPUBuffer.from_bytes(b"orphaned data") await cached_store._cache.set("orphan_key", test_data) cached_store._state.key_insert_times["orphan_key"] = time.monotonic() # Cache should return data for performance (no source verification) result = await cached_store.get("orphan_key", default_buffer_prototype()) assert result is not None assert result.to_bytes() == b"orphaned data" # Cache entry should remain (performance optimization) assert await cached_store._cache.exists("orphan_key") assert "orphan_key" in cached_store._state.key_insert_times async def test_cache_coherency_through_expiration(self) -> None: """Test that cache coherency is managed through cache expiration, not source verification.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( source_store, cache_store=cache_store, max_age_seconds=1, # Short expiration for coherency ) # Add data to both stores test_data = CPUBuffer.from_bytes(b"original data") await cached_store.set("coherency_key", test_data) # Remove from source (simulating external deletion) await source_store.delete("coherency_key") # Cache should still return cached data (performance optimization) result = await cached_store.get("coherency_key", default_buffer_prototype()) assert result is not None assert result.to_bytes() == b"original data" # Wait for cache expiration await asyncio.sleep(1.1) # Now stale cache should be refreshed from source result = await cached_store.get("coherency_key", default_buffer_prototype()) assert result is None # Key no longer exists in source async def test_cache_info(self, cached_store: CacheStore) -> None: """Test cache_info method returns correct information.""" # Test initial state info = cached_store.cache_info() # Check all expected keys are present expected_keys = { "cache_store_type", "max_age_seconds", "max_size", "current_size", "cache_set_data", "tracked_keys", "cached_keys", } assert set(info.keys()) == expected_keys # Check initial values assert info["cache_store_type"] == "MemoryStore" assert info["max_age_seconds"] == "infinity" assert info["max_size"] is None # Default unlimited assert info["current_size"] == 0 assert info["cache_set_data"] is True assert info["tracked_keys"] == 0 assert info["cached_keys"] == 0 # Add some data and verify tracking test_data = CPUBuffer.from_bytes(b"test data for cache info") await cached_store.set("info_test_key", test_data) # Check updated info updated_info = cached_store.cache_info() assert updated_info["tracked_keys"] == 1 assert updated_info["cached_keys"] == 1 assert updated_info["current_size"] > 0 # Should have some size now async def test_cache_info_with_max_size(self) -> None: """Test cache_info with max_size configuration.""" source_store = MemoryStore() cache_store = MemoryStore() # Create cache with specific max_size and max_age cached_store = CacheStore( source_store, cache_store=cache_store, max_size=1024, max_age_seconds=300, ) info = cached_store.cache_info() assert info["max_size"] == 1024 assert info["max_age_seconds"] == 300 assert info["current_size"] == 0 async def test_clear_cache(self, cached_store: CacheStore) -> None: """Test clear_cache method clears all cache data and tracking.""" # Add some test data test_data1 = CPUBuffer.from_bytes(b"test data 1") test_data2 = CPUBuffer.from_bytes(b"test data 2") await cached_store.set("clear_test_1", test_data1) await cached_store.set("clear_test_2", test_data2) # Verify data is cached info_before = cached_store.cache_info() assert info_before["tracked_keys"] == 2 assert info_before["cached_keys"] == 2 assert info_before["current_size"] > 0 # Verify data exists in cache assert await cached_store._cache.exists("clear_test_1") assert await cached_store._cache.exists("clear_test_2") # Clear the cache await cached_store.clear_cache() # Verify cache is cleared info_after = cached_store.cache_info() assert info_after["tracked_keys"] == 0 assert info_after["cached_keys"] == 0 assert info_after["current_size"] == 0 # Verify data is removed from cache store (if it supports clear) if hasattr(cached_store._cache, "clear"): # If cache store supports clear, all data should be gone assert not await cached_store._cache.exists("clear_test_1") assert not await cached_store._cache.exists("clear_test_2") # Verify data still exists in source store assert await cached_store._store.exists("clear_test_1") assert await cached_store._store.exists("clear_test_2") async def test_max_age_infinity(self) -> None: """Test cache with infinite max age.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_age_seconds="infinity") # Add data and verify it never expires test_data = CPUBuffer.from_bytes(b"test data") await cached_store.set("test_key", test_data) # Even after time passes, key should be fresh assert cached_store._is_key_fresh("test_key") async def test_max_age_numeric(self) -> None: """Test cache with numeric max age.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( source_store, cache_store=cache_store, max_age_seconds=1, # 1 second ) # Add data test_data = CPUBuffer.from_bytes(b"test data") await cached_store.set("test_key", test_data) # Key should be fresh initially assert cached_store._is_key_fresh("test_key") # Manually set old timestamp to test expiration cached_store._state.key_insert_times["test_key"] = time.monotonic() - 2 # 2 seconds ago # Key should now be stale assert not cached_store._is_key_fresh("test_key") async def test_cache_set_data_disabled(self) -> None: """Test cache behavior when cache_set_data is False.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, cache_set_data=False) # Set data test_data = CPUBuffer.from_bytes(b"test data") await cached_store.set("test_key", test_data) # Data should be in source but not in cache assert await source_store.exists("test_key") assert not await cache_store.exists("test_key") # Cache info should show no cached data info = cached_store.cache_info() assert info["cache_set_data"] is False assert info["cached_keys"] == 0 async def test_eviction_with_max_size(self) -> None: """Test LRU eviction when max_size is exceeded.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( source_store, cache_store=cache_store, max_size=100, # Small cache size ) # Add data that exceeds cache size small_data = CPUBuffer.from_bytes(b"a" * 40) # 40 bytes medium_data = CPUBuffer.from_bytes(b"b" * 40) # 40 bytes large_data = CPUBuffer.from_bytes(b"c" * 40) # 40 bytes (would exceed 100 byte limit) # Set first two items await cached_store.set("key1", small_data) await cached_store.set("key2", medium_data) # Cache should have 2 items info = cached_store.cache_info() assert info["cached_keys"] == 2 assert info["current_size"] == 80 # Add third item - should trigger eviction of first item await cached_store.set("key3", large_data) # Cache should still have items but first one may be evicted info = cached_store.cache_info() assert info["current_size"] <= 100 async def test_value_exceeds_max_size(self) -> None: """Test behavior when a single value exceeds max_size.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( source_store, cache_store=cache_store, max_size=50, # Small cache size ) # Try to cache data larger than max_size large_data = CPUBuffer.from_bytes(b"x" * 100) # 100 bytes > 50 byte limit await cached_store.set("large_key", large_data) # Data should be in source but not cached assert await source_store.exists("large_key") info = cached_store.cache_info() assert info["cached_keys"] == 0 assert info["current_size"] == 0 async def test_get_nonexistent_key(self) -> None: """Test getting a key that doesn't exist in either store.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store) # Try to get nonexistent key result = await cached_store.get("nonexistent", default_buffer_prototype()) assert result is None # Should not create any cache entries info = cached_store.cache_info() assert info["cached_keys"] == 0 async def test_delete_both_stores(self) -> None: """Test that delete removes from both source and cache stores.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store) # Add data test_data = CPUBuffer.from_bytes(b"test data") await cached_store.set("test_key", test_data) # Verify it's in both stores assert await source_store.exists("test_key") assert await cache_store.exists("test_key") # Delete await cached_store.delete("test_key") # Verify it's removed from both assert not await source_store.exists("test_key") assert not await cache_store.exists("test_key") # Verify tracking is updated info = cached_store.cache_info() assert info["cached_keys"] == 0 async def test_invalid_max_age_seconds(self) -> None: """Test that invalid max_age_seconds values raise ValueError.""" source_store = MemoryStore() cache_store = MemoryStore() with pytest.raises(ValueError, match="max_age_seconds string value must be 'infinity'"): CacheStore(source_store, cache_store=cache_store, max_age_seconds="invalid") async def test_unlimited_cache_size(self) -> None: """Test behavior when max_size is None (unlimited).""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( source_store, cache_store=cache_store, max_size=None, # Unlimited cache ) # Add large amounts of data for i in range(10): large_data = CPUBuffer.from_bytes(b"x" * 1000) # 1KB each await cached_store.set(f"large_key_{i}", large_data) # All should be cached since there's no size limit info = cached_store.cache_info() assert info["cached_keys"] == 10 assert info["current_size"] == 10000 # 10 * 1000 bytes async def test_evict_key_exception_handling(self) -> None: """Test exception handling in _evict_key method.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=100) # Add some data test_data = CPUBuffer.from_bytes(b"test data") await cached_store.set("test_key", test_data) # Manually corrupt the tracking to trigger exception # Remove from one structure but not others to create inconsistency del cached_store._state.cache_order["test_key"] # Try to evict - should handle the KeyError gracefully await cached_store._evict_key("test_key") # Should still work and not crash info = cached_store.cache_info() assert isinstance(info, dict) async def test_get_no_cache_delete_tracking(self) -> None: """Test _get_no_cache when key doesn't exist and needs cleanup.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store) # First, add key to cache tracking but not to source test_data = CPUBuffer.from_bytes(b"test data") await cache_store.set("phantom_key", test_data) await cached_store._track_entry("phantom_key", test_data) # Verify it's in tracking assert "phantom_key" in cached_store._state.cache_order assert "phantom_key" in cached_store._state.key_insert_times # Now try to get it - since it's not in source, should clean up tracking result = await cached_store._get_no_cache("phantom_key", default_buffer_prototype()) assert result is None # Should have cleaned up tracking assert "phantom_key" not in cached_store._state.cache_order assert "phantom_key" not in cached_store._state.key_insert_times async def test_accommodate_value_no_max_size(self) -> None: """Test _accommodate_value early return when max_size is None.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( source_store, cache_store=cache_store, max_size=None, # No size limit ) # This should return early without doing anything await cached_store._accommodate_value(1000000) # Large value # Should not affect anything since max_size is None info = cached_store.cache_info() assert info["current_size"] == 0 async def test_concurrent_set_operations(self) -> None: """Test that concurrent set operations don't corrupt cache size tracking.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=1000) # Create 10 concurrent set operations async def set_data(key: str) -> None: data = CPUBuffer.from_bytes(b"x" * 50) await cached_store.set(key, data) # Run concurrently await asyncio.gather(*[set_data(f"key_{i}") for i in range(10)]) info = cached_store.cache_info() # Expected: 10 keys * 50 bytes = 500 bytes assert info["cached_keys"] == 10 assert info["current_size"] == 500 # WOULD FAIL due to race condition async def test_concurrent_eviction_race(self) -> None: """Test concurrent evictions don't corrupt size tracking.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=200) # Fill cache to near capacity data = CPUBuffer.from_bytes(b"x" * 80) await cached_store.set("key1", data) await cached_store.set("key2", data) # Now trigger two concurrent sets that both need to evict async def set_large(key: str) -> None: large_data = CPUBuffer.from_bytes(b"y" * 100) await cached_store.set(key, large_data) await asyncio.gather(set_large("key3"), set_large("key4")) info = cached_store.cache_info() # Size should be consistent with tracked keys assert info["current_size"] <= 200 # Might pass # But verify actual cache store size matches tracking total_size = sum( cached_store._state.key_sizes.get(k, 0) for k in cached_store._state.cache_order ) assert total_size == info["current_size"] # WOULD FAIL async def test_concurrent_get_and_evict(self) -> None: """Test get operations during eviction don't cause corruption.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=100) # Setup data = CPUBuffer.from_bytes(b"x" * 40) await cached_store.set("key1", data) await cached_store.set("key2", data) # Concurrent: read key1 while adding key3 (triggers eviction) async def read_key() -> None: for _ in range(100): await cached_store.get("key1", default_buffer_prototype()) async def write_key() -> None: for i in range(10): new_data = CPUBuffer.from_bytes(b"y" * 40) await cached_store.set(f"new_{i}", new_data) await asyncio.gather(read_key(), write_key()) # Verify consistency info = cached_store.cache_info() assert info["current_size"] <= 100 assert len(cached_store._state.cache_order) == len(cached_store._state.key_sizes) async def test_eviction_actually_deletes_from_cache_store(self) -> None: """Test that eviction removes keys from cache_store, not just tracking.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=100) # Add data that will be evicted data1 = CPUBuffer.from_bytes(b"x" * 60) data2 = CPUBuffer.from_bytes(b"y" * 60) await cached_store.set("key1", data1) # Verify key1 is in cache_store assert await cache_store.exists("key1") # Add key2, which should evict key1 await cached_store.set("key2", data2) # Check tracking - key1 should be removed assert "key1" not in cached_store._state.cache_order assert "key1" not in cached_store._state.key_sizes # CRITICAL: key1 should also be removed from cache_store assert not await cache_store.exists("key1"), ( "Evicted key still exists in cache_store! _evict_key doesn't actually delete." ) # But key1 should still exist in source store assert await source_store.exists("key1") async def test_eviction_no_orphaned_keys(self) -> None: """Test that eviction doesn't leave orphaned keys in cache_store.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=150) # Add multiple keys that will cause evictions for i in range(10): data = CPUBuffer.from_bytes(b"x" * 60) await cached_store.set(f"key_{i}", data) # Check tracking info = cached_store.cache_info() tracked_keys = info["cached_keys"] # Count actual keys in cache_store actual_keys = 0 async for _ in cache_store.list(): actual_keys += 1 # Cache store should have same number of keys as tracking assert actual_keys == tracked_keys, ( f"Cache store has {actual_keys} keys but tracking shows {tracked_keys}. " f"Eviction doesn't delete from cache_store!" ) async def test_size_accounting_with_key_updates(self) -> None: """Test that updating the same key replaces size instead of accumulating.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=500) # Set initial value data1 = CPUBuffer.from_bytes(b"x" * 100) await cached_store.set("same_key", data1) info1 = cached_store.cache_info() assert info1["current_size"] == 100 # Update with different size data2 = CPUBuffer.from_bytes(b"y" * 200) await cached_store.set("same_key", data2) info2 = cached_store.cache_info() # Should be 200, not 300 (update replaces, doesn't accumulate) assert info2["current_size"] == 200, ( f"Expected size 200 but got {info2['current_size']}. " "Updating same key should replace, not accumulate." ) async def test_all_tracked_keys_exist_in_cache_store(self) -> None: """Test invariant: all keys in tracking should exist in cache_store.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(source_store, cache_store=cache_store, max_size=500) # Add some data for i in range(5): data = CPUBuffer.from_bytes(b"x" * 50) await cached_store.set(f"key_{i}", data) # Every str key in tracking should exist in cache_store # (tuple keys are byte-range entries stored in-memory, not in the Store) for entry_key in cached_store._state.cache_order: if isinstance(entry_key, str): assert await cache_store.exists(entry_key), ( f"Key '{entry_key}' is tracked but doesn't exist in cache_store" ) # Every str key in _key_sizes should exist in cache_store for entry_key in cached_store._state.key_sizes: if isinstance(entry_key, str): assert await cache_store.exists(entry_key), ( f"Key '{entry_key}' has size tracked but doesn't exist in cache_store" ) # Additional coverage tests for 100% coverage async def test_cache_store_requires_delete_support(self) -> None: """Test that CacheStore validates cache_store supports deletes.""" from unittest.mock import MagicMock # Create a mock store that doesn't support deletes source_store = MemoryStore() cache_store = MagicMock() cache_store.supports_deletes = False with pytest.raises(ValueError, match="does not support deletes"): CacheStore(store=source_store, cache_store=cache_store) async def test_evict_key_exception_handling_with_real_error( self, monkeypatch: pytest.MonkeyPatch ) -> None: """Test _evict_key exception handling when deletion fails.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store, max_size=100) # Set up a key in tracking buffer = CPUBuffer.from_bytes(b"test data") await cached_store.set("test_key", buffer) # Mock the cache delete to raise an exception async def failing_delete(key: str) -> None: raise RuntimeError("Simulated cache deletion failure") monkeypatch.setattr(cache_store, "delete", failing_delete) # Attempt to evict should raise the exception with pytest.raises(RuntimeError, match="Simulated cache deletion failure"): async with cached_store._state.lock: await cached_store._evict_key("test_key") async def test_cache_stats_method(self) -> None: """Test cache_stats method returns correct statistics.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store, max_size=1000) # Initially, stats should be zero stats = cached_store.cache_stats() assert stats["hits"] == 0 assert stats["misses"] == 0 assert stats["evictions"] == 0 assert stats["total_requests"] == 0 assert stats["hit_rate"] == 0.0 # Perform some operations buffer = CPUBuffer.from_bytes(b"x" * 100) # Write to source store directly to avoid affecting stats await source_store.set("key1", buffer) # First get is a miss (not in cache yet) result1 = await cached_store.get("key1", default_buffer_prototype()) assert result1 is not None # Second get is a hit (now in cache) result2 = await cached_store.get("key1", default_buffer_prototype()) assert result2 is not None stats = cached_store.cache_stats() assert stats["hits"] == 1 assert stats["misses"] == 1 assert stats["total_requests"] == 2 assert stats["hit_rate"] == 0.5 async def test_cache_stats_with_evictions(self) -> None: """Test cache_stats tracks evictions correctly.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( store=source_store, cache_store=cache_store, max_size=150, # Small size to force eviction ) # Add items that will trigger eviction buffer1 = CPUBuffer.from_bytes(b"x" * 100) buffer2 = CPUBuffer.from_bytes(b"y" * 100) await cached_store.set("key1", buffer1) await cached_store.set("key2", buffer2) # Should evict key1 stats = cached_store.cache_stats() assert stats["evictions"] == 1 def test_repr_method(self) -> None: """Test __repr__ returns useful string representation.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore( store=source_store, cache_store=cache_store, max_age_seconds=60, max_size=1024 ) repr_str = repr(cached_store) # Check that repr contains key information assert "CacheStore" in repr_str assert "max_age_seconds=60" in repr_str assert "max_size=1024" in repr_str assert "current_size=0" in repr_str assert "cached_keys=0" in repr_str async def test_cache_stats_zero_division_protection(self) -> None: """Test cache_stats handles zero requests correctly.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store) # With no requests, hit_rate should be 0.0 (not NaN or error) stats = cached_store.cache_stats() assert stats["hit_rate"] == 0.0 assert stats["total_requests"] == 0 async def test_byte_range_does_not_corrupt_cache(self) -> None: """Test that fetching a byte range does not store partial data under the full key. Reproduces https://github.com/zarr-developers/zarr-python/issues/3690: when a byte-range read populates the cache, subsequent reads of different ranges (or the full key) return wrong data. """ source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store) full_data = b"bar baz" await source_store.set("foo", CPUBuffer.from_bytes(full_data)) proto = default_buffer_prototype() # First read: byte range [0, 3) -> b"bar" bar = await cached_store.get("foo", proto, byte_range=RangeByteRequest(0, 3)) assert bar is not None assert bar.to_bytes() == b"bar" # Second read: different byte range [4, 7) -> b"baz" baz = await cached_store.get("foo", proto, byte_range=RangeByteRequest(4, 7)) assert baz is not None assert baz.to_bytes() == b"baz" # Third read: full key -> full data full = await cached_store.get("foo", proto) assert full is not None assert full.to_bytes() == full_data async def test_full_read_then_byte_range(self) -> None: """Test that a cached full read correctly serves subsequent byte-range requests.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store) full_data = b"hello world" await source_store.set("key", CPUBuffer.from_bytes(full_data)) proto = default_buffer_prototype() # Full read populates cache full = await cached_store.get("key", proto) assert full is not None assert full.to_bytes() == full_data # Byte-range reads should return the correct slices part = await cached_store.get("key", proto, byte_range=RangeByteRequest(0, 5)) assert part is not None assert part.to_bytes() == b"hello" part2 = await cached_store.get("key", proto, byte_range=RangeByteRequest(6, 11)) assert part2 is not None assert part2.to_bytes() == b"world" suffix = await cached_store.get("key", proto, byte_range=SuffixByteRequest(5)) assert suffix is not None assert suffix.to_bytes() == b"world" async def test_byte_range_set_then_read(self) -> None: """Test that data written via set() can be read back with byte ranges.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store) full_data = b"abcdefghij" await cached_store.set("key", CPUBuffer.from_bytes(full_data)) proto = default_buffer_prototype() # Byte-range reads from the cached data mid = await cached_store.get("key", proto, byte_range=RangeByteRequest(3, 7)) assert mid is not None assert mid.to_bytes() == b"defg" # Full read should still work full = await cached_store.get("key", proto) assert full is not None assert full.to_bytes() == full_data async def test_set_invalidates_cached_byte_ranges(self) -> None: """Test that set() invalidates previously cached byte-range entries.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store) proto = default_buffer_prototype() # Populate source and cache some byte ranges await source_store.set("key", CPUBuffer.from_bytes(b"old data!!")) r1 = await cached_store.get("key", proto, byte_range=RangeByteRequest(0, 3)) assert r1 is not None assert r1.to_bytes() == b"old" # Byte-range entry should be in range_cache assert ("key", RangeByteRequest(0, 3)) in cached_store._state.cache_order # Overwrite via set() — range entries must be invalidated await cached_store.set("key", CPUBuffer.from_bytes(b"NEW DATA!!")) # The old range entry should be gone from tracking and range_cache assert ("key", RangeByteRequest(0, 3)) not in cached_store._state.cache_order assert "key" not in cached_store._state.range_cache # A fresh byte-range read should return the new data r2 = await cached_store.get("key", proto, byte_range=RangeByteRequest(0, 3)) assert r2 is not None assert r2.to_bytes() == b"NEW" async def test_delete_invalidates_cached_byte_ranges(self) -> None: """Test that delete() removes previously cached byte-range entries.""" source_store = MemoryStore() cache_store = MemoryStore() cached_store = CacheStore(store=source_store, cache_store=cache_store) proto = default_buffer_prototype() # Populate and cache a byte range await source_store.set("key", CPUBuffer.from_bytes(b"hello world")) r = await cached_store.get("key", proto, byte_range=RangeByteRequest(0, 5)) assert r is not None assert r.to_bytes() == b"hello" assert ("key", RangeByteRequest(0, 5)) in cached_store._state.cache_order # Delete the key — range entries must be cleaned up await cached_store.delete("key") assert ("key", RangeByteRequest(0, 5)) not in cached_store._state.cache_order assert "key" not in cached_store._state.range_cache # Key is gone from source result = await cached_store.get("key", proto) assert result is None zarr-python-3.2.1/tests/test_group.py000066400000000000000000002425141517635743000177440ustar00rootroot00000000000000from __future__ import annotations import contextlib import inspect import json import operator import pickle import re import time import warnings from typing import TYPE_CHECKING, Any, Literal, get_args import numpy as np import pytest from numcodecs import Blosc import zarr import zarr.api.asynchronous import zarr.api.synchronous import zarr.storage from zarr import Array, AsyncArray, AsyncGroup, Group from zarr.abc.store import Store from zarr.core import sync_group from zarr.core._info import GroupInfo from zarr.core.buffer import default_buffer_prototype from zarr.core.config import config as zarr_config from zarr.core.dtype.common import unpack_dtype_json from zarr.core.dtype.npy.int import UInt8 from zarr.core.group import ( ConsolidatedMetadata, GroupMetadata, ImplicitGroupMarker, _build_metadata_v3, _get_roots, _parse_hierarchy_dict, create_hierarchy, create_nodes, create_rooted_hierarchy, get_node, ) from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.core.sync import _collect_aiterator, sync from zarr.errors import ( ContainsArrayError, ContainsGroupError, MetadataValidationError, ZarrUserWarning, ) from zarr.storage import LocalStore, MemoryStore, StorePath, ZipStore from zarr.storage._common import make_store_path from zarr.storage._utils import _join_paths, normalize_path from zarr.testing.store import LatencyStore from .conftest import meta_from_array, parse_store if TYPE_CHECKING: from collections.abc import Callable from _pytest.compat import LEGACY_PATH from zarr.core.buffer.core import Buffer from zarr.core.common import JSON, ZarrFormat @pytest.fixture(params=["local", "memory", "zip"]) async def store(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> Store: result = await parse_store(request.param, str(tmpdir)) if not isinstance(result, Store): raise TypeError(f"Wrong store class returned by test fixture! got {result} instead") return result @pytest.fixture(params=[True, False]) def overwrite(request: pytest.FixtureRequest) -> bool: result = request.param if not isinstance(result, bool): raise TypeError("Wrong type returned by test fixture.") return result def test_group_init(store: Store, zarr_format: ZarrFormat) -> None: """ Test that initializing a group from an asyncgroup works. """ agroup = sync(AsyncGroup.from_store(store=store, zarr_format=zarr_format)) group = Group(agroup) assert group._async_group == agroup async def test_create_creates_parents(store: Store, zarr_format: ZarrFormat) -> None: # prepare a root node, with some data set await zarr.api.asynchronous.open_group( store=store, path="a", zarr_format=zarr_format, attributes={"key": "value"} ) objs = {x async for x in store.list()} if zarr_format == 2: assert objs == {".zgroup", ".zattrs", "a/.zgroup", "a/.zattrs"} else: assert objs == {"zarr.json", "a/zarr.json"} # test that root group node was created root = await zarr.api.asynchronous.open_group( store=store, ) agroup = await root.getitem("a") assert agroup.attrs == {"key": "value"} # create a child node with a couple intermediates await zarr.api.asynchronous.open_group(store=store, path="a/b/c/d", zarr_format=zarr_format) parts = ["a", "a/b", "a/b/c"] if zarr_format == 2: files = [".zattrs", ".zgroup"] else: files = ["zarr.json"] expected = [f"{part}/{file}" for file in files for part in parts] if zarr_format == 2: expected.extend([".zgroup", ".zattrs", "a/b/c/d/.zgroup", "a/b/c/d/.zattrs"]) else: expected.extend(["zarr.json", "a/b/c/d/zarr.json"]) expected = sorted(expected) result = sorted([x async for x in store.list_prefix("")]) assert result == expected paths = ["a", "a/b", "a/b/c"] for path in paths: g = await zarr.api.asynchronous.open_group(store=store, path=path) assert isinstance(g, AsyncGroup) if path == "a": # ensure we didn't overwrite the root attributes assert g.attrs == {"key": "value"} else: assert g.attrs == {} @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("root_name", ["", "/", "a", "/a"]) @pytest.mark.parametrize("branch_name", ["foo", "/foo", "foo/bar", "/foo/bar"]) def test_group_name_properties( store: Store, zarr_format: ZarrFormat, root_name: str, branch_name: str ) -> None: """ Test that the path, name, and basename attributes of a group and its subgroups are consistent """ root = Group.from_store(store=StorePath(store=store, path=root_name), zarr_format=zarr_format) assert root.path == normalize_path(root_name) assert root.name == f"/{root.path}" assert root.basename == root.path branch = root.create_group(branch_name) if root.path == "": assert branch.path == normalize_path(branch_name) else: assert branch.path == "/".join([root.path, normalize_path(branch_name)]) assert branch.name == f"/{branch.path}" assert branch.basename == branch_name.split("/")[-1] @pytest.mark.parametrize("consolidated_metadata", [True, False]) def test_group_members(store: Store, zarr_format: ZarrFormat, consolidated_metadata: bool) -> None: """ Test that `Group.members` returns correct values, i.e. the arrays and groups (explicit and implicit) contained in that group. """ # group/ # subgroup/ # subsubgroup/ # subsubsubgroup # subarray path = "group" group = Group.from_store( store=store, zarr_format=zarr_format, ) members_expected: dict[str, Array | Group] = {} members_expected["subgroup"] = group.create_group("subgroup") # make a sub-sub-subgroup, to ensure that the children calculation doesn't go # too deep in the hierarchy subsubgroup = members_expected["subgroup"].create_group("subsubgroup") subsubsubgroup = subsubgroup.create_group("subsubsubgroup") members_expected["subarray"] = group.create_array( "subarray", shape=(100,), dtype="uint8", chunks=(10,), overwrite=True ) # add an extra object to the domain of the group. # the list of children should ignore this object. sync( store.set( f"{path}/extra_object-1", default_buffer_prototype().buffer.from_bytes(b"000000"), ) ) # add an extra object under a directory-like prefix in the domain of the group. # this creates a directory with a random key in it # this should not show up as a member sync( store.set( f"{path}/extra_directory/extra_object-2", default_buffer_prototype().buffer.from_bytes(b"000000"), ) ) # this warning shows up when extra objects show up in the hierarchy warn_context = pytest.warns( ZarrUserWarning, match=r"(?:Object at .* is not recognized as a component of a Zarr hierarchy.)|(?:Consolidated metadata is currently not part in the Zarr format 3 specification.)", ) if consolidated_metadata: if isinstance(store, ZipStore): with warn_context: with pytest.warns(UserWarning, match="Duplicate name: "): zarr.consolidate_metadata(store=store, zarr_format=zarr_format) else: with warn_context: zarr.consolidate_metadata(store=store, zarr_format=zarr_format) # now that we've consolidated the store, we shouldn't get the warnings from the unrecognized objects anymore # we use a nullcontext to handle these cases warn_context = contextlib.nullcontext() group = zarr.open_consolidated(store=store, zarr_format=zarr_format) with warn_context: members_observed = group.members() # members are not guaranteed to be ordered, so sort before comparing assert sorted(dict(members_observed)) == sorted(members_expected) # partial with warn_context: members_observed = group.members(max_depth=1) members_expected["subgroup/subsubgroup"] = subsubgroup # members are not guaranteed to be ordered, so sort before comparing assert sorted(dict(members_observed)) == sorted(members_expected) # total with warn_context: members_observed = group.members(max_depth=None) members_expected["subgroup/subsubgroup/subsubsubgroup"] = subsubsubgroup # members are not guaranteed to be ordered, so sort before comparing assert sorted(dict(members_observed)) == sorted(members_expected) with pytest.raises(ValueError, match="max_depth"): members_observed = group.members(max_depth=-1) def test_group(store: Store, zarr_format: ZarrFormat) -> None: """ Test basic Group routines. """ store_path = StorePath(store) agroup = AsyncGroup(metadata=GroupMetadata(zarr_format=zarr_format), store_path=store_path) group = Group(agroup) assert agroup.metadata is group.metadata assert agroup.store_path == group.store_path == store_path # create two groups foo = group.create_group("foo") bar = foo.create_group("bar", attributes={"baz": "qux"}) # create an array from the "bar" group data = np.arange(0, 4 * 4, dtype="uint16").reshape((4, 4)) arr = bar.create_array("baz", shape=data.shape, dtype=data.dtype, chunks=(2, 2), overwrite=True) arr[:] = data # check the array assert arr == bar["baz"] assert arr.shape == data.shape assert arr.dtype == data.dtype # TODO: update this once the array api settles down assert arr.chunks == (2, 2) bar2 = foo["bar"] assert dict(bar2.attrs) == {"baz": "qux"} # update a group's attributes if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): bar2.attrs.update({"name": "bar"}) else: bar2.attrs.update({"name": "bar"}) # bar.attrs was modified in-place assert dict(bar2.attrs) == {"baz": "qux", "name": "bar"} # and the attrs were modified in the store bar3 = foo["bar"] assert dict(bar3.attrs) == {"baz": "qux", "name": "bar"} def test_group_create(store: Store, overwrite: bool, zarr_format: ZarrFormat) -> None: """ Test that `Group.from_store` works as expected. """ attributes = {"foo": 100} group = Group.from_store( store, attributes=attributes, zarr_format=zarr_format, overwrite=overwrite ) assert group.attrs == attributes if not overwrite: with pytest.raises(ContainsGroupError): _ = Group.from_store(store, overwrite=overwrite, zarr_format=zarr_format) def test_group_open(store: Store, zarr_format: ZarrFormat, overwrite: bool) -> None: """ Test the `Group.open` method. """ spath = StorePath(store) # attempt to open a group that does not exist with pytest.raises(FileNotFoundError): Group.open(store) # create the group attrs = {"path": "foo"} group_created = Group.from_store( store, attributes=attrs, zarr_format=zarr_format, overwrite=overwrite ) assert group_created.attrs == attrs assert group_created.metadata.zarr_format == zarr_format assert group_created.store_path == spath # attempt to create a new group in place, to test overwrite new_attrs = {"path": "bar"} if not overwrite: with pytest.raises(ContainsGroupError): Group.from_store(store, attributes=attrs, zarr_format=zarr_format, overwrite=overwrite) else: if not store.supports_deletes: pytest.skip( "Store does not support deletes but `overwrite` is True, requiring deletes to override a group" ) group_created_again = Group.from_store( store, attributes=new_attrs, zarr_format=zarr_format, overwrite=overwrite ) assert group_created_again.attrs == new_attrs assert group_created_again.metadata.zarr_format == zarr_format assert group_created_again.store_path == spath @pytest.mark.parametrize("consolidated", [True, False]) def test_group_getitem(store: Store, zarr_format: ZarrFormat, consolidated: bool) -> None: """ Test the `Group.__getitem__` method. """ group = Group.from_store(store, zarr_format=zarr_format) subgroup = group.create_group(name="subgroup") subarray = group.create_array(name="subarray", shape=(10,), chunks=(10,), dtype="uint8") subsubarray = subgroup.create_array(name="subarray", shape=(10,), chunks=(10,), dtype="uint8") if consolidated: if zarr_format == 3: with pytest.warns( # noqa: PT031 ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): group = zarr.api.synchronous.consolidate_metadata( store=store, zarr_format=zarr_format ) else: group = zarr.api.synchronous.consolidate_metadata( store=store, zarr_format=zarr_format ) else: if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): group = zarr.api.synchronous.consolidate_metadata( store=store, zarr_format=zarr_format ) else: group = zarr.api.synchronous.consolidate_metadata( store=store, zarr_format=zarr_format ) # we're going to assume that `group.metadata` is correct, and reuse that to focus # on indexing in this test. Other tests verify the correctness of group.metadata object.__setattr__( subgroup.metadata, "consolidated_metadata", ConsolidatedMetadata( metadata={"subarray": group.metadata.consolidated_metadata.metadata["subarray"]} ), ) assert group["subgroup"] == subgroup assert group["subarray"] == subarray assert group["subgroup"]["subarray"] == subsubarray assert group["subgroup/subarray"] == subsubarray with pytest.raises(KeyError): group["nope"] with pytest.raises(KeyError, match="subarray/subsubarray"): group["subarray/subsubarray"] # Now test the mixed case if consolidated: object.__setattr__( group.metadata.consolidated_metadata.metadata["subgroup"], "consolidated_metadata", None, ) # test the implementation directly with pytest.raises(KeyError): group._async_group._getitem_consolidated( group.store_path, "subgroup/subarray", prefix="/" ) with pytest.raises(KeyError): # We've chosen to trust the consolidated metadata, which doesn't # contain this array group["subgroup/subarray"] with pytest.raises(KeyError, match="subarray/subsubarray"): group["subarray/subsubarray"] def test_group_get_with_default(store: Store, zarr_format: ZarrFormat) -> None: group = Group.from_store(store, zarr_format=zarr_format) # default behavior result = group.get("subgroup") assert result is None # custom default result = group.get("subgroup", 8) assert result == 8 # now with a group subgroup = group.require_group("subgroup") if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): subgroup.attrs["foo"] = "bar" else: subgroup.attrs["foo"] = "bar" result = group.get("subgroup", 8) assert result.attrs["foo"] == "bar" @pytest.mark.parametrize("consolidated", [True, False]) def test_group_delitem(store: Store, zarr_format: ZarrFormat, consolidated: bool) -> None: """ Test the `Group.__delitem__` method. """ if not store.supports_deletes: pytest.skip("store does not support deletes") group = Group.from_store(store, zarr_format=zarr_format) subgroup = group.create_group(name="subgroup") subarray = group.create_array(name="subarray", shape=(10,), chunks=(10,), dtype="uint8") if consolidated: if zarr_format == 3: with pytest.warns( # noqa: PT031 ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): group = zarr.api.synchronous.consolidate_metadata( store=store, zarr_format=zarr_format ) else: group = zarr.api.synchronous.consolidate_metadata( store=store, zarr_format=zarr_format ) else: group = zarr.api.synchronous.consolidate_metadata(store=store, zarr_format=zarr_format) object.__setattr__( subgroup.metadata, "consolidated_metadata", ConsolidatedMetadata(metadata={}) ) assert group["subgroup"] == subgroup assert group["subarray"] == subarray del group["subgroup"] with pytest.raises(KeyError): group["subgroup"] del group["subarray"] with pytest.raises(KeyError): group["subarray"] def test_group_iter(store: Store, zarr_format: ZarrFormat) -> None: """ Test the `Group.__iter__` method. """ group = Group.from_store(store, zarr_format=zarr_format) assert list(group) == [] def test_group_len(store: Store, zarr_format: ZarrFormat) -> None: """ Test the `Group.__len__` method. """ group = Group.from_store(store, zarr_format=zarr_format) assert len(group) == 0 def test_group_setitem(store: Store, zarr_format: ZarrFormat) -> None: """ Test the `Group.__setitem__` method. """ group = Group.from_store(store, zarr_format=zarr_format) arr = np.ones((2, 4)) group["key"] = arr assert list(group.array_keys()) == ["key"] assert group["key"].shape == (2, 4) np.testing.assert_array_equal(group["key"][:], arr) if store.supports_deletes: key = "key" else: # overwriting with another array requires deletes # for stores that don't support this, we just use a new key key = "key2" # overwrite with another array arr = np.zeros((3, 5)) group[key] = arr assert key in list(group.array_keys()) assert group[key].shape == (3, 5) np.testing.assert_array_equal(group[key], arr) def test_group_contains(store: Store, zarr_format: ZarrFormat) -> None: """ Test the `Group.__contains__` method """ group = Group.from_store(store, zarr_format=zarr_format) assert "foo" not in group _ = group.create_group(name="foo") assert "foo" in group @pytest.mark.parametrize("consolidate", [True, False]) def test_group_child_iterators(store: Store, zarr_format: ZarrFormat, consolidate: bool): group = Group.from_store(store, zarr_format=zarr_format) expected_group_keys = ["g0", "g1"] expected_group_values = [group.create_group(name=name) for name in expected_group_keys] expected_groups = list(zip(expected_group_keys, expected_group_values, strict=False)) fill_value = 3 dtype = UInt8() expected_group_values[0].create_group("subgroup") expected_group_values[0].create_array( "subarray", shape=(1,), dtype=dtype, fill_value=fill_value ) expected_array_keys = ["a0", "a1"] expected_array_values = [ group.create_array(name=name, shape=(1,), dtype=dtype, fill_value=fill_value) for name in expected_array_keys ] expected_arrays = list(zip(expected_array_keys, expected_array_values, strict=False)) if consolidate: if zarr_format == 3: with pytest.warns( # noqa: PT031 ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): group = zarr.consolidate_metadata(store) else: group = zarr.consolidate_metadata(store) else: if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): group = zarr.consolidate_metadata(store) else: group = zarr.consolidate_metadata(store) if zarr_format == 2: metadata = { "subarray": { "attributes": {}, "dtype": unpack_dtype_json(dtype.to_json(zarr_format=zarr_format)), "fill_value": fill_value, "shape": (1,), "chunks": (1,), "order": "C", "filters": None, "compressor": Blosc(), "zarr_format": zarr_format, }, "subgroup": { "attributes": {}, "consolidated_metadata": { "metadata": {}, "kind": "inline", "must_understand": False, }, "node_type": "group", "zarr_format": zarr_format, }, } else: metadata = { "subarray": { "attributes": {}, "chunk_grid": { "configuration": {"chunk_shape": (1,)}, "name": "regular", }, "chunk_key_encoding": { "configuration": {"separator": "/"}, "name": "default", }, "codecs": ( {"configuration": {"endian": "little"}, "name": "bytes"}, {"configuration": {}, "name": "zstd"}, ), "data_type": unpack_dtype_json(dtype.to_json(zarr_format=zarr_format)), "fill_value": fill_value, "node_type": "array", "shape": (1,), "zarr_format": zarr_format, }, "subgroup": { "attributes": {}, "consolidated_metadata": { "metadata": {}, "kind": "inline", "must_understand": False, }, "node_type": "group", "zarr_format": zarr_format, }, } object.__setattr__( expected_group_values[0].metadata, "consolidated_metadata", ConsolidatedMetadata.from_dict( { "kind": "inline", "metadata": metadata, "must_understand": False, } ), ) object.__setattr__( expected_group_values[1].metadata, "consolidated_metadata", ConsolidatedMetadata(metadata={}), ) result = sorted(group.groups(), key=operator.itemgetter(0)) assert result == expected_groups assert sorted(group.groups(), key=operator.itemgetter(0)) == expected_groups assert sorted(group.group_keys()) == expected_group_keys assert sorted(group.group_values(), key=lambda x: x.name) == expected_group_values assert sorted(group.arrays(), key=operator.itemgetter(0)) == expected_arrays assert sorted(group.array_keys()) == expected_array_keys assert sorted(group.array_values(), key=lambda x: x.name) == expected_array_values def test_group_update_attributes(store: Store, zarr_format: ZarrFormat) -> None: """ Test the behavior of `Group.update_attributes` """ attrs = {"foo": 100} group = Group.from_store(store, zarr_format=zarr_format, attributes=attrs) assert group.attrs == attrs new_attrs = {"bar": 100} if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): new_group = group.update_attributes(new_attrs) else: new_group = group.update_attributes(new_attrs) updated_attrs = attrs.copy() updated_attrs.update(new_attrs) assert new_group.attrs == updated_attrs async def test_group_update_attributes_async(store: Store, zarr_format: ZarrFormat) -> None: """ Test the behavior of `Group.update_attributes_async` """ attrs = {"foo": 100} group = Group.from_store(store, zarr_format=zarr_format, attributes=attrs) assert group.attrs == attrs new_attrs = {"bar": 100} if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name: "): new_group = await group.update_attributes_async(new_attrs) else: new_group = await group.update_attributes_async(new_attrs) assert new_group.attrs == new_attrs @pytest.mark.parametrize("name", ["a", "/a"]) def test_group_create_array( store: Store, zarr_format: ZarrFormat, overwrite: bool, name: str, ) -> None: """ Test `Group.from_store` """ group = Group.from_store(store, zarr_format=zarr_format) shape = (10, 10) dtype = "uint8" data = np.arange(np.prod(shape)).reshape(shape).astype(dtype) array = group.create_array(name=name, shape=shape, dtype=dtype) array[:] = data if not overwrite: with pytest.raises(ContainsArrayError): # noqa: PT012 a = group.create_array(name=name, shape=shape, dtype=dtype) a[:] = data assert array.path == normalize_path(name) assert array.name == f"/{array.path}" assert array.shape == shape assert array.dtype == np.dtype(dtype) assert np.array_equal(array[:], data) @pytest.mark.parametrize("method", ["create_array", "create_group"]) def test_create_with_parent_array(store: Store, zarr_format: ZarrFormat, method: str): """Test that groups/arrays cannot be created under a parent array.""" # create a group with a child array group = Group.from_store(store, zarr_format=zarr_format) group.create_array(name="arr_1", shape=(10, 10), dtype="uint8") error_msg = r"A parent of .* is an array - only groups may have child nodes." if method == "create_array": with pytest.raises(ValueError, match=error_msg): group.create_array("arr_1/group_1/group_2/arr_2", shape=(10, 10), dtype="uint8") else: with pytest.raises(ValueError, match=error_msg): group.create_group("arr_1/group_1/group_2/group_3") LikeMethodName = Literal["zeros_like", "ones_like", "empty_like", "full_like"] @pytest.mark.parametrize("method_name", get_args(LikeMethodName)) @pytest.mark.parametrize("out_shape", ["keep", (10, 10)]) @pytest.mark.parametrize("out_chunks", ["keep", (10, 10)]) @pytest.mark.parametrize("out_dtype", ["keep", "int8"]) def test_group_array_like_creation( zarr_format: ZarrFormat, method_name: LikeMethodName, out_shape: Literal["keep"] | tuple[int, ...], out_chunks: Literal["keep"] | tuple[int, ...], out_dtype: str, ) -> None: """ Test Group.{zeros_like, ones_like, empty_like, full_like}, ensuring that we can override the shape, chunks, and dtype of the array-like object provided to these functions with appropriate keyword arguments """ ref_arr = zarr.ones(store={}, shape=(11, 12), dtype="uint8", chunks=(11, 12)) group = Group.from_store({}, zarr_format=zarr_format) kwargs = {} if method_name == "full_like": expect_fill = 4 kwargs["fill_value"] = expect_fill meth = group.full_like elif method_name == "zeros_like": expect_fill = 0 meth = group.zeros_like elif method_name == "ones_like": expect_fill = 1 meth = group.ones_like elif method_name == "empty_like": expect_fill = ref_arr.fill_value meth = group.empty_like else: raise AssertionError if out_shape != "keep": kwargs["shape"] = out_shape expect_shape = out_shape else: expect_shape = ref_arr.shape if out_chunks != "keep": kwargs["chunks"] = out_chunks expect_chunks = out_chunks else: expect_chunks = ref_arr.chunks if out_dtype != "keep": kwargs["dtype"] = out_dtype expect_dtype = out_dtype else: expect_dtype = ref_arr.dtype new_arr = meth(name="foo", data=ref_arr, **kwargs) assert new_arr.shape == expect_shape assert new_arr.chunks == expect_chunks assert new_arr.dtype == expect_dtype assert np.all(new_arr[:] == expect_fill) def test_group_array_creation( store: Store, zarr_format: ZarrFormat, ): group = Group.from_store(store, zarr_format=zarr_format) shape = (10, 10) empty_array = group.empty(name="empty", shape=shape) assert isinstance(empty_array, Array) assert empty_array.fill_value == 0 assert empty_array.shape == shape assert empty_array.store_path.store == store assert empty_array.store_path.path == "empty" empty_like_array = group.empty_like(name="empty_like", data=empty_array) assert isinstance(empty_like_array, Array) assert empty_like_array.fill_value == 0 assert empty_like_array.shape == shape assert empty_like_array.store_path.store == store empty_array_bool = group.empty(name="empty_bool", shape=shape, dtype=np.dtype("bool")) assert isinstance(empty_array_bool, Array) assert not empty_array_bool.fill_value assert empty_array_bool.shape == shape assert empty_array_bool.store_path.store == store empty_like_array_bool = group.empty_like(name="empty_like_bool", data=empty_array_bool) assert isinstance(empty_like_array_bool, Array) assert not empty_like_array_bool.fill_value assert empty_like_array_bool.shape == shape assert empty_like_array_bool.store_path.store == store zeros_array = group.zeros(name="zeros", shape=shape) assert isinstance(zeros_array, Array) assert zeros_array.fill_value == 0 assert zeros_array.shape == shape assert zeros_array.store_path.store == store zeros_like_array = group.zeros_like(name="zeros_like", data=zeros_array) assert isinstance(zeros_like_array, Array) assert zeros_like_array.fill_value == 0 assert zeros_like_array.shape == shape assert zeros_like_array.store_path.store == store ones_array = group.ones(name="ones", shape=shape) assert isinstance(ones_array, Array) assert ones_array.fill_value == 1 assert ones_array.shape == shape assert ones_array.store_path.store == store ones_like_array = group.ones_like(name="ones_like", data=ones_array) assert isinstance(ones_like_array, Array) assert ones_like_array.fill_value == 1 assert ones_like_array.shape == shape assert ones_like_array.store_path.store == store full_array = group.full(name="full", shape=shape, fill_value=42) assert isinstance(full_array, Array) assert full_array.fill_value == 42 assert full_array.shape == shape assert full_array.store_path.store == store full_like_array = group.full_like(name="full_like", data=full_array, fill_value=43) assert isinstance(full_like_array, Array) assert full_like_array.fill_value == 43 assert full_like_array.shape == shape assert full_like_array.store_path.store == store @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.parametrize("overwrite", [True, False]) @pytest.mark.parametrize("extant_node", ["array", "group"]) def test_group_creation_existing_node( store: Store, zarr_format: ZarrFormat, overwrite: bool, extant_node: Literal["array", "group"], ) -> None: """ Check that an existing array or group is handled as expected during group creation. """ spath = StorePath(store) group = Group.from_store(spath, zarr_format=zarr_format) expected_exception: type[ContainsArrayError | ContainsGroupError] attributes: dict[str, JSON] = {"old": True} if extant_node == "array": expected_exception = ContainsArrayError _ = group.create_array("extant", shape=(10,), dtype="uint8", attributes=attributes) elif extant_node == "group": expected_exception = ContainsGroupError _ = group.create_group("extant", attributes=attributes) else: raise AssertionError new_attributes = {"new": True} if overwrite: if not store.supports_deletes: pytest.skip("store does not support deletes but overwrite is True") node_new = Group.from_store( spath / "extant", attributes=new_attributes, zarr_format=zarr_format, overwrite=overwrite, ) assert node_new.attrs == new_attributes else: with pytest.raises(expected_exception): node_new = Group.from_store( spath / "extant", attributes=new_attributes, zarr_format=zarr_format, overwrite=overwrite, ) async def test_asyncgroup_create( store: Store, overwrite: bool, zarr_format: ZarrFormat, ) -> None: """ Test that `AsyncGroup.from_store` works as expected. """ spath = StorePath(store=store) attributes = {"foo": 100} agroup = await AsyncGroup.from_store( store, attributes=attributes, overwrite=overwrite, zarr_format=zarr_format, ) assert agroup.metadata == GroupMetadata(zarr_format=zarr_format, attributes=attributes) assert agroup.store_path == await make_store_path(store) if not overwrite: with pytest.raises(ContainsGroupError): agroup = await AsyncGroup.from_store( spath, attributes=attributes, overwrite=overwrite, zarr_format=zarr_format, ) # create an array at our target path collision_name = "foo" _ = await zarr.api.asynchronous.create_array( spath / collision_name, shape=(10,), dtype="uint8", zarr_format=zarr_format ) with pytest.raises(ContainsArrayError): _ = await AsyncGroup.from_store( StorePath(store=store) / collision_name, attributes=attributes, overwrite=overwrite, zarr_format=zarr_format, ) async def test_asyncgroup_attrs(store: Store, zarr_format: ZarrFormat) -> None: attributes = {"foo": 100} agroup = await AsyncGroup.from_store(store, zarr_format=zarr_format, attributes=attributes) assert agroup.attrs == agroup.metadata.attributes == attributes async def test_asyncgroup_open( store: Store, zarr_format: ZarrFormat, ) -> None: """ Create an `AsyncGroup`, then ensure that we can open it using `AsyncGroup.open` """ attributes = {"foo": 100} group_w = await AsyncGroup.from_store( store=store, attributes=attributes, overwrite=False, zarr_format=zarr_format, ) group_r = await AsyncGroup.open(store=store, zarr_format=zarr_format) assert group_w.attrs == group_w.attrs == attributes assert group_w == group_r async def test_asyncgroup_open_wrong_format( store: Store, zarr_format: ZarrFormat, ) -> None: _ = await AsyncGroup.from_store(store=store, overwrite=False, zarr_format=zarr_format) zarr_format_wrong: ZarrFormat # try opening with the wrong zarr format if zarr_format == 3: zarr_format_wrong = 2 elif zarr_format == 2: zarr_format_wrong = 3 else: raise AssertionError with pytest.raises(FileNotFoundError): await AsyncGroup.open(store=store, zarr_format=zarr_format_wrong) # todo: replace the dict[str, Any] type with something a bit more specific # should this be async? @pytest.mark.parametrize( "data", [ {"zarr_format": 3, "node_type": "group", "attributes": {"foo": 100}}, {"zarr_format": 2, "attributes": {"foo": 100}}, ], ) def test_asyncgroup_from_dict(store: Store, data: dict[str, Any]) -> None: """ Test that we can create an AsyncGroup from a dict """ path = "test" store_path = StorePath(store=store, path=path) group = AsyncGroup.from_dict(store_path, data=data) assert group.metadata.zarr_format == data["zarr_format"] assert group.metadata.attributes == data["attributes"] # todo: replace this with a declarative API where we model a full hierarchy async def test_asyncgroup_getitem(store: Store, zarr_format: ZarrFormat) -> None: """ Create an `AsyncGroup`, then create members of that group, and ensure that we can access those members via the `AsyncGroup.getitem` method. """ agroup = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) array_name = "sub_array" sub_array = await agroup.create_array(name=array_name, shape=(10,), dtype="uint8", chunks=(2,)) assert await agroup.getitem(array_name) == sub_array sub_group_path = "sub_group" sub_group = await agroup.create_group(sub_group_path, attributes={"foo": 100}) assert await agroup.getitem(sub_group_path) == sub_group # check that asking for a nonexistent key raises KeyError with pytest.raises(KeyError): await agroup.getitem("foo") async def test_asyncgroup_delitem(store: Store, zarr_format: ZarrFormat) -> None: if not store.supports_deletes: pytest.skip("store does not support deletes") agroup = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) array_name = "sub_array" _ = await agroup.create_array( name=array_name, shape=(10,), dtype="uint8", chunks=(2,), attributes={"foo": 100}, ) await agroup.delitem(array_name) # todo: clean up the code duplication here if zarr_format == 2: assert not await agroup.store_path.store.exists(f"{array_name}/.zarray") assert not await agroup.store_path.store.exists(f"{array_name}/.zattrs") elif zarr_format == 3: assert not await agroup.store_path.store.exists(f"{array_name}/zarr.json") else: raise AssertionError sub_group_path = "sub_group" _ = await agroup.create_group(sub_group_path, attributes={"foo": 100}) await agroup.delitem(sub_group_path) if zarr_format == 2: assert not await agroup.store_path.store.exists(f"{array_name}/.zgroup") assert not await agroup.store_path.store.exists(f"{array_name}/.zattrs") elif zarr_format == 3: assert not await agroup.store_path.store.exists(f"{array_name}/zarr.json") else: raise AssertionError @pytest.mark.parametrize("name", ["a", "/a"]) async def test_asyncgroup_create_group( store: Store, name: str, zarr_format: ZarrFormat, ) -> None: agroup = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) attributes = {"foo": 999} subgroup = await agroup.create_group(name=name, attributes=attributes) assert isinstance(subgroup, AsyncGroup) assert subgroup.path == normalize_path(name) assert subgroup.name == f"/{subgroup.path}" assert subgroup.attrs == attributes assert subgroup.store_path.path == subgroup.path assert subgroup.store_path.store == store assert subgroup.metadata.zarr_format == zarr_format async def test_asyncgroup_create_array( store: Store, zarr_format: ZarrFormat, overwrite: bool ) -> None: """ Test that the AsyncGroup.create_array method works correctly. We ensure that array properties specified in create_array are present on the resulting array. """ agroup = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) if not overwrite: with pytest.raises(ContainsGroupError): agroup = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) shape = (10,) dtype = "uint8" chunk_shape = (4,) attributes: dict[str, JSON] = {"foo": 100} sub_node_path = "sub_array" subnode = await agroup.create_array( name=sub_node_path, shape=shape, dtype=dtype, chunks=chunk_shape, attributes=attributes, ) assert isinstance(subnode, AsyncArray) assert subnode.attrs == attributes assert subnode.store_path.path == sub_node_path assert subnode.store_path.store == store assert subnode.shape == shape assert subnode.dtype == dtype assert subnode._chunk_grid.chunk_shape == chunk_shape assert subnode.metadata.zarr_format == zarr_format async def test_asyncgroup_update_attributes(store: Store, zarr_format: ZarrFormat) -> None: """ Test that the AsyncGroup.update_attributes method works correctly. """ attributes_old = {"foo": 10} attributes_new = {"baz": "new"} agroup = await AsyncGroup.from_store( store=store, zarr_format=zarr_format, attributes=attributes_old ) if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name"): agroup_new_attributes = await agroup.update_attributes(attributes_new) else: agroup_new_attributes = await agroup.update_attributes(attributes_new) attributes_updated = attributes_old.copy() attributes_updated.update(attributes_new) assert agroup_new_attributes.attrs == attributes_updated @pytest.mark.parametrize("store", ["local"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_serializable_async_group(store: LocalStore, zarr_format: ZarrFormat) -> None: expected = await AsyncGroup.from_store( store=store, attributes={"foo": 999}, zarr_format=zarr_format ) p = pickle.dumps(expected) actual = pickle.loads(p) assert actual == expected @pytest.mark.parametrize("store", ["local"], indirect=["store"]) @pytest.mark.parametrize("zarr_format", [2, 3]) def test_serializable_sync_group(store: LocalStore, zarr_format: ZarrFormat) -> None: expected = Group.from_store(store=store, attributes={"foo": 999}, zarr_format=zarr_format) p = pickle.dumps(expected) actual = pickle.loads(p) assert actual == expected @pytest.mark.parametrize("consolidated_metadata", [True, False]) async def test_group_members_async(store: Store, consolidated_metadata: bool) -> None: group = await AsyncGroup.from_store( store=store, ) a0 = await group.create_array("a0", shape=(1,), dtype="uint8") g0 = await group.create_group("g0") a1 = await g0.create_array("a1", shape=(1,), dtype="uint8") g1 = await g0.create_group("g1") a2 = await g1.create_array("a2", shape=(1,), dtype="uint8") g2 = await g1.create_group("g2") # immediate children children = sorted([x async for x in group.members()], key=operator.itemgetter(0)) assert children == [ ("a0", a0), ("g0", g0), ] nmembers = await group.nmembers() assert nmembers == 2 # partial children = sorted([x async for x in group.members(max_depth=1)], key=operator.itemgetter(0)) expected = [ ("a0", a0), ("g0", g0), ("g0/a1", a1), ("g0/g1", g1), ] assert children == expected nmembers = await group.nmembers(max_depth=1) assert nmembers == 4 # all children all_children = sorted( [x async for x in group.members(max_depth=None)], key=operator.itemgetter(0) ) expected = [ ("a0", a0), ("g0", g0), ("g0/a1", a1), ("g0/g1", g1), ("g0/g1/a2", a2), ("g0/g1/g2", g2), ] assert all_children == expected if consolidated_metadata: with pytest.warns( # noqa: PT031 ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name"): await zarr.api.asynchronous.consolidate_metadata(store=store) else: await zarr.api.asynchronous.consolidate_metadata(store=store) group = await zarr.api.asynchronous.open_group(store=store) nmembers = await group.nmembers(max_depth=None) assert nmembers == 6 with pytest.raises(ValueError, match="max_depth"): [x async for x in group.members(max_depth=-1)] if consolidated_metadata: # test for mixed known and unknown metadata. # For now, we trust the consolidated metadata. object.__setattr__( group.metadata.consolidated_metadata.metadata["g0"].consolidated_metadata.metadata[ "g1" ], "consolidated_metadata", None, ) # test depth=0 nmembers = await group.nmembers(max_depth=0) assert nmembers == 2 # test depth=1 nmembers = await group.nmembers(max_depth=1) assert nmembers == 4 # test depth=None all_children = sorted( [x async for x in group.members(max_depth=None)], key=operator.itemgetter(0) ) assert len(all_children) == 4 nmembers = await group.nmembers(max_depth=None) assert nmembers == 4 # test depth<0 with pytest.raises(ValueError, match="max_depth"): await group.nmembers(max_depth=-1) async def test_require_group(store: LocalStore | MemoryStore, zarr_format: ZarrFormat) -> None: root = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) # create foo group _ = await root.create_group("foo", attributes={"foo": 100}) # test that we can get the group using require_group foo_group = await root.require_group("foo") assert foo_group.attrs == {"foo": 100} # test that we can get the group using require_group and overwrite=True if store.supports_deletes: foo_group = await root.require_group("foo", overwrite=True) assert foo_group.attrs == {} _ = await foo_group.create_array( "bar", shape=(10,), dtype="uint8", chunks=(2,), attributes={"foo": 100} ) # test that overwriting a group w/ children fails # TODO: figure out why ensure_no_existing_node is not catching the foo.bar array # # with pytest.raises(ContainsArrayError): # await root.require_group("foo", overwrite=True) # test that requiring a group where an array is fails with pytest.raises(TypeError): await foo_group.require_group("bar") async def test_require_groups(store: LocalStore | MemoryStore, zarr_format: ZarrFormat) -> None: root = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) # create foo group _ = await root.create_group("foo", attributes={"foo": 100}) # create bar group _ = await root.create_group("bar", attributes={"bar": 200}) foo_group, bar_group = await root.require_groups("foo", "bar") assert foo_group.attrs == {"foo": 100} assert bar_group.attrs == {"bar": 200} # get a mix of existing and new groups foo_group, spam_group = await root.require_groups("foo", "spam") assert foo_group.attrs == {"foo": 100} assert spam_group.attrs == {} # no names no_group = await root.require_groups() assert no_group == () async def test_require_array(store: Store, zarr_format: ZarrFormat) -> None: root = await AsyncGroup.from_store(store=store, zarr_format=zarr_format) foo1 = await root.require_array("foo", shape=(10,), dtype="i8", attributes={"foo": 101}) assert foo1.attrs == {"foo": 101} foo2 = await root.require_array("foo", shape=(10,), dtype="i8") assert foo2.attrs == {"foo": 101} # exact = False _ = await root.require_array("foo", shape=10, dtype="f8") # errors w/ exact True with pytest.raises(TypeError, match="Incompatible dtype"): await root.require_array("foo", shape=(10,), dtype="f8", exact=True) with pytest.raises(TypeError, match="Incompatible shape"): await root.require_array("foo", shape=(100, 100), dtype="i8") with pytest.raises(TypeError, match="Incompatible dtype"): await root.require_array("foo", shape=(10,), dtype="f4") _ = await root.create_group("bar") with pytest.raises(TypeError, match="Incompatible object"): await root.require_array("bar", shape=(10,), dtype="int8") @pytest.mark.parametrize("consolidate", [True, False]) async def test_members_name(store: Store, consolidate: bool, zarr_format: ZarrFormat): group = Group.from_store(store=store, zarr_format=zarr_format) a = group.create_group(name="a") a.create_array("array", shape=(1,), dtype="uint8") b = a.create_group(name="b") b.create_array("array", shape=(1,), dtype="uint8") if consolidate: if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name"): # noqa: PT031 if zarr_format == 3: with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): group = zarr.api.synchronous.consolidate_metadata(store) else: group = zarr.api.synchronous.consolidate_metadata(store) else: if zarr_format == 3: with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): group = zarr.api.synchronous.consolidate_metadata(store) else: group = zarr.api.synchronous.consolidate_metadata(store) result = group["a"]["b"] assert result.name == "/a/b" paths = sorted(x.name for _, x in group.members(max_depth=None)) expected = ["/a", "/a/array", "/a/b", "/a/b/array"] assert paths == expected # regression test for https://github.com/zarr-developers/zarr-python/pull/2356 g = zarr.open_group(store, use_consolidated=False) with warnings.catch_warnings(): warnings.simplefilter("error") assert list(g) async def test_open_mutable_mapping(): group = await zarr.api.asynchronous.open_group( store={}, ) assert isinstance(group.store_path.store, MemoryStore) def test_open_mutable_mapping_sync(): group = zarr.open_group( store={}, ) assert isinstance(group.store_path.store, MemoryStore) async def test_open_ambiguous_node(): zarr_json_bytes = default_buffer_prototype().buffer.from_bytes( json.dumps({"zarr_format": 3, "node_type": "group"}).encode("utf-8") ) zgroup_bytes = default_buffer_prototype().buffer.from_bytes( json.dumps({"zarr_format": 2}).encode("utf-8") ) store: dict[str, Buffer] = {"zarr.json": zarr_json_bytes, ".zgroup": zgroup_bytes} with pytest.warns( ZarrUserWarning, match=r"Both zarr\.json \(Zarr format 3\) and \.zgroup \(Zarr format 2\) metadata objects exist at", ): await AsyncGroup.open(store, zarr_format=None) class TestConsolidated: async def test_group_getitem_consolidated(self, store: Store) -> None: root = await AsyncGroup.from_store(store=store) # Set up the test structure with # / # g0/ # group /g0 # g1/ # group /g0/g1 # g2/ # group /g0/g1/g2 # x1/ # group /x0 # x2/ # group /x0/x1 # x3/ # group /x0/x1/x2 g0 = await root.create_group("g0") g1 = await g0.create_group("g1") await g1.create_group("g2") x0 = await root.create_group("x0") x1 = await x0.create_group("x1") await x1.create_group("x2") with pytest.warns( # noqa: PT031 ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name"): await zarr.api.asynchronous.consolidate_metadata(store) else: await zarr.api.asynchronous.consolidate_metadata(store) # On disk, we've consolidated all the metadata in the root zarr.json group = await zarr.api.asynchronous.open(store=store) rg0 = await group.getitem("g0") expected = ConsolidatedMetadata( metadata={ "g1": GroupMetadata( attributes={}, zarr_format=3, consolidated_metadata=ConsolidatedMetadata( metadata={ "g2": GroupMetadata( attributes={}, zarr_format=3, consolidated_metadata=ConsolidatedMetadata(metadata={}), ) } ), ), } ) assert rg0.metadata.consolidated_metadata == expected rg1 = await rg0.getitem("g1") assert rg1.metadata.consolidated_metadata == expected.metadata["g1"].consolidated_metadata rg2 = await rg1.getitem("g2") assert rg2.metadata.consolidated_metadata == ConsolidatedMetadata(metadata={}) async def test_group_delitem_consolidated(self, store: Store) -> None: if isinstance(store, ZipStore): raise pytest.skip("Not implemented") root = await AsyncGroup.from_store(store=store) # Set up the test structure with # / # g0/ # group /g0 # g1/ # group /g0/g1 # g2/ # group /g0/g1/g2 # data # array # x1/ # group /x0 # x2/ # group /x0/x1 # x3/ # group /x0/x1/x2 # data # array g0 = await root.create_group("g0") g1 = await g0.create_group("g1") g2 = await g1.create_group("g2") await g2.create_array("data", shape=(1,), dtype="uint8") x0 = await root.create_group("x0") x1 = await x0.create_group("x1") x2 = await x1.create_group("x2") await x2.create_array("data", shape=(1,), dtype="uint8") with pytest.warns( # noqa: PT031 ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): if isinstance(store, ZipStore): with pytest.warns(UserWarning, match="Duplicate name"): await zarr.api.asynchronous.consolidate_metadata(store) else: await zarr.api.asynchronous.consolidate_metadata(store) group = await zarr.api.asynchronous.open_consolidated(store=store) assert len(group.metadata.consolidated_metadata.metadata) == 2 assert "g0" in group.metadata.consolidated_metadata.metadata await group.delitem("g0") assert len(group.metadata.consolidated_metadata.metadata) == 1 assert "g0" not in group.metadata.consolidated_metadata.metadata def test_open_consolidated_raises(self, store: Store) -> None: if isinstance(store, ZipStore): raise pytest.skip("Not implemented") root = Group.from_store(store=store) # fine to be missing by default zarr.open_group(store=store) with pytest.raises(ValueError, match="Consolidated metadata requested."): zarr.open_group(store=store, use_consolidated=True) # Now create consolidated metadata... root.create_group("g0") with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): zarr.consolidate_metadata(store) # and explicitly ignore it. group = zarr.open_group(store=store, use_consolidated=False) assert group.metadata.consolidated_metadata is None async def test_open_consolidated_raises_async(self, store: Store) -> None: if isinstance(store, ZipStore): raise pytest.skip("Not implemented") root = await AsyncGroup.from_store(store=store) # fine to be missing by default await zarr.api.asynchronous.open_group(store=store) with pytest.raises(ValueError, match="Consolidated metadata requested."): await zarr.api.asynchronous.open_group(store=store, use_consolidated=True) # Now create consolidated metadata... await root.create_group("g0") with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await zarr.api.asynchronous.consolidate_metadata(store) # and explicitly ignore it. group = await zarr.api.asynchronous.open_group(store=store, use_consolidated=False) assert group.metadata.consolidated_metadata is None class TestGroupMetadata: def test_from_dict_extra_fields(self): data = { "attributes": {"key": "value"}, "_nczarr_superblock": {"version": "2.0.0"}, "zarr_format": 2, } result = GroupMetadata.from_dict(data) expected = GroupMetadata(attributes={"key": "value"}, zarr_format=2) assert result == expected class TestInfo: def test_info(self): store = zarr.storage.MemoryStore() A = zarr.group(store=store, path="A") B = A.create_group(name="B") B.create_array(name="x", shape=(1,), dtype="uint8") B.create_array(name="y", shape=(2,), dtype="uint8") result = A.info expected = GroupInfo( _name="A", _read_only=False, _store_type="MemoryStore", _zarr_format=3, ) assert result == expected result = A.info_complete() expected = GroupInfo( _name="A", _read_only=False, _store_type="MemoryStore", _zarr_format=3, _count_members=3, _count_arrays=2, _count_groups=1, ) assert result == expected def test_update_attrs() -> None: # regression test for https://github.com/zarr-developers/zarr-python/issues/2328 root = Group.from_store( MemoryStore(), ) root.attrs["foo"] = "bar" assert root.attrs["foo"] == "bar" @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) def test_delitem_removes_children(store: Store, zarr_format: ZarrFormat) -> None: # https://github.com/zarr-developers/zarr-python/issues/2191 g1 = zarr.group(store=store, zarr_format=zarr_format) g1.create_group("0") g1.create_group("0/0") arr = g1.create_array("0/0/0", shape=(1,), dtype="uint8") arr[:] = 1 del g1["0"] with pytest.raises(KeyError): g1["0/0"] @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_nodes( impl: Literal["async", "sync"], store: Store, zarr_format: ZarrFormat ) -> None: """ Ensure that ``create_nodes`` can create a zarr hierarchy from a model of that hierarchy in dict form. Note that this creates an incomplete Zarr hierarchy. """ node_spec = { "group": GroupMetadata(attributes={"foo": 10}), "group/array_0": meta_from_array(np.arange(3), zarr_format=zarr_format), "group/array_1": meta_from_array(np.arange(4), zarr_format=zarr_format), "group/subgroup/array_0": meta_from_array(np.arange(4), zarr_format=zarr_format), "group/subgroup/array_1": meta_from_array(np.arange(5), zarr_format=zarr_format), } if impl == "sync": observed_nodes = dict(sync_group.create_nodes(store=store, nodes=node_spec)) elif impl == "async": observed_nodes = dict(await _collect_aiterator(create_nodes(store=store, nodes=node_spec))) else: raise ValueError(f"Invalid impl: {impl}") assert node_spec == {k: v.metadata for k, v in observed_nodes.items()} @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_create_nodes_concurrency_limit(store: MemoryStore) -> None: """ Test that the execution time of create_nodes can be constrained by the async concurrency configuration setting. """ set_latency = 0.02 num_groups = 10 groups = {str(idx): GroupMetadata() for idx in range(num_groups)} latency_store = LatencyStore(store, set_latency=set_latency) # check how long it takes to iterate over the groups # if create_nodes is sensitive to IO latency, # this should take (num_groups * get_latency) seconds # otherwise, it should take only marginally more than get_latency seconds with zarr_config.set({"async.concurrency": 1}): start = time.time() _ = tuple(sync_group.create_nodes(store=latency_store, nodes=groups)) elapsed = time.time() - start assert elapsed > num_groups * set_latency @pytest.mark.parametrize( ("a_func", "b_func"), [ (zarr.core.group.AsyncGroup.create_array, zarr.core.group.Group.create_array), (zarr.core.group.AsyncGroup.create_hierarchy, zarr.core.group.Group.create_hierarchy), (zarr.core.group.create_hierarchy, zarr.core.sync_group.create_hierarchy), (zarr.core.group.create_nodes, zarr.core.sync_group.create_nodes), (zarr.core.group.create_rooted_hierarchy, zarr.core.sync_group.create_rooted_hierarchy), (zarr.core.group.get_node, zarr.core.sync_group.get_node), ], ) def test_consistent_signatures( a_func: Callable[[object], object], b_func: Callable[[object], object] ) -> None: """ Ensure that pairs of functions have consistent signatures """ base_sig = inspect.signature(a_func) test_sig = inspect.signature(b_func) wrong: dict[str, list[object]] = { "missing_from_test": [], "missing_from_base": [], "wrong_type": [], } for key, value in base_sig.parameters.items(): if key not in test_sig.parameters: wrong["missing_from_test"].append((key, value)) for key, value in test_sig.parameters.items(): if key not in base_sig.parameters: wrong["missing_from_base"].append((key, value)) if base_sig.parameters[key] != value: wrong["wrong_type"].append({key: {"test": value, "base": base_sig.parameters[key]}}) assert wrong["missing_from_base"] == [] assert wrong["missing_from_test"] == [] assert wrong["wrong_type"] == [] @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("overwrite", [True, False]) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_hierarchy( impl: Literal["async", "sync"], store: Store, overwrite: bool, zarr_format: ZarrFormat ) -> None: """ Test that ``create_hierarchy`` can create a complete Zarr hierarchy, even if the input describes an incomplete one. """ hierarchy_spec = { "group": GroupMetadata(attributes={"path": "group"}, zarr_format=zarr_format), "group/array_0": meta_from_array( np.arange(3), attributes={"path": "group/array_0"}, zarr_format=zarr_format ), "group/subgroup/array_0": meta_from_array( np.arange(4), attributes={"path": "group/subgroup/array_0"}, zarr_format=zarr_format ), } pre_existing_nodes = { "group/extra": GroupMetadata(zarr_format=zarr_format, attributes={"path": "group/extra"}), "": GroupMetadata(zarr_format=zarr_format, attributes={"name": "root"}), } # we expect create_hierarchy to insert a group that was missing from the hierarchy spec expected_meta = hierarchy_spec | {"group/subgroup": GroupMetadata(zarr_format=zarr_format)} # initialize the group with some nodes _ = dict(sync_group.create_nodes(store=store, nodes=pre_existing_nodes)) if impl == "sync": created = dict( sync_group.create_hierarchy(store=store, nodes=hierarchy_spec, overwrite=overwrite) ) elif impl == "async": created = { k: v async for k, v in create_hierarchy( store=store, nodes=hierarchy_spec, overwrite=overwrite ) } else: raise ValueError(f"Invalid impl: {impl}") if not overwrite: extra_group = sync_group.get_node(store=store, path="group/extra", zarr_format=zarr_format) assert extra_group.metadata.attributes == {"path": "group/extra"} else: with pytest.raises(FileNotFoundError): await get_node(store=store, path="group/extra", zarr_format=zarr_format) assert expected_meta == {k: v.metadata for k, v in created.items()} @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("extant_node", ["array", "group"]) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_hierarchy_existing_nodes( impl: Literal["async", "sync"], store: Store, extant_node: Literal["array", "group"], zarr_format: ZarrFormat, ) -> None: """ Test that create_hierarchy with overwrite = False will not overwrite an existing array or group, and raises an exception instead. """ extant_node_path = "node" if extant_node == "array": extant_metadata = meta_from_array( np.zeros(4), zarr_format=zarr_format, attributes={"extant": True} ) new_metadata = meta_from_array(np.zeros(4), zarr_format=zarr_format) err_cls = ContainsArrayError else: extant_metadata = GroupMetadata(zarr_format=zarr_format, attributes={"extant": True}) new_metadata = GroupMetadata(zarr_format=zarr_format) err_cls = ContainsGroupError # write the extant metadata tuple(sync_group.create_nodes(store=store, nodes={extant_node_path: extant_metadata})) msg = f"{extant_node} exists in store {store!r} at path {extant_node_path!r}." # ensure that we cannot invoke create_hierarchy with overwrite=False here if impl == "sync": with pytest.raises(err_cls, match=re.escape(msg)): tuple( sync_group.create_hierarchy( store=store, nodes={"node": new_metadata}, overwrite=False ) ) elif impl == "async": with pytest.raises(err_cls, match=re.escape(msg)): tuple( [ x async for x in create_hierarchy( store=store, nodes={"node": new_metadata}, overwrite=False ) ] ) else: raise ValueError(f"Invalid impl: {impl}") # ensure that the extant metadata was not overwritten assert ( await get_node(store=store, path=extant_node_path, zarr_format=zarr_format) ).metadata.attributes == {"extant": True} @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("overwrite", [True, False]) @pytest.mark.parametrize("group_path", ["", "foo"]) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_group_create_hierarchy( store: Store, zarr_format: ZarrFormat, overwrite: bool, group_path: str, impl: Literal["async", "sync"], ) -> None: """ Test that the Group.create_hierarchy method creates specified nodes and returns them in a dict. Also test that off-target nodes are not deleted, and that the root group is not deleted """ root_attrs = {"root": True} g = sync_group.create_rooted_hierarchy( store=store, nodes={group_path: GroupMetadata(zarr_format=zarr_format, attributes=root_attrs)}, ) node_spec = { "a": GroupMetadata(zarr_format=zarr_format, attributes={"name": "a"}), "a/b": GroupMetadata(zarr_format=zarr_format, attributes={"name": "a/b"}), "a/b/c": meta_from_array( np.zeros(5), zarr_format=zarr_format, attributes={"name": "a/b/c"} ), } # This node should be kept if overwrite is True extant_spec = {"b": GroupMetadata(zarr_format=zarr_format, attributes={"name": "b"})} if impl == "async": extant_created = dict( await _collect_aiterator(g._async_group.create_hierarchy(extant_spec, overwrite=False)) ) nodes_created = dict( await _collect_aiterator( g._async_group.create_hierarchy(node_spec, overwrite=overwrite) ) ) elif impl == "sync": extant_created = dict(g.create_hierarchy(extant_spec, overwrite=False)) nodes_created = dict(g.create_hierarchy(node_spec, overwrite=overwrite)) all_members = dict(g.members(max_depth=None)) for k, v in node_spec.items(): assert all_members[k].metadata == v == nodes_created[k].metadata # if overwrite is True, the extant nodes should be erased for k, v in extant_spec.items(): if overwrite: assert k in all_members else: assert all_members[k].metadata == v == extant_created[k].metadata # ensure that we left the root group as-is assert ( sync_group.get_node(store=store, path=group_path, zarr_format=zarr_format).attrs.asdict() == root_attrs ) @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("overwrite", [True, False]) def test_group_create_hierarchy_no_root( store: Store, zarr_format: ZarrFormat, overwrite: bool ) -> None: """ Test that the Group.create_hierarchy method will error if the dict provided contains a root. """ g = Group.from_store(store, zarr_format=zarr_format) tree = { "": GroupMetadata(zarr_format=zarr_format, attributes={"name": "a"}), } with pytest.raises( ValueError, match="It is an error to use this method to create a root node. " ): _ = dict(g.create_hierarchy(tree, overwrite=overwrite)) class TestParseHierarchyDict: """ Tests for the function that parses dicts of str : Metadata pairs, ensuring that the output models a valid Zarr hierarchy """ @staticmethod def test_normed_keys() -> None: """ Test that keys get normalized properly """ nodes = { "a": GroupMetadata(), "/b": GroupMetadata(), "": GroupMetadata(), "/a//c////": GroupMetadata(), } observed = _parse_hierarchy_dict(data=nodes) expected = {normalize_path(k): v for k, v in nodes.items()} assert observed == expected @staticmethod def test_empty() -> None: """ Test that an empty dict passes through """ assert _parse_hierarchy_dict(data={}) == {} @staticmethod def test_implicit_groups() -> None: """ Test that implicit groups were added as needed. """ requested = {"a/b/c": GroupMetadata()} expected = requested | { "": ImplicitGroupMarker(), "a": ImplicitGroupMarker(), "a/b": ImplicitGroupMarker(), } observed = _parse_hierarchy_dict(data=requested) assert observed == expected @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_group_create_hierarchy_invalid_mixed_zarr_format( store: Store, zarr_format: ZarrFormat ) -> None: """ Test that ``Group.create_hierarchy`` will raise an error if the zarr_format of the nodes is different from the parent group. """ other_format = 2 if zarr_format == 3 else 3 g = Group.from_store(store, zarr_format=other_format) tree = { "a": GroupMetadata(zarr_format=zarr_format, attributes={"name": "a"}), "a/b": meta_from_array(np.zeros(5), zarr_format=zarr_format, attributes={"name": "a/c"}), } msg = "The zarr_format of the nodes must be the same as the parent group." with pytest.raises(ValueError, match=msg): _ = tuple(g.create_hierarchy(tree)) @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("defect", ["array/array", "array/group"]) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_hierarchy_invalid_nested( impl: Literal["async", "sync"], store: Store, defect: tuple[str, str], zarr_format: ZarrFormat ) -> None: """ Test that create_hierarchy will not create a Zarr array that contains a Zarr group or Zarr array. """ if defect == "array/array": hierarchy_spec = { "array_0": meta_from_array(np.arange(3), zarr_format=zarr_format), "array_0/subarray": meta_from_array(np.arange(4), zarr_format=zarr_format), } elif defect == "array/group": hierarchy_spec = { "array_0": meta_from_array(np.arange(3), zarr_format=zarr_format), "array_0/subgroup": GroupMetadata(attributes={"foo": 10}, zarr_format=zarr_format), } msg = "Only Zarr groups can contain other nodes." if impl == "sync": with pytest.raises(ValueError, match=msg): tuple(sync_group.create_hierarchy(store=store, nodes=hierarchy_spec)) elif impl == "async": with pytest.raises(ValueError, match=msg): await _collect_aiterator(create_hierarchy(store=store, nodes=hierarchy_spec)) @pytest.mark.parametrize("store", ["memory"], indirect=True) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_hierarchy_invalid_mixed_format( impl: Literal["async", "sync"], store: Store ) -> None: """ Test that create_hierarchy will not create a Zarr group that contains a both Zarr v2 and Zarr v3 nodes. """ msg = ( "Got data with both Zarr v2 and Zarr v3 nodes, which is invalid. " "The following keys map to Zarr v2 nodes: ['v2']. " "The following keys map to Zarr v3 nodes: ['v3']." "Ensure that all nodes have the same Zarr format." ) nodes = { "v2": GroupMetadata(zarr_format=2), "v3": GroupMetadata(zarr_format=3), } if impl == "sync": with pytest.raises(ValueError, match=re.escape(msg)): tuple( sync_group.create_hierarchy( store=store, nodes=nodes, ) ) elif impl == "async": with pytest.raises(ValueError, match=re.escape(msg)): await _collect_aiterator( create_hierarchy( store=store, nodes=nodes, ) ) else: raise ValueError(f"Invalid impl: {impl}") @pytest.mark.parametrize("store", ["memory", "local"], indirect=True) @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.parametrize("root_key", ["", "root"]) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_rooted_hierarchy_group( impl: Literal["async", "sync"], store: Store, zarr_format, root_key: str ) -> None: """ Test that the _create_rooted_hierarchy can create a group. """ root_meta = {root_key: GroupMetadata(zarr_format=zarr_format, attributes={"path": root_key})} group_names = ["a", "a/b"] array_names = ["a/b/c", "a/b/d"] # just to ensure that we don't use the same name twice in tests assert set(group_names) & set(array_names) == set() groups_expected_meta = { _join_paths([root_key, node_name]): GroupMetadata( zarr_format=zarr_format, attributes={"path": node_name} ) for node_name in group_names } arrays_expected_meta = { _join_paths([root_key, node_name]): meta_from_array(np.zeros(4), zarr_format=zarr_format) for node_name in array_names } nodes_create = root_meta | groups_expected_meta | arrays_expected_meta if impl == "sync": g = sync_group.create_rooted_hierarchy(store=store, nodes=nodes_create) assert isinstance(g, Group) members = g.members(max_depth=None) elif impl == "async": g = await create_rooted_hierarchy(store=store, nodes=nodes_create) assert isinstance(g, AsyncGroup) members = await _collect_aiterator(g.members(max_depth=None)) else: raise ValueError(f"Unknown implementation: {impl}") assert g.metadata.attributes == {"path": root_key} members_observed_meta = {k: v.metadata for k, v in members} members_expected_meta_relative = { k.removeprefix(root_key).lstrip("/"): v for k, v in (groups_expected_meta | arrays_expected_meta).items() } assert members_observed_meta == members_expected_meta_relative @pytest.mark.parametrize("store", ["memory", "local"], indirect=True) @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.parametrize("root_key", ["", "root"]) @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_rooted_hierarchy_array( impl: Literal["async", "sync"], store: Store, zarr_format, root_key: str ) -> None: """ Test that _create_rooted_hierarchy can create an array. """ root_meta = { root_key: meta_from_array( np.arange(3), zarr_format=zarr_format, attributes={"path": root_key} ) } nodes_create = root_meta if impl == "sync": a = sync_group.create_rooted_hierarchy(store=store, nodes=nodes_create, overwrite=True) assert isinstance(a, Array) elif impl == "async": a = await create_rooted_hierarchy(store=store, nodes=nodes_create, overwrite=True) assert isinstance(a, AsyncArray) else: raise ValueError(f"Invalid impl: {impl}") assert a.metadata.attributes == {"path": root_key} @pytest.mark.parametrize("impl", ["async", "sync"]) async def test_create_rooted_hierarchy_invalid(impl: Literal["async", "sync"]) -> None: """ Ensure _create_rooted_hierarchy will raise a ValueError if the input does not contain a root node. """ zarr_format = 3 nodes = { "a": GroupMetadata(zarr_format=zarr_format), "b": GroupMetadata(zarr_format=zarr_format), } msg = "The input does not specify a root node. " if impl == "sync": with pytest.raises(ValueError, match=msg): sync_group.create_rooted_hierarchy(store=store, nodes=nodes) elif impl == "async": with pytest.raises(ValueError, match=msg): await create_rooted_hierarchy(store=store, nodes=nodes) else: raise ValueError(f"Invalid impl: {impl}") @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_group_members_performance(store: Store) -> None: """ Test that the execution time of Group.members is less than the number of members times the latency for accessing each member. """ get_latency = 0.1 # use the input store to create some groups group_create = zarr.group(store=store) num_groups = 10 # Create some groups for i in range(num_groups): group_create.create_group(f"group{i}") latency_store = LatencyStore(store, get_latency=get_latency) # create a group with some latency on get operations group_read = zarr.group(store=latency_store) # check how long it takes to iterate over the groups # if .members is sensitive to IO latency, # this should take (num_groups * get_latency) seconds # otherwise, it should take only marginally more than get_latency seconds start = time.time() _ = group_read.members() elapsed = time.time() - start assert elapsed < (num_groups * get_latency) @pytest.mark.parametrize("store", ["memory"], indirect=True) def test_group_members_concurrency_limit(store: MemoryStore) -> None: """ Test that the execution time of Group.members can be constrained by the async concurrency configuration setting. """ get_latency = 0.02 # use the input store to create some groups group_create = zarr.group(store=store) num_groups = 10 # Create some groups for i in range(num_groups): group_create.create_group(f"group{i}") latency_store = LatencyStore(store, get_latency=get_latency) # create a group with some latency on get operations group_read = zarr.group(store=latency_store) # check how long it takes to iterate over the groups # if .members is sensitive to IO latency, # this should take (num_groups * get_latency) seconds # otherwise, it should take only marginally more than get_latency seconds with zarr_config.set({"async.concurrency": 1}): start = time.time() _ = group_read.members() elapsed = time.time() - start assert elapsed > num_groups * get_latency @pytest.mark.parametrize("option", ["array", "group", "invalid"]) def test_build_metadata_v3(option: Literal["array", "group", "invalid"]) -> None: """ Test that _build_metadata_v3 returns the correct metadata for a v3 array or group """ match option: case "array": metadata_dict = meta_from_array(np.arange(10), zarr_format=3).to_dict() assert _build_metadata_v3(metadata_dict) == ArrayV3Metadata.from_dict(metadata_dict) case "group": metadata_dict = GroupMetadata(attributes={"foo": 10}, zarr_format=3).to_dict() assert _build_metadata_v3(metadata_dict) == GroupMetadata.from_dict(metadata_dict) case "invalid": metadata_dict = GroupMetadata(zarr_format=3).to_dict() metadata_dict.pop("node_type") # TODO: fix the error message msg = "Required key 'node_type' is missing from the provided metadata document." with pytest.raises(MetadataValidationError, match=msg): _build_metadata_v3(metadata_dict) @pytest.mark.parametrize("roots", [("",), ("a", "b")]) def test_get_roots(roots: tuple[str, ...]): root_nodes = {k: GroupMetadata(attributes={"name": k}) for k in roots} child_nodes = { _join_paths([k, "foo"]): GroupMetadata(attributes={"name": _join_paths([k, "foo"])}) for k in roots } data = root_nodes | child_nodes assert set(_get_roots(data)) == set(roots) def test_open_array_as_group(): z = zarr.create_array(shape=(40, 50), chunks=(10, 10), dtype="f8", store={}) with pytest.raises(ContainsArrayError): zarr.open_group(z.store) zarr-python-3.2.1/tests/test_indexing.py000066400000000000000000002161641517635743000204170ustar00rootroot00000000000000from __future__ import annotations import itertools from collections import Counter from typing import TYPE_CHECKING, Any from uuid import uuid4 import numpy as np import numpy.typing as npt import pytest from numpy.testing import assert_array_equal import zarr from zarr import Array from zarr.core.buffer import default_buffer_prototype from zarr.core.indexing import ( BasicSelection, CoordinateSelection, OrthogonalSelection, Selection, _ArrayIndexingOrder, _iter_grid, _iter_regions, ceildiv, make_slice_selection, normalize_integer_selection, oindex, oindex_set, replace_ellipsis, ) from zarr.registry import get_ndbuffer_class from zarr.storage import MemoryStore, StorePath if TYPE_CHECKING: from collections.abc import AsyncGenerator from zarr.abc.store import ByteRequest from zarr.core.buffer import BufferPrototype from zarr.core.buffer.core import Buffer @pytest.fixture async def store() -> AsyncGenerator[StorePath]: return StorePath(await MemoryStore.open()) def zarr_array_from_numpy_array( store: StorePath, a: npt.NDArray[Any], chunk_shape: tuple[int, ...] | None = None, ) -> zarr.Array: z = zarr.create_array( store=store / str(uuid4()), shape=a.shape, dtype=a.dtype, chunks=chunk_shape or a.shape, chunk_key_encoding={"name": "v2", "separator": "."}, ) z[()] = a return z class CountingDict(MemoryStore): counter: Counter[tuple[str, str]] @classmethod async def open(cls) -> CountingDict: store = await super().open() store.counter = Counter() return store async def get( self, key: str, prototype: BufferPrototype, byte_range: tuple[int | None, int | None] | None = None, ) -> Buffer | None: key_suffix = "/".join(key.split("/")[1:]) self.counter["__getitem__", key_suffix] += 1 return await super().get(key, prototype, byte_range) async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None: key_suffix = "/".join(key.split("/")[1:]) self.counter["__setitem__", key_suffix] += 1 return await super().set(key, value, byte_range) def get_sync( self, key: str, *, prototype: BufferPrototype | None = None, byte_range: ByteRequest | None = None, ) -> Buffer | None: key_suffix = "/".join(key.split("/")[1:]) self.counter["__getitem__", key_suffix] += 1 return super().get_sync(key, prototype=prototype, byte_range=byte_range) def set_sync(self, key: str, value: Buffer) -> None: key_suffix = "/".join(key.split("/")[1:]) self.counter["__setitem__", key_suffix] += 1 return super().set_sync(key, value) def test_normalize_integer_selection() -> None: assert 1 == normalize_integer_selection(1, 100) assert 99 == normalize_integer_selection(-1, 100) with pytest.raises(IndexError): normalize_integer_selection(100, 100) with pytest.raises(IndexError): normalize_integer_selection(1000, 100) with pytest.raises(IndexError): normalize_integer_selection(-1000, 100) def test_replace_ellipsis() -> None: # 1D, single item assert (0,) == replace_ellipsis(0, (100,)) # 1D assert (slice(None),) == replace_ellipsis(Ellipsis, (100,)) assert (slice(None),) == replace_ellipsis(slice(None), (100,)) assert (slice(None, 100),) == replace_ellipsis(slice(None, 100), (100,)) assert (slice(0, None),) == replace_ellipsis(slice(0, None), (100,)) assert (slice(None),) == replace_ellipsis((slice(None), Ellipsis), (100,)) assert (slice(None),) == replace_ellipsis((Ellipsis, slice(None)), (100,)) # 2D, single item assert (0, 0) == replace_ellipsis((0, 0), (100, 100)) assert (-1, 1) == replace_ellipsis((-1, 1), (100, 100)) # 2D, single col/row assert (0, slice(None)) == replace_ellipsis((0, slice(None)), (100, 100)) assert (0, slice(None)) == replace_ellipsis((0,), (100, 100)) assert (slice(None), 0) == replace_ellipsis((slice(None), 0), (100, 100)) # 2D slice assert (slice(None), slice(None)) == replace_ellipsis(Ellipsis, (100, 100)) assert (slice(None), slice(None)) == replace_ellipsis(slice(None), (100, 100)) assert (slice(None), slice(None)) == replace_ellipsis((slice(None), slice(None)), (100, 100)) assert (slice(None), slice(None)) == replace_ellipsis((Ellipsis, slice(None)), (100, 100)) assert (slice(None), slice(None)) == replace_ellipsis((slice(None), Ellipsis), (100, 100)) assert (slice(None), slice(None)) == replace_ellipsis( (slice(None), Ellipsis, slice(None)), (100, 100) ) assert (slice(None), slice(None)) == replace_ellipsis( (Ellipsis, slice(None), slice(None)), (100, 100) ) assert (slice(None), slice(None)) == replace_ellipsis( (slice(None), slice(None), Ellipsis), (100, 100) ) @pytest.mark.parametrize( ("value", "dtype"), [ (42, "uint8"), pytest.param( (b"aaa", 1, 4.2), [("foo", "S3"), ("bar", "i4"), ("baz", "f8")], marks=pytest.mark.xfail ), ], ) @pytest.mark.parametrize("use_out", [True, False]) def test_get_basic_selection_0d(store: StorePath, use_out: bool, value: Any, dtype: Any) -> None: # setup arr_np = np.array(value, dtype=dtype) arr_z = zarr_array_from_numpy_array(store, arr_np) assert_array_equal(arr_np, arr_z.get_basic_selection(Ellipsis)) assert_array_equal(arr_np, arr_z[...]) assert value == arr_z.get_basic_selection(()) assert value == arr_z[()] if use_out: # test out param b = default_buffer_prototype().nd_buffer.from_numpy_array(np.zeros_like(arr_np)) arr_z.get_basic_selection(Ellipsis, out=b) assert_array_equal(arr_np, b.as_ndarray_like()) # todo: uncomment the structured array tests when we can make them pass, # or delete them if we formally decide not to support structured dtypes. # test structured array # value = (b"aaa", 1, 4.2) # a = np.array(value, dtype=[("foo", "S3"), ("bar", "i4"), ("baz", "f8")]) # z = zarr_array_from_numpy_array(store, a) # z[()] = value # assert_array_equal(a, z.get_basic_selection(Ellipsis)) # assert_array_equal(a, z[...]) # assert a[()] == z.get_basic_selection(()) # assert a[()] == z[()] # assert b"aaa" == z.get_basic_selection((), fields="foo") # assert b"aaa" == z["foo"] # assert a[["foo", "bar"]] == z.get_basic_selection((), fields=["foo", "bar"]) # assert a[["foo", "bar"]] == z["foo", "bar"] # # test out param # b = NDBuffer.from_numpy_array(np.zeros_like(a)) # z.get_basic_selection(Ellipsis, out=b) # assert_array_equal(a, b) # c = NDBuffer.from_numpy_array(np.zeros_like(a[["foo", "bar"]])) # z.get_basic_selection(Ellipsis, out=c, fields=["foo", "bar"]) # assert_array_equal(a[["foo", "bar"]], c) basic_selections_1d: list[BasicSelection] = [ # single value 42, -1, # slices slice(0, 1050), slice(50, 150), slice(0, 2000), slice(-150, -50), slice(-2000, 2000), slice(0, 0), # empty result slice(-1, 0), # empty result # total selections slice(None), Ellipsis, (), (Ellipsis, slice(None)), # slice with step slice(None), slice(None, None), slice(None, None, 1), slice(None, None, 10), slice(None, None, 100), slice(None, None, 1000), slice(None, None, 10000), slice(0, 1050), slice(0, 1050, 1), slice(0, 1050, 10), slice(0, 1050, 100), slice(0, 1050, 1000), slice(0, 1050, 10000), slice(1, 31, 3), slice(1, 31, 30), slice(1, 31, 300), slice(81, 121, 3), slice(81, 121, 30), slice(81, 121, 300), slice(50, 150), slice(50, 150, 1), slice(50, 150, 10), ] basic_selections_1d_bad = [ # only positive step supported slice(None, None, -1), slice(None, None, -10), slice(None, None, -100), slice(None, None, -1000), slice(None, None, -10000), slice(1050, -1, -1), slice(1050, -1, -10), slice(1050, -1, -100), slice(1050, -1, -1000), slice(1050, -1, -10000), slice(1050, 0, -1), slice(1050, 0, -10), slice(1050, 0, -100), slice(1050, 0, -1000), slice(1050, 0, -10000), slice(150, 50, -1), slice(150, 50, -10), slice(31, 1, -3), slice(121, 81, -3), slice(-1, 0, -1), # bad stuff 2.3, "foo", b"xxx", None, (0, 0), (slice(None), slice(None)), ] def _test_get_basic_selection( a: npt.NDArray[Any] | Array, z: Array, selection: BasicSelection ) -> None: expect = a[selection] actual = z.get_basic_selection(selection) assert_array_equal(expect, actual) actual = z[selection] assert_array_equal(expect, actual) # test out param b = default_buffer_prototype().nd_buffer.from_numpy_array( np.empty(shape=expect.shape, dtype=expect.dtype) ) z.get_basic_selection(selection, out=b) assert_array_equal(expect, b.as_numpy_array()) # noinspection PyStatementEffect def test_get_basic_selection_1d(store: StorePath) -> None: # setup a = np.arange(1050, dtype=int) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) for selection in basic_selections_1d: _test_get_basic_selection(a, z, selection) for selection_bad in basic_selections_1d_bad: with pytest.raises(IndexError): z.get_basic_selection(selection_bad) # type: ignore[arg-type] with pytest.raises(IndexError): z[selection_bad] # type: ignore[index] with pytest.raises(IndexError): z.get_basic_selection([1, 0]) # type: ignore[arg-type] basic_selections_2d: list[BasicSelection] = [ # single row 42, -1, (42, slice(None)), (-1, slice(None)), # single col (slice(None), 4), (slice(None), -1), # row slices slice(None), slice(0, 1000), slice(250, 350), slice(0, 2000), slice(-350, -250), slice(0, 0), # empty result slice(-1, 0), # empty result slice(-2000, 0), slice(-2000, 2000), # 2D slices (slice(None), slice(1, 5)), (slice(250, 350), slice(None)), (slice(250, 350), slice(1, 5)), (slice(250, 350), slice(-5, -1)), (slice(250, 350), slice(-50, 50)), (slice(250, 350, 10), slice(1, 5)), (slice(250, 350), slice(1, 5, 2)), (slice(250, 350, 33), slice(1, 5, 3)), # total selections (slice(None), slice(None)), Ellipsis, (), (Ellipsis, slice(None)), (Ellipsis, slice(None), slice(None)), ] basic_selections_2d_bad = [ # bad stuff 2.3, "foo", b"xxx", None, (2.3, slice(None)), # only positive step supported slice(None, None, -1), (slice(None, None, -1), slice(None)), (0, 0, 0), (slice(None), slice(None), slice(None)), ] # noinspection PyStatementEffect def test_get_basic_selection_2d(store: StorePath) -> None: # setup a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) for selection in basic_selections_2d: _test_get_basic_selection(a, z, selection) bad_selections = basic_selections_2d_bad + [ # integer arrays [0, 1], (slice(None), [0, 1]), ] for selection_bad in bad_selections: with pytest.raises(IndexError): z.get_basic_selection(selection_bad) # type: ignore[arg-type] # check fallback on fancy indexing fancy_selection = ([0, 1], [0, 1]) np.testing.assert_array_equal(z[fancy_selection], [0, 11]) def test_fancy_indexing_fallback_on_get_setitem(store: StorePath) -> None: z = zarr_array_from_numpy_array(store, np.zeros((20, 20))) z[[1, 2, 3], [1, 2, 3]] = 1 np.testing.assert_array_equal( z[:4, :4], [ [0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], ], ) np.testing.assert_array_equal(z[[1, 2, 3], [1, 2, 3]], 1) # test broadcasting np.testing.assert_array_equal(z[1, [1, 2, 3]], [1, 0, 0]) # test 1D fancy indexing z2 = zarr_array_from_numpy_array(store, np.zeros(5)) z2[[1, 2, 3]] = 1 np.testing.assert_array_equal(z2[:], [0, 1, 1, 1, 0]) @pytest.mark.parametrize( ("index", "expected_result"), [ # Single iterable of integers ([0, 1], [[0, 1, 2], [3, 4, 5]]), # List first, then slice (([0, 1], slice(None)), [[0, 1, 2], [3, 4, 5]]), # List first, then slice (([0, 1], slice(1, None)), [[1, 2], [4, 5]]), # Slice first, then list ((slice(0, 2), [0, 2]), [[0, 2], [3, 5]]), # Slices only ((slice(0, 2), slice(0, 2)), [[0, 1], [3, 4]]), # List with repeated index (([1, 0, 1], slice(1, None)), [[4, 5], [1, 2], [4, 5]]), # 1D indexing (([1, 0, 1]), [[3, 4, 5], [0, 1, 2], [3, 4, 5]]), ], ) def test_orthogonal_indexing_fallback_on_getitem_2d( store: StorePath, index: Selection, expected_result: npt.ArrayLike ) -> None: """ Tests the orthogonal indexing fallback on __getitem__ for a 2D matrix. In addition to checking expected behavior, all indexing is also checked against numpy. """ # [0, 1, 2], # [3, 4, 5], # [6, 7, 8] a = np.arange(9).reshape(3, 3) z = zarr_array_from_numpy_array(store, a) np.testing.assert_array_equal(z[index], a[index], err_msg="Indexing disagrees with numpy") np.testing.assert_array_equal(z[index], expected_result) def test_setitem_zarr_array_as_value() -> None: # Regression test for https://github.com/zarr-developers/zarr-python/issues/3611 # Assigning a zarr Array as the value used to raise # SyncError("Calling sync() from within a running loop") because the codec # pipeline tried to index the zarr array inside an already-running async loop. src = zarr.array(np.arange(10), chunks=(5,)) dst = zarr.zeros(10, chunks=(5,), dtype=src.dtype) # Full assignment dst[:] = src assert_array_equal(dst[:], np.arange(10)) # Slice assignment dst2 = zarr.zeros(10, chunks=(5,), dtype=src.dtype) dst2[2:7] = src[2:7] assert_array_equal(dst2[2:7], np.arange(2, 7)) @pytest.mark.skip(reason="fails on ubuntu, windows; numpy=2.2; in CI") def test_setitem_repeated_index(): array = zarr.array(data=np.zeros((4,)), chunks=(1,)) indexer = np.array([-1, -1, 0, 0]) array.oindex[(indexer,)] = [0, 1, 2, 3] np.testing.assert_array_equal(array[:], np.array([3, 0, 0, 1])) indexer = np.array([-1, 0, 0, -1]) array.oindex[(indexer,)] = [0, 1, 2, 3] np.testing.assert_array_equal(array[:], np.array([2, 0, 0, 3])) Index = list[int] | tuple[slice | int | list[int], ...] @pytest.mark.parametrize( ("index", "expected_result"), [ # Single iterable of integers ([0, 1], [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], [15, 16, 17]]]), # One slice, two integers ((slice(0, 2), 1, 1), [4, 13]), # One integer, two slices ((slice(0, 2), 1, slice(0, 2)), [[3, 4], [12, 13]]), # Two slices and a list ((slice(0, 2), [1, 2], slice(0, 2)), [[[3, 4], [6, 7]], [[12, 13], [15, 16]]]), ], ) def test_orthogonal_indexing_fallback_on_getitem_3d( store: StorePath, index: Selection, expected_result: npt.ArrayLike ) -> None: """ Tests the orthogonal indexing fallback on __getitem__ for a 3D matrix. In addition to checking expected behavior, all indexing is also checked against numpy. """ # [[[ 0, 1, 2], # [ 3, 4, 5], # [ 6, 7, 8]], # [[ 9, 10, 11], # [12, 13, 14], # [15, 16, 17]], # [[18, 19, 20], # [21, 22, 23], # [24, 25, 26]]] a = np.arange(27).reshape(3, 3, 3) z = zarr_array_from_numpy_array(store, a) np.testing.assert_array_equal(z[index], a[index], err_msg="Indexing disagrees with numpy") np.testing.assert_array_equal(z[index], expected_result) @pytest.mark.parametrize( ("index", "expected_result"), [ # Single iterable of integers ([0, 1], [[1, 1, 1], [1, 1, 1], [0, 0, 0]]), # List and slice combined (([0, 1], slice(1, 3)), [[0, 1, 1], [0, 1, 1], [0, 0, 0]]), # Index repetition is ignored on setitem (([0, 1, 1, 1, 1, 1, 1], slice(1, 3)), [[0, 1, 1], [0, 1, 1], [0, 0, 0]]), # Slice with step (([0, 2], slice(None, None, 2)), [[1, 0, 1], [0, 0, 0], [1, 0, 1]]), ], ) def test_orthogonal_indexing_fallback_on_setitem_2d( store: StorePath, index: Selection, expected_result: npt.ArrayLike ) -> None: """ Tests the orthogonal indexing fallback on __setitem__ for a 3D matrix. In addition to checking expected behavior, all indexing is also checked against numpy. """ # Slice + fancy index a = np.zeros((3, 3)) z = zarr_array_from_numpy_array(store, a) z[index] = 1 a[index] = 1 np.testing.assert_array_equal(z[:], expected_result) np.testing.assert_array_equal(z[:], a, err_msg="Indexing disagrees with numpy") def test_fancy_indexing_doesnt_mix_with_implicit_slicing(store: StorePath) -> None: z2 = zarr_array_from_numpy_array(store, np.zeros((5, 5, 5))) with pytest.raises(IndexError): z2[[1, 2, 3], [1, 2, 3]] = 2 with pytest.raises(IndexError): np.testing.assert_array_equal(z2[[1, 2, 3], [1, 2, 3]], 0) with pytest.raises(IndexError): z2[..., [1, 2, 3]] = 2 # type: ignore[index] with pytest.raises(IndexError): np.testing.assert_array_equal(z2[..., [1, 2, 3]], 0) # type: ignore[index] @pytest.mark.parametrize( ("value", "dtype"), [ (42, "uint8"), pytest.param( (b"aaa", 1, 4.2), [("foo", "S3"), ("bar", "i4"), ("baz", "f8")], marks=pytest.mark.xfail ), ], ) def test_set_basic_selection_0d( store: StorePath, value: Any, dtype: str | list[tuple[str, str]] ) -> None: arr_np = np.array(value, dtype=dtype) arr_np_zeros = np.zeros_like(arr_np, dtype=dtype) arr_z = zarr_array_from_numpy_array(store, arr_np_zeros) assert_array_equal(arr_np_zeros, arr_z) arr_z.set_basic_selection(Ellipsis, value) assert_array_equal(value, arr_z) arr_z[...] = 0 assert_array_equal(arr_np_zeros, arr_z) arr_z[...] = value assert_array_equal(value, arr_z) # todo: uncomment the structured array tests when we can make them pass, # or delete them if we formally decide not to support structured dtypes. # arr_z.set_basic_selection(Ellipsis, v["foo"], fields="foo") # assert v["foo"] == arr_z["foo"] # assert arr_np_zeros["bar"] == arr_z["bar"] # assert arr_np_zeros["baz"] == arr_z["baz"] # arr_z["bar"] = v["bar"] # assert v["foo"] == arr_z["foo"] # assert v["bar"] == arr_z["bar"] # assert arr_np_zeros["baz"] == arr_z["baz"] # # multiple field assignment not supported # with pytest.raises(IndexError): # arr_z.set_basic_selection(Ellipsis, v[["foo", "bar"]], fields=["foo", "bar"]) # with pytest.raises(IndexError): # arr_z[..., "foo", "bar"] = v[["foo", "bar"]] def _test_get_orthogonal_selection( a: npt.NDArray[Any], z: Array, selection: OrthogonalSelection ) -> None: expect = oindex(a, selection) actual = z.get_orthogonal_selection(selection) assert_array_equal(expect, actual) actual = z.oindex[selection] assert_array_equal(expect, actual) # noinspection PyStatementEffect def test_get_orthogonal_selection_1d_bool(store: StorePath) -> None: # setup a = np.arange(1050, dtype=int) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.1, 0.01: ix = np.random.binomial(1, p, size=a.shape[0]).astype(bool) _test_get_orthogonal_selection(a, z, ix) # test errors with pytest.raises(IndexError): z.oindex[np.zeros(50, dtype=bool)] # too short with pytest.raises(IndexError): z.oindex[np.zeros(2000, dtype=bool)] # too long with pytest.raises(IndexError): # too many dimensions z.oindex[[[True, False], [False, True]]] # type: ignore[index] # noinspection PyStatementEffect def test_get_orthogonal_selection_1d_int(store: StorePath) -> None: # setup a = np.arange(550, dtype=int) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.01: # sorted integer arrays ix = np.random.choice(a.shape[0], size=int(a.shape[0] * p), replace=True) ix.sort() _test_get_orthogonal_selection(a, z, ix) selections = basic_selections_1d + [ # test wraparound [0, 3, 10, -23, -12, -1], # explicit test not sorted [3, 105, 23, 127], ] for selection in selections: _test_get_orthogonal_selection(a, z, selection) bad_selections = basic_selections_1d_bad + [ [a.shape[0] + 1], # out of bounds [-(a.shape[0] + 1)], # out of bounds [[2, 4], [6, 8]], # too many dimensions ] for bad_selection in bad_selections: with pytest.raises(IndexError): z.get_orthogonal_selection(bad_selection) # type: ignore[arg-type] with pytest.raises(IndexError): z.oindex[bad_selection] # type: ignore[index] def _test_get_orthogonal_selection_2d( a: npt.NDArray[Any], z: Array, ix0: npt.NDArray[np.bool], ix1: npt.NDArray[np.bool] ) -> None: selections = [ # index both axes with array (ix0, ix1), # mixed indexing with array / slice (ix0, slice(1, 5)), (ix0, slice(1, 5, 2)), (slice(250, 350), ix1), (slice(250, 350, 10), ix1), # mixed indexing with array / int (ix0, 4), (42, ix1), ] for selection in selections: _test_get_orthogonal_selection(a, z, selection) # noinspection PyStatementEffect def test_get_orthogonal_selection_2d(store: StorePath) -> None: # setup a = np.arange(5400, dtype=int).reshape(600, 9) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.01: # boolean arrays ix0 = np.random.binomial(1, p, size=a.shape[0]).astype(bool) ix1 = np.random.binomial(1, 0.5, size=a.shape[1]).astype(bool) _test_get_orthogonal_selection_2d(a, z, ix0, ix1) # mixed int array / bool array selections = ( (ix0, np.nonzero(ix1)[0]), (np.nonzero(ix0)[0], ix1), ) for selection in selections: _test_get_orthogonal_selection(a, z, selection) # sorted integer arrays ix0 = np.random.choice(a.shape[0], size=int(a.shape[0] * p), replace=True) ix1 = np.random.choice(a.shape[1], size=int(a.shape[1] * 0.5), replace=True) ix0.sort() ix1.sort() _test_get_orthogonal_selection_2d(a, z, ix0, ix1) for selection_2d in basic_selections_2d: _test_get_orthogonal_selection(a, z, selection_2d) for selection_2d_bad in basic_selections_2d_bad: with pytest.raises(IndexError): z.get_orthogonal_selection(selection_2d_bad) # type: ignore[arg-type] with pytest.raises(IndexError): z.oindex[selection_2d_bad] # type: ignore[index] def _test_get_orthogonal_selection_3d( a: npt.NDArray, z: Array, ix0: npt.NDArray[np.bool], ix1: npt.NDArray[np.bool], ix2: npt.NDArray[np.bool], ) -> None: selections = [ # single value (60, 15, 4), (-1, -1, -1), # index all axes with array (ix0, ix1, ix2), # mixed indexing with single array / slices (ix0, slice(10, 20), slice(1, 5)), (slice(30, 50), ix1, slice(1, 5)), (slice(30, 50), slice(10, 20), ix2), (ix0, slice(10, 20, 5), slice(1, 5, 2)), (slice(30, 50, 3), ix1, slice(1, 5, 2)), (slice(30, 50, 3), slice(10, 20, 5), ix2), # mixed indexing with single array / ints (ix0, 15, 4), (60, ix1, 4), (60, 15, ix2), # mixed indexing with single array / slice / int (ix0, slice(10, 20), 4), (15, ix1, slice(1, 5)), (slice(30, 50), 15, ix2), # mixed indexing with two array / slice (ix0, ix1, slice(1, 5)), (slice(30, 50), ix1, ix2), (ix0, slice(10, 20), ix2), # mixed indexing with two array / integer (ix0, ix1, 4), (15, ix1, ix2), (ix0, 15, ix2), ] for selection in selections: _test_get_orthogonal_selection(a, z, selection) def test_get_orthogonal_selection_3d(store: StorePath) -> None: # setup a = np.arange(32400, dtype=int).reshape(120, 30, 9) z = zarr_array_from_numpy_array(store, a, chunk_shape=(60, 20, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.01: # boolean arrays ix0 = np.random.binomial(1, p, size=a.shape[0]).astype(bool) ix1 = np.random.binomial(1, 0.5, size=a.shape[1]).astype(bool) ix2 = np.random.binomial(1, 0.5, size=a.shape[2]).astype(bool) _test_get_orthogonal_selection_3d(a, z, ix0, ix1, ix2) # sorted integer arrays ix0 = np.random.choice(a.shape[0], size=int(a.shape[0] * p), replace=True) ix1 = np.random.choice(a.shape[1], size=int(a.shape[1] * 0.5), replace=True) ix2 = np.random.choice(a.shape[2], size=int(a.shape[2] * 0.5), replace=True) ix0.sort() ix1.sort() ix2.sort() _test_get_orthogonal_selection_3d(a, z, ix0, ix1, ix2) def test_orthogonal_indexing_edge_cases(store: StorePath) -> None: a = np.arange(6).reshape(1, 2, 3) z = zarr_array_from_numpy_array(store, a, chunk_shape=(1, 2, 3)) expect = oindex(a, (0, slice(None), [0, 1, 2])) actual = z.oindex[0, :, [0, 1, 2]] assert_array_equal(expect, actual) expect = oindex(a, (0, slice(None), [True, True, True])) actual = z.oindex[0, :, [True, True, True]] assert_array_equal(expect, actual) def _test_set_orthogonal_selection( v: npt.NDArray[np.int_], a: npt.NDArray[Any], z: Array, selection: OrthogonalSelection ) -> None: for value in 42, oindex(v, selection), oindex(v, selection).tolist(): if isinstance(value, list) and value == []: # skip these cases as cannot preserve all dimensions continue # setup expectation a[:] = 0 oindex_set(a, selection, value) # long-form API z[:] = 0 z.set_orthogonal_selection(selection, value) assert_array_equal(a, z[:]) # short-form API z[:] = 0 z.oindex[selection] = value assert_array_equal(a, z[:]) def test_set_orthogonal_selection_1d(store: StorePath) -> None: # setup v = np.arange(550, dtype=int) a = np.empty(v.shape, dtype=int) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) # test with different degrees of sparseness np.random.seed(42) for p in 0.5, 0.01: # boolean arrays ix = np.random.binomial(1, p, size=a.shape[0]).astype(bool) _test_set_orthogonal_selection(v, a, z, ix) # sorted integer arrays ix = np.random.choice(a.shape[0], size=int(a.shape[0] * p), replace=True) ix.sort() _test_set_orthogonal_selection(v, a, z, ix) # basic selections for selection in basic_selections_1d: _test_set_orthogonal_selection(v, a, z, selection) def test_set_item_1d_last_two_chunks(store: StorePath): # regression test for GH2849 g = zarr.open_group(store=store, zarr_format=3, mode="w") a = g.create_array("bar", shape=(10,), chunks=(3,), dtype=int) data = np.array([7, 8, 9]) a[slice(7, 10)] = data np.testing.assert_array_equal(a[slice(7, 10)], data) z = zarr.open_group(store=store, mode="w") z.create_array("zoo", dtype=float, shape=()) z["zoo"][...] = np.array(1) # why doesn't [:] work? np.testing.assert_equal(z["zoo"][()], np.array(1)) z = zarr.open_group(store=store, mode="w") z.create_array("zoo", dtype=float, shape=()) z["zoo"][...] = 1 # why doesn't [:] work? np.testing.assert_equal(z["zoo"][()], np.array(1)) def _test_set_orthogonal_selection_2d( v: npt.NDArray[np.int_], a: npt.NDArray[np.int_], z: Array, ix0: npt.NDArray[np.bool], ix1: npt.NDArray[np.bool], ) -> None: selections = [ # index both axes with array (ix0, ix1), # mixed indexing with array / slice or int (ix0, slice(1, 5)), (slice(250, 350), ix1), (ix0, 4), (42, ix1), ] for selection in selections: _test_set_orthogonal_selection(v, a, z, selection) def test_set_orthogonal_selection_2d(store: StorePath) -> None: # setup v = np.arange(5400, dtype=int).reshape(600, 9) a = np.empty_like(v) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.01: # boolean arrays ix0 = np.random.binomial(1, p, size=a.shape[0]).astype(bool) ix1 = np.random.binomial(1, 0.5, size=a.shape[1]).astype(bool) _test_set_orthogonal_selection_2d(v, a, z, ix0, ix1) # sorted integer arrays ix0 = np.random.choice(a.shape[0], size=int(a.shape[0] * p), replace=True) ix1 = np.random.choice(a.shape[1], size=int(a.shape[1] * 0.5), replace=True) ix0.sort() ix1.sort() _test_set_orthogonal_selection_2d(v, a, z, ix0, ix1) for selection in basic_selections_2d: _test_set_orthogonal_selection(v, a, z, selection) def _test_set_orthogonal_selection_3d( v: npt.NDArray[np.int_], a: npt.NDArray[np.int_], z: Array, ix0: npt.NDArray[np.bool], ix1: npt.NDArray[np.bool], ix2: npt.NDArray[np.bool], ) -> None: selections = ( # single value (60, 15, 4), (-1, -1, -1), # index all axes with bool array (ix0, ix1, ix2), # mixed indexing with single bool array / slice or int (ix0, slice(10, 20), slice(1, 5)), (slice(30, 50), ix1, slice(1, 5)), (slice(30, 50), slice(10, 20), ix2), (ix0, 15, 4), (60, ix1, 4), (60, 15, ix2), (ix0, slice(10, 20), 4), (slice(30, 50), ix1, 4), (slice(30, 50), 15, ix2), # indexing with two arrays / slice (ix0, ix1, slice(1, 5)), # indexing with two arrays / integer (ix0, ix1, 4), ) for selection in selections: _test_set_orthogonal_selection(v, a, z, selection) def test_set_orthogonal_selection_3d(store: StorePath) -> None: # setup v = np.arange(32400, dtype=int).reshape(120, 30, 9) a = np.empty_like(v) z = zarr_array_from_numpy_array(store, a, chunk_shape=(60, 20, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.01: # boolean arrays ix0 = np.random.binomial(1, p, size=a.shape[0]).astype(bool) ix1 = np.random.binomial(1, 0.5, size=a.shape[1]).astype(bool) ix2 = np.random.binomial(1, 0.5, size=a.shape[2]).astype(bool) _test_set_orthogonal_selection_3d(v, a, z, ix0, ix1, ix2) # sorted integer arrays ix0 = np.random.choice(a.shape[0], size=int(a.shape[0] * p), replace=True) ix1 = np.random.choice(a.shape[1], size=int(a.shape[1] * 0.5), replace=True) ix2 = np.random.choice(a.shape[2], size=int(a.shape[2] * 0.5), replace=True) ix0.sort() ix1.sort() ix2.sort() _test_set_orthogonal_selection_3d(v, a, z, ix0, ix1, ix2) def test_orthogonal_indexing_fallback_on_get_setitem(store: StorePath) -> None: z = zarr_array_from_numpy_array(store, np.zeros((20, 20))) z[[1, 2, 3], [1, 2, 3]] = 1 np.testing.assert_array_equal( z[:4, :4], [ [0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], ], ) np.testing.assert_array_equal(z[[1, 2, 3], [1, 2, 3]], 1) # test broadcasting np.testing.assert_array_equal(z[1, [1, 2, 3]], [1, 0, 0]) # test 1D fancy indexing z2 = zarr_array_from_numpy_array(store, np.zeros(5)) z2[[1, 2, 3]] = 1 np.testing.assert_array_equal(z2[:], [0, 1, 1, 1, 0]) def _test_get_coordinate_selection( a: npt.NDArray, z: Array, selection: CoordinateSelection ) -> None: expect = a[selection] actual = z.get_coordinate_selection(selection) assert_array_equal(expect, actual) actual = z.vindex[selection] assert_array_equal(expect, actual) coordinate_selections_1d_bad = [ # slice not supported slice(5, 15), slice(None), Ellipsis, # bad stuff 2.3, "foo", b"xxx", None, (0, 0), (slice(None), slice(None)), ] # noinspection PyStatementEffect def test_get_coordinate_selection_1d(store: StorePath) -> None: # setup a = np.arange(1050, dtype=int) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) np.random.seed(42) # test with different degrees of sparseness for p in 2, 0.5, 0.1, 0.01: n = int(a.size * p) ix = np.random.choice(a.shape[0], size=n, replace=True) _test_get_coordinate_selection(a, z, ix) ix.sort() _test_get_coordinate_selection(a, z, ix) ix = ix[::-1] _test_get_coordinate_selection(a, z, ix) selections = [ # test single item 42, -1, # test wraparound [0, 3, 10, -23, -12, -1], # test out of order [3, 105, 23, 127], # not monotonically increasing # test multi-dimensional selection np.array([[2, 4], [6, 8]]), ] for selection in selections: _test_get_coordinate_selection(a, z, selection) # test errors bad_selections = coordinate_selections_1d_bad + [ [a.shape[0] + 1], # out of bounds [-(a.shape[0] + 1)], # out of bounds ] for selection in bad_selections: with pytest.raises(IndexError): z.get_coordinate_selection(selection) # type: ignore[arg-type] with pytest.raises(IndexError): z.vindex[selection] # type: ignore[index] def test_get_coordinate_selection_2d(store: StorePath) -> None: # setup a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) ix0: npt.ArrayLike ix1: npt.ArrayLike # test with different degrees of sparseness for p in 2, 0.5, 0.1, 0.01: n = int(a.size * p) ix0 = np.random.choice(a.shape[0], size=n, replace=True) ix1 = np.random.choice(a.shape[1], size=n, replace=True) selections = [ # single value (42, 4), (-1, -1), # index both axes with array (ix0, ix1), # mixed indexing with array / int (ix0, 4), (42, ix1), (42, 4), ] for selection in selections: _test_get_coordinate_selection(a, z, selection) # not monotonically increasing (first dim) ix0 = [3, 3, 4, 2, 5] ix1 = [1, 3, 5, 7, 9] _test_get_coordinate_selection(a, z, (ix0, ix1)) # not monotonically increasing (second dim) ix0 = [1, 1, 2, 2, 5] ix1 = [1, 3, 2, 1, 0] _test_get_coordinate_selection(a, z, (ix0, ix1)) # multi-dimensional selection ix0 = np.array([[1, 1, 2], [2, 2, 5]]) ix1 = np.array([[1, 3, 2], [1, 0, 0]]) _test_get_coordinate_selection(a, z, (ix0, ix1)) selection = slice(5, 15), [1, 2, 3] with pytest.raises(IndexError): z.get_coordinate_selection(selection) # type:ignore[arg-type] selection = [1, 2, 3], slice(5, 15) with pytest.raises(IndexError): z.get_coordinate_selection(selection) # type:ignore[arg-type] selection = Ellipsis, [1, 2, 3] with pytest.raises(IndexError): z.get_coordinate_selection(selection) # type:ignore[arg-type] selection = Ellipsis with pytest.raises(IndexError): z.get_coordinate_selection(selection) # type:ignore[arg-type] def _test_set_coordinate_selection( v: npt.NDArray, a: npt.NDArray, z: Array, selection: CoordinateSelection ) -> None: for value in 42, v[selection], v[selection].tolist(): # setup expectation a[:] = 0 a[selection] = value # test long-form API z[:] = 0 z.set_coordinate_selection(selection, value) assert_array_equal(a, z[:]) # test short-form API z[:] = 0 z.vindex[selection] = value assert_array_equal(a, z[:]) def test_set_coordinate_selection_1d(store: StorePath) -> None: # setup v = np.arange(550, dtype=int) a = np.empty(v.shape, dtype=v.dtype) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.01: n = int(a.size * p) ix = np.random.choice(a.shape[0], size=n, replace=True) _test_set_coordinate_selection(v, a, z, ix) # multi-dimensional selection ix = np.array([[2, 4], [6, 8]]) _test_set_coordinate_selection(v, a, z, ix) for selection in coordinate_selections_1d_bad: with pytest.raises(IndexError): z.set_coordinate_selection(selection, 42) # type:ignore[arg-type] with pytest.raises(IndexError): z.vindex[selection] = 42 # type:ignore[index] def test_set_coordinate_selection_2d(store: StorePath) -> None: # setup v = np.arange(5400, dtype=int).reshape(600, 9) a = np.empty_like(v) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.01: n = int(a.size * p) ix0 = np.random.choice(a.shape[0], size=n, replace=True) ix1 = np.random.choice(a.shape[1], size=n, replace=True) selections = ( (42, 4), (-1, -1), # index both axes with array (ix0, ix1), # mixed indexing with array / int (ix0, 4), (42, ix1), ) for selection in selections: _test_set_coordinate_selection(v, a, z, selection) # multi-dimensional selection ix0 = np.array([[1, 2, 3], [4, 5, 6]]) ix1 = np.array([[1, 3, 2], [2, 0, 5]]) _test_set_coordinate_selection(v, a, z, (ix0, ix1)) def _test_get_block_selection( a: npt.NDArray[Any], z: Array, selection: BasicSelection, expected_idx: slice | tuple[slice, ...], ) -> None: expect = a[expected_idx] actual = z.get_block_selection(selection) assert_array_equal(expect, actual) actual = z.blocks[selection] assert_array_equal(expect, actual) block_selections_1d: list[BasicSelection] = [ # test single item 0, 5, # test wraparound -1, -4, # test slice slice(5), slice(None, 3), slice(5, 6), slice(-3, -1), slice(None), # Full slice ] block_selections_1d_array_projection: list[slice] = [ # test single item slice(100), slice(500, 600), # test wraparound slice(1000, None), slice(700, 800), # test slice slice(500), slice(None, 300), slice(500, 600), slice(800, 1000), slice(None), ] block_selections_1d_bad = [ # slice not supported slice(3, 8, 2), # bad stuff 2.3, # "foo", # TODO b"xxx", None, (0, 0), (slice(None), slice(None)), [0, 5, 3], ] def test_get_block_selection_1d(store: StorePath) -> None: # setup a = np.arange(1050, dtype=int) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) for selection, expected_idx in zip( block_selections_1d, block_selections_1d_array_projection, strict=True ): _test_get_block_selection(a, z, selection, expected_idx) bad_selections = block_selections_1d_bad + [ z._chunk_grid.get_nchunks() + 1, # out of bounds -(z._chunk_grid.get_nchunks() + 1), # out of bounds ] for selection_bad in bad_selections: with pytest.raises(IndexError): z.get_block_selection(selection_bad) # type:ignore[arg-type] with pytest.raises(IndexError): z.blocks[selection_bad] # type:ignore[index] block_selections_2d: list[BasicSelection] = [ # test single item (0, 0), (1, 2), # test wraparound (-1, -1), (-3, -2), # test slice (slice(1), slice(2)), (slice(None, 2), slice(-2, -1)), (slice(2, 3), slice(-2, None)), (slice(-3, -1), slice(-3, -2)), (slice(None), slice(None)), # Full slice ] block_selections_2d_array_projection: list[tuple[slice, slice]] = [ # test single item (slice(300), slice(3)), (slice(300, 600), slice(6, 9)), # test wraparound (slice(900, None), slice(9, None)), (slice(300, 600), slice(6, 9)), # test slice (slice(300), slice(6)), (slice(None, 600), slice(6, 9)), (slice(600, 900), slice(6, None)), (slice(300, 900), slice(3, 6)), (slice(None), slice(None)), # Full slice ] def test_get_block_selection_2d(store: StorePath) -> None: # setup a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) for selection, expected_idx in zip( block_selections_2d, block_selections_2d_array_projection, strict=True ): _test_get_block_selection(a, z, selection, expected_idx) selection = slice(5, 15), [1, 2, 3] with pytest.raises(IndexError): z.get_block_selection(selection) selection = Ellipsis, [1, 2, 3] with pytest.raises(IndexError): z.get_block_selection(selection) selection = slice(15, 20), slice(None) with pytest.raises(IndexError): # out of bounds z.get_block_selection(selection) def _test_set_block_selection( v: npt.NDArray[Any], a: npt.NDArray[Any], z: zarr.Array, selection: BasicSelection, expected_idx: slice, ) -> None: for value in 42, v[expected_idx], v[expected_idx].tolist(): # setup expectation a[:] = 0 a[expected_idx] = value # test long-form API z[:] = 0 z.set_block_selection(selection, value) assert_array_equal(a, z[:]) # test short-form API z[:] = 0 z.blocks[selection] = value assert_array_equal(a, z[:]) def test_set_block_selection_1d(store: StorePath) -> None: # setup v = np.arange(1050, dtype=int) a = np.empty(v.shape, dtype=v.dtype) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) for selection, expected_idx in zip( block_selections_1d, block_selections_1d_array_projection, strict=True ): _test_set_block_selection(v, a, z, selection, expected_idx) for selection_bad in block_selections_1d_bad: with pytest.raises(IndexError): z.set_block_selection(selection_bad, 42) # type:ignore[arg-type] with pytest.raises(IndexError): z.blocks[selection_bad] = 42 # type:ignore[index] def test_set_block_selection_2d(store: StorePath) -> None: # setup v = np.arange(10000, dtype=int).reshape(1000, 10) a = np.empty(v.shape, dtype=v.dtype) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) for selection, expected_idx in zip( block_selections_2d, block_selections_2d_array_projection, strict=True ): _test_set_block_selection(v, a, z, selection, expected_idx) selection = slice(5, 15), [1, 2, 3] with pytest.raises(IndexError): z.set_block_selection(selection, 42) selection = Ellipsis, [1, 2, 3] with pytest.raises(IndexError): z.set_block_selection(selection, 42) selection = slice(15, 20), slice(None) with pytest.raises(IndexError): # out of bounds z.set_block_selection(selection, 42) def _test_get_mask_selection(a: npt.NDArray[Any], z: Array, selection: npt.NDArray) -> None: expect = a[selection] actual = z.get_mask_selection(selection) assert_array_equal(expect, actual) actual = z.vindex[selection] assert_array_equal(expect, actual) actual = z[selection] assert_array_equal(expect, actual) mask_selections_1d_bad = [ # slice not supported slice(5, 15), slice(None), Ellipsis, # bad stuff 2.3, "foo", b"xxx", None, (0, 0), (slice(None), slice(None)), ] # noinspection PyStatementEffect def test_get_mask_selection_1d(store: StorePath) -> None: # setup a = np.arange(1050, dtype=int) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.1, 0.01: ix = np.random.binomial(1, p, size=a.shape[0]).astype(bool) _test_get_mask_selection(a, z, ix) # test errors bad_selections = mask_selections_1d_bad + [ np.zeros(50, dtype=bool), # too short np.zeros(2000, dtype=bool), # too long [[True, False], [False, True]], # too many dimensions ] for selection in bad_selections: with pytest.raises(IndexError): z.get_mask_selection(selection) # type: ignore[arg-type] with pytest.raises(IndexError): z.vindex[selection] # type:ignore[index] # noinspection PyStatementEffect def test_get_mask_selection_2d(store: StorePath) -> None: # setup a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.1, 0.01: ix = np.random.binomial(1, p, size=a.size).astype(bool).reshape(a.shape) _test_get_mask_selection(a, z, ix) # test errors with pytest.raises(IndexError): z.vindex[np.zeros((1000, 5), dtype=bool)] # too short with pytest.raises(IndexError): z.vindex[np.zeros((2000, 10), dtype=bool)] # too long with pytest.raises(IndexError): z.vindex[[True, False]] # wrong no. dimensions def _test_set_mask_selection( v: npt.NDArray, a: npt.NDArray, z: Array, selection: npt.NDArray ) -> None: a[:] = 0 z[:] = 0 a[selection] = v[selection] z.set_mask_selection(selection, v[selection]) assert_array_equal(a, z[:]) z[:] = 0 z.vindex[selection] = v[selection] assert_array_equal(a, z[:]) z[:] = 0 z[selection] = v[selection] assert_array_equal(a, z[:]) def test_set_mask_selection_1d(store: StorePath) -> None: # setup v = np.arange(1050, dtype=int) a = np.empty_like(v) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.1, 0.01: ix = np.random.binomial(1, p, size=a.shape[0]).astype(bool) _test_set_mask_selection(v, a, z, ix) for selection in mask_selections_1d_bad: with pytest.raises(IndexError): z.set_mask_selection(selection, 42) # type: ignore[arg-type] with pytest.raises(IndexError): z.vindex[selection] = 42 # type: ignore[index] def test_set_mask_selection_2d(store: StorePath) -> None: # setup v = np.arange(10000, dtype=int).reshape(1000, 10) a = np.empty_like(v) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.1, 0.01: ix = np.random.binomial(1, p, size=a.size).astype(bool).reshape(a.shape) _test_set_mask_selection(v, a, z, ix) def test_get_selection_out(store: StorePath) -> None: # basic selections a = np.arange(1050) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) selections = [ slice(50, 150), slice(0, 1050), slice(1, 2), ] for selection in selections: expect = a[selection] out = get_ndbuffer_class().from_numpy_array(np.empty(expect.shape)) z.get_basic_selection(selection, out=out) assert_array_equal(expect, out.as_numpy_array()[:]) with pytest.raises(TypeError): z.get_basic_selection(Ellipsis, out=[]) # type: ignore[arg-type] # orthogonal selections a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.1, 0.01: ix0 = np.random.binomial(1, p, size=a.shape[0]).astype(bool) ix1 = np.random.binomial(1, 0.5, size=a.shape[1]).astype(bool) selections = [ # index both axes with array (ix0, ix1), # mixed indexing with array / slice (ix0, slice(1, 5)), (slice(250, 350), ix1), # mixed indexing with array / int (ix0, 4), (42, ix1), # mixed int array / bool array (ix0, np.nonzero(ix1)[0]), (np.nonzero(ix0)[0], ix1), ] for selection in selections: expect = oindex(a, selection) out = get_ndbuffer_class().from_numpy_array(np.zeros(expect.shape, dtype=expect.dtype)) z.get_orthogonal_selection(selection, out=out) assert_array_equal(expect, out.as_numpy_array()[:]) # coordinate selections a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) np.random.seed(42) # test with different degrees of sparseness for p in 0.5, 0.1, 0.01: n = int(a.size * p) ix0 = np.random.choice(a.shape[0], size=n, replace=True) ix1 = np.random.choice(a.shape[1], size=n, replace=True) selections = [ # index both axes with array (ix0, ix1), # mixed indexing with array / int (ix0, 4), (42, ix1), ] for selection in selections: expect = a[selection] out = get_ndbuffer_class().from_numpy_array(np.zeros(expect.shape, dtype=expect.dtype)) z.get_coordinate_selection(selection, out=out) assert_array_equal(expect, out.as_numpy_array()[:]) @pytest.mark.xfail(reason="fields are not supported in v3") def test_get_selections_with_fields(store: StorePath) -> None: a = np.array( [("aaa", 1, 4.2), ("bbb", 2, 8.4), ("ccc", 3, 12.6)], dtype=[("foo", "S3"), ("bar", "i4"), ("baz", "f8")], ) z = zarr_array_from_numpy_array(store, a, chunk_shape=(2,)) fields_fixture: list[str | list[str]] = [ "foo", ["foo"], ["foo", "bar"], ["foo", "baz"], ["bar", "baz"], ["foo", "bar", "baz"], ["bar", "foo"], ["baz", "bar", "foo"], ] for fields in fields_fixture: # total selection expect = a[fields] actual = z.get_basic_selection(Ellipsis, fields=fields) assert_array_equal(expect, actual) # alternative API if isinstance(fields, str): actual = z[fields] assert_array_equal(expect, actual) elif len(fields) == 2: actual = z[fields[0], fields[1]] assert_array_equal(expect, actual) if isinstance(fields, str): actual = z[..., fields] assert_array_equal(expect, actual) elif len(fields) == 2: actual = z[..., fields[0], fields[1]] assert_array_equal(expect, actual) # basic selection with slice expect = a[fields][0:2] actual = z.get_basic_selection(slice(0, 2), fields=fields) assert_array_equal(expect, actual) # alternative API if isinstance(fields, str): actual = z[0:2, fields] assert_array_equal(expect, actual) elif len(fields) == 2: actual = z[0:2, fields[0], fields[1]] assert_array_equal(expect, actual) # basic selection with single item expect = a[fields][1] actual = z.get_basic_selection(1, fields=fields) assert_array_equal(expect, actual) # alternative API if isinstance(fields, str): actual = z[1, fields] assert_array_equal(expect, actual) elif len(fields) == 2: actual = z[1, fields[0], fields[1]] assert_array_equal(expect, actual) # orthogonal selection ix = [0, 2] expect = a[fields][ix] actual = z.get_orthogonal_selection(ix, fields=fields) assert_array_equal(expect, actual) # alternative API if isinstance(fields, str): actual = z.oindex[ix, fields] assert_array_equal(expect, actual) elif len(fields) == 2: actual = z.oindex[ix, fields[0], fields[1]] assert_array_equal(expect, actual) # coordinate selection ix = [0, 2] expect = a[fields][ix] actual = z.get_coordinate_selection(ix, fields=fields) assert_array_equal(expect, actual) # alternative API if isinstance(fields, str): actual = z.vindex[ix, fields] assert_array_equal(expect, actual) elif len(fields) == 2: actual = z.vindex[ix, fields[0], fields[1]] assert_array_equal(expect, actual) # mask selection ix = [True, False, True] expect = a[fields][ix] actual = z.get_mask_selection(ix, fields=fields) assert_array_equal(expect, actual) # alternative API if isinstance(fields, str): actual = z.vindex[ix, fields] assert_array_equal(expect, actual) elif len(fields) == 2: actual = z.vindex[ix, fields[0], fields[1]] assert_array_equal(expect, actual) # missing/bad fields with pytest.raises(IndexError): z.get_basic_selection(Ellipsis, fields=["notafield"]) with pytest.raises(IndexError): z.get_basic_selection(Ellipsis, fields=slice(None)) # type: ignore[arg-type] @pytest.mark.xfail(reason="fields are not supported in v3") def test_set_selections_with_fields(store: StorePath) -> None: v = np.array( [("aaa", 1, 4.2), ("bbb", 2, 8.4), ("ccc", 3, 12.6)], dtype=[("foo", "S3"), ("bar", "i4"), ("baz", "f8")], ) a = np.empty_like(v) z = zarr_array_from_numpy_array(store, v, chunk_shape=(2,)) fields_fixture: list[str | list[str]] = [ "foo", [], ["foo"], ["foo", "bar"], ["foo", "baz"], ["bar", "baz"], ["foo", "bar", "baz"], ["bar", "foo"], ["baz", "bar", "foo"], ] for fields in fields_fixture: # currently multi-field assignment is not supported in numpy, so we won't support # it either if isinstance(fields, list) and len(fields) > 1: with pytest.raises(IndexError): z.set_basic_selection(Ellipsis, v, fields=fields) with pytest.raises(IndexError): z.set_orthogonal_selection([0, 2], v, fields=fields) # type: ignore[arg-type] with pytest.raises(IndexError): z.set_coordinate_selection([0, 2], v, fields=fields) with pytest.raises(IndexError): z.set_mask_selection([True, False, True], v, fields=fields) # type: ignore[arg-type] else: if isinstance(fields, list) and len(fields) == 1: # work around numpy does not support multi-field assignment even if there # is only one field key = fields[0] elif isinstance(fields, list) and len(fields) == 0: # work around numpy ambiguity about what is a field selection key = Ellipsis else: key = fields # setup expectation a[:] = ("", 0, 0) z[:] = ("", 0, 0) assert_array_equal(a, z[:]) a[key] = v[key] # total selection z.set_basic_selection(Ellipsis, v[key], fields=fields) assert_array_equal(a, z[:]) # basic selection with slice a[:] = ("", 0, 0) z[:] = ("", 0, 0) a[key][0:2] = v[key][0:2] z.set_basic_selection(slice(0, 2), v[key][0:2], fields=fields) assert_array_equal(a, z[:]) # orthogonal selection a[:] = ("", 0, 0) z[:] = ("", 0, 0) ix = [0, 2] a[key][ix] = v[key][ix] z.set_orthogonal_selection(ix, v[key][ix], fields=fields) assert_array_equal(a, z[:]) # coordinate selection a[:] = ("", 0, 0) z[:] = ("", 0, 0) ix = [0, 2] a[key][ix] = v[key][ix] z.set_coordinate_selection(ix, v[key][ix], fields=fields) assert_array_equal(a, z[:]) # mask selection a[:] = ("", 0, 0) z[:] = ("", 0, 0) ix = [True, False, True] a[key][ix] = v[key][ix] z.set_mask_selection(ix, v[key][ix], fields=fields) assert_array_equal(a, z[:]) def test_slice_selection_uints() -> None: arr = np.arange(24).reshape((4, 6)) idx = np.uint64(3) slice_sel = make_slice_selection((idx,)) assert arr[tuple(slice_sel)].shape == (1, 6) def test_numpy_int_indexing(store: StorePath) -> None: a = np.arange(1050) z = zarr_array_from_numpy_array(store, a, chunk_shape=(100,)) assert a[42] == z[42] assert a[np.int64(42)] == z[np.int64(42)] @pytest.mark.parametrize( ("shape", "chunks", "ops"), [ # 1D test cases ((1070,), (50,), [("__getitem__", (slice(200, 400),))]), ((1070,), (50,), [("__getitem__", (slice(200, 400, 100),))]), ( (1070,), (50,), [ ("__getitem__", (slice(200, 400),)), ("__setitem__", (slice(200, 400, 100),)), ], ), # 2D test cases ( (40, 50), (5, 8), [ ("__getitem__", (slice(6, 37, 13), (slice(4, 10)))), ("__setitem__", (slice(None), (slice(None)))), ], ), ], ) async def test_accessed_chunks( shape: tuple[int, ...], chunks: tuple[int, ...], ops: list[tuple[str, tuple[slice, ...]]] ) -> None: # Test that only the required chunks are accessed during basic selection operations # shape: array shape # chunks: chunk size # ops: list of tuples with (optype, tuple of slices) # optype = "__getitem__" or "__setitem__", tuple length must match number of dims # Use a counting dict as the backing store so we can track the items access store = await CountingDict.open() z = zarr_array_from_numpy_array(StorePath(store), np.zeros(shape), chunk_shape=chunks) for ii, (optype, slices) in enumerate(ops): # Resolve the slices into the accessed chunks for each dimension chunks_per_dim = [] for N, C, sl in zip(shape, chunks, slices, strict=True): chunk_ind = np.arange(N, dtype=int)[sl] // C chunks_per_dim.append(np.unique(chunk_ind)) # Combine and generate the cartesian product to determine the chunks keys that # will be accessed chunks_accessed = [".".join(map(str, comb)) for comb in itertools.product(*chunks_per_dim)] counts_before = store.counter.copy() # Perform the operation if optype == "__getitem__": z[slices] else: z[slices] = ii # Get the change in counts delta_counts = store.counter - counts_before # Check that the access counts for the operation have increased by one for all # the chunks we expect to be included for ci in chunks_accessed: assert delta_counts.pop((optype, ci)) == 1 # If the chunk was partially written to it will also have been read once. We # don't determine if the chunk was actually partial here, just that the # counts are consistent that this might have happened if optype == "__setitem__": assert ("__getitem__", ci) not in delta_counts or delta_counts.pop( ("__getitem__", ci) ) == 1 # Check that no other chunks were accessed assert len(delta_counts) == 0 @pytest.mark.parametrize( "selection", [ # basic selection [...], [1, ...], [slice(None)], [1, 3], [[1, 2, 3], 9], [np.arange(1000)], [slice(5, 15)], [slice(2, 4), 4], [[1, 3]], # mask selection [np.tile([True, False], (1000, 5))], [np.full((1000, 10), False)], # coordinate selection [[1, 2, 3, 4], [5, 6, 7, 8]], [[100, 200, 300], [4, 5, 6]], ], ) def test_indexing_equals_numpy(store: StorePath, selection: Selection) -> None: a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) # note: in python 3.10 a[*selection] is not valid unpacking syntax expected = a[*selection,] actual = z[*selection,] assert_array_equal(expected, actual, err_msg=f"selection: {selection}") @pytest.mark.parametrize( "selection", [ [np.tile([True, False], 500), np.tile([True, False], 5)], [np.full(1000, False), np.tile([True, False], 5)], [np.full(1000, True), np.full(10, True)], [np.full(1000, True), [True, False] * 5], ], ) def test_orthogonal_bool_indexing_like_numpy_ix( store: StorePath, selection: list[npt.ArrayLike] ) -> None: a = np.arange(10000, dtype=int).reshape(1000, 10) z = zarr_array_from_numpy_array(store, a, chunk_shape=(300, 3)) expected = a[np.ix_(*selection)] # note: in python 3.10 z[*selection] is not valid unpacking syntax actual = z[*selection,] assert_array_equal(expected, actual, err_msg=f"{selection=}") @pytest.mark.parametrize("ndim", [1, 2, 3]) @pytest.mark.parametrize("origin_0d", [None, (0,), (1,)]) @pytest.mark.parametrize("selection_shape_0d", [None, (2,), (3,)]) def test_iter_grid( ndim: int, origin_0d: tuple[int] | None, selection_shape_0d: tuple[int] | None ) -> None: """ Test that iter_grid works as expected for 1, 2, and 3 dimensions. """ grid_shape = (10, 5, 7)[:ndim] if origin_0d is not None: origin_kwarg = origin_0d * ndim origin = origin_kwarg else: origin_kwarg = None origin = (0,) * ndim if selection_shape_0d is not None: selection_shape_kwarg = selection_shape_0d * ndim selection_shape = selection_shape_kwarg else: selection_shape_kwarg = None selection_shape = tuple(gs - o for gs, o in zip(grid_shape, origin, strict=False)) observed = tuple( _iter_grid(grid_shape, origin=origin_kwarg, selection_shape=selection_shape_kwarg) ) # generate a numpy array of indices, and index it coord_array = np.array(list(itertools.product(*[range(s) for s in grid_shape]))).reshape( (*grid_shape, ndim) ) coord_array_indexed = coord_array[ tuple(slice(o, o + s, 1) for o, s in zip(origin, selection_shape, strict=False)) + (range(ndim),) ] expected = tuple(map(tuple, coord_array_indexed.reshape(-1, ndim).tolist())) assert observed == expected def test_iter_grid_invalid() -> None: """ Ensure that a selection_shape that exceeds the grid_shape + origin produces an indexing error. """ with pytest.raises(IndexError): list(_iter_grid((5,), origin=(0,), selection_shape=(10,))) def test_indexing_with_zarr_array(store: StorePath) -> None: # regression test for https://github.com/zarr-developers/zarr-python/issues/2133 a = np.arange(10) za = zarr.array(a, chunks=2, store=store, path="a") ix = [False, True, False, True, False, True, False, True, False, True] ii = [0, 2, 4, 5] zix = zarr.array(ix, chunks=2, store=store, dtype="bool", path="ix") zii = zarr.array(ii, chunks=2, store=store, dtype="i4", path="ii") assert_array_equal(a[ix], za[zix]) assert_array_equal(a[ix], za.oindex[zix]) assert_array_equal(a[ix], za.vindex[zix]) assert_array_equal(a[ii], za[zii]) assert_array_equal(a[ii], za.oindex[zii]) @pytest.mark.parametrize("store", ["local", "memory"], indirect=["store"]) @pytest.mark.parametrize("shape", [(0, 2, 3), (0,), (3, 0)]) def test_zero_sized_chunks(store: StorePath, shape: list[int]) -> None: # Chunk sizes must be >= 1 per spec; use 1 for zero-extent dimensions. chunks = tuple(max(1, s) for s in shape) z = zarr.create_array(store=store, shape=shape, chunks=chunks, zarr_format=3, dtype="f8") z[...] = 42 assert_array_equal(z[...], np.zeros(shape, dtype="f8")) @pytest.mark.parametrize("store", ["memory"], indirect=["store"]) def test_vectorized_indexing_incompatible_shape(store) -> None: # GH2469 shape = (4, 4) chunks = (2, 2) fill_value = 32767 arr = zarr.create( shape, store=store, chunks=chunks, dtype=np.int16, fill_value=fill_value, codecs=[zarr.codecs.BytesCodec(), zarr.codecs.BloscCodec()], ) with pytest.raises(ValueError, match="Attempting to set"): arr[np.array([1, 2]), np.array([1, 2])] = np.array([[-1, -2], [-3, -4]]) def test_iter_chunk_regions(): chunks = (2, 3) a = zarr.create((10, 10), chunks=chunks) a[:] = 1 for region in a._iter_chunk_regions(): assert_array_equal(a[region], np.ones_like(a[region])) a[region] = 0 assert_array_equal(a[region], np.zeros_like(a[region])) @pytest.mark.parametrize( ("domain_shape", "region_shape", "origin", "selection_shape"), [ ((9,), (1,), None, (9,)), ((9,), (1,), (0,), (9,)), ((3,), (2,), (0,), (1,)), ((9,), (2,), (2,), (2,)), ((9, 9), (2, 1), None, None), ((9, 9), (4, 1), None, None), ], ) @pytest.mark.parametrize("order", ["lexicographic"]) @pytest.mark.parametrize("trim_excess", [True, False]) def test_iter_regions( domain_shape: tuple[int, ...], region_shape: tuple[int, ...], origin: tuple[int, ...] | None, selection_shape: tuple[int, ...] | None, order: _ArrayIndexingOrder, trim_excess: bool, ) -> None: """ Test that iter_regions properly iterates over contiguous regions of a gridded domain. """ expected_slices_by_dim: list[list[slice]] = [] origin_parsed: tuple[int, ...] selection_shape_parsed: tuple[int, ...] if origin is None: origin_parsed = (0,) * len(domain_shape) else: origin_parsed = origin if selection_shape is None: selection_shape_parsed = tuple( ceildiv(ds, rs) - o for ds, o, rs in zip(domain_shape, origin_parsed, region_shape, strict=True) ) else: selection_shape_parsed = selection_shape for d_s, r_s, o, ss in zip( domain_shape, region_shape, origin_parsed, selection_shape_parsed, strict=True ): _expected_slices: list[slice] = [] start = o * r_s for incr in range(start, start + ss * r_s, r_s): if trim_excess: term = min(incr + r_s, d_s) else: term = incr + r_s _expected_slices.append(slice(incr, term, 1)) expected_slices_by_dim.append(_expected_slices) expected = tuple(itertools.product(*expected_slices_by_dim)) observed = tuple( _iter_regions( domain_shape, region_shape, origin=origin, selection_shape=selection_shape, order=order, trim_excess=trim_excess, ) ) assert observed == expected class TestAsync: @pytest.mark.parametrize( ("indexer", "expected"), [ # int ((0,), np.array([1, 2])), ((1,), np.array([3, 4])), ((0, 1), np.array(2)), # slice ((slice(None),), np.array([[1, 2], [3, 4]])), ((slice(0, 1),), np.array([[1, 2]])), ((slice(1, 2),), np.array([[3, 4]])), ((slice(0, 2),), np.array([[1, 2], [3, 4]])), ((slice(0, 0),), np.empty(shape=(0, 2), dtype="i8")), # ellipsis ((...,), np.array([[1, 2], [3, 4]])), ((0, ...), np.array([1, 2])), ((..., 0), np.array([1, 3])), ((0, 1, ...), np.array(2)), # combined ((0, slice(None)), np.array([1, 2])), ((slice(None), 0), np.array([1, 3])), ((slice(None), slice(None)), np.array([[1, 2], [3, 4]])), # array of ints (([0]), np.array([[1, 2]])), (([1]), np.array([[3, 4]])), (([0], [1]), np.array(2)), (([0, 1], [0]), np.array([[1], [3]])), (([0, 1], [0, 1]), np.array([[1, 2], [3, 4]])), # boolean array (np.array([True, True]), np.array([[1, 2], [3, 4]])), (np.array([True, False]), np.array([[1, 2]])), (np.array([False, True]), np.array([[3, 4]])), (np.array([False, False]), np.empty(shape=(0, 2), dtype="i8")), ], ) @pytest.mark.asyncio async def test_async_oindex(self, store, indexer, expected): z = zarr.create_array(store=store, shape=(2, 2), chunks=(1, 1), zarr_format=3, dtype="i8") z[...] = np.array([[1, 2], [3, 4]]) async_zarr = z._async_array result = await async_zarr.oindex.getitem(indexer) assert_array_equal(result, expected) @pytest.mark.asyncio async def test_async_oindex_with_zarr_array(self, store): group = zarr.create_group(store=store, zarr_format=3) z1 = group.create_array(name="z1", shape=(2, 2), chunks=(1, 1), dtype="i8") z1[...] = np.array([[1, 2], [3, 4]]) async_zarr = z1._async_array # create boolean zarr array to index with z2 = group.create_array(name="z2", shape=(2,), chunks=(1,), dtype="?") z2[...] = np.array([True, False]) result = await async_zarr.oindex.getitem(z2) expected = np.array([[1, 2]]) assert_array_equal(result, expected) @pytest.mark.parametrize( ("indexer", "expected"), [ (([0], [0]), np.array(1)), (([0, 1], [0, 1]), np.array([1, 4])), (np.array([[False, True], [False, True]]), np.array([2, 4])), ], ) @pytest.mark.asyncio async def test_async_vindex(self, store, indexer, expected): z = zarr.create_array(store=store, shape=(2, 2), chunks=(1, 1), zarr_format=3, dtype="i8") z[...] = np.array([[1, 2], [3, 4]]) async_zarr = z._async_array result = await async_zarr.vindex.getitem(indexer) assert_array_equal(result, expected) @pytest.mark.asyncio async def test_async_vindex_with_zarr_array(self, store): group = zarr.create_group(store=store, zarr_format=3) z1 = group.create_array(name="z1", shape=(2, 2), chunks=(1, 1), dtype="i8") z1[...] = np.array([[1, 2], [3, 4]]) async_zarr = z1._async_array # create boolean zarr array to index with z2 = group.create_array(name="z2", shape=(2, 2), chunks=(1, 1), dtype="?") z2[...] = np.array([[False, True], [False, True]]) result = await async_zarr.vindex.getitem(z2) expected = np.array([2, 4]) assert_array_equal(result, expected) @pytest.mark.asyncio async def test_async_invalid_indexer(self, store): z = zarr.create_array(store=store, shape=(2, 2), chunks=(1, 1), zarr_format=3, dtype="i8") z[...] = np.array([[1, 2], [3, 4]]) async_zarr = z._async_array with pytest.raises(IndexError): await async_zarr.vindex.getitem("invalid_indexer") with pytest.raises(IndexError): await async_zarr.oindex.getitem("invalid_indexer") zarr-python-3.2.1/tests/test_info.py000066400000000000000000000104471517635743000175410ustar00rootroot00000000000000import textwrap import pytest from zarr.codecs.bytes import BytesCodec from zarr.core._info import ArrayInfo, GroupInfo, human_readable_size from zarr.core.common import ZarrFormat from zarr.core.dtype.npy.int import Int32 ZARR_FORMATS = [2, 3] @pytest.mark.parametrize("zarr_format", ZARR_FORMATS) def test_group_info_repr(zarr_format: ZarrFormat) -> None: info = GroupInfo( _name="a", _store_type="MemoryStore", _read_only=False, _zarr_format=zarr_format ) result = repr(info) expected = textwrap.dedent(f"""\ Name : a Type : Group Zarr format : {zarr_format} Read-only : False Store type : MemoryStore""") assert result == expected @pytest.mark.parametrize("zarr_format", ZARR_FORMATS) def test_group_info_complete(zarr_format: ZarrFormat) -> None: info = GroupInfo( _name="a", _store_type="MemoryStore", _zarr_format=zarr_format, _read_only=False, _count_arrays=10, _count_groups=4, _count_members=14, ) result = repr(info) expected = textwrap.dedent(f"""\ Name : a Type : Group Zarr format : {zarr_format} Read-only : False Store type : MemoryStore No. members : 14 No. arrays : 10 No. groups : 4""") assert result == expected @pytest.mark.parametrize("zarr_format", ZARR_FORMATS) def test_array_info(zarr_format: ZarrFormat) -> None: info = ArrayInfo( _zarr_format=zarr_format, _data_type=Int32(), _fill_value=0, _shape=(100, 100), _chunk_shape=(10, 100), _order="C", _read_only=True, _store_type="MemoryStore", _serializer=BytesCodec(), ) result = repr(info) assert result == textwrap.dedent(f"""\ Type : Array Zarr format : {zarr_format} Data type : Int32(endianness='little') Fill value : 0 Shape : (100, 100) Chunk shape : (10, 100) Order : C Read-only : True Store type : MemoryStore Filters : () Serializer : BytesCodec(endian=) Compressors : ()""") @pytest.mark.parametrize("zarr_format", ZARR_FORMATS) @pytest.mark.parametrize("bytes_things", [(1_000_000, "976.6K", 500_000, "488.3K", "2.0", 5)]) def test_array_info_complete( zarr_format: ZarrFormat, bytes_things: tuple[int, str, int, str, str, int] ) -> None: ( count_bytes, count_bytes_formatted, count_bytes_stored, count_bytes_stored_formatted, storage_ratio_formatted, count_chunks_initialized, ) = bytes_things info = ArrayInfo( _zarr_format=zarr_format, _data_type=Int32(), _fill_value=0, _shape=(100, 100), _chunk_shape=(10, 100), _order="C", _read_only=True, _store_type="MemoryStore", _serializer=BytesCodec(), _count_bytes=count_bytes, _count_bytes_stored=count_bytes_stored, _count_chunks_initialized=count_chunks_initialized, ) result = repr(info) assert result == textwrap.dedent(f"""\ Type : Array Zarr format : {zarr_format} Data type : Int32(endianness='little') Fill value : 0 Shape : (100, 100) Chunk shape : (10, 100) Order : C Read-only : True Store type : MemoryStore Filters : () Serializer : BytesCodec(endian=) Compressors : () No. bytes : {count_bytes} ({count_bytes_formatted}) No. bytes stored : {count_bytes_stored} ({count_bytes_stored_formatted}) Storage ratio : {storage_ratio_formatted} Chunks Initialized : 5""") @pytest.mark.parametrize( ("size", "expected"), [ (1, "1"), (2**10, "1.0K"), (2**20, "1.0M"), (2**30, "1.0G"), (2**40, "1.0T"), (2**50, "1.0P"), ], ) def test_human_readable_size(size: int, expected: str) -> None: result = human_readable_size(size) assert result == expected zarr-python-3.2.1/tests/test_metadata/000077500000000000000000000000001517635743000200065ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_metadata/__init__.py000066400000000000000000000000001517635743000221050ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_metadata/conftest.py000066400000000000000000000025301517635743000222050ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any from zarr.codecs.bytes import BytesCodec if TYPE_CHECKING: from zarr.core.metadata.v3 import ArrayMetadataJSON_V3 def minimal_metadata_dict_v3( extra_fields: dict[str, Any] | None = None, **overrides: Any ) -> ArrayMetadataJSON_V3: """Build a minimal valid V3 array metadata JSON dict. The output matches the shape of ``ArrayV3Metadata.to_dict()`` — all fields that ``to_dict`` always emits are included. Parameters ---------- extra_fields : dict, optional Extra keys to inject into the dict (e.g. extension fields). **overrides Override any of the standard metadata fields. """ 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": "/"}}, "fill_value": 0, "codecs": (BytesCodec().to_dict(),), # type: ignore[typeddict-item] "attributes": {}, "storage_transformers": (), } d.update(overrides) # type: ignore[typeddict-item] if extra_fields is not None: d.update(extra_fields) # type: ignore[typeddict-item] return d zarr-python-3.2.1/tests/test_metadata/test_consolidated.py000066400000000000000000001002451517635743000240710ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Any import numpy as np import pytest from numcodecs import Blosc import zarr.api.asynchronous import zarr.api.synchronous import zarr.storage from zarr import AsyncGroup from zarr.api.asynchronous import ( consolidate_metadata, group, open, open_consolidated, ) from zarr.core.buffer import cpu, default_buffer_prototype from zarr.core.dtype import parse_dtype from zarr.core.group import ConsolidatedMetadata, GroupMetadata from zarr.core.metadata import ArrayV3Metadata from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.errors import ZarrUserWarning from zarr.storage import StorePath if TYPE_CHECKING: from zarr.abc.store import Store from zarr.core.common import JSON, ZarrFormat @pytest.fixture async def memory_store_with_hierarchy(memory_store: Store) -> Store: g = await group(store=memory_store, attributes={"foo": "bar"}) dtype = "uint8" await g.create_array(name="air", shape=(1, 2, 3), dtype=dtype) await g.create_array(name="lat", shape=(1,), dtype=dtype) await g.create_array(name="lon", shape=(2,), dtype=dtype) await g.create_array(name="time", shape=(3,), dtype=dtype) child = await g.create_group("child", attributes={"key": "child"}) await child.create_array("array", shape=(4, 4), attributes={"key": "child"}, dtype=dtype) grandchild = await child.create_group("grandchild", attributes={"key": "grandchild"}) await grandchild.create_array( "array", shape=(4, 4), attributes={"key": "grandchild"}, dtype=dtype ) await grandchild.create_group("empty_group", attributes={"key": "empty"}) return memory_store class TestConsolidated: async def test_open_consolidated_false_raises(self) -> None: store = zarr.storage.MemoryStore() with pytest.raises(TypeError, match="use_consolidated"): await zarr.api.asynchronous.open_consolidated(store, use_consolidated=False) # type: ignore[arg-type] def test_open_consolidated_false_raises_sync(self) -> None: store = zarr.storage.MemoryStore() with pytest.raises(TypeError, match="use_consolidated"): zarr.open_consolidated(store, use_consolidated=False) # type: ignore[arg-type] async def test_consolidated(self, memory_store_with_hierarchy: Store) -> None: # TODO: Figure out desired keys in # TODO: variety in the hierarchies # More nesting # arrays under arrays # single array # etc. with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await consolidate_metadata(memory_store_with_hierarchy) group2 = await AsyncGroup.open(memory_store_with_hierarchy) array_metadata: dict[str, JSON] = { "attributes": {}, "chunk_key_encoding": { "configuration": {"separator": "/"}, "name": "default", }, "codecs": ( {"configuration": {"endian": "little"}, "name": "bytes"}, {"configuration": {"level": 0, "checksum": False}, "name": "zstd"}, ), "data_type": "uint8", "fill_value": 0, "node_type": "array", # "shape": (1, 2, 3), "zarr_format": 3, } expected = GroupMetadata( attributes={"foo": "bar"}, consolidated_metadata=ConsolidatedMetadata( kind="inline", must_understand=False, metadata={ "air": ArrayV3Metadata.from_dict( { "shape": (1, 2, 3), "chunk_grid": { "configuration": {"chunk_shape": (1, 2, 3)}, "name": "regular", }, **array_metadata, } ), "lat": ArrayV3Metadata.from_dict( { "shape": (1,), "chunk_grid": { "configuration": {"chunk_shape": (1,)}, "name": "regular", }, **array_metadata, } ), "lon": ArrayV3Metadata.from_dict( { "shape": (2,), "chunk_grid": { "configuration": {"chunk_shape": (2,)}, "name": "regular", }, **array_metadata, } ), "time": ArrayV3Metadata.from_dict( { "shape": (3,), "chunk_grid": { "configuration": {"chunk_shape": (3,)}, "name": "regular", }, **array_metadata, } ), "child": GroupMetadata( attributes={"key": "child"}, consolidated_metadata=ConsolidatedMetadata( metadata={ "array": ArrayV3Metadata.from_dict( { **array_metadata, "attributes": {"key": "child"}, "shape": (4, 4), "chunk_grid": { "configuration": {"chunk_shape": (4, 4)}, "name": "regular", }, } ), "grandchild": GroupMetadata( attributes={"key": "grandchild"}, consolidated_metadata=ConsolidatedMetadata( metadata={ # known to be empty child group "empty_group": GroupMetadata( consolidated_metadata=ConsolidatedMetadata( metadata={} ), attributes={"key": "empty"}, ), "array": ArrayV3Metadata.from_dict( { **array_metadata, "attributes": {"key": "grandchild"}, "shape": (4, 4), "chunk_grid": { "configuration": {"chunk_shape": (4, 4)}, "name": "regular", }, } ), } ), ), }, ), ), }, ), ) assert group2.metadata == expected group3 = await open(store=memory_store_with_hierarchy) assert group3.metadata == expected group4 = await open_consolidated(store=memory_store_with_hierarchy) assert group4.metadata == expected buf = await memory_store_with_hierarchy.get( "zarr.json", prototype=default_buffer_prototype() ) assert buf is not None result_raw = json.loads(buf.to_bytes())["consolidated_metadata"] assert result_raw["kind"] == "inline" assert sorted(result_raw["metadata"]) == [ "air", "child", "child/array", "child/grandchild", "child/grandchild/array", "child/grandchild/empty_group", "lat", "lon", "time", ] def test_consolidated_sync(self, memory_store: Store) -> None: g = zarr.api.synchronous.group(store=memory_store, attributes={"foo": "bar"}) dtype = "uint8" g.create_array(name="air", shape=(1, 2, 3), dtype=dtype) g.create_array(name="lat", shape=(1,), dtype=dtype) g.create_array(name="lon", shape=(2,), dtype=dtype) g.create_array(name="time", shape=(3,), dtype=dtype) with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): zarr.api.synchronous.consolidate_metadata(memory_store) group2 = zarr.Group.open(memory_store) array_metadata: dict[str, JSON] = { "attributes": {}, "chunk_key_encoding": { "configuration": {"separator": "/"}, "name": "default", }, "codecs": ( {"configuration": {"endian": "little"}, "name": "bytes"}, {"configuration": {"level": 0, "checksum": False}, "name": "zstd"}, ), "data_type": dtype, "fill_value": 0, "node_type": "array", # "shape": (1, 2, 3), "zarr_format": 3, } expected = GroupMetadata( attributes={"foo": "bar"}, consolidated_metadata=ConsolidatedMetadata( kind="inline", must_understand=False, metadata={ "air": ArrayV3Metadata.from_dict( { "shape": (1, 2, 3), "chunk_grid": { "configuration": {"chunk_shape": (1, 2, 3)}, "name": "regular", }, **array_metadata, } ), "lat": ArrayV3Metadata.from_dict( { "shape": (1,), "chunk_grid": { "configuration": {"chunk_shape": (1,)}, "name": "regular", }, **array_metadata, } ), "lon": ArrayV3Metadata.from_dict( { "shape": (2,), "chunk_grid": { "configuration": {"chunk_shape": (2,)}, "name": "regular", }, **array_metadata, } ), "time": ArrayV3Metadata.from_dict( { "shape": (3,), "chunk_grid": { "configuration": {"chunk_shape": (3,)}, "name": "regular", }, **array_metadata, } ), }, ), ) assert group2.metadata == expected group3 = zarr.api.synchronous.open(store=memory_store) assert group3.metadata == expected group4 = zarr.api.synchronous.open_consolidated(store=memory_store) assert group4.metadata == expected async def test_not_writable_raises(self, memory_store: zarr.storage.MemoryStore) -> None: await group(store=memory_store, attributes={"foo": "bar"}) read_store = zarr.storage.MemoryStore(store_dict=memory_store._store_dict, read_only=True) with pytest.raises(ValueError, match="does not support writing"): await consolidate_metadata(read_store) async def test_non_root_node(self, memory_store_with_hierarchy: Store) -> None: with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await consolidate_metadata(memory_store_with_hierarchy, path="child") root = await AsyncGroup.open(memory_store_with_hierarchy) child = await AsyncGroup.open(StorePath(memory_store_with_hierarchy) / "child") assert root.metadata.consolidated_metadata is None assert child.metadata.consolidated_metadata is not None assert "air" not in child.metadata.consolidated_metadata.metadata assert "grandchild" in child.metadata.consolidated_metadata.metadata def test_consolidated_metadata_from_dict(self) -> None: data: dict[str, JSON] = {"must_understand": False} # missing kind with pytest.raises(ValueError, match="kind='None'"): ConsolidatedMetadata.from_dict(data) # invalid kind data["kind"] = "invalid" with pytest.raises(ValueError, match="kind='invalid'"): ConsolidatedMetadata.from_dict(data) # missing metadata data["kind"] = "inline" with pytest.raises(TypeError, match="Unexpected type for 'metadata'"): ConsolidatedMetadata.from_dict(data) data["kind"] = "inline" # empty is fine data["metadata"] = {} ConsolidatedMetadata.from_dict(data) def test_flatten(self) -> None: array_metadata: dict[str, Any] = { "attributes": {}, "chunk_key_encoding": { "configuration": {"separator": "/"}, "name": "default", }, "codecs": ({"configuration": {"endian": "little"}, "name": "bytes"},), "data_type": "float64", "fill_value": np.float64(0.0), "node_type": "array", # "shape": (1, 2, 3), "zarr_format": 3, } metadata = ConsolidatedMetadata( kind="inline", must_understand=False, metadata={ "air": ArrayV3Metadata.from_dict( { "shape": (1, 2, 3), "chunk_grid": { "configuration": {"chunk_shape": (1, 2, 3)}, "name": "regular", }, **array_metadata, } ), "lat": ArrayV3Metadata.from_dict( { "shape": (1,), "chunk_grid": { "configuration": {"chunk_shape": (1,)}, "name": "regular", }, **array_metadata, } ), "child": GroupMetadata( attributes={"key": "child"}, consolidated_metadata=ConsolidatedMetadata( metadata={ "array": ArrayV3Metadata.from_dict( { **array_metadata, "attributes": {"key": "child"}, "shape": (4, 4), "chunk_grid": { "configuration": {"chunk_shape": (4, 4)}, "name": "regular", }, } ), "grandchild": GroupMetadata( attributes={"key": "grandchild"}, consolidated_metadata=ConsolidatedMetadata( metadata={ "array": ArrayV3Metadata.from_dict( { **array_metadata, "attributes": {"key": "grandchild"}, "shape": (4, 4), "chunk_grid": { "configuration": {"chunk_shape": (4, 4)}, "name": "regular", }, } ) } ), ), }, ), ), }, ) result = metadata.flattened_metadata expected = { "air": metadata.metadata["air"], "lat": metadata.metadata["lat"], "child": GroupMetadata( attributes={"key": "child"}, consolidated_metadata=ConsolidatedMetadata(metadata={}) ), "child/array": metadata.metadata["child"].consolidated_metadata.metadata["array"], # type: ignore[union-attr] "child/grandchild": GroupMetadata( attributes={"key": "grandchild"}, consolidated_metadata=ConsolidatedMetadata(metadata={}), ), "child/grandchild/array": ( metadata.metadata["child"] .consolidated_metadata.metadata["grandchild"] # type: ignore[union-attr] .consolidated_metadata.metadata["array"] ), } assert result == expected def test_invalid_metadata_raises(self) -> None: payload: dict[str, JSON] = { "kind": "inline", "must_understand": False, "metadata": { "foo": [1, 2, 3] # invalid }, } with pytest.raises(TypeError, match="key='foo', type='list'"): ConsolidatedMetadata.from_dict(payload) def test_to_dict_empty(self) -> None: meta = ConsolidatedMetadata( metadata={ "empty": GroupMetadata( attributes={"key": "empty"}, consolidated_metadata=ConsolidatedMetadata(metadata={}), ) } ) result = meta.to_dict() expected = { "kind": "inline", "must_understand": False, "metadata": { "empty": { "attributes": {"key": "empty"}, "consolidated_metadata": { "kind": "inline", "must_understand": False, "metadata": {}, }, "node_type": "group", "zarr_format": 3, } }, } assert result == expected @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_to_dict_order( self, memory_store: zarr.storage.MemoryStore, zarr_format: ZarrFormat ) -> None: with zarr.config.set(default_zarr_format=zarr_format): g = await group(store=memory_store) # Create groups in non-lexicographix order dtype = "float32" await g.create_array(name="b", shape=(1,), dtype=dtype) child = await g.create_group("c", attributes={"key": "child"}) await g.create_array(name="a", shape=(1,), dtype=dtype) await child.create_array("e", shape=(1,), dtype=dtype) await child.create_array("d", shape=(1,), dtype=dtype) # Consolidate metadata and re-open store if zarr_format == 3: with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await zarr.api.asynchronous.consolidate_metadata(memory_store) else: await zarr.api.asynchronous.consolidate_metadata(memory_store) g2 = await zarr.api.asynchronous.open_group(store=memory_store) assert g2.metadata.consolidated_metadata is not None assert list(g2.metadata.consolidated_metadata.metadata) == ["a", "b", "c"] assert list(g2.metadata.consolidated_metadata.flattened_metadata) == [ "a", "b", "c", "c/d", "c/e", ] @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_open_consolidated_raises_async(self, zarr_format: ZarrFormat) -> None: store = zarr.storage.MemoryStore() await AsyncGroup.from_store(store, zarr_format=zarr_format) with pytest.raises(ValueError): await zarr.api.asynchronous.open_consolidated(store, zarr_format=zarr_format) with pytest.raises(ValueError): await zarr.api.asynchronous.open_consolidated(store, zarr_format=None) @pytest.fixture async def v2_consolidated_metadata_empty_dataset( self, memory_store: zarr.storage.MemoryStore ) -> AsyncGroup: zgroup_bytes = cpu.Buffer.from_bytes(json.dumps({"zarr_format": 2}).encode()) zmetadata_bytes = cpu.Buffer.from_bytes( b'{"metadata":{".zgroup":{"zarr_format":2}},"zarr_consolidated_format":1}' ) return AsyncGroup._from_bytes_v2( StorePath(memory_store, path=""), zgroup_bytes, zattrs_bytes=None, consolidated_metadata_bytes=zmetadata_bytes, ) async def test_consolidated_metadata_backwards_compatibility( self, v2_consolidated_metadata_empty_dataset: AsyncGroup ) -> None: """ Test that consolidated metadata handles a missing .zattrs key. This is necessary for backwards compatibility with zarr-python 2.x. See https://github.com/zarr-developers/zarr-python/issues/2694 """ store = zarr.storage.MemoryStore() await zarr.api.asynchronous.open(store=store, zarr_format=2) await zarr.api.asynchronous.consolidate_metadata(store) result = await zarr.api.asynchronous.open_consolidated(store, zarr_format=2) assert result.metadata == v2_consolidated_metadata_empty_dataset.metadata async def test_consolidated_metadata_v2(self) -> None: store = zarr.storage.MemoryStore() g = await AsyncGroup.from_store(store, attributes={"key": "root"}, zarr_format=2) dtype = parse_dtype("uint8", zarr_format=2) await g.create_array(name="a", shape=(1,), attributes={"key": "a"}, dtype=dtype) g1 = await g.create_group(name="g1", attributes={"key": "g1"}) await g1.create_group(name="g2", attributes={"key": "g2"}) await zarr.api.asynchronous.consolidate_metadata(store) result = await zarr.api.asynchronous.open_consolidated(store, zarr_format=2) expected = GroupMetadata( attributes={"key": "root"}, zarr_format=2, consolidated_metadata=ConsolidatedMetadata( metadata={ "a": ArrayV2Metadata( shape=(1,), dtype=dtype, attributes={"key": "a"}, chunks=(1,), fill_value=0, compressor=Blosc(), order="C", ), "g1": GroupMetadata( attributes={"key": "g1"}, zarr_format=2, consolidated_metadata=ConsolidatedMetadata( metadata={ "g2": GroupMetadata( attributes={"key": "g2"}, zarr_format=2, consolidated_metadata=ConsolidatedMetadata(metadata={}), ) } ), ), } ), ) assert result.metadata == expected @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_use_consolidated_false( self, memory_store: zarr.storage.MemoryStore, zarr_format: ZarrFormat ) -> None: with zarr.config.set(default_zarr_format=zarr_format): g = await group(store=memory_store, attributes={"foo": "bar"}) await g.create_group(name="a") # test a stale read if zarr_format == 3: with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await zarr.api.asynchronous.consolidate_metadata(memory_store) else: await zarr.api.asynchronous.consolidate_metadata(memory_store) await g.create_group(name="b") stale = await zarr.api.asynchronous.open_group(store=memory_store) assert len([x async for x in stale.members()]) == 1 assert stale.metadata.consolidated_metadata assert list(stale.metadata.consolidated_metadata.metadata) == ["a"] # bypass stale data good = await zarr.api.asynchronous.open_group( store=memory_store, use_consolidated=False ) assert len([x async for x in good.members()]) == 2 # reconsolidate if zarr_format == 3: with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await zarr.api.asynchronous.consolidate_metadata(memory_store) else: await zarr.api.asynchronous.consolidate_metadata(memory_store) good = await zarr.api.asynchronous.open_group(store=memory_store) assert len([x async for x in good.members()]) == 2 assert good.metadata.consolidated_metadata assert sorted(good.metadata.consolidated_metadata.metadata) == ["a", "b"] async def test_stale_child_metadata_ignored( self, memory_store: zarr.storage.MemoryStore ) -> None: # https://github.com/zarr-developers/zarr-python/issues/2921 # When consolidating metadata, we should ignore any (possibly stale) metadata # from previous consolidations, *including at child nodes*. root = await zarr.api.asynchronous.group(store=memory_store, zarr_format=3) await root.create_group("foo") await zarr.api.asynchronous.consolidate_metadata(memory_store, path="foo") await root.create_group("foo/bar/spam") with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await zarr.api.asynchronous.consolidate_metadata(memory_store) reopened = await zarr.api.asynchronous.open_consolidated(store=memory_store, zarr_format=3) result = [x[0] async for x in reopened.members(max_depth=None)] expected = ["foo", "foo/bar", "foo/bar/spam"] assert result == expected async def test_use_consolidated_for_children_members( self, memory_store: zarr.storage.MemoryStore ) -> None: # A test that has *unconsolidated* metadata at the root group, but discovers # a child group with consolidated metadata. root = await zarr.api.asynchronous.create_group(store=memory_store) await root.create_group("a/b") # Consolidate metadata at "a/b" await zarr.api.asynchronous.consolidate_metadata(memory_store, path="a/b") # Add a new group a/b/c, that's not present in the CM at "a/b" await root.create_group("a/b/c") # Now according to the consolidated metadata, "a" has children ["b"] # but according to the unconsolidated metadata, "a" has children ["b", "c"] group = await zarr.api.asynchronous.open_group(store=memory_store, path="a") with pytest.warns(ZarrUserWarning, match="Object at 'c' not found"): result = sorted([x[0] async for x in group.members(max_depth=None)]) expected = ["b"] assert result == expected result = sorted( [x[0] async for x in group.members(max_depth=None, use_consolidated_for_children=False)] ) expected = ["b", "b/c"] assert result == expected async def test_absolute_path_for_subgroup(self, memory_store: zarr.storage.MemoryStore) -> None: root = await zarr.api.asynchronous.create_group(store=memory_store) await root.create_group("a/b") with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await zarr.api.asynchronous.consolidate_metadata(memory_store) group = await zarr.api.asynchronous.open_group(store=memory_store) subgroup = await group.getitem("/a") assert isinstance(subgroup, AsyncGroup) members = [x async for x in subgroup.keys()] # noqa: SIM118 assert members == ["b"] @pytest.mark.parametrize("fill_value", [np.nan, np.inf, -np.inf]) async def test_consolidated_metadata_encodes_special_chars( memory_store: Store, zarr_format: ZarrFormat, fill_value: float ) -> None: root = await group(store=memory_store, zarr_format=zarr_format) _time = await root.create_array("time", shape=(12,), dtype=np.float64, fill_value=fill_value) if zarr_format == 3: with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): await zarr.api.asynchronous.consolidate_metadata(memory_store) else: await zarr.api.asynchronous.consolidate_metadata(memory_store) root = await group(store=memory_store, zarr_format=zarr_format) root_buffer = root.metadata.to_buffer_dict(default_buffer_prototype()) if zarr_format == 2: root_metadata = json.loads(root_buffer[".zmetadata"].to_bytes().decode("utf-8"))["metadata"] elif zarr_format == 3: root_metadata = json.loads(root_buffer["zarr.json"].to_bytes().decode("utf-8"))[ "consolidated_metadata" ]["metadata"] expected_fill_value = _time._zdtype.to_json_scalar(fill_value, zarr_format=2) if zarr_format == 2: assert root_metadata["time/.zarray"]["fill_value"] == expected_fill_value elif zarr_format == 3: assert root_metadata["time"]["fill_value"] == expected_fill_value class NonConsolidatedStore(zarr.storage.MemoryStore): """A store that doesn't support consolidated metadata""" @property def supports_consolidated_metadata(self) -> bool: return False async def test_consolidate_metadata_raises_for_self_consolidating_stores() -> None: """Verify calling consolidate_metadata on a non supporting stores raises an error.""" memory_store = NonConsolidatedStore() root = await zarr.api.asynchronous.create_group(store=memory_store) await root.create_group("a/b") with pytest.raises(TypeError, match="doesn't support consolidated metadata"): await zarr.api.asynchronous.consolidate_metadata(memory_store) async def test_open_group_in_non_consolidating_stores() -> None: memory_store = NonConsolidatedStore() root = await zarr.api.asynchronous.create_group(store=memory_store) await root.create_group("a/b") # Opening a group without consolidatedion works as expected await AsyncGroup.open(memory_store, use_consolidated=False) # let the Store opt out of consolidation await AsyncGroup.open(memory_store, use_consolidated=None) # Opening a group with use_consolidated=True should fail with pytest.raises(ValueError, match="doesn't support consolidated metadata"): await AsyncGroup.open(memory_store, use_consolidated=True) zarr-python-3.2.1/tests/test_metadata/test_v2.py000066400000000000000000000276051517635743000217600ustar00rootroot00000000000000from __future__ import annotations import json from typing import TYPE_CHECKING, Literal import numpy as np import pytest import zarr.api.asynchronous import zarr.storage from zarr.core.buffer import cpu from zarr.core.buffer.core import default_buffer_prototype from zarr.core.dtype.npy.float import Float32, Float64 from zarr.core.dtype.npy.int import Int16 from zarr.core.group import ConsolidatedMetadata, GroupMetadata from zarr.core.metadata import ArrayV2Metadata from zarr.core.metadata.v2 import parse_zarr_format from zarr.errors import ZarrUserWarning if TYPE_CHECKING: from pathlib import Path from typing import Any from zarr.abc.codec import Codec from zarr.core.common import JSON def test_parse_zarr_format_valid() -> None: assert parse_zarr_format(2) == 2 @pytest.mark.parametrize("data", [None, 1, 3, 4, 5, "3"]) def test_parse_zarr_format_invalid(data: Any) -> None: with pytest.raises(ValueError, match=f"Invalid value. Expected 2. Got {data}"): parse_zarr_format(data) @pytest.mark.parametrize("attributes", [None, {"foo": "bar"}]) @pytest.mark.parametrize("filters", [None, [{"id": "gzip", "level": 1}]]) @pytest.mark.parametrize("compressor", [None, {"id": "gzip", "level": 1}]) @pytest.mark.parametrize("fill_value", [None, 0, 1]) @pytest.mark.parametrize("order", ["C", "F"]) @pytest.mark.parametrize("dimension_separator", [".", "/", None]) def test_metadata_to_dict( compressor: Codec | None, filters: tuple[Codec] | None, fill_value: Any, order: Literal["C", "F"], dimension_separator: Literal[".", "/"] | None, attributes: dict[str, Any] | None, ) -> None: shape = (1, 2, 3) chunks = (1,) * len(shape) data_type = "|u1" metadata_dict = { "zarr_format": 2, "shape": shape, "chunks": chunks, "dtype": data_type, "order": order, "compressor": compressor, "filters": filters, "fill_value": fill_value, } if attributes is not None: metadata_dict["attributes"] = attributes if dimension_separator is not None: metadata_dict["dimension_separator"] = dimension_separator metadata = ArrayV2Metadata.from_dict(metadata_dict) observed = metadata.to_dict() expected = metadata_dict.copy() if attributes is None: assert observed["attributes"] == {} observed.pop("attributes") if dimension_separator is None: expected_dimension_sep = "." assert observed["dimension_separator"] == expected_dimension_sep observed.pop("dimension_separator") assert observed == expected def test_filters_empty_tuple_warns() -> None: metadata_dict = { "zarr_format": 2, "shape": (1,), "chunks": (1,), "dtype": "|u1", "order": "C", "compressor": None, "filters": (), "fill_value": 0, } with pytest.warns( ZarrUserWarning, match="Found an empty list of filters in the array metadata document." ): meta = ArrayV2Metadata.from_dict(metadata_dict) assert meta.filters is None class TestConsolidated: @pytest.fixture async def v2_consolidated_metadata( self, memory_store: zarr.storage.MemoryStore ) -> zarr.storage.MemoryStore: zmetadata: dict[str, JSON] = { "metadata": { ".zattrs": { "Conventions": "COARDS", }, ".zgroup": {"zarr_format": 2}, "air/.zarray": { "chunks": [730], "compressor": None, "dtype": " None: # .zgroup, .zattrs, .metadata store = v2_consolidated_metadata group = zarr.open_consolidated(store=store, zarr_format=2) assert group.metadata.consolidated_metadata is not None expected = ConsolidatedMetadata( metadata={ "air": ArrayV2Metadata( shape=(730,), fill_value=0, chunks=(730,), attributes={"_ARRAY_DIMENSIONS": ["time"], "dataset": "NMC Reanalysis"}, dtype=Int16(), order="C", filters=None, dimension_separator=".", compressor=None, ), "time": ArrayV2Metadata( shape=(730,), fill_value=0.0, chunks=(730,), attributes={ "_ARRAY_DIMENSIONS": ["time"], "calendar": "standard", "long_name": "Time", "standard_name": "time", "units": "hours since 1800-01-01", }, dtype=Float32(), order="C", filters=None, dimension_separator=".", compressor=None, ), "nested": GroupMetadata( attributes={"key": "value"}, zarr_format=2, consolidated_metadata=ConsolidatedMetadata( metadata={ "array": ArrayV2Metadata( shape=(730,), fill_value=0.0, chunks=(730,), attributes={ "calendar": "standard", }, dtype=Float32(), order="C", filters=None, dimension_separator=".", compressor=None, ) } ), ), }, kind="inline", must_understand=False, ) result = group.metadata.consolidated_metadata assert result == expected async def test_getitem_consolidated( self, v2_consolidated_metadata: zarr.storage.MemoryStore ) -> None: store = v2_consolidated_metadata group = await zarr.api.asynchronous.open_consolidated(store=store, zarr_format=2) air = await group.getitem("air") assert isinstance(air, zarr.AsyncArray) assert air.metadata.shape == (730,) def test_from_dict_extra_fields() -> None: data = { "_nczarr_array": {"dimrefs": ["/dim1", "/dim2"], "storage": "chunked"}, "attributes": {"key": "value"}, "chunks": [8], "compressor": None, "dtype": " None: compressor_config: dict[str, JSON] = {"id": "zstd", "level": 5, "checksum": False} arr = zarr.create_array( {}, shape=(10,), chunks=(10,), dtype="int32", compressors=compressor_config, zarr_format=2, ) metadata = json.loads( arr.metadata.to_buffer_dict(default_buffer_prototype())[".zarray"].to_bytes() ) assert "checksum" not in metadata["compressor"] @pytest.mark.parametrize("fill_value", [np.void((0, 0), np.dtype([("foo", "i4"), ("bar", "i4")]))]) def test_structured_dtype_fill_value_serialization( tmp_path: Path, fill_value: np.void | np.dtype[Any] ) -> None: zarr_format: Literal[2] = 2 group_path = tmp_path / "test.zarr" root_group = zarr.open_group(group_path, mode="w", zarr_format=zarr_format) dtype = np.dtype([("foo", "i4"), ("bar", "i4")]) root_group.create_array( name="structured_dtype", shape=(100, 100), chunks=(100, 100), dtype=dtype, fill_value=fill_value, ) zarr.consolidate_metadata(root_group.store, zarr_format=zarr_format) root_group = zarr.open_group(group_path, mode="r") observed = root_group.metadata.consolidated_metadata.metadata["structured_dtype"].fill_value # type: ignore[union-attr] assert observed == fill_value zarr-python-3.2.1/tests/test_metadata/test_v3.py000066400000000000000000000301201517635743000217430ustar00rootroot00000000000000"""Tests for zarr v3 metadata classes and parsing helpers.""" from __future__ import annotations import json from typing import TYPE_CHECKING import pytest from tests.conftest import Expect, ExpectFail from tests.test_metadata.conftest import minimal_metadata_dict_v3 from zarr.core.buffer import default_buffer_prototype from zarr.core.config import config from zarr.core.dtype import UInt8 from zarr.core.group import GroupMetadata, parse_node_type from zarr.core.metadata.v3 import ( ARRAY_METADATA_KEYS, ArrayMetadataJSON_V3, ArrayV3Metadata, parse_codecs, parse_dimension_names, parse_node_type_array, parse_zarr_format, ) from zarr.errors import ( MetadataValidationError, NodeTypeValidationError, UnknownCodecError, ) if TYPE_CHECKING: from typing import Any # --------------------------------------------------------------------------- # Parsing helpers # --------------------------------------------------------------------------- def test_parse_zarr_format_valid() -> None: """The integer 3 is the only valid zarr_format for v3.""" assert parse_zarr_format(3) == 3 @pytest.mark.parametrize("data", [None, 1, 2, 4, 5, "3"]) def test_parse_zarr_format_invalid(data: Any) -> None: """Non-3 values are rejected.""" with pytest.raises(MetadataValidationError): parse_zarr_format(data) def test_parse_node_type_valid() -> None: """'array' and 'group' are the only valid node types.""" assert parse_node_type("array") == "array" assert parse_node_type("group") == "group" @pytest.mark.parametrize("data", [None, 2, "other"]) def test_parse_node_type_invalid(data: Any) -> None: """Non-string and unrecognized values are rejected.""" with pytest.raises(MetadataValidationError): parse_node_type(data) def test_parse_node_type_array_valid() -> None: """parse_node_type_array accepts only 'array'.""" assert parse_node_type_array("array") == "array" @pytest.mark.parametrize("data", [None, "group"]) def test_parse_node_type_array_invalid(data: Any) -> None: """parse_node_type_array rejects 'group' and non-string values.""" with pytest.raises(NodeTypeValidationError): parse_node_type_array(data) @pytest.mark.parametrize("data", [None, ("a", "b", "c"), ["a", "a", "a"], ()]) def test_parse_dimension_names_valid(data: Any) -> None: """None, tuples of strings, lists of strings, and empty tuples are accepted.""" result = parse_dimension_names(data) if data is None: assert result is None else: assert result == tuple(data) @pytest.mark.parametrize("data", [[1, 2, "a"], [None, 3]]) def test_parse_dimension_names_invalid(data: Any) -> None: """Iterables containing non-string elements are rejected.""" with pytest.raises(TypeError, match="Expected either None or"): parse_dimension_names(data) def test_parse_codecs_unknown_raises(monkeypatch: pytest.MonkeyPatch) -> None: """An unregistered codec name raises UnknownCodecError.""" from collections import defaultdict import zarr.registry from zarr.registry import Registry monkeypatch.setattr(zarr.registry, "_codec_registries", defaultdict(Registry)) with pytest.raises(UnknownCodecError): parse_codecs([{"name": "unknown"}]) # --------------------------------------------------------------------------- # Types # --------------------------------------------------------------------------- def test_array_metadata_keys_matches_typeddict() -> None: """ Test that the variable modelling the set of keys for array v3 metadata matches the keys of the typeddict model for the metadata. """ assert ARRAY_METADATA_KEYS == set(ArrayMetadataJSON_V3.__annotations__.keys()) # --------------------------------------------------------------------------- # ArrayV3Metadata: round-trip # --------------------------------------------------------------------------- # Codecs after evolution for single-byte (uint8) and multi-byte (float64) types. _UINT8_CODECS = ({"name": "bytes"},) _FLOAT64_CODECS = ({"name": "bytes", "configuration": {"endian": "little"}},) @pytest.mark.parametrize( "case", [ Expect( input={}, output=minimal_metadata_dict_v3(codecs=_UINT8_CODECS), id="minimal", ), Expect( input={"attributes": {"key": "value"}}, output=minimal_metadata_dict_v3(attributes={"key": "value"}, codecs=_UINT8_CODECS), id="with_attributes", ), Expect( input={"dimension_names": ("x", "y")}, output=minimal_metadata_dict_v3(dimension_names=("x", "y"), codecs=_UINT8_CODECS), id="with_dimension_names", ), Expect( input={"storage_transformers": ()}, output=minimal_metadata_dict_v3(storage_transformers=(), codecs=_UINT8_CODECS), id="with_storage_transformers", ), Expect( input={"data_type": "float64", "fill_value": 0.0}, output=minimal_metadata_dict_v3( data_type="float64", fill_value=0.0, codecs=_FLOAT64_CODECS ), id="float64", ), Expect( input={"chunk_key_encoding": {"name": "v2", "configuration": {"separator": "."}}}, output=minimal_metadata_dict_v3( chunk_key_encoding={"name": "v2", "configuration": {"separator": "."}}, codecs=_UINT8_CODECS, ), id="v2_chunk_key_encoding", ), Expect( input={"data_type": "float64", "fill_value": "NaN"}, output=minimal_metadata_dict_v3( data_type="float64", fill_value="NaN", codecs=_FLOAT64_CODECS ), id="nan_fill_value", ), Expect( input={"data_type": "float64", "fill_value": "Infinity"}, output=minimal_metadata_dict_v3( data_type="float64", fill_value="Infinity", codecs=_FLOAT64_CODECS ), id="inf_fill_value", ), Expect( input={"data_type": "float64", "fill_value": "-Infinity"}, output=minimal_metadata_dict_v3( data_type="float64", fill_value="-Infinity", codecs=_FLOAT64_CODECS ), id="neg_inf_fill_value", ), Expect( input={ "attributes": {}, "storage_transformers": (), "extra_fields": {"my_ext": {"must_understand": False, "data": [1, 2, 3]}}, }, output=minimal_metadata_dict_v3( attributes={}, storage_transformers=(), codecs=_UINT8_CODECS, extra_fields={"my_ext": {"must_understand": False, "data": [1, 2, 3]}}, ), id="extra_fields", ), ], ids=lambda case: case.id, ) def test_array_metadata_roundtrip(case: Expect[dict[str, Any], dict[str, Any]]) -> None: """from_dict(d).to_dict() produces the expected output, including codec evolution.""" d = minimal_metadata_dict_v3(**case.input) m = ArrayV3Metadata.from_dict(d) # type: ignore[arg-type] assert m.to_dict() == case.output # --------------------------------------------------------------------------- # ArrayV3Metadata: failure modes # --------------------------------------------------------------------------- @pytest.mark.parametrize( "case", [ ExpectFail( input={"dimension_names": ("x", "y", "z")}, exception=ValueError, msg="dimension_names.*shape", id="dimension_names_length_mismatch", ), ExpectFail( input={"data_type": "uint8", "fill_value": {}}, exception=TypeError, msg=".*", id="invalid_fill_value_type", ), ], ids=lambda case: case.id, ) def test_array_metadata_from_dict_fails(case: ExpectFail[dict[str, Any]]) -> None: """from_dict rejects invalid metadata documents.""" d = minimal_metadata_dict_v3(**case.input) with pytest.raises(case.exception, match=case.msg): ArrayV3Metadata.from_dict(d) # type: ignore[arg-type] @pytest.mark.parametrize( "case", [ ExpectFail( input=minimal_metadata_dict_v3(extra_fields={"my_ext": {"must_understand": True}}), exception=MetadataValidationError, msg="disallowed extra fields", id="must_understand_true", ), ExpectFail( input=minimal_metadata_dict_v3(extra_fields={"my_ext": 42}), exception=MetadataValidationError, msg="disallowed extra fields", id="non_dict_extra_field", ), ], ids=lambda case: case.id, ) def test_array_metadata_extra_fields_rejected(case: ExpectFail[dict[str, Any]]) -> None: """from_dict rejects extra fields that don't conform to the spec.""" with pytest.raises(case.exception, match=case.msg): ArrayV3Metadata.from_dict(case.input) def test_init_extra_fields_collision() -> None: """Extra field keys that collide with reserved metadata field names are rejected.""" extra_fields: dict[str, object] = {"shape": (10,), "data_type": "uint8"} with pytest.raises(ValueError, match="collide with keys reserved"): ArrayV3Metadata( shape=(10,), data_type=UInt8(), chunk_grid={"name": "regular", "configuration": {"chunk_shape": (10,)}}, chunk_key_encoding={"name": "default", "configuration": {"separator": "/"}}, fill_value=0, codecs=({"name": "bytes", "configuration": {"endian": "little"}},), attributes={}, dimension_names=None, extra_fields=extra_fields, # type: ignore[arg-type] ) # --------------------------------------------------------------------------- # JSON indent # --------------------------------------------------------------------------- @pytest.mark.parametrize("indent", [2, 4, None]) def test_json_indent(indent: int | None) -> None: """The json_indent config setting controls indentation in to_buffer_dict output.""" with config.set({"json_indent": indent}): m = GroupMetadata() d = m.to_buffer_dict(default_buffer_prototype())["zarr.json"].to_bytes() assert d == json.dumps(json.loads(d), indent=indent).encode() # --------------------------------------------------------------------------- # GroupMetadata.to_dict # --------------------------------------------------------------------------- @pytest.mark.parametrize("attributes", [None, {"foo": "bar"}]) def test_group_metadata_to_dict(attributes: dict[str, Any] | None) -> None: """GroupMetadata.to_dict produces the expected v3 JSON structure.""" meta = GroupMetadata(attributes=attributes) assert meta.to_dict() == { "zarr_format": 3, "node_type": "group", "attributes": attributes or {}, } @pytest.mark.parametrize("attributes", [None, {"foo": "bar"}]) def test_group_metadata_to_dict_consolidated(attributes: dict[str, Any] | None) -> None: """GroupMetadata.to_dict includes consolidated_metadata when present.""" from zarr import consolidate_metadata, create_group from zarr.errors import ZarrUserWarning store: dict[str, object] = {} group = create_group(store, attributes=attributes, zarr_format=3) group.create_group("foo") with pytest.warns( ZarrUserWarning, match="Consolidated metadata is currently not part in the Zarr format 3 specification.", ): group = consolidate_metadata(store) assert group.metadata.to_dict() == { "zarr_format": 3, "node_type": "group", "attributes": attributes or {}, "consolidated_metadata": { "kind": "inline", "must_understand": False, "metadata": { "foo": { "attributes": {}, "zarr_format": 3, "node_type": "group", "consolidated_metadata": { "kind": "inline", "metadata": {}, "must_understand": False, }, } }, }, } zarr-python-3.2.1/tests/test_properties.py000066400000000000000000000351731517635743000210050ustar00rootroot00000000000000import itertools import json import numbers from collections.abc import Generator from typing import Any import numpy as np import pytest from numpy.testing import assert_array_equal import zarr from zarr.core.buffer import default_buffer_prototype pytest.importorskip("hypothesis") import hypothesis.extra.numpy as npst import hypothesis.strategies as st from hypothesis import assume, given, settings from zarr.abc.store import Store from zarr.core.common import ZARR_JSON, ZARRAY_JSON, ZATTRS_JSON from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.core.sync import sync from zarr.testing.strategies import ( array_metadata, arrays, basic_indices, complex_rectilinear_arrays, numpy_arrays, orthogonal_indices, rectilinear_arrays, simple_arrays, stores, zarr_formats, ) @pytest.fixture(autouse=True) def _enable_rectilinear_chunks() -> Generator[None, None, None]: """Enable rectilinear chunks for all property tests since strategies may generate them.""" with zarr.config.set({"array.rectilinear_chunks": True}): yield def deep_equal(a: Any, b: Any) -> bool: """Deep equality check with handling of special cases for array metadata classes""" if isinstance(a, (complex, np.complexfloating)) and isinstance( b, (complex, np.complexfloating) ): a_real, a_imag = float(a.real), float(a.imag) b_real, b_imag = float(b.real), float(b.imag) if np.isnan(a_real) and np.isnan(b_real): real_eq = True else: real_eq = a_real == b_real if np.isnan(a_imag) and np.isnan(b_imag): imag_eq = True else: imag_eq = a_imag == b_imag return real_eq and imag_eq if isinstance(a, (float, np.floating)) and isinstance(b, (float, np.floating)): if np.isnan(a) and np.isnan(b): return True return a == b if isinstance(a, np.datetime64) and isinstance(b, np.datetime64): if np.isnat(a) and np.isnat(b): return True return a == b if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): if a.shape != b.shape: return False return all(itertools.starmap(deep_equal, zip(a.flat, b.flat, strict=False))) if isinstance(a, dict) and isinstance(b, dict): if set(a.keys()) != set(b.keys()): return False return all(deep_equal(a[k], b[k]) for k in a) if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): if len(a) != len(b): return False return all(itertools.starmap(deep_equal, zip(a, b, strict=False))) return a == b @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @given(data=st.data()) def test_array_roundtrip(data: st.DataObject) -> None: nparray = data.draw(numpy_arrays()) zarray = data.draw(arrays(arrays=st.just(nparray))) assert_array_equal(nparray, zarray[:]) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @given(array=arrays()) def test_array_creates_implicit_groups(array): path = array.path ancestry = path.split("/")[:-1] for i in range(len(ancestry)): parent = "/".join(ancestry[: i + 1]) if array.metadata.zarr_format == 2: assert ( sync(array.store.get(f"{parent}/.zgroup", prototype=default_buffer_prototype())) is not None ) elif array.metadata.zarr_format == 3: assert ( sync(array.store.get(f"{parent}/zarr.json", prototype=default_buffer_prototype())) is not None ) # this decorator removes timeout; not ideal but it should avoid intermittent CI failures @pytest.mark.asyncio @settings(deadline=None) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @given(data=st.data()) async def test_basic_indexing(data: st.DataObject) -> None: zarray = data.draw(st.one_of(simple_arrays(), rectilinear_arrays())) nparray = zarray[:] indexer = data.draw(basic_indices(shape=nparray.shape)) # sync get actual = zarray[indexer] assert_array_equal(nparray[indexer], actual) # async get async_zarray = zarray._async_array actual = await async_zarray.getitem(indexer) assert_array_equal(nparray[indexer], actual) # sync set new_data = data.draw(numpy_arrays(shapes=st.just(actual.shape), dtype=nparray.dtype)) zarray[indexer] = new_data nparray[indexer] = new_data assert_array_equal(nparray, zarray[:]) # TODO test async setitem? @pytest.mark.asyncio @settings(deadline=None) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @given(data=st.data()) async def test_basic_indexing_complex_rectilinear(data: st.DataObject) -> None: nparray, zarray = data.draw(complex_rectilinear_arrays()) indexer = data.draw(basic_indices(shape=nparray.shape)) assert_array_equal(nparray[indexer], zarray[indexer]) @pytest.mark.asyncio @given(data=st.data()) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") async def test_oindex(data: st.DataObject) -> None: # integer_array_indices can't handle 0-size dimensions. zarray = data.draw( st.one_of( simple_arrays(shapes=npst.array_shapes(max_dims=4, min_side=1)), rectilinear_arrays(shapes=npst.array_shapes(max_dims=4, min_side=1, max_side=20)), ) ) nparray = zarray[:] zindexer, npindexer = data.draw(orthogonal_indices(shape=nparray.shape)) # sync get actual = zarray.oindex[zindexer] assert_array_equal(nparray[npindexer], actual) # async get async_zarray = zarray._async_array actual = await async_zarray.oindex.getitem(zindexer) assert_array_equal(nparray[npindexer], actual) # sync get assume(zarray.shards is None) # GH2834 for idxr in npindexer: if isinstance(idxr, np.ndarray) and idxr.size != np.unique(idxr).size: # behaviour of setitem with repeated indices is not guaranteed in practice assume(False) new_data = data.draw(numpy_arrays(shapes=st.just(actual.shape), dtype=nparray.dtype)) nparray[npindexer] = new_data zarray.oindex[zindexer] = new_data assert_array_equal(nparray, zarray[:]) # note: async oindex setitem not yet implemented @pytest.mark.asyncio @given(data=st.data()) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") async def test_vindex(data: st.DataObject) -> None: # integer_array_indices can't handle 0-size dimensions. zarray = data.draw( st.one_of( simple_arrays(shapes=npst.array_shapes(max_dims=4, min_side=1)), rectilinear_arrays(shapes=npst.array_shapes(max_dims=3, min_side=1, max_side=20)), ) ) nparray = zarray[:] indexer = data.draw( npst.integer_array_indices( shape=nparray.shape, result_shape=npst.array_shapes(min_side=1, max_dims=None) ) ) # sync get actual = zarray.vindex[indexer] assert_array_equal(nparray[indexer], actual) # async get async_zarray = zarray._async_array actual = await async_zarray.vindex.getitem(indexer) assert_array_equal(nparray[indexer], actual) # sync set # FIXME! # when the indexer is such that a value gets overwritten multiple times, # I think the output depends on chunking. # new_data = data.draw(npst.arrays(shape=st.just(actual.shape), dtype=nparray.dtype)) # nparray[indexer] = new_data # zarray.vindex[indexer] = new_data # assert_array_equal(nparray, zarray[:]) # note: async vindex setitem not yet implemented @given(store=stores, meta=array_metadata()) # type: ignore[misc] @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") async def test_roundtrip_array_metadata_from_store( store: Store, meta: ArrayV2Metadata | ArrayV3Metadata ) -> None: """ Verify that the I/O for metadata in a store are lossless. This test serializes an ArrayV2Metadata or ArrayV3Metadata object to a dict of buffers via `to_buffer_dict`, writes each buffer to a store under keys prefixed with "0/", and then reads them back. The test asserts that each retrieved buffer exactly matches the original buffer. """ asdict = meta.to_buffer_dict(prototype=default_buffer_prototype()) for key, expected in asdict.items(): await store.set(f"0/{key}", expected) actual = await store.get(f"0/{key}", prototype=default_buffer_prototype()) assert actual == expected @given(data=st.data(), zarr_format=zarr_formats) @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_roundtrip_array_metadata_from_json(data: st.DataObject, zarr_format: int) -> None: """ Verify that JSON serialization and deserialization of metadata is lossless. For Zarr v2: - The metadata is split into two JSON documents (one for array data and one for attributes). The test merges the attributes back before deserialization. For Zarr v3: - All metadata is stored in a single JSON document. No manual merger is necessary. The test then converts both the original and round-tripped metadata objects into dictionaries using `dataclasses.asdict` and uses a deep equality check to verify that the roundtrip has preserved all fields (including special cases like NaN, Infinity, complex numbers, and datetime values). """ metadata = data.draw(array_metadata(zarr_formats=st.just(zarr_format))) buffer_dict = metadata.to_buffer_dict(prototype=default_buffer_prototype()) if zarr_format == 2: zarray_dict = json.loads(buffer_dict[ZARRAY_JSON].to_bytes().decode()) zattrs_dict = json.loads(buffer_dict[ZATTRS_JSON].to_bytes().decode()) # zattrs and zarray are separate in v2, we have to add attributes back prior to `from_dict` zarray_dict["attributes"] = zattrs_dict metadata_roundtripped = ArrayV2Metadata.from_dict(zarray_dict) else: zarray_dict = json.loads(buffer_dict[ZARR_JSON].to_bytes().decode()) metadata_roundtripped = ArrayV3Metadata.from_dict(zarray_dict) orig = metadata.to_dict() rt = metadata_roundtripped.to_dict() assert deep_equal(orig, rt), f"Roundtrip mismatch:\nOriginal: {orig}\nRoundtripped: {rt}" # @st.composite # def advanced_indices(draw, *, shape): # basic_idxr = draw( # basic_indices( # shape=shape, min_dims=len(shape), max_dims=len(shape), allow_ellipsis=False # ).filter(lambda x: isinstance(x, tuple)) # ) # int_idxr = draw( # npst.integer_array_indices(shape=shape, result_shape=npst.array_shapes(max_dims=1)) # ) # args = tuple( # st.sampled_from((l, r)) for l, r in zip_longest(basic_idxr, int_idxr, fillvalue=slice(None)) # ) # return draw(st.tuples(*args)) # @given(st.data()) # def test_roundtrip_object_array(data): # nparray = data.draw(np_arrays) # zarray = data.draw(arrays(arrays=st.just(nparray))) # assert_array_equal(nparray, zarray[:]) def serialized_complex_float_is_valid( serialized: tuple[numbers.Real | str, numbers.Real | str], ) -> bool: """ Validate that the serialized representation of a complex float conforms to the spec. The specification requires that a serialized complex float must be either: - A JSON number, or - One of the strings "NaN", "Infinity", or "-Infinity". Args: serialized: The value produced by JSON serialization for a complex floating point number. Returns: bool: True if the serialized value is valid according to the spec, False otherwise. """ return ( isinstance(serialized, tuple) and len(serialized) == 2 and all(serialized_float_is_valid(x) for x in serialized) ) def serialized_float_is_valid(serialized: numbers.Real | str) -> bool: """ Validate that the serialized representation of a float conforms to the spec. The specification requires that a serialized float must be either: - A JSON number, or - One of the strings "NaN", "Infinity", or "-Infinity". Args: serialized: The value produced by JSON serialization for a floating point number. Returns: bool: True if the serialized value is valid according to the spec, False otherwise. """ if isinstance(serialized, numbers.Real): return True return serialized in ("NaN", "Infinity", "-Infinity") @given(meta=array_metadata()) # type: ignore[misc] @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_array_metadata_meets_spec(meta: ArrayV2Metadata | ArrayV3Metadata) -> None: """ Validate that the array metadata produced by the library conforms to the relevant spec (V2 vs V3). For ArrayV2Metadata: - Ensures that 'zarr_format' is 2. - Verifies that 'filters' is either None or a tuple (and not an empty tuple). For ArrayV3Metadata: - Ensures that 'zarr_format' is 3. For both versions: - If the dtype is a floating point of some kind, verifies of fill values: * NaN is serialized as the string "NaN" * Positive Infinity is serialized as the string "Infinity" * Negative Infinity is serialized as the string "-Infinity" * Other fill values are preserved as-is. - If the dtype is a complex number of some kind, verifies that each component of the fill value (real and imaginary) satisfies the serialization rules for floating point numbers. - If the dtype is a datetime of some kind, verifies that `NaT` values are serialized as "NaT". Note: This test validates spec-compliance for array metadata serialization. It is a work-in-progress and should be expanded as further edge cases are identified. """ asdict_dict = meta.to_dict() # version-specific validations if isinstance(meta, ArrayV2Metadata): assert asdict_dict["filters"] != () assert asdict_dict["filters"] is None or isinstance(asdict_dict["filters"], tuple) assert asdict_dict["zarr_format"] == 2 else: assert asdict_dict["zarr_format"] == 3 # version-agnostic validations dtype_native = meta.dtype.to_native_dtype() if dtype_native.kind == "f": assert serialized_float_is_valid(asdict_dict["fill_value"]) elif dtype_native.kind == "c": # fill_value should be a two-element array [real, imag]. assert serialized_complex_float_is_valid(asdict_dict["fill_value"]) elif dtype_native.kind in ("M", "m") and np.isnat(meta.fill_value): assert asdict_dict["fill_value"] == -9223372036854775808 zarr-python-3.2.1/tests/test_regression/000077500000000000000000000000001517635743000204065ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_regression/__init__.py000066400000000000000000000000001517635743000225050ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_regression/scripts/000077500000000000000000000000001517635743000220755ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_regression/scripts/__init__.py000066400000000000000000000000001517635743000241740ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_regression/scripts/v2.18.py000066400000000000000000000047361517635743000232370ustar00rootroot00000000000000# /// script # requires-python = ">=3.12" # dependencies = [ # "zarr==2.18", # "numcodecs==0.15" # ] # /// import argparse import zarr from zarr._storage.store import BaseStore def copy_group( *, node: zarr.hierarchy.Group, store: zarr.storage.BaseStore, path: str, overwrite: bool ) -> zarr.hierarchy.Group: result = zarr.group(store=store, path=path, overwrite=overwrite) result.attrs.put(node.attrs.asdict()) for key, child in node.items(): child_path = f"{path}/{key}" if isinstance(child, zarr.hierarchy.Group): copy_group(node=child, store=store, path=child_path, overwrite=overwrite) elif isinstance(child, zarr.core.Array): copy_array(node=child, store=store, overwrite=overwrite, path=child_path) return result def copy_array( *, node: zarr.core.Array, store: BaseStore, path: str, overwrite: bool ) -> zarr.core.Array: result = zarr.create( shape=node.shape, dtype=node.dtype, fill_value=node.fill_value, chunks=node.chunks, compressor=node.compressor, filters=node.filters, order=node.order, dimension_separator=node._dimension_separator, store=store, path=path, overwrite=overwrite, ) result.attrs.put(node.attrs.asdict()) result[:] = node[:] return result def copy_node( node: zarr.hierarchy.Group | zarr.core.Array, store: BaseStore, path: str, overwrite: bool ) -> zarr.hierarchy.Group | zarr.core.Array: if isinstance(node, zarr.hierarchy.Group): return copy_group(node=node, store=store, path=path, overwrite=overwrite) elif isinstance(node, zarr.core.Array): return copy_array(node=node, store=store, path=path, overwrite=overwrite) else: raise TypeError(f"Unexpected node type: {type(node)}") # pragma: no cover def cli() -> None: parser = argparse.ArgumentParser( description="Copy a zarr hierarchy from one location to another" ) parser.add_argument("source", type=str, help="Path to the source zarr hierarchy") parser.add_argument("destination", type=str, help="Path to the destination zarr hierarchy") args = parser.parse_args() src, dst = args.source, args.destination root_src = zarr.open(src, mode="r") result = copy_node(node=root_src, store=zarr.NestedDirectoryStore(dst), path="", overwrite=True) print(f"successfully created {result} at {dst}") def main() -> None: cli() if __name__ == "__main__": main() zarr-python-3.2.1/tests/test_regression/scripts/v3.0.8.py000066400000000000000000000036361517635743000233130ustar00rootroot00000000000000# /// script # requires-python = "==3.12" # dependencies = [ # "zarr==3.0.8", # "numcodecs==0.16.3" # ] # /// import argparse import zarr from zarr.abc.store import Store def copy_group( *, node: zarr.Group, store: Store, path: str, overwrite: bool ) -> zarr.Group: result = zarr.create_group( store=store, path=path, overwrite=overwrite, attributes=node.attrs.asdict(), zarr_format=node.metadata.zarr_format) for key, child in node.members(): child_path = f"{path}/{key}" if isinstance(child, zarr.Group): copy_group(node=child, store=store, path=child_path, overwrite=overwrite) else: copy_array(node=child, store=store, overwrite=overwrite, path=child_path) return result def copy_array( *, node: zarr.Array, store: Store, path: str, overwrite: bool ) -> zarr.Array: result = zarr.from_array(store, name=path, data=node, write_data=True) return result def copy_node( node: zarr.Group | zarr.Array, store: Store, path: str, overwrite: bool ) -> zarr.Group | zarr.Array: if isinstance(node, zarr.Group): return copy_group(node=node, store=store, path=path, overwrite=overwrite) else: return copy_array(node=node, store=store, path=path, overwrite=overwrite) def cli() -> None: parser = argparse.ArgumentParser( description="Copy a zarr hierarchy from one location to another" ) parser.add_argument("source", type=str, help="Path to the source zarr hierarchy") parser.add_argument("destination", type=str, help="Path to the destination zarr hierarchy") args = parser.parse_args() src, dst = args.source, args.destination root_src = zarr.open(src, mode="r") result = copy_node(node=root_src, store=dst, path="", overwrite=True) print(f"successfully created {result} at {dst}") def main() -> None: cli() if __name__ == "__main__": main() zarr-python-3.2.1/tests/test_regression/test_v2_dtype_regression.py000066400000000000000000000203771517635743000260240ustar00rootroot00000000000000import subprocess import sys from dataclasses import dataclass from itertools import product from pathlib import Path from typing import TYPE_CHECKING, Literal import numpy as np import pytest from numcodecs import LZ4, LZMA, Blosc, GZip, VLenBytes, VLenUTF8, Zstd import zarr import zarr.abc import zarr.abc.codec import zarr.codecs as zarrcodecs from zarr.abc.numcodec import Numcodec from zarr.core.chunk_key_encodings import V2ChunkKeyEncoding from zarr.core.dtype.npy.bytes import VariableLengthBytes from zarr.core.dtype.npy.string import VariableLengthUTF8 from zarr.storage import LocalStore from zarr.types import ArrayV2, ArrayV3 if TYPE_CHECKING: from zarr.core.dtype import ZDTypeLike ZarrPythonVersion = Literal["2.18", "3.0.8"] def runner_installed() -> bool: """ Check if a PEP-723 compliant python script runner is installed. """ try: subprocess.check_output(["uv", "--version"]) return True # noqa: TRY300 except FileNotFoundError: return False @dataclass(kw_only=True) class ArrayParams: values: np.ndarray[tuple[int], np.dtype[np.generic]] fill_value: np.generic | str | int | bytes filters: tuple[Numcodec, ...] = () serializer: str | None = None compressor: Numcodec basic_codecs: tuple[Numcodec, ...] = GZip(), Blosc(), LZ4(), LZMA(), Zstd() basic_dtypes = "|b", ">i2", ">i4", ">f4", ">f8", "c8", "c16", "M8[10us]", "m8[4ps]" string_dtypes = "U4" bytes_dtypes = ">S1", "V10", " ArrayV2: """ Writes a zarr array to a temporary directory based on the provided ArrayParams. The array is returned. """ dest = tmp_path / "in" store = LocalStore(dest) array_params: ArrayParams = request.param compressor = array_params.compressor chunk_key_encoding = V2ChunkKeyEncoding(separator="/") dtype: ZDTypeLike if array_params.values.dtype == np.dtype("|O") and array_params.serializer == "vlen-utf8": dtype = VariableLengthUTF8() # type: ignore[assignment] filters = array_params.filters + (VLenUTF8(),) elif array_params.values.dtype == np.dtype("|O") and array_params.serializer == "vlen-bytes": dtype = VariableLengthBytes() filters = array_params.filters + (VLenBytes(),) else: dtype = array_params.values.dtype filters = array_params.filters z = zarr.create_array( store, shape=array_params.values.shape, dtype=dtype, chunks=array_params.values.shape, compressors=compressor, filters=filters, fill_value=array_params.fill_value, order="C", chunk_key_encoding=chunk_key_encoding, write_data=True, zarr_format=2, ) z[:] = array_params.values return z @pytest.fixture def source_array_v3(tmp_path: Path, request: pytest.FixtureRequest) -> ArrayV3: """ Writes a zarr array to a temporary directory based on the provided ArrayParams. The array is returned. """ dest = tmp_path / "in" store = LocalStore(dest) array_params: ArrayParams = request.param chunk_key_encoding = V2ChunkKeyEncoding(separator="/") dtype: ZDTypeLike serializer: Literal["auto"] | zarr.abc.codec.Codec if array_params.values.dtype == np.dtype("|O") and array_params.serializer == "vlen-utf8": dtype = VariableLengthUTF8() # type: ignore[assignment] serializer = zarrcodecs.VLenUTF8Codec() elif array_params.values.dtype == np.dtype("|O") and array_params.serializer == "vlen-bytes": dtype = VariableLengthBytes() serializer = zarrcodecs.VLenBytesCodec() else: dtype = array_params.values.dtype serializer = "auto" if array_params.compressor == GZip(): compressor = zarrcodecs.GzipCodec() else: msg = ( "This test is only compatible with gzip compression at the moment, because the author" "did not want to implement a complete abstraction layer for v2 and v3 codecs in this test." ) raise ValueError(msg) z = zarr.create_array( store, shape=array_params.values.shape, dtype=dtype, chunks=array_params.values.shape, compressors=compressor, filters=array_params.filters, serializer=serializer, fill_value=array_params.fill_value, chunk_key_encoding=chunk_key_encoding, write_data=True, zarr_format=3, ) z[:] = array_params.values return z # TODO: make this dynamic based on the installed scripts script_paths = [Path(__file__).resolve().parent / "scripts" / "v2.18.py"] @pytest.mark.skipif( sys.platform == "darwin" and sys.version_info >= (3, 14), reason="Numcodecs pinned to 0.15 does not build on newer macos installations with newer python versions: see discussion https://github.com/zarr-developers/zarr-python/pull/3564#issuecomment-4081145034", ) @pytest.mark.skipif(not runner_installed(), reason="no python script runner installed") @pytest.mark.parametrize( "source_array_v2", array_cases_v2_18, indirect=True, ids=tuple(map(str, array_cases_v2_18)) ) @pytest.mark.parametrize("script_path", script_paths) def test_roundtrip_v2(source_array_v2: ArrayV2, tmp_path: Path, script_path: Path) -> None: out_path = tmp_path / "out" copy_op = subprocess.run( [ "uv", "run", str(script_path), str(source_array_v2.store).removeprefix("file://"), str(out_path), ], capture_output=True, text=True, ) assert copy_op.returncode == 0, f"stdout {copy_op.stdout}\n stderr{copy_op.stderr}" out_array = zarr.open_array(store=out_path, mode="r", zarr_format=2) assert source_array_v2.metadata.to_dict() == out_array.metadata.to_dict() assert np.array_equal(source_array_v2[:], out_array[:]) @pytest.mark.skipif(not runner_installed(), reason="no python script runner installed") @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") @pytest.mark.parametrize( "source_array_v3", array_cases_v3_08, indirect=True, ids=tuple(map(str, array_cases_v3_08)) ) def test_roundtrip_v3(source_array_v3: ArrayV3, tmp_path: Path) -> None: script_path = Path(__file__).resolve().parent / "scripts" / "v3.0.8.py" out_path = tmp_path / "out" copy_op = subprocess.run( [ "uv", "run", str(script_path), str(source_array_v3.store).removeprefix("file://"), str(out_path), ], capture_output=True, text=True, ) assert copy_op.returncode == 0 out_array = zarr.open_array(store=out_path, mode="r", zarr_format=3) assert source_array_v3.metadata.to_dict() == out_array.metadata.to_dict() assert np.array_equal(source_array_v3[:], out_array[:]) zarr-python-3.2.1/tests/test_store/000077500000000000000000000000001517635743000173625ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_store/__init__.py000066400000000000000000000000001517635743000214610ustar00rootroot00000000000000zarr-python-3.2.1/tests/test_store/test_core.py000066400000000000000000000236531517635743000217340ustar00rootroot00000000000000import tempfile from collections.abc import Callable, Generator from pathlib import Path from typing import Any, Literal import pytest from _pytest.compat import LEGACY_PATH import zarr from zarr import Group from zarr.core.common import AccessModeLiteral, ZarrFormat from zarr.storage import FsspecStore, LocalStore, MemoryStore, StoreLike, StorePath, ZipStore from zarr.storage._common import contains_array, contains_group, make_store_path from zarr.storage._utils import ( _join_paths, _normalize_path_keys, _normalize_paths, _relativize_path, normalize_path, ) @pytest.fixture( params=["none", "temp_dir_str", "temp_dir_path", "store_path", "memory_store", "dict"] ) def store_like( request: pytest.FixtureRequest, ) -> Generator[None | str | Path | StorePath | MemoryStore | dict[Any, Any], None, None]: if request.param == "none": yield None elif request.param == "temp_dir_str": with tempfile.TemporaryDirectory() as temp_dir: yield temp_dir elif request.param == "temp_dir_path": with tempfile.TemporaryDirectory() as temp_dir: yield Path(temp_dir) elif request.param == "store_path": yield StorePath(store=MemoryStore(store_dict={}), path="/") elif request.param == "memory_store": yield MemoryStore(store_dict={}) elif request.param == "dict": yield {} @pytest.mark.parametrize("path", ["foo", "foo/bar"]) @pytest.mark.parametrize("write_group", [True, False]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_contains_group( local_store: LocalStore, path: str, write_group: bool, zarr_format: ZarrFormat ) -> None: """ Test that the contains_group method correctly reports the existence of a group. """ root = Group.from_store(store=local_store, zarr_format=zarr_format) if write_group: root.create_group(path) store_path = StorePath(local_store, path=path) assert await contains_group(store_path, zarr_format=zarr_format) == write_group @pytest.mark.parametrize("path", ["foo", "foo/bar"]) @pytest.mark.parametrize("write_array", [True, False]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_contains_array( local_store: LocalStore, path: str, write_array: bool, zarr_format: ZarrFormat ) -> None: """ Test that the contains array method correctly reports the existence of an array. """ root = Group.from_store(store=local_store, zarr_format=zarr_format) if write_array: root.create_array(path, shape=(100,), chunks=(10,), dtype="i4") store_path = StorePath(local_store, path=path) assert await contains_array(store_path, zarr_format=zarr_format) == write_array @pytest.mark.parametrize("func", [contains_array, contains_group]) async def test_contains_invalid_format_raises( local_store: LocalStore, func: Callable[[Any], Any] ) -> None: """ Test contains_group and contains_array raise errors for invalid zarr_formats """ store_path = StorePath(local_store) with pytest.raises(ValueError): assert await func(store_path, zarr_format="3.0") # type: ignore[call-arg] @pytest.mark.parametrize("path", [None, "", "bar"]) async def test_make_store_path_none(path: str) -> None: """ Test that creating a store_path with None creates a memorystore """ store_path = await make_store_path(None, path=path) assert isinstance(store_path.store, MemoryStore) assert store_path.path == normalize_path(path) @pytest.mark.parametrize("path", [None, "", "bar"]) @pytest.mark.parametrize("store_type", [str, Path]) @pytest.mark.parametrize("mode", ["r", "w"]) async def test_make_store_path_local( tmpdir: LEGACY_PATH, store_type: type[str] | type[Path] | type[LocalStore], path: str, mode: AccessModeLiteral, ) -> None: """ Test the various ways of invoking make_store_path that create a LocalStore """ store_like = store_type(str(tmpdir)) store_path = await make_store_path(store_like, path=path, mode=mode) assert isinstance(store_path.store, LocalStore) assert Path(store_path.store.root) == Path(tmpdir) assert store_path.path == normalize_path(path) assert store_path.read_only == (mode == "r") @pytest.mark.parametrize("path", [None, "", "bar"]) @pytest.mark.parametrize("mode", ["r", "w"]) async def test_make_store_path_store_path( tmp_path: Path, path: str, mode: AccessModeLiteral ) -> None: """ Test invoking make_store_path when the input is another store_path. In particular we want to ensure that a new path is handled correctly. """ ro = mode == "r" store_like = await StorePath.open( LocalStore(str(tmp_path), read_only=ro), path="root", mode=mode ) store_path = await make_store_path(store_like, path=path, mode=mode) assert isinstance(store_path.store, LocalStore) assert Path(store_path.store.root) == tmp_path path_normalized = normalize_path(path) assert store_path.path == (store_like / path_normalized).path assert store_path.read_only == ro @pytest.mark.parametrize("modes", [(True, "w"), (False, "x")]) async def test_store_path_invalid_mode_raises( tmp_path: Path, modes: tuple[bool, Literal["w", "x"]] ) -> None: """ Test that ValueErrors are raise for invalid mode. """ with pytest.raises(ValueError): await StorePath.open(LocalStore(str(tmp_path), read_only=modes[0]), path="", mode=modes[1]) # type: ignore[arg-type] async def test_make_store_path_invalid() -> None: """ Test that invalid types raise TypeError """ with pytest.raises(TypeError): await make_store_path(1) async def test_make_store_path_fsspec() -> None: pytest.importorskip("fsspec") pytest.importorskip("requests") pytest.importorskip("aiohttp") store_path = await make_store_path("http://foo.com/bar") assert isinstance(store_path.store, FsspecStore) async def test_make_store_path_storage_options_raises(store_like: StoreLike) -> None: with pytest.raises(TypeError, match="storage_options"): await make_store_path(store_like, storage_options={"foo": "bar"}) async def test_unsupported() -> None: with pytest.raises(TypeError, match="Unsupported type for store_like: 'int'"): await make_store_path(1) @pytest.mark.parametrize( "path", [ "/foo/bar", "//foo/bar", "foo///bar", "foo/bar///", Path("foo/bar"), b"foo/bar", ], ) def test_normalize_path_valid(path: str | bytes | Path) -> None: assert normalize_path(path) == "foo/bar" def test_normalize_path_upath() -> None: upath = pytest.importorskip("upath") assert normalize_path(upath.UPath("foo/bar", protocol="memory")) == "memory:/foo/bar" def test_normalize_path_none() -> None: assert normalize_path(None) == "" @pytest.mark.parametrize("path", [".", ".."]) def test_normalize_path_invalid(path: str) -> None: with pytest.raises(ValueError): normalize_path(path) @pytest.mark.parametrize("paths", [("", "foo"), ("foo", "bar")]) def test_join_paths(paths: tuple[str, str]) -> None: """ Test that _join_paths joins paths in a way that is robust to an empty string """ observed = _join_paths(paths) if paths[0] == "": assert observed == paths[1] else: assert observed == "/".join(paths) class TestNormalizePaths: @staticmethod def test_valid() -> None: """ Test that path normalization works as expected """ paths = ["a", "b", "c", "d", "", "//a///b//"] assert _normalize_paths(paths) == tuple(normalize_path(p) for p in paths) @staticmethod @pytest.mark.parametrize("paths", [("", "/"), ("///a", "a")]) def test_invalid(paths: tuple[str, str]) -> None: """ Test that name collisions after normalization raise a ``ValueError`` """ msg = ( f"After normalization, the value '{paths[1]}' collides with '{paths[0]}'. " f"Both '{paths[1]}' and '{paths[0]}' normalize to the same value: '{normalize_path(paths[0])}'. " f"You should use either '{paths[1]}' or '{paths[0]}', but not both." ) with pytest.raises(ValueError, match=msg): _normalize_paths(paths) def test_normalize_path_keys() -> None: """ Test that ``_normalize_path_keys`` just applies the normalize_path function to each key of its input """ data = {"a": 10, "//b": 10} assert _normalize_path_keys(data) == {normalize_path(k): v for k, v in data.items()} @pytest.mark.parametrize( ("path", "prefix", "expected"), [ ("a", "", "a"), ("a/b/c", "a/b", "c"), ("a/b/c", "a", "b/c"), ], ) def test_relativize_path_valid(path: str, prefix: str, expected: str) -> None: """ Test the normal behavior of the _relativize_path function. Prefixes should be removed from the path argument. """ assert _relativize_path(path=path, prefix=prefix) == expected def test_relativize_path_invalid() -> None: path = "a/b/c" prefix = "b" msg = f"The first component of {path} does not start with {prefix}." with pytest.raises(ValueError, match=msg): _relativize_path(path="a/b/c", prefix="b") def test_different_open_mode(tmp_path: LEGACY_PATH) -> None: # Test with a store that implements .with_read_only() store = MemoryStore() zarr.create((100,), store=store, zarr_format=2, path="a") arr = zarr.open_array(store=store, path="a", zarr_format=2, mode="r") assert arr.store.read_only # Test with a store that doesn't implement .with_read_only() zarr_path = tmp_path / "foo.zarr" zip_store = ZipStore(zarr_path, mode="w") zarr.create((100,), store=zip_store, zarr_format=2, path="a") with pytest.raises( ValueError, match="Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store. Please use a read-only store or a storage class that implements .with_read_only().", ): zarr.open_array(store=zip_store, path="a", zarr_format=2, mode="r") zarr-python-3.2.1/tests/test_store/test_fsspec.py000066400000000000000000000513371517635743000222670ustar00rootroot00000000000000from __future__ import annotations import json import os import re from typing import TYPE_CHECKING, Any import numpy as np import pytest from packaging.version import parse as parse_version import zarr.api.asynchronous from zarr import Array from zarr.abc.store import OffsetByteRequest from zarr.core.buffer import Buffer, cpu, default_buffer_prototype from zarr.core.sync import _collect_aiterator, sync from zarr.errors import ZarrUserWarning from zarr.storage import FsspecStore from zarr.storage._common import make_store from zarr.storage._fsspec import _make_async from zarr.testing.store import StoreTests if TYPE_CHECKING: import pathlib from collections.abc import Generator from pathlib import Path import botocore.client import s3fs from zarr.core.common import JSON # Warning filter due to https://github.com/boto/boto3/issues/3889 pytestmark = [ pytest.mark.filterwarnings( re.escape("ignore:datetime.datetime.utcnow() is deprecated:DeprecationWarning") ), # TODO: fix these warnings pytest.mark.filterwarnings("ignore:Unclosed client session:ResourceWarning"), pytest.mark.filterwarnings( "ignore:coroutine 'ClientCreatorContext.__aexit__' was never awaited:RuntimeWarning" ), # s3fs finalizers can fail when sessions are garbage collected without being entered pytest.mark.filterwarnings( "ignore:Exception ignored in.*finalize object.*:pytest.PytestUnraisableExceptionWarning" ), ] fsspec = pytest.importorskip("fsspec") s3fs = pytest.importorskip("s3fs") requests = pytest.importorskip("requests") moto_server = pytest.importorskip("moto.moto_server.threaded_moto_server") moto = pytest.importorskip("moto") botocore = pytest.importorskip("botocore") # ### amended from s3fs ### # test_bucket_name = "test" secure_bucket_name = "test-secure" port = 5555 endpoint_url = f"http://127.0.0.1:{port}/" @pytest.fixture(scope="module") def s3_base() -> Generator[None, None, None]: # writable local S3 system # This fixture is module-scoped, meaning that we can reuse the MotoServer across all tests server = moto_server.ThreadedMotoServer(ip_address="127.0.0.1", port=port) server.start() if "AWS_SECRET_ACCESS_KEY" not in os.environ: os.environ["AWS_SECRET_ACCESS_KEY"] = "foo" if "AWS_ACCESS_KEY_ID" not in os.environ: os.environ["AWS_ACCESS_KEY_ID"] = "foo" yield server.stop() def get_boto3_client() -> botocore.client.BaseClient: # NB: we use the sync botocore client for setup session = botocore.session.Session() return session.create_client("s3", endpoint_url=endpoint_url) @pytest.fixture(autouse=True) def s3(s3_base: None) -> Generator[s3fs.S3FileSystem, None, None]: """ Quoting Martin Durant: pytest-asyncio creates a new event loop for each async test. When an async-mode s3fs instance is made from async, it will be assigned to the loop from which it is made. That means that if you use s3fs again from a subsequent test, you will have the same identical instance, but be running on a different loop - which fails. For the rest: it's very convenient to clean up the state of the store between tests, make sure we start off blank each time. https://github.com/zarr-developers/zarr-python/pull/1785#discussion_r1634856207 """ client = get_boto3_client() client.create_bucket(Bucket=test_bucket_name, ACL="public-read") s3fs.S3FileSystem.clear_instance_cache() s3 = s3fs.S3FileSystem(anon=False, client_kwargs={"endpoint_url": endpoint_url}) session = sync(s3.set_session()) s3.invalidate_cache() yield s3 requests.post(f"{endpoint_url}/moto-api/reset") client.close() sync(session.close()) # ### end from s3fs ### # async def test_basic() -> None: store = FsspecStore.from_url( f"s3://{test_bucket_name}/foo/spam/", storage_options={"endpoint_url": endpoint_url, "anon": False}, ) assert store.fs.asynchronous assert store.path == f"{test_bucket_name}/foo/spam" assert await _collect_aiterator(store.list()) == () assert not await store.exists("foo") data = b"hello" await store.set("foo", cpu.Buffer.from_bytes(data)) assert await store.exists("foo") buf = await store.get("foo", prototype=default_buffer_prototype()) assert buf is not None assert buf.to_bytes() == data out = await store.get_partial_values( prototype=default_buffer_prototype(), key_ranges=[("foo", OffsetByteRequest(1))] ) assert out[0] is not None assert out[0].to_bytes() == data[1:] class TestFsspecStoreS3(StoreTests[FsspecStore, cpu.Buffer]): store_cls = FsspecStore buffer_cls = cpu.Buffer @pytest.fixture def store_kwargs(self) -> dict[str, str | bool]: try: from fsspec import url_to_fs except ImportError: # before fsspec==2024.3.1 from fsspec.core import url_to_fs fs, path = url_to_fs( f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False, asynchronous=True ) return {"fs": fs, "path": path} @pytest.fixture async def store(self, store_kwargs: dict[str, Any]) -> FsspecStore: return self.store_cls(**store_kwargs) async def get(self, store: FsspecStore, key: str) -> Buffer: # make a new, synchronous instance of the filesystem because this test is run in sync code new_fs = fsspec.filesystem( "s3", endpoint_url=store.fs.endpoint_url, anon=store.fs.anon, asynchronous=False ) return self.buffer_cls.from_bytes(new_fs.cat(f"{store.path}/{key}")) async def set(self, store: FsspecStore, key: str, value: Buffer) -> None: # make a new, synchronous instance of the filesystem because this test is run in sync code new_fs = fsspec.filesystem( "s3", endpoint_url=store.fs.endpoint_url, anon=store.fs.anon, asynchronous=False ) new_fs.write_bytes(f"{store.path}/{key}", value.to_bytes()) def test_store_repr(self, store: FsspecStore) -> None: assert str(store) == "" def test_store_supports_writes(self, store: FsspecStore) -> None: assert store.supports_writes def test_store_supports_listing(self, store: FsspecStore) -> None: assert store.supports_listing async def test_fsspec_store_from_uri(self, store: FsspecStore) -> None: storage_options = { "endpoint_url": endpoint_url, "anon": False, } meta: dict[str, JSON] = { "attributes": {"key": "value"}, "zarr_format": 3, "node_type": "group", } await store.set( "zarr.json", self.buffer_cls.from_bytes(json.dumps(meta).encode()), ) group = await zarr.api.asynchronous.open_group( store=f"s3://{test_bucket_name}", storage_options=storage_options ) assert dict(group.attrs) == {"key": "value"} meta = { "attributes": {"key": "value-2"}, "zarr_format": 3, "node_type": "group", } await store.set( "directory-2/zarr.json", self.buffer_cls.from_bytes(json.dumps(meta).encode()), ) group = await zarr.api.asynchronous.open_group( store=f"s3://{test_bucket_name}/directory-2", storage_options=storage_options ) assert dict(group.attrs) == {"key": "value-2"} meta = { "attributes": {"key": "value-3"}, "zarr_format": 3, "node_type": "group", } await store.set( "directory-3/zarr.json", self.buffer_cls.from_bytes(json.dumps(meta).encode()), ) group = await zarr.api.asynchronous.open_group( store=f"s3://{test_bucket_name}", path="directory-3", storage_options=storage_options ) assert dict(group.attrs) == {"key": "value-3"} @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.03.01"), reason="Prior bug in from_upath", ) def test_from_upath(self) -> None: upath = pytest.importorskip("upath") path = upath.UPath( f"s3://{test_bucket_name}/foo/bar/", endpoint_url=endpoint_url, anon=False, asynchronous=True, ) result = FsspecStore.from_upath(path) assert result.fs.endpoint_url == endpoint_url assert result.fs.asynchronous assert result.path == f"{test_bucket_name}/foo/bar" def test_init_warns_if_fs_asynchronous_is_false(self) -> None: try: from fsspec import url_to_fs except ImportError: # before fsspec==2024.3.1 from fsspec.core import url_to_fs fs, path = url_to_fs( f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False, asynchronous=False ) store_kwargs = {"fs": fs, "path": path} with pytest.warns(ZarrUserWarning, match=r".* was not created with `asynchronous=True`.*"): self.store_cls(**store_kwargs) async def test_empty_nonexistent_path(self, store_kwargs: dict[str, Any]) -> None: # regression test for https://github.com/zarr-developers/zarr-python/pull/2343 store_kwargs["path"] += "/abc" store = await self.store_cls.open(**store_kwargs) assert await store.is_empty("") async def test_delete_dir_unsupported_deletes(self, store: FsspecStore) -> None: store.supports_deletes = False with pytest.raises( NotImplementedError, match="This method is only available for stores that support deletes.", ): await store.delete_dir("test_prefix") def array_roundtrip(store: FsspecStore) -> None: """ Round trip an array using a Zarr store Args: store: FsspecStore """ data = np.ones((3, 3)) arr = zarr.create_array(store=store, overwrite=True, data=data) assert isinstance(arr, Array) # Read set values arr2 = zarr.open_array(store=store) assert isinstance(arr2, Array) np.testing.assert_array_equal(arr[:], data) @pytest.mark.parametrize( ("root", "key", "expected"), [ # `"/"` as root collapses so that bare-key backends (notably # ReferenceFileSystem) get the right key. Regression test for # https://github.com/zarr-developers/zarr-python/issues/3922 . ("/", "zarr.json", "zarr.json"), ("", "zarr.json", "zarr.json"), # Trailing slashes on the root are stripped before joining. ("foo/", "zarr.json", "foo/zarr.json"), ("foo", "zarr.json", "foo/zarr.json"), # Leading slashes on the root are preserved -- absolute filesystem # paths must stay absolute. Regression test for the titiler-xarray # breakage that #3924 introduced when `normalize_path` was applied to # `FsspecStore.path`. ("/home/runner/data.zarr", "zarr.json", "/home/runner/data.zarr/zarr.json"), ("/home/runner/data.zarr/", "zarr.json", "/home/runner/data.zarr/zarr.json"), # Multi-segment keys. ("/home/foo", "a/b/zarr.json", "/home/foo/a/b/zarr.json"), ("", "a/b/zarr.json", "a/b/zarr.json"), # Trailing slash on the result is stripped (relevant when key is ""). ("/home/foo", "", "/home/foo"), ], ) def test_dereference_path(root: str, key: str, expected: str) -> None: """Verify the contract `_dereference_path` provides for `FsspecStore`. `FsspecStore.path` is stored verbatim; the join with a key must collapse a sentinel `"/"` root, strip trailing slashes, and preserve leading slashes on absolute paths. """ from zarr.storage._utils import _dereference_path assert _dereference_path(root, key) == expected async def test_fsspec_store_open_group_via_reference_filesystem() -> None: """End-to-end regression test for https://github.com/zarr-developers/zarr-python/issues/3922 . ``ReferenceFileSystem`` keys its refs by bare strings like ``"zarr.json"``. The bug was that ``FsspecStore(fs=ref_fs, path="/")`` produced ``"//zarr.json"`` at the join site and failed to find the entry, raising ``GroupNotFoundError``. This test pins ``path="/"`` explicitly to keep coverage even if the default value changes later. """ import json from fsspec.implementations.reference import ReferenceFileSystem group_json = json.dumps({"zarr_format": 3, "node_type": "group", "attributes": {}}) fs = ReferenceFileSystem( fo={"version": 1, "refs": {"zarr.json": group_json}}, asynchronous=True, ) store = FsspecStore(fs=fs, path="/", read_only=True) group = await zarr.api.asynchronous.open_group(store, mode="r") assert group.metadata.zarr_format == 3 async def test_fsspec_store_read_array_chunk_via_reference_filesystem() -> None: """End-to-end regression test that exercises the byte-range read path against ``ReferenceFileSystem``. Beyond opening a group (covered by ``test_fsspec_store_open_group_via_reference_filesystem``), this test constructs a small zarr v3 array whose chunk lives in the refs dict and reads it through the store. Path-handling bugs on the byte-range fetch path (used by kerchunk-style virtualization) would surface here rather than at metadata-open time. """ import json import numpy as np from fsspec.implementations.reference import ReferenceFileSystem # Construct a minimal v3 zarr: a single 1-D uint8 array of length 4 with # one chunk of size 4. The chunk bytes are little-endian uint8s 1..4. array_meta = json.dumps( { "zarr_format": 3, "node_type": "array", "shape": [4], "chunk_grid": {"name": "regular", "configuration": {"chunk_shape": [4]}}, "data_type": "uint8", "chunk_key_encoding": {"name": "default", "configuration": {"separator": "/"}}, "fill_value": 0, "codecs": [{"name": "bytes", "configuration": {"endian": "little"}}], "attributes": {}, } ) chunk_bytes = bytes([1, 2, 3, 4]) refs: dict[str, str] = { "zarr.json": array_meta, # ReferenceFileSystem accepts raw bytes via base64 encoding or # latin-1-decoded strings; latin-1 round-trips bytes 1:1. "c/0": chunk_bytes.decode("latin-1"), } fs = ReferenceFileSystem( fo={"version": 1, "refs": refs}, asynchronous=True, ) store = FsspecStore(fs=fs, path="/", read_only=True) array = await zarr.api.asynchronous.open_array(store=store, mode="r") data = await array.getitem(slice(None)) np.testing.assert_array_equal(data, np.array([1, 2, 3, 4], dtype="uint8")) @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) def test_wrap_sync_filesystem(tmp_path: pathlib.Path) -> None: """The local fs is not async so we should expect it to be wrapped automatically""" from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper store = FsspecStore.from_url(f"file://{tmp_path}", storage_options={"auto_mkdir": True}) assert isinstance(store.fs, AsyncFileSystemWrapper) assert store.fs.async_impl array_roundtrip(store) @pytest.mark.skipif( parse_version(fsspec.__version__) >= parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) def test_wrap_sync_filesystem_raises(tmp_path: pathlib.Path) -> None: """The local fs is not async so we should expect it to be wrapped automatically""" with pytest.raises(ImportError, match="The filesystem .*"): FsspecStore.from_url(f"file://{tmp_path}", storage_options={"auto_mkdir": True}) @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) def test_no_wrap_async_filesystem() -> None: """An async fs should not be wrapped automatically; fsspec's s3 filesystem is such an fs""" from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper store = FsspecStore.from_url( f"s3://{test_bucket_name}/foo/spam/", storage_options={"endpoint_url": endpoint_url, "anon": False, "asynchronous": True}, read_only=False, ) assert not isinstance(store.fs, AsyncFileSystemWrapper) assert store.fs.async_impl array_roundtrip(store) @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) def test_open_fsmap_file(tmp_path: pathlib.Path) -> None: min_fsspec_with_async_wrapper = parse_version("2024.12.0") current_version = parse_version(fsspec.__version__) fs = fsspec.filesystem("file", auto_mkdir=True) mapper = fs.get_mapper(tmp_path) if current_version < min_fsspec_with_async_wrapper: # Expect ImportError for older versions with pytest.raises( ImportError, match=r"The filesystem .* is synchronous, and the required AsyncFileSystemWrapper is not available.*", ): array_roundtrip(mapper) else: # Newer versions should work array_roundtrip(mapper) @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) def test_open_fsmap_file_raises(tmp_path: pathlib.Path) -> None: fsspec = pytest.importorskip("fsspec.implementations.local") fs = fsspec.LocalFileSystem(auto_mkdir=False) mapper = fs.get_mapper(tmp_path) with pytest.raises(FileNotFoundError, match="No such file or directory: .*"): array_roundtrip(mapper) @pytest.mark.parametrize("asynchronous", [True, False]) def test_open_fsmap_s3(asynchronous: bool) -> None: s3_filesystem = s3fs.S3FileSystem( asynchronous=asynchronous, endpoint_url=endpoint_url, anon=False ) mapper = s3_filesystem.get_mapper(f"s3://{test_bucket_name}/map/foo/") array_roundtrip(mapper) def test_open_s3map_raises() -> None: with pytest.raises(TypeError, match="Unsupported type for store_like:.*"): zarr.open(store=0, mode="w", shape=(3, 3)) s3_filesystem = s3fs.S3FileSystem(asynchronous=True, endpoint_url=endpoint_url, anon=False) mapper = s3_filesystem.get_mapper(f"s3://{test_bucket_name}/map/foo/") with pytest.raises( ValueError, match="'path' was provided but is not used for FSMap store_like objects" ): zarr.open(store=mapper, path="bar", mode="w", shape=(3, 3)) with pytest.raises( TypeError, match="'storage_options' is only used when the store is passed as an FSSpec URI string.", ): zarr.open(store=mapper, storage_options={"anon": True}, mode="w", shape=(3, 3)) @pytest.mark.parametrize("asynchronous", [True, False]) def test_make_async(asynchronous: bool) -> None: s3_filesystem = s3fs.S3FileSystem( asynchronous=asynchronous, endpoint_url=endpoint_url, anon=False ) fs = _make_async(s3_filesystem) assert fs.asynchronous @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) async def test_delete_dir_wrapped_filesystem(tmp_path: Path) -> None: from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper from fsspec.implementations.local import LocalFileSystem wrapped_fs = AsyncFileSystemWrapper(LocalFileSystem(auto_mkdir=True)) store = FsspecStore(wrapped_fs, read_only=False, path=f"{tmp_path}/test/path") assert isinstance(store.fs, AsyncFileSystemWrapper) assert store.fs.asynchronous await store.set("zarr.json", cpu.Buffer.from_bytes(b"root")) await store.set("foo-bar/zarr.json", cpu.Buffer.from_bytes(b"root")) await store.set("foo/zarr.json", cpu.Buffer.from_bytes(b"bar")) await store.set("foo/c/0", cpu.Buffer.from_bytes(b"chunk")) await store.delete_dir("foo") assert await store.exists("zarr.json") assert await store.exists("foo-bar/zarr.json") assert not await store.exists("foo/zarr.json") assert not await store.exists("foo/c/0") @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) async def test_with_read_only_auto_mkdir(tmp_path: Path) -> None: """ Test that creating a read-only copy of a store backed by the local file system does not error if auto_mkdir is False. """ store_w = FsspecStore.from_url(f"file://{tmp_path}", storage_options={"auto_mkdir": False}) _ = store_w.with_read_only() @pytest.mark.skipif( parse_version(fsspec.__version__) < parse_version("2024.12.0"), reason="No AsyncFileSystemWrapper", ) async def test_memory_scheme() -> None: """Test that the "memory" scheme creates a `MemoryFileSystem`-backed store""" store = await make_store("memory://test") assert isinstance(store, FsspecStore) assert store.fs.protocol == "memory" zarr-python-3.2.1/tests/test_store/test_latency.py000066400000000000000000000041011517635743000224260ustar00rootroot00000000000000from __future__ import annotations import pytest from zarr.core.buffer import default_buffer_prototype from zarr.storage import MemoryStore from zarr.testing.store import LatencyStore async def test_latency_store_with_read_only_round_trip() -> None: """ Ensure that LatencyStore.with_read_only returns another LatencyStore with the requested read_only state, preserves latency configuration, and does not change the original wrapper. """ base = await MemoryStore.open() # Start from a read-only underlying store ro_base = base.with_read_only(read_only=True) latency_ro = LatencyStore(ro_base, get_latency=0.01, set_latency=0.02) assert latency_ro.read_only assert latency_ro.get_latency == pytest.approx(0.01) assert latency_ro.set_latency == pytest.approx(0.02) buf = default_buffer_prototype().buffer.from_bytes(b"abcd") # Cannot write through the read-only wrapper with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await latency_ro.set("key", buf) # Create a writable wrapper from the read-only one writer = latency_ro.with_read_only(read_only=False) assert isinstance(writer, LatencyStore) assert not writer.read_only # Latency configuration is preserved assert writer.get_latency == latency_ro.get_latency assert writer.set_latency == latency_ro.set_latency # Writes via the writable wrapper succeed await writer.set("key", buf) out = await writer.get("key", prototype=default_buffer_prototype()) assert out is not None assert out.to_bytes() == buf.to_bytes() # Creating a read-only copy from the writable wrapper works and is enforced reader = writer.with_read_only(read_only=True) assert isinstance(reader, LatencyStore) assert reader.read_only with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await reader.set("other", buf) # The original read-only wrapper remains read-only assert latency_ro.read_only zarr-python-3.2.1/tests/test_store/test_local.py000066400000000000000000000170061517635743000220710ustar00rootroot00000000000000from __future__ import annotations import json import pathlib import re from typing import TYPE_CHECKING import numpy as np import pytest import zarr from zarr import create_array from zarr.core.buffer import Buffer, cpu from zarr.core.sync import sync from zarr.storage import LocalStore from zarr.storage._local import _atomic_write from zarr.testing.store import StoreTests from zarr.testing.utils import assert_bytes_equal if TYPE_CHECKING: from zarr.core.buffer import BufferPrototype class TestLocalStore(StoreTests[LocalStore, cpu.Buffer]): store_cls = LocalStore buffer_cls = cpu.Buffer async def get(self, store: LocalStore, key: str) -> Buffer: return self.buffer_cls.from_bytes((store.root / key).read_bytes()) async def set(self, store: LocalStore, key: str, value: Buffer) -> None: parent = (store.root / key).parent if not parent.exists(): parent.mkdir(parents=True) (store.root / key).write_bytes(value.to_bytes()) @pytest.fixture def store_kwargs(self, tmpdir: str) -> dict[str, str]: return {"root": str(tmpdir)} def test_store_repr(self, store: LocalStore) -> None: assert str(store) == f"file://{store.root.as_posix()}" def test_store_supports_writes(self, store: LocalStore) -> None: assert store.supports_writes def test_store_supports_listing(self, store: LocalStore) -> None: assert store.supports_listing async def test_empty_with_empty_subdir(self, store: LocalStore) -> None: assert await store.is_empty("") (store.root / "foo/bar").mkdir(parents=True) assert await store.is_empty("") def test_creates_new_directory(self, tmp_path: pathlib.Path) -> None: target = tmp_path.joinpath("a", "b", "c") assert not target.exists() store = self.store_cls(root=target) zarr.group(store=store) def test_invalid_root_raises(self) -> None: """ Test that a TypeError is raised when a non-str/Path type is used for the `root` argument """ with pytest.raises( TypeError, match=r"'root' must be a string or Path instance. Got an instance of instead.", ): LocalStore(root=0) # type: ignore[arg-type] async def test_get_with_prototype_default(self, store: LocalStore) -> None: """ Ensure that data can be read via ``store.get`` if the prototype keyword argument is unspecified, i.e. set to ``None``. """ data_buf = self.buffer_cls.from_bytes(b"\x01\x02\x03\x04") key = "c/0" await self.set(store, key, data_buf) observed = await store.get(key, prototype=None) assert_bytes_equal(observed, data_buf) @pytest.mark.parametrize("ndim", [0, 1, 3]) @pytest.mark.parametrize( "destination", ["destination", "foo/bar/destintion", pathlib.Path("foo/bar/destintion")] ) async def test_move( self, tmp_path: pathlib.Path, ndim: int, destination: pathlib.Path | str ) -> None: origin = tmp_path / "origin" if isinstance(destination, str): destination = str(tmp_path / destination) else: destination = tmp_path / destination print(type(destination)) store = await LocalStore.open(root=origin) shape = (4,) * ndim chunks = (2,) * ndim data = np.arange(4**ndim) if ndim > 0: data = data.reshape(*shape) array = create_array(store, data=data, chunks=chunks or "auto") await store.move(destination) assert store.root == pathlib.Path(destination) assert pathlib.Path(destination).exists() assert not origin.exists() assert np.array_equal(array[...], data) store2 = await LocalStore.open(root=origin) with pytest.raises( FileExistsError, match=re.escape(f"Destination root {destination} already exists") ): await store2.move(destination) @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) async def test_get_bytes_with_prototype_none( self, store: LocalStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_bytes works with prototype=None.""" data = b"hello world" key = "test_key" await self.set(store, key, self.buffer_cls.from_bytes(data)) result = await store._get_bytes(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) def test_get_bytes_sync_with_prototype_none( self, store: LocalStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_bytes_sync works with prototype=None.""" data = b"hello world" key = "test_key" sync(self.set(store, key, self.buffer_cls.from_bytes(data))) result = store._get_bytes_sync(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) async def test_get_json_with_prototype_none( self, store: LocalStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_json works with prototype=None.""" data = {"foo": "bar", "number": 42} key = "test.json" await self.set(store, key, self.buffer_cls.from_bytes(json.dumps(data).encode())) result = await store._get_json(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) def test_get_json_sync_with_prototype_none( self, store: LocalStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_json_sync works with prototype=None.""" data = {"foo": "bar", "number": 42} key = "test.json" sync(self.set(store, key, self.buffer_cls.from_bytes(json.dumps(data).encode()))) result = store._get_json_sync(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("exclusive", [True, False]) def test_atomic_write_successful(tmp_path: pathlib.Path, exclusive: bool) -> None: path = tmp_path / "data" with _atomic_write(path, "wb", exclusive=exclusive) as f: f.write(b"abc") assert path.read_bytes() == b"abc" assert list(path.parent.iterdir()) == [path] # no temp files @pytest.mark.parametrize("exclusive", [True, False]) def test_atomic_write_incomplete(tmp_path: pathlib.Path, exclusive: bool) -> None: path = tmp_path / "data" with pytest.raises(RuntimeError): # noqa: PT012 with _atomic_write(path, "wb", exclusive=exclusive) as f: f.write(b"a") raise RuntimeError assert not path.exists() assert list(path.parent.iterdir()) == [] # no temp files def test_atomic_write_non_exclusive_preexisting(tmp_path: pathlib.Path) -> None: path = tmp_path / "data" with path.open("wb") as f: f.write(b"xyz") assert path.read_bytes() == b"xyz" with _atomic_write(path, "wb", exclusive=False) as f: f.write(b"abc") assert path.read_bytes() == b"abc" assert list(path.parent.iterdir()) == [path] # no temp files def test_atomic_write_exclusive_preexisting(tmp_path: pathlib.Path) -> None: path = tmp_path / "data" with path.open("wb") as f: f.write(b"xyz") assert path.read_bytes() == b"xyz" with pytest.raises(FileExistsError): with _atomic_write(path, "wb", exclusive=True) as f: f.write(b"abc") assert path.read_bytes() == b"xyz" assert list(path.parent.iterdir()) == [path] # no temp files zarr-python-3.2.1/tests/test_store/test_logging.py000066400000000000000000000153741517635743000224330ustar00rootroot00000000000000from __future__ import annotations import logging from typing import TYPE_CHECKING, TypedDict import pytest import zarr from zarr.core.buffer import Buffer, cpu, default_buffer_prototype from zarr.storage import LocalStore, LoggingStore from zarr.testing.store import StoreTests if TYPE_CHECKING: from pathlib import Path from zarr.abc.store import Store class StoreKwargs(TypedDict): store: LocalStore log_level: str class TestLoggingStore(StoreTests[LoggingStore[LocalStore], cpu.Buffer]): # store_cls is needed to do an isinstance check, so can't be a subscripted generic store_cls = LoggingStore # type: ignore[assignment] buffer_cls = cpu.Buffer async def get(self, store: LoggingStore[LocalStore], key: str) -> Buffer: return self.buffer_cls.from_bytes((store._store.root / key).read_bytes()) async def set(self, store: LoggingStore[LocalStore], key: str, value: Buffer) -> None: parent = (store._store.root / key).parent if not parent.exists(): parent.mkdir(parents=True) (store._store.root / key).write_bytes(value.to_bytes()) @pytest.fixture def store_kwargs(self, tmp_path: Path) -> StoreKwargs: return {"store": LocalStore(str(tmp_path)), "log_level": "DEBUG"} @pytest.fixture def open_kwargs(self, tmp_path: Path) -> dict[str, type[LocalStore] | str]: return {"store_cls": LocalStore, "root": str(tmp_path), "log_level": "DEBUG"} @pytest.fixture def store(self, store_kwargs: StoreKwargs) -> LoggingStore[LocalStore]: return self.store_cls(**store_kwargs) def test_store_supports_writes(self, store: LoggingStore[LocalStore]) -> None: assert store.supports_writes def test_store_supports_listing(self, store: LoggingStore[LocalStore]) -> None: assert store.supports_listing def test_store_repr(self, store: LoggingStore[LocalStore]) -> None: assert f"{store!r}" == f"LoggingStore(LocalStore, 'file://{store._store.root.as_posix()}')" def test_store_str(self, store: LoggingStore[LocalStore]) -> None: assert str(store) == f"logging-file://{store._store.root.as_posix()}" async def test_default_handler( self, local_store: LocalStore, capsys: pytest.CaptureFixture[str] ) -> None: # Store and then remove existing handlers to enter default handler code path handlers = logging.getLogger().handlers[:] for h in handlers: logging.getLogger().removeHandler(h) # Test logs are sent to stdout wrapped = LoggingStore(store=local_store) buffer = default_buffer_prototype().buffer res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) # type: ignore[func-returns-value] assert res is None captured = capsys.readouterr() assert len(captured) == 2 assert "Calling LocalStore.set" in captured.out assert "Finished LocalStore.set" in captured.out # Restore handlers for h in handlers: logging.getLogger().addHandler(h) def test_is_open_setter_raises(self, store: LoggingStore[LocalStore]) -> None: "Test that a user cannot change `_is_open` without opening the underlying store." with pytest.raises( NotImplementedError, match="LoggingStore must be opened via the `_open` method" ): store._is_open = True async def test_with_read_only_round_trip(self, local_store: LocalStore) -> None: """ Ensure that LoggingStore.with_read_only returns another LoggingStore with the requested read_only state, preserves logging configuration, and does not change the original store. """ # Start from a read-only underlying store ro_store = local_store.with_read_only(read_only=True) wrapped_ro = LoggingStore(store=ro_store, log_level="INFO") assert wrapped_ro.read_only buf = default_buffer_prototype().buffer.from_bytes(b"0123") # Cannot write through the read-only wrapper with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await wrapped_ro.set("foo", buf) # Create a writable wrapper writer = wrapped_ro.with_read_only(read_only=False) assert isinstance(writer, LoggingStore) assert not writer.read_only # logging configuration is preserved assert writer.log_level == wrapped_ro.log_level assert writer.log_handler == wrapped_ro.log_handler # Writes via the writable wrapper succeed await writer.set("foo", buf) out = await writer.get("foo", prototype=default_buffer_prototype()) assert out is not None assert out.to_bytes() == buf.to_bytes() # The original wrapper remains read-only assert wrapped_ro.read_only with pytest.raises( ValueError, match="store was opened in read-only mode and does not support writing" ): await wrapped_ro.set("bar", buf) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) async def test_logging_store(store: Store, caplog: pytest.LogCaptureFixture) -> None: wrapped = LoggingStore(store=store, log_level="DEBUG") buffer = default_buffer_prototype().buffer caplog.clear() res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) # type: ignore[func-returns-value] assert res is None assert len(caplog.record_tuples) == 2 for tup in caplog.record_tuples: assert str(store) in tup[0] assert f"Calling {type(store).__name__}.set" in caplog.record_tuples[0][2] assert f"Finished {type(store).__name__}.set" in caplog.record_tuples[1][2] caplog.clear() keys = [k async for k in wrapped.list()] assert keys == ["foo/bar/c/0"] assert len(caplog.record_tuples) == 2 for tup in caplog.record_tuples: assert str(store) in tup[0] assert f"Calling {type(store).__name__}.list" in caplog.record_tuples[0][2] assert f"Finished {type(store).__name__}.list" in caplog.record_tuples[1][2] @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) async def test_logging_store_counter(store: Store) -> None: wrapped = LoggingStore(store=store, log_level="DEBUG") arr = zarr.create(shape=(10,), store=wrapped, overwrite=True) arr[:] = 1 assert wrapped.counter["set"] == 2 assert wrapped.counter["list"] == 0 assert wrapped.counter["list_dir"] == 0 assert wrapped.counter["list_prefix"] == 0 if store.supports_deletes: assert wrapped.counter["get"] == 0 # 1 if overwrite=False assert wrapped.counter["delete_dir"] == 1 else: assert wrapped.counter["get"] == 1 assert wrapped.counter["delete_dir"] == 0 zarr-python-3.2.1/tests/test_store/test_memory.py000066400000000000000000000472421517635743000223140ustar00rootroot00000000000000from __future__ import annotations import json import re from typing import TYPE_CHECKING, Any import numpy as np import numpy.typing as npt import pytest import zarr from zarr.core.buffer import Buffer, cpu, gpu from zarr.core.sync import sync from zarr.errors import ZarrUserWarning from zarr.storage import GpuMemoryStore, ManagedMemoryStore, MemoryStore from zarr.testing.store import StoreTests from zarr.testing.utils import gpu_test if TYPE_CHECKING: from zarr.core.buffer import BufferPrototype from zarr.core.common import ZarrFormat # TODO: work out where this warning is coming from and fix it @pytest.mark.filterwarnings( re.escape("ignore:coroutine 'ClientCreatorContext.__aexit__' was never awaited") ) class TestMemoryStore(StoreTests[MemoryStore, cpu.Buffer]): store_cls = MemoryStore buffer_cls = cpu.Buffer async def set(self, store: MemoryStore, key: str, value: Buffer) -> None: store._store_dict[key] = value async def get(self, store: MemoryStore, key: str) -> Buffer: return store._store_dict[key] @pytest.fixture(params=[None, True]) def store_kwargs(self, request: pytest.FixtureRequest) -> dict[str, Any]: kwargs: dict[str, Any] if request.param is True: kwargs = {"store_dict": {}} else: kwargs = {"store_dict": None} return kwargs @pytest.fixture async def store(self, store_kwargs: dict[str, Any]) -> MemoryStore: return self.store_cls(**store_kwargs) def test_store_repr(self, store: MemoryStore) -> None: assert str(store) == f"memory://{id(store._store_dict)}" def test_store_supports_writes(self, store: MemoryStore) -> None: assert store.supports_writes def test_store_supports_listing(self, store: MemoryStore) -> None: assert store.supports_listing async def test_list_prefix(self, store: MemoryStore) -> None: assert True @pytest.mark.parametrize("dtype", ["uint8", "float32", "int64"]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_deterministic_size( self, store: MemoryStore, dtype: npt.DTypeLike, zarr_format: ZarrFormat ) -> None: a = zarr.empty( store=store, shape=(3,), chunks=(1000,), dtype=dtype, zarr_format=zarr_format, overwrite=True, ) a[...] = 1 a.resize((1000,)) np.testing.assert_array_equal(a[:3], 1) np.testing.assert_array_equal(a[3:], 0) @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) async def test_get_bytes_with_prototype_none( self, store: MemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_bytes works with prototype=None.""" data = b"hello world" key = "test_key" await self.set(store, key, self.buffer_cls.from_bytes(data)) result = await store._get_bytes(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) def test_get_bytes_sync_with_prototype_none( self, store: MemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_bytes_sync works with prototype=None.""" data = b"hello world" key = "test_key" sync(self.set(store, key, self.buffer_cls.from_bytes(data))) result = store._get_bytes_sync(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) async def test_get_json_with_prototype_none( self, store: MemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_json works with prototype=None.""" data = {"foo": "bar", "number": 42} key = "test.json" await self.set(store, key, self.buffer_cls.from_bytes(json.dumps(data).encode())) result = await store._get_json(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) def test_get_json_sync_with_prototype_none( self, store: MemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_json_sync works with prototype=None.""" data = {"foo": "bar", "number": 42} key = "test.json" sync(self.set(store, key, self.buffer_cls.from_bytes(json.dumps(data).encode()))) result = store._get_json_sync(key, prototype=buffer_cls) assert result == data # TODO: fix this warning @pytest.mark.filterwarnings("ignore:Unclosed client session:ResourceWarning") @gpu_test class TestGpuMemoryStore(StoreTests[GpuMemoryStore, gpu.Buffer]): store_cls = GpuMemoryStore buffer_cls = gpu.Buffer async def set(self, store: GpuMemoryStore, key: str, value: gpu.Buffer) -> None: # type: ignore[override] store._store_dict[key] = value async def get(self, store: MemoryStore, key: str) -> Buffer: return store._store_dict[key] @pytest.fixture(params=[None, True]) def store_kwargs(self, request: pytest.FixtureRequest) -> dict[str, Any]: kwargs: dict[str, Any] if request.param is True: kwargs = {"store_dict": {}} else: kwargs = {"store_dict": None} return kwargs @pytest.fixture async def store(self, store_kwargs: dict[str, Any]) -> GpuMemoryStore: return self.store_cls(**store_kwargs) def test_store_repr(self, store: GpuMemoryStore) -> None: assert str(store) == f"gpumemory://{id(store._store_dict)}" def test_store_supports_writes(self, store: GpuMemoryStore) -> None: assert store.supports_writes def test_store_supports_listing(self, store: GpuMemoryStore) -> None: assert store.supports_listing async def test_list_prefix(self, store: GpuMemoryStore) -> None: assert True def test_dict_reference(self, store: GpuMemoryStore) -> None: store_dict: dict[str, Any] = {} result = GpuMemoryStore(store_dict=store_dict) assert result._store_dict is store_dict def test_from_dict(self) -> None: d = { "a": gpu.Buffer.from_bytes(b"aaaa"), "b": cpu.Buffer.from_bytes(b"bbbb"), } msg = "Creating a zarr.buffer.gpu.Buffer with an array that does not support the __cuda_array_interface__ for zero-copy transfers, falling back to slow copy based path" with pytest.warns(ZarrUserWarning, match=msg): result = GpuMemoryStore.from_dict(d) for v in result._store_dict.values(): assert type(v) is gpu.Buffer class TestManagedMemoryStore(StoreTests[ManagedMemoryStore, cpu.Buffer]): store_cls = ManagedMemoryStore buffer_cls = cpu.Buffer async def set(self, store: ManagedMemoryStore, key: str, value: Buffer) -> None: store._store_dict[key] = value async def get(self, store: ManagedMemoryStore, key: str) -> Buffer: return store._store_dict[key] @pytest.fixture def store_kwargs(self, request: pytest.FixtureRequest) -> dict[str, Any]: # Use a unique name per test to avoid sharing state between tests # but ensure the name is deterministic for equality tests # Replace '/' with '-' since store names cannot contain '/' sanitized_name = request.node.name.replace("/", "-") return {"name": f"test-{sanitized_name}"} @pytest.fixture async def store(self, store_kwargs: dict[str, Any]) -> ManagedMemoryStore: return self.store_cls(**store_kwargs) def test_store_repr(self, store: ManagedMemoryStore) -> None: assert str(store) == f"memory://{store.name}" async def test_serializable_store(self, store: ManagedMemoryStore) -> None: """ Test pickling semantics for ManagedMemoryStore. When pickled and unpickled within the same process (where the original store still exists in the registry), the unpickled store reconnects to the same backing dict. """ import pickle # Add some data to the store await store.set("test-key", self.buffer_cls.from_bytes(b"test-value")) # Pickle and unpickle the store pickled = pickle.dumps(store) store2 = pickle.loads(pickled) # The unpickled store should reconnect to the same backing dict assert store2._store_dict is store._store_dict assert store2.name == store.name assert store2.path == store.path assert store2.read_only == store.read_only # The data should be accessible result = await store2.get("test-key") assert result is not None assert result.to_bytes() == b"test-value" async def test_pickle_with_path(self) -> None: """Test that path is preserved through pickle round-trip.""" import pickle store = ManagedMemoryStore(name="pickle-path-test", path="some/path") await store.set("key", self.buffer_cls.from_bytes(b"value")) pickled = pickle.dumps(store) store2 = pickle.loads(pickled) assert store2.path == "some/path" assert store2._store_dict is store._store_dict # Check that operations use the path correctly result = await store2.get("key") assert result is not None assert result.to_bytes() == b"value" def test_pickle_after_gc(self) -> None: """ Test that unpickling after the original store is garbage collected creates a new empty store with the same name (in the same process). """ import gc import pickle # Create a store with a unique name and pickle it store = ManagedMemoryStore(name="gc-pickle-test") store._store_dict["key"] = self.buffer_cls.from_bytes(b"value") pickled = pickle.dumps(store) # Delete the store and garbage collect del store gc.collect() # Unpickling should create a new store with an empty dict store2 = pickle.loads(pickled) assert store2.name == "gc-pickle-test" # The dict is empty because the original was garbage collected assert len(store2._store_dict) == 0 async def test_cross_process_detection(self) -> None: """ Test that unpickling a ManagedMemoryStore in a different process raises an error. This prevents silent data loss when a store is pickled and unpickled in a different process (e.g., with multiprocessing). """ import os store = ManagedMemoryStore(name="cross-process-test") await store.set("key", self.buffer_cls.from_bytes(b"value")) # Get the reduce tuple and modify the state to simulate a different process cls, args, state = store.__reduce__() state["created_pid"] = os.getpid() + 1 # Fake a different process ID # Manually reconstruct what pickle.loads would do # This simulates unpickling data that was pickled in a different process reconstructed = cls(*args) with pytest.raises(RuntimeError, match="was created in process"): reconstructed.__setstate__(state) def test_store_supports_writes(self, store: ManagedMemoryStore) -> None: assert store.supports_writes def test_store_supports_listing(self, store: ManagedMemoryStore) -> None: assert store.supports_listing @pytest.mark.parametrize("dtype", ["uint8", "float32", "int64"]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_deterministic_size( self, store: MemoryStore, dtype: npt.DTypeLike, zarr_format: ZarrFormat ) -> None: a = zarr.empty( store=store, shape=(3,), chunks=(1000,), dtype=dtype, zarr_format=zarr_format, overwrite=True, ) a[...] = 1 a.resize((1000,)) np.testing.assert_array_equal(a[:3], 1) np.testing.assert_array_equal(a[3:], 0) @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) async def test_get_bytes_with_prototype_none( self, store: ManagedMemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_bytes works with prototype=None.""" data = b"hello world" key = "test_key" await self.set(store, key, self.buffer_cls.from_bytes(data)) result = await store._get_bytes(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) def test_get_bytes_sync_with_prototype_none( self, store: ManagedMemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_bytes_sync works with prototype=None.""" data = b"hello world" key = "test_key" sync(self.set(store, key, self.buffer_cls.from_bytes(data))) result = store._get_bytes_sync(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) async def test_get_json_with_prototype_none( self, store: ManagedMemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_json works with prototype=None.""" data = {"foo": "bar", "number": 42} key = "test.json" await self.set(store, key, self.buffer_cls.from_bytes(json.dumps(data).encode())) result = await store._get_json(key, prototype=buffer_cls) assert result == data @pytest.mark.parametrize("buffer_cls", [None, cpu.buffer_prototype]) def test_get_json_sync_with_prototype_none( self, store: ManagedMemoryStore, buffer_cls: None | BufferPrototype ) -> None: """Test that get_json_sync works with prototype=None.""" data = {"foo": "bar", "number": 42} key = "test.json" sync(self.set(store, key, self.buffer_cls.from_bytes(json.dumps(data).encode()))) result = store._get_json_sync(key, prototype=buffer_cls) assert result == data def test_from_url(self, store: ManagedMemoryStore) -> None: """Test that from_url creates a store sharing the same dict.""" url = str(store) store2 = ManagedMemoryStore.from_url(url) assert store2._store_dict is store._store_dict def test_from_url_with_path(self, store: ManagedMemoryStore) -> None: """Test that from_url extracts path component from URL.""" url = f"{store}/some/path" store2 = ManagedMemoryStore.from_url(url) assert store2._store_dict is store._store_dict assert store2.path == "some/path" assert str(store2) == url def test_from_url_invalid(self) -> None: """Test that from_url raises ValueError for non-existent store.""" with pytest.raises(ValueError, match="Memory store not found"): ManagedMemoryStore.from_url("memory://nonexistent-store") def test_from_url_not_memory_scheme(self) -> None: """Test that from_url raises ValueError for non-memory URLs.""" with pytest.raises(ValueError, match="Expected a 'memory://' URL"): ManagedMemoryStore.from_url("file:///tmp/test") def test_named_store(self) -> None: """Test that stores can be created with explicit names.""" store = ManagedMemoryStore(name="my-test-store") assert store.name == "my-test-store" assert str(store) == "memory://my-test-store" def test_named_store_shares_dict(self) -> None: """Test that creating a store with the same name shares the dict.""" store1 = ManagedMemoryStore(name="shared-store") store2 = ManagedMemoryStore(name="shared-store") assert store1._store_dict is store2._store_dict assert store1.name == store2.name def test_auto_generated_name(self) -> None: """Test that stores get auto-generated names when none provided.""" store = ManagedMemoryStore() assert store.name is not None assert str(store) == f"memory://{store.name}" def test_with_read_only_shares_dict(self, store: ManagedMemoryStore) -> None: """Test that with_read_only creates a store sharing the same dict.""" store2 = store.with_read_only(True) assert store2._store_dict is store._store_dict assert store2.read_only is True assert store.read_only is False def test_with_read_only_preserves_path(self) -> None: """Test that with_read_only preserves the path.""" store = ManagedMemoryStore(name="path-test", path="some/path") store2 = store.with_read_only(True) assert store2.path == "some/path" assert store2._store_dict is store._store_dict async def test_path_prefix_operations(self) -> None: """Test that store operations use the path prefix correctly.""" store = ManagedMemoryStore(name="prefix-test") store_with_path = ManagedMemoryStore.from_url("memory://prefix-test/subdir") # Write via store_with_path await store_with_path.set("key", self.buffer_cls.from_bytes(b"value")) # The key should be stored with the prefix in the underlying dict assert "subdir/key" in store._store_dict assert "key" not in store._store_dict # Read via store_with_path should work result = await store_with_path.get("key") assert result is not None assert result.to_bytes() == b"value" # Read via store without path should use full key result2 = await store.get("subdir/key") assert result2 is not None assert result2.to_bytes() == b"value" async def test_path_list_operations(self) -> None: """Test that list operations filter by path prefix.""" store = ManagedMemoryStore(name="list-test") # Set up some keys at different paths await store.set("a/key1", self.buffer_cls.from_bytes(b"v1")) await store.set("a/key2", self.buffer_cls.from_bytes(b"v2")) await store.set("b/key3", self.buffer_cls.from_bytes(b"v3")) # Create a store with path "a" store_a = ManagedMemoryStore.from_url("memory://list-test/a") # list() should only return keys under "a", without the "a/" prefix keys = [k async for k in store_a.list()] assert sorted(keys) == ["key1", "key2"] async def test_path_exists(self) -> None: """Test that exists() uses the path prefix.""" store = ManagedMemoryStore(name="exists-test") await store.set("prefix/key", self.buffer_cls.from_bytes(b"value")) store_with_path = ManagedMemoryStore.from_url("memory://exists-test/prefix") assert await store_with_path.exists("key") assert not await store_with_path.exists("prefix/key") def test_path_normalization(self) -> None: """Test that paths are normalized.""" store1 = ManagedMemoryStore(name="norm-test", path="a/b/") store2 = ManagedMemoryStore(name="norm-test", path="/a/b") store3 = ManagedMemoryStore(name="norm-test", path="a//b") assert store1.path == "a/b" assert store2.path == "a/b" assert store3.path == "a/b" def test_name_cannot_contain_slash(self) -> None: """Test that store names cannot contain '/'.""" with pytest.raises(ValueError, match="cannot contain '/'"): ManagedMemoryStore(name="foo/bar") def test_garbage_collection(self) -> None: """Test that the dict is garbage collected when no stores reference it.""" import gc store = ManagedMemoryStore() url = str(store) # URL should resolve while store exists store2 = ManagedMemoryStore.from_url(url) assert store2._store_dict is store._store_dict # Delete both stores del store del store2 gc.collect() # URL should no longer resolve with pytest.raises(ValueError, match="garbage collected"): ManagedMemoryStore.from_url(url) zarr-python-3.2.1/tests/test_store/test_object.py000066400000000000000000000101001517635743000222310ustar00rootroot00000000000000# ruff: noqa: E402 from pathlib import Path from typing import TypedDict import pytest obstore = pytest.importorskip("obstore") from hypothesis.stateful import ( run_state_machine_as_test, ) from obstore.store import LocalStore, MemoryStore from zarr.core.buffer import Buffer, cpu from zarr.storage import ObjectStore from zarr.testing.stateful import ZarrHierarchyStateMachine from zarr.testing.store import StoreTests class StoreKwargs(TypedDict): store: LocalStore read_only: bool class TestObjectStore(StoreTests[ObjectStore[LocalStore], cpu.Buffer]): # store_cls is needed to do an isinstance check, so can't be a subscripted generic store_cls = ObjectStore # type: ignore[assignment] buffer_cls = cpu.Buffer @pytest.fixture def store_kwargs(self, tmp_path: Path) -> StoreKwargs: store = LocalStore(prefix=tmp_path) return {"store": store, "read_only": False} @pytest.fixture def store(self, store_kwargs: StoreKwargs) -> ObjectStore[LocalStore]: return self.store_cls(**store_kwargs) async def get(self, store: ObjectStore[LocalStore], key: str) -> Buffer: assert isinstance(store.store, LocalStore) new_local_store = LocalStore(prefix=store.store.prefix) return self.buffer_cls.from_bytes(obstore.get(new_local_store, key).bytes()) async def set(self, store: ObjectStore[LocalStore], key: str, value: Buffer) -> None: assert isinstance(store.store, LocalStore) new_local_store = LocalStore(prefix=store.store.prefix) obstore.put(new_local_store, key, value.to_bytes()) def test_store_repr(self, store: ObjectStore[LocalStore]) -> None: from fnmatch import fnmatch pattern = "ObjectStore(object_store://LocalStore(*))" assert fnmatch(f"{store!r}", pattern) def test_store_supports_writes(self, store: ObjectStore[LocalStore]) -> None: assert store.supports_writes def test_store_supports_partial_writes(self, store: ObjectStore[LocalStore]) -> None: assert not store.supports_partial_writes def test_store_supports_listing(self, store: ObjectStore[LocalStore]) -> None: assert store.supports_listing def test_store_equal(self, store: ObjectStore[LocalStore]) -> None: """Test store equality""" # Test equality against a different instance type assert store != 0 # Test equality against a different store type new_memory_store = ObjectStore(MemoryStore()) assert store != new_memory_store # Test equality against a read only store assert isinstance(store.store, LocalStore) new_local_store = ObjectStore(LocalStore(prefix=store.store.prefix), read_only=True) assert store != new_local_store # Test two memory stores cannot be equal second_memory_store = ObjectStore(MemoryStore()) assert new_memory_store != second_memory_store def test_store_init_raises(self) -> None: """Test __init__ raises appropriate error for improper store type""" with pytest.raises(TypeError): ObjectStore("path/to/store") # type: ignore[type-var] async def test_store_getsize(self, store: ObjectStore[LocalStore]) -> None: buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") await self.set(store, "key", buf) size = await store.getsize("key") assert size == len(buf) async def test_store_getsize_prefix(self, store: ObjectStore[LocalStore]) -> None: buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") await self.set(store, "c/key1/0", buf) await self.set(store, "c/key2/0", buf) size = await store.getsize_prefix("c/key1") assert size == len(buf) total_size = await store.getsize_prefix("c") assert total_size == len(buf) * 2 @pytest.mark.slow_hypothesis def test_zarr_hierarchy() -> None: sync_store = ObjectStore(MemoryStore()) def mk_test_instance_sync() -> ZarrHierarchyStateMachine: return ZarrHierarchyStateMachine(sync_store) run_state_machine_as_test(mk_test_instance_sync) # type: ignore[no-untyped-call] zarr-python-3.2.1/tests/test_store/test_stateful.py000066400000000000000000000036331517635743000226270ustar00rootroot00000000000000# Stateful tests for arbitrary Zarr stores. from collections.abc import Generator import pytest from hypothesis.stateful import ( run_state_machine_as_test, ) import zarr from zarr.abc.store import Store from zarr.storage import LocalStore, ZipStore from zarr.testing.stateful import ZarrHierarchyStateMachine, ZarrStoreStateMachine pytestmark = [ pytest.mark.slow_hypothesis, # TODO: work out where this warning is coming from and fix pytest.mark.filterwarnings("ignore:Unclosed client session:ResourceWarning"), ] @pytest.fixture(autouse=True) def _enable_rectilinear_chunks() -> Generator[None, None, None]: """Enable rectilinear chunks since strategies may generate them.""" with zarr.config.set({"array.rectilinear_chunks": True}): yield @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") def test_zarr_hierarchy(sync_store: Store) -> None: def mk_test_instance_sync() -> ZarrHierarchyStateMachine: return ZarrHierarchyStateMachine(sync_store) if isinstance(sync_store, ZipStore): pytest.skip(reason="ZipStore does not support delete") run_state_machine_as_test(mk_test_instance_sync) # type: ignore[no-untyped-call] def test_zarr_store(sync_store: Store) -> None: def mk_test_instance_sync() -> ZarrStoreStateMachine: return ZarrStoreStateMachine(sync_store) if isinstance(sync_store, ZipStore): pytest.skip(reason="ZipStore does not support delete") if isinstance(sync_store, LocalStore): # This test uses arbitrary keys, which are passed to `set` and `delete`. # It assumes that `set` and `delete` are the only two operations that modify state. # But LocalStore, directories can hang around even after a key is delete-d. pytest.skip(reason="Test isn't suitable for LocalStore.") run_state_machine_as_test(mk_test_instance_sync) # type: ignore[no-untyped-call] zarr-python-3.2.1/tests/test_store/test_utils.py000066400000000000000000000061201517635743000221320ustar00rootroot00000000000000from __future__ import annotations import sys from unittest.mock import patch import pytest from zarr.storage._utils import ParsedStoreUrl, parse_store_url class TestParseStoreUrl: """Tests for parse_store_url.""" def test_memory_url(self) -> None: result = parse_store_url("memory://mystore") assert result == ParsedStoreUrl( scheme="memory", name="mystore", path="", raw="memory://mystore" ) def test_memory_url_with_path(self) -> None: result = parse_store_url("memory://mystore/path/to/data") assert result == ParsedStoreUrl( scheme="memory", name="mystore", path="path/to/data", raw="memory://mystore/path/to/data", ) def test_memory_url_no_name(self) -> None: result = parse_store_url("memory://") assert result.scheme == "memory" assert result.name is None def test_s3_url(self) -> None: result = parse_store_url("s3://bucket/key") assert result == ParsedStoreUrl( scheme="s3", name="bucket", path="key", raw="s3://bucket/key" ) def test_file_url(self) -> None: result = parse_store_url("file:///tmp/test") assert result.scheme == "file" def test_local_absolute_path(self) -> None: result = parse_store_url("/local/path") assert result == ParsedStoreUrl(scheme="", name=None, path="/local/path", raw="/local/path") def test_local_relative_path(self) -> None: result = parse_store_url("relative/path") assert result == ParsedStoreUrl( scheme="", name=None, path="relative/path", raw="relative/path" ) @pytest.mark.parametrize( "url", [ "C:\\Users\\foo", "C:/Users/foo", "D:/data/zarr", "c:/test", ], ) def test_windows_drive_letter(self, url: str) -> None: """On Windows, bare drive-letter paths must be treated as local paths.""" with patch.object(sys, "platform", "win32"): result = parse_store_url(url) assert result.scheme == "" assert result.name is None assert result.path == url assert result.raw == url @pytest.mark.parametrize( "url", [ "file:///C:/Users/foo", "file://C:/Users/foo", ], ) def test_file_url_with_drive_letter_on_windows(self, url: str) -> None: """file:// URLs with drive letters are not treated as bare paths.""" with patch.object(sys, "platform", "win32"): result = parse_store_url(url) assert result.scheme == "file" @pytest.mark.parametrize( "url", [ "C:\\Users\\foo", "C:/Users/foo", ], ) def test_drive_letter_not_special_on_non_windows(self, url: str) -> None: """On non-Windows platforms, drive-letter paths go through urlparse.""" with patch.object(sys, "platform", "linux"): result = parse_store_url(url) # urlparse interprets the drive letter as a scheme assert result.scheme == "c" zarr-python-3.2.1/tests/test_store/test_wrapper.py000066400000000000000000000107261517635743000224610ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, TypedDict import pytest from zarr.abc.store import ByteRequest, Store from zarr.core.buffer import Buffer from zarr.core.buffer.cpu import Buffer as CPUBuffer from zarr.core.buffer.cpu import buffer_prototype from zarr.storage import LocalStore, WrapperStore from zarr.testing.store import StoreTests if TYPE_CHECKING: from pathlib import Path from zarr.core.buffer.core import BufferPrototype class StoreKwargs(TypedDict): store: LocalStore class OpenKwargs(TypedDict): store_cls: type[LocalStore] root: str # TODO: fix this warning @pytest.mark.filterwarnings( "ignore:coroutine 'ClientCreatorContext.__aexit__' was never awaited:RuntimeWarning" ) class TestWrapperStore(StoreTests[WrapperStore[Any], Buffer]): store_cls = WrapperStore buffer_cls = CPUBuffer async def get(self, store: WrapperStore[LocalStore], key: str) -> Buffer: return self.buffer_cls.from_bytes((store._store.root / key).read_bytes()) async def set(self, store: WrapperStore[LocalStore], key: str, value: Buffer) -> None: parent = (store._store.root / key).parent if not parent.exists(): parent.mkdir(parents=True) (store._store.root / key).write_bytes(value.to_bytes()) @pytest.fixture def store_kwargs(self, tmp_path: Path) -> StoreKwargs: return {"store": LocalStore(str(tmp_path))} @pytest.fixture def open_kwargs(self, tmp_path: Path) -> OpenKwargs: return {"store_cls": LocalStore, "root": str(tmp_path)} def test_store_supports_writes(self, store: WrapperStore[LocalStore]) -> None: assert store.supports_writes def test_store_supports_listing(self, store: WrapperStore[LocalStore]) -> None: assert store.supports_listing def test_store_repr(self, store: WrapperStore[LocalStore]) -> None: assert f"{store!r}" == f"WrapperStore(LocalStore, 'file://{store._store.root.as_posix()}')" def test_store_str(self, store: WrapperStore[LocalStore]) -> None: assert str(store) == f"wrapping-file://{store._store.root.as_posix()}" def test_check_writeable(self, store: WrapperStore[LocalStore]) -> None: """ Test _check_writeable() runs without errors. """ store._check_writable() def test_close(self, store: WrapperStore[LocalStore]) -> None: "Test store can be closed" store.close() assert not store._is_open def test_is_open_setter_raises(self, store: WrapperStore[LocalStore]) -> None: """ Test that a user cannot change `_is_open` without opening the underlying store. """ with pytest.raises( NotImplementedError, match="WrapperStore must be opened via the `_open` method" ): store._is_open = True # TODO: work out where warning is coming from and fix @pytest.mark.filterwarnings( "ignore:coroutine 'ClientCreatorContext.__aexit__' was never awaited:RuntimeWarning" ) @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=True) async def test_wrapped_set(store: Store, capsys: pytest.CaptureFixture[str]) -> None: # define a class that prints when it sets class NoisySetter(WrapperStore[Store]): async def set(self, key: str, value: Buffer) -> None: print(f"setting {key}") await super().set(key, value) key = "foo" value = CPUBuffer.from_bytes(b"bar") store_wrapped = NoisySetter(store) await store_wrapped.set(key, value) captured = capsys.readouterr() assert f"setting {key}" in captured.out assert await store_wrapped.get(key, buffer_prototype) == value @pytest.mark.filterwarnings("ignore:Unclosed client session:ResourceWarning") @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=True) async def test_wrapped_get(store: Store, capsys: pytest.CaptureFixture[str]) -> None: # define a class that prints when it sets class NoisyGetter(WrapperStore[Any]): async def get( self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None ) -> None: print(f"getting {key}") await super().get(key, prototype=prototype, byte_range=byte_range) key = "foo" value = CPUBuffer.from_bytes(b"bar") store_wrapped = NoisyGetter(store) await store_wrapped.set(key, value) await store_wrapped.get(key, buffer_prototype) captured = capsys.readouterr() assert f"getting {key}" in captured.out zarr-python-3.2.1/tests/test_store/test_zip.py000066400000000000000000000143411517635743000216000ustar00rootroot00000000000000from __future__ import annotations import os import shutil import tempfile import zipfile from typing import TYPE_CHECKING import numpy as np import pytest import zarr from zarr import create_array from zarr.core.buffer import Buffer, cpu, default_buffer_prototype from zarr.core.group import Group from zarr.storage import ZipStore from zarr.testing.store import StoreTests if TYPE_CHECKING: from pathlib import Path from typing import Any # TODO: work out where this is coming from and fix pytestmark = [ pytest.mark.filterwarnings( "ignore:coroutine method 'aclose' of 'ZipStore.list' was never awaited:RuntimeWarning" ) ] class TestZipStore(StoreTests[ZipStore, cpu.Buffer]): store_cls = ZipStore buffer_cls = cpu.Buffer @pytest.fixture def store_kwargs(self) -> dict[str, str | bool]: fd, temp_path = tempfile.mkstemp() os.close(fd) os.unlink(temp_path) return {"path": temp_path, "mode": "w", "read_only": False} async def get(self, store: ZipStore, key: str) -> Buffer: buf = store._get(key, prototype=default_buffer_prototype()) assert buf is not None return buf async def set(self, store: ZipStore, key: str, value: Buffer) -> None: return store._set(key, value) def test_store_read_only(self, store: ZipStore) -> None: assert not store.read_only async def test_read_only_store_raises(self, store_kwargs: dict[str, Any]) -> None: # we need to create the zipfile in write mode before switching to read mode store = await self.store_cls.open(**store_kwargs) store.close() kwargs = {**store_kwargs, "mode": "a", "read_only": True} store = await self.store_cls.open(**kwargs) assert store._zmode == "a" assert store.read_only # set with pytest.raises(ValueError): await store.set("foo", cpu.Buffer.from_bytes(b"bar")) def test_store_repr(self, store: ZipStore) -> None: assert str(store) == f"zip://{store.path}" def test_store_supports_writes(self, store: ZipStore) -> None: assert store.supports_writes def test_store_supports_listing(self, store: ZipStore) -> None: assert store.supports_listing # TODO: fix this warning @pytest.mark.filterwarnings("ignore:Unclosed client session:ResourceWarning") def test_api_integration(self, store: ZipStore) -> None: root = zarr.open_group(store=store, mode="a") data = np.arange(10000, dtype=np.uint16).reshape(100, 100) z = root.create_array( shape=data.shape, chunks=(10, 10), name="foo", dtype=np.uint16, fill_value=99 ) z[:] = data assert np.array_equal(data, z[:]) # you can overwrite existing chunks but zipfile will issue a warning with pytest.warns(UserWarning, match="Duplicate name: 'foo/c/0/0'"): z[0, 0] = 100 # TODO: assigning an entire chunk to fill value ends up deleting the chunk which is not supported # a work around will be needed here. with pytest.raises(NotImplementedError): z[0:10, 0:10] = 99 bar = root.create_group("bar", attributes={"hello": "world"}) assert "hello" in dict(bar.attrs) # keys cannot be deleted with pytest.raises(NotImplementedError): del root["bar"] store.close() @pytest.mark.parametrize("read_only", [True, False]) async def test_store_open_read_only( self, store_kwargs: dict[str, Any], read_only: bool ) -> None: if read_only: # create an empty zipfile with zipfile.ZipFile(store_kwargs["path"], mode="w"): pass await super().test_store_open_read_only(store_kwargs, read_only) @pytest.mark.parametrize(("zip_mode", "read_only"), [("w", False), ("a", False), ("x", False)]) async def test_zip_open_mode_translation( self, store_kwargs: dict[str, Any], zip_mode: str, read_only: bool ) -> None: kws = {**store_kwargs, "mode": zip_mode} store = await self.store_cls.open(**kws) assert store.read_only == read_only def test_externally_zipped_store(self, tmp_path: Path) -> None: # See: https://github.com/zarr-developers/zarr-python/issues/2757 zarr_path = tmp_path / "foo.zarr" root = zarr.open_group(store=zarr_path, mode="w") root.require_group("foo") assert isinstance(foo := root["foo"], Group) # noqa: RUF018 foo["bar"] = np.array([1]) shutil.make_archive(str(zarr_path), "zip", zarr_path) zip_path = tmp_path / "foo.zarr.zip" zipped = zarr.open_group(ZipStore(zip_path, mode="r"), mode="r") assert list(zipped.keys()) == list(root.keys()) assert isinstance(group := zipped["foo"], Group) assert list(group.keys()) == list(group.keys()) async def test_list_without_explicit_open(self, tmp_path: Path) -> None: # ZipStore.list(), list_dir(), and exists() should auto-open # the zip file just like _get() and _set() do. zip_path = tmp_path / "data.zip" zarr_path = tmp_path / "foo.zarr" root = zarr.open_group(store=zarr_path, mode="w") root["x"] = np.array([1, 2, 3]) shutil.make_archive(str(zarr_path), "zip", zarr_path) shutil.move(f"{zarr_path}.zip", zip_path) store = ZipStore(zip_path, mode="r") assert not store._is_open keys = [k async for k in store.list()] assert len(keys) > 0 store2 = ZipStore(zip_path, mode="r") assert not store2._is_open assert await store2.exists(keys[0]) store3 = ZipStore(zip_path, mode="r") assert not store3._is_open dir_keys = [k async for k in store3.list_dir("")] assert len(dir_keys) > 0 async def test_move(self, tmp_path: Path) -> None: origin = tmp_path / "origin.zip" destination = tmp_path / "some_folder" / "destination.zip" store = await ZipStore.open(path=origin, mode="a") array = create_array(store, data=np.arange(10)) await store.move(str(destination)) assert store.path == destination assert destination.exists() assert not origin.exists() assert np.array_equal(array[...], np.arange(10)) zarr-python-3.2.1/tests/test_sync.py000066400000000000000000000114611517635743000175570ustar00rootroot00000000000000import asyncio from collections.abc import AsyncGenerator from unittest.mock import AsyncMock, patch import pytest import zarr from zarr.core.sync import ( SyncError, SyncMixin, _get_executor, _get_lock, _get_loop, cleanup_resources, loop, sync, ) @pytest.fixture(params=[True, False]) def sync_loop(request: pytest.FixtureRequest) -> asyncio.AbstractEventLoop | None: if request.param is True: return _get_loop() else: return None @pytest.fixture def clean_state(): # use this fixture to make sure no existing threads/loops exist in zarr.core.sync cleanup_resources() yield cleanup_resources() def test_get_loop() -> None: # test that calling _get_loop() twice returns the same loop loop = _get_loop() loop2 = _get_loop() assert loop is loop2 def test_get_lock() -> None: # test that calling _get_lock() twice returns the same lock lock = _get_lock() lock2 = _get_lock() assert lock is lock2 def test_sync(sync_loop: asyncio.AbstractEventLoop | None) -> None: foo = AsyncMock(return_value="foo") assert sync(foo(), loop=sync_loop) == "foo" foo.assert_awaited_once() def test_sync_raises(sync_loop: asyncio.AbstractEventLoop | None) -> None: foo = AsyncMock(side_effect=ValueError("foo-bar")) with pytest.raises(ValueError, match="foo-bar"): sync(foo(), loop=sync_loop) foo.assert_awaited_once() def test_sync_timeout() -> None: duration = 0.02 async def foo() -> None: await asyncio.sleep(duration) with pytest.raises(asyncio.TimeoutError): sync(foo(), timeout=duration / 10) def test_sync_raises_if_no_coroutine(sync_loop: asyncio.AbstractEventLoop | None) -> None: def foo() -> str: return "foo" with pytest.raises(TypeError): sync(foo(), loop=sync_loop) # type: ignore[arg-type] @pytest.mark.filterwarnings("ignore:coroutine.*was never awaited") def test_sync_raises_if_loop_is_closed() -> None: loop = _get_loop() foo = AsyncMock(return_value="foo") with patch.object(loop, "is_closed", return_value=True): with pytest.raises(RuntimeError): sync(foo(), loop=loop) foo.assert_not_awaited() @pytest.mark.filterwarnings("ignore:Unclosed client session:ResourceWarning") @pytest.mark.filterwarnings("ignore:coroutine.*was never awaited") def test_sync_raises_if_calling_sync_from_within_a_running_loop( sync_loop: asyncio.AbstractEventLoop | None, ) -> None: def foo() -> str: # technically, this should be an async function but doing that # yields a warning because it is never awaited by the inner function return "foo" async def bar() -> str: return sync(foo(), loop=sync_loop) # type: ignore[arg-type] with pytest.raises(SyncError): sync(bar(), loop=sync_loop) @pytest.mark.filterwarnings("ignore:coroutine.*was never awaited") def test_sync_raises_if_loop_is_invalid_type() -> None: foo = AsyncMock(return_value="foo") with pytest.raises(TypeError): sync(foo(), loop=1) # type: ignore[arg-type] foo.assert_not_awaited() def test_sync_mixin(sync_loop) -> None: class AsyncFoo: def __init__(self) -> None: pass async def foo(self) -> str: return "foo" async def bar(self) -> AsyncGenerator: for i in range(10): yield i class SyncFoo(SyncMixin): def __init__(self, async_foo: AsyncFoo) -> None: self._async_foo = async_foo def foo(self) -> str: return self._sync(self._async_foo.foo()) def bar(self) -> list[int]: return self._sync_iter(self._async_foo.bar()) async_foo = AsyncFoo() foo = SyncFoo(async_foo) assert foo.foo() == "foo" assert foo.bar() == list(range(10)) @pytest.mark.parametrize("workers", [None, 1, 2]) def test_threadpool_executor(clean_state, workers: int | None) -> None: with zarr.config.set({"threading.max_workers": workers}): _ = zarr.zeros(shape=(1,)) # trigger executor creation assert loop != [None] # confirm loop was created if workers is None: # confirm no executor was created if no workers were specified # (this is the default behavior) assert loop[0]._default_executor is None else: # confirm executor was created and attached to loop as the default executor # note: python doesn't have a direct way to get the default executor so we # use the private attribute assert _get_executor() is loop[0]._default_executor assert _get_executor()._max_workers == workers def test_cleanup_resources_idempotent() -> None: _get_executor() # trigger resource creation (iothread, loop, thread-pool) cleanup_resources() cleanup_resources() zarr-python-3.2.1/tests/test_sync_codec_pipeline.py000066400000000000000000000122501517635743000225760ustar00rootroot00000000000000from __future__ import annotations from typing import Any import numpy as np import pytest from zarr.abc.codec import ArrayBytesCodec, Codec from zarr.codecs.bytes import BytesCodec from zarr.codecs.crc32c_ import Crc32cCodec from zarr.codecs.gzip import GzipCodec from zarr.codecs.transpose import TransposeCodec from zarr.codecs.zstd import ZstdCodec from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import Buffer, NDBuffer, default_buffer_prototype from zarr.core.codec_pipeline import ChunkTransform from zarr.core.dtype import get_data_type_from_native_dtype class AsyncOnlyCodec(ArrayBytesCodec): """A codec that only supports async, for testing rejection of non-sync codecs.""" is_fixed_size = True async def _decode_single(self, chunk_data: Buffer, chunk_spec: ArraySpec) -> NDBuffer: raise NotImplementedError # pragma: no cover async def _encode_single(self, chunk_data: NDBuffer, chunk_spec: ArraySpec) -> Buffer | None: raise NotImplementedError # pragma: no cover def compute_encoded_size(self, input_byte_length: int, chunk_spec: ArraySpec) -> int: return input_byte_length # pragma: no cover def _make_array_spec(shape: tuple[int, ...], dtype: np.dtype[np.generic]) -> ArraySpec: zdtype = get_data_type_from_native_dtype(dtype) return ArraySpec( shape=shape, dtype=zdtype, fill_value=zdtype.cast_scalar(0), config=ArrayConfig(order="C", write_empty_chunks=True), prototype=default_buffer_prototype(), ) def _make_nd_buffer(arr: np.ndarray[Any, np.dtype[Any]]) -> NDBuffer: return default_buffer_prototype().nd_buffer.from_numpy_array(arr) @pytest.mark.parametrize( ("shape", "codecs"), [ ((100,), (BytesCodec(),)), ((100,), (BytesCodec(), GzipCodec())), ((3, 4), (TransposeCodec(order=(1, 0)), BytesCodec(), ZstdCodec())), ], ids=["bytes-only", "with-compression", "full-chain"], ) def test_construction(shape: tuple[int, ...], codecs: tuple[Codec, ...]) -> None: """Construction succeeds when all codecs implement SupportsSyncCodec.""" spec = _make_array_spec(shape, np.dtype("float64")) ChunkTransform(codecs=codecs, array_spec=spec) @pytest.mark.parametrize( ("shape", "codecs"), [ ((100,), (AsyncOnlyCodec(),)), ((3, 4), (TransposeCodec(order=(1, 0)), AsyncOnlyCodec())), ], ids=["async-only", "mixed-sync-and-async"], ) def test_construction_rejects_non_sync(shape: tuple[int, ...], codecs: tuple[Codec, ...]) -> None: """Construction raises TypeError when any codec lacks SupportsSyncCodec.""" spec = _make_array_spec(shape, np.dtype("float64")) with pytest.raises(TypeError, match="AsyncOnlyCodec"): ChunkTransform(codecs=codecs, array_spec=spec) @pytest.mark.parametrize( ("arr", "codecs"), [ (np.arange(100, dtype="float64"), (BytesCodec(),)), (np.arange(100, dtype="float64"), (BytesCodec(), GzipCodec(level=1))), ( np.arange(12, dtype="float64").reshape(3, 4), (TransposeCodec(order=(1, 0)), BytesCodec(), ZstdCodec(level=1)), ), (np.arange(100, dtype="float64"), (BytesCodec(), Crc32cCodec())), (np.arange(50, dtype="int32"), (BytesCodec(), ZstdCodec(level=1))), ], ids=["bytes-only", "gzip", "transpose+zstd", "crc32c", "int32"], ) def test_encode_decode_roundtrip( arr: np.ndarray[Any, np.dtype[Any]], codecs: tuple[Codec, ...] ) -> None: """Data survives a full encode/decode cycle.""" spec = _make_array_spec(arr.shape, arr.dtype) chain = ChunkTransform(codecs=codecs, array_spec=spec) nd_buf = _make_nd_buffer(arr) encoded = chain.encode(nd_buf) assert encoded is not None decoded = chain.decode(encoded) np.testing.assert_array_equal(arr, decoded.as_numpy_array()) @pytest.mark.parametrize( ("shape", "codecs", "input_size", "expected_size"), [ ((100,), (BytesCodec(),), 800, 800), ((100,), (BytesCodec(), Crc32cCodec()), 800, 804), ((3, 4), (TransposeCodec(order=(1, 0)), BytesCodec()), 96, 96), ], ids=["bytes-only", "crc32c", "transpose"], ) def test_compute_encoded_size( shape: tuple[int, ...], codecs: tuple[Codec, ...], input_size: int, expected_size: int, ) -> None: """compute_encoded_size returns the correct byte length.""" spec = _make_array_spec(shape, np.dtype("float64")) chain = ChunkTransform(codecs=codecs, array_spec=spec) assert chain.compute_encoded_size(input_size, spec) == expected_size def test_encode_returns_none_propagation() -> None: """When an AA codec returns None, encode short-circuits and returns None.""" class NoneReturningAACodec(TransposeCodec): """An ArrayArrayCodec that always returns None from encode.""" def _encode_sync(self, chunk_array: NDBuffer, chunk_spec: ArraySpec) -> NDBuffer | None: return None spec = _make_array_spec((3, 4), np.dtype("float64")) chain = ChunkTransform( codecs=(NoneReturningAACodec(order=(1, 0)), BytesCodec()), array_spec=spec, ) arr = np.arange(12, dtype="float64").reshape(3, 4) nd_buf = _make_nd_buffer(arr) assert chain.encode(nd_buf) is None zarr-python-3.2.1/tests/test_tree.py000066400000000000000000000061551517635743000175460ustar00rootroot00000000000000import textwrap from typing import Any import pytest import zarr @pytest.mark.parametrize("root_name", [None, "root"]) @pytest.mark.parametrize("atty", [True, False]) @pytest.mark.parametrize("plain", [True, False]) def test_tree(root_name: Any, atty: bool, plain: bool, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("sys.stdout.isatty", lambda: atty) if atty and not plain: BOPEN = "\x1b[1m" BCLOSE = "\x1b[0m" else: BOPEN = "" BCLOSE = "" g = zarr.group(path=root_name) A = g.create_group("A") B = g.create_group("B") C = B.create_group("C") D = C.create_group("C") A.create_array(name="x", shape=(2), dtype="float64") A.create_array(name="y", shape=(0,), dtype="int8") B.create_array(name="x", shape=(0,), dtype="float64") C.create_array(name="x", shape=(0,), dtype="float64") D.create_array(name="x", shape=(0,), dtype="float64") result = repr(g.tree(plain=plain)) root = root_name or "" expected = textwrap.dedent(f"""\ {BOPEN}/{root}{BCLOSE} ├── {BOPEN}A{BCLOSE} │ ├── {BOPEN}x{BCLOSE} (2,) float64 │ └── {BOPEN}y{BCLOSE} (0,) int8 └── {BOPEN}B{BCLOSE} ├── {BOPEN}C{BCLOSE} │ ├── {BOPEN}C{BCLOSE} │ │ └── {BOPEN}x{BCLOSE} (0,) float64 │ └── {BOPEN}x{BCLOSE} (0,) float64 └── {BOPEN}x{BCLOSE} (0,) float64 """) assert result == expected result = repr(g.tree(level=0, plain=plain)) expected = textwrap.dedent(f"""\ {BOPEN}/{root}{BCLOSE} ├── {BOPEN}A{BCLOSE} └── {BOPEN}B{BCLOSE} """) assert result == expected if not plain: tree = g.tree(plain=False) bundle = tree._repr_mimebundle_() assert "text/plain" in bundle assert "text/html" in bundle assert "A" in bundle["text/html"] assert "x" in bundle["text/html"] assert " None: g = zarr.group() g.create_group("a") g.create_group("b") g.create_group("c") g.create_group("d") g.create_group("e") result = repr(g.tree(max_nodes=3, plain=True)) assert "Truncated at max_nodes=3" in result # Should show exactly 3 nodes (lines with ── connectors). lines = result.strip().split("\n") node_lines = [line for line in lines if "──" in line] assert len(node_lines) == 3 # Full tree should not show truncation message. full = repr(g.tree(max_nodes=500, plain=True)) assert "truncated" not in full def test_tree_html_escaping() -> None: g = zarr.group() g.create_group("") tree = g.tree() bundle = tree._repr_mimebundle_() assert "<img" in bundle["text/html"] assert "" in bundle["text/plain"] def test_expand_not_implemented() -> None: g = zarr.group() with pytest.raises(NotImplementedError): g.tree(expand=True) zarr-python-3.2.1/tests/test_unified_chunk_grid.py000066400000000000000000003011551517635743000224250ustar00rootroot00000000000000""" Tests for the unified ChunkGrid design (POC). Tests the core ChunkGrid with FixedDimension/VaryingDimension internals, ChunkSpec, serialization round-trips, indexing with rectilinear grids, and end-to-end array creation + read/write. """ from __future__ import annotations from typing import TYPE_CHECKING, Any import numpy as np import pytest import zarr from zarr.core.chunk_grids import ( ChunkGrid, ChunkSpec, FixedDimension, VaryingDimension, _is_rectilinear_chunks, ) from zarr.core.common import compress_rle, expand_rle from zarr.core.metadata.v3 import ( RectilinearChunkGridMetadata, RectilinearChunkGridMetadataJSON, RegularChunkGridMetadata, parse_chunk_grid, ) from zarr.errors import BoundsCheckError from zarr.storage import MemoryStore if TYPE_CHECKING: from collections.abc import Generator from pathlib import Path @pytest.fixture(autouse=True) def _enable_rectilinear_chunks() -> Generator[None, None, None]: """Enable rectilinear chunks for all tests in this module.""" with zarr.config.set({"array.rectilinear_chunks": True}): yield def _edges(grid: ChunkGrid, dim: int) -> tuple[int, ...]: """Extract the per-chunk edge lengths for *dim* from a ChunkGrid.""" d = grid._dimensions[dim] if isinstance(d, FixedDimension): return tuple(d.size for _ in range(d.nchunks)) if isinstance(d, VaryingDimension): return tuple(d.edges) raise TypeError(f"Unexpected dimension type: {type(d)}") # --------------------------------------------------------------------------- # Dimension index_to_chunk bounds tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("dim", "index", "match"), [ (VaryingDimension([10, 20, 30], extent=60), 60, "out of bounds"), (VaryingDimension([10, 20, 30], extent=60), 100, "out of bounds"), (FixedDimension(size=10, extent=95), 95, "out of bounds"), (FixedDimension(size=10, extent=95), -1, "Negative"), ], ids=[ "varying-at-extent", "varying-past-extent", "fixed-at-extent", "fixed-negative", ], ) def test_dimension_index_to_chunk_bounds( dim: FixedDimension | VaryingDimension, index: int, match: str ) -> None: """Out-of-bounds or negative indices raise IndexError for both dimension types""" with pytest.raises(IndexError, match=match): dim.index_to_chunk(index) @pytest.mark.parametrize( ("dim", "index", "expected"), [ (VaryingDimension([10, 20, 30], extent=60), 59, 2), (FixedDimension(size=10, extent=95), 94, 9), ], ids=["varying-last-valid", "fixed-last-valid"], ) def test_dimension_index_to_chunk_last_valid( dim: FixedDimension | VaryingDimension, index: int, expected: int ) -> None: """Last valid index maps to the correct chunk for both dimension types""" assert dim.index_to_chunk(index) == expected # --------------------------------------------------------------------------- # Rectilinear feature flag tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( "action", [ lambda: RectilinearChunkGridMetadata(chunk_shapes=((10, 20), (25, 25))), lambda: RectilinearChunkGridMetadata.from_dict( { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[10, 20, 30], [50, 50]]}, } ), lambda: zarr.create_array(MemoryStore(), shape=(30,), chunks=[[10, 20]], dtype="int32"), ], ids=["constructor", "from_dict", "create_array"], ) def test_rectilinear_feature_flag_blocked(action: Any) -> None: """Rectilinear chunk operations raise ValueError when the feature flag is disabled""" with zarr.config.set({"array.rectilinear_chunks": False}): with pytest.raises(ValueError, match="experimental and disabled by default"): action() def test_rectilinear_feature_flag_enabled() -> None: """Rectilinear chunk grid construction succeeds when the feature flag is enabled""" with zarr.config.set({"array.rectilinear_chunks": True}): grid = RectilinearChunkGridMetadata(chunk_shapes=((10, 20), (25, 25))) assert grid.ndim == 2 # --------------------------------------------------------------------------- # FixedDimension tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ( "size", "extent", "chunk_ix", "expected_nchunks", "expected_chunk_size", "expected_data_size", "expected_offset", ), [ (10, 100, 0, 10, 10, 10, 0), (10, 100, 1, 10, 10, 10, 10), (10, 100, 9, 10, 10, 10, 90), (10, 95, 9, 10, 10, 5, 90), # boundary chunk (0, 0, None, 0, None, None, None), # zero-size ], ids=["start", "middle", "end", "boundary", "zero-size"], ) def test_fixed_dimension( size: int, extent: int, chunk_ix: int | None, expected_nchunks: int, expected_chunk_size: int | None, expected_data_size: int | None, expected_offset: int | None, ) -> None: """FixedDimension properties match expected values for various chunk/extent combinations""" d = FixedDimension(size=size, extent=extent) assert d.nchunks == expected_nchunks if chunk_ix is not None: assert d.chunk_size(chunk_ix) == expected_chunk_size assert d.data_size(chunk_ix) == expected_data_size assert d.chunk_offset(chunk_ix) == expected_offset @pytest.mark.parametrize( ("idx", "expected"), [(0, 0), (9, 0), (10, 1), (25, 2)], ) def test_fixed_dimension_index_to_chunk(idx: int, expected: int) -> None: """FixedDimension.index_to_chunk maps element indices to correct chunk indices""" d = FixedDimension(size=10, extent=100) assert d.index_to_chunk(idx) == expected def test_fixed_dimension_indices_to_chunks() -> None: """FixedDimension.indices_to_chunks vectorizes index-to-chunk mapping over an array""" d = FixedDimension(size=10, extent=100) indices = np.array([0, 5, 10, 15, 99]) np.testing.assert_array_equal(d.indices_to_chunks(indices), [0, 0, 1, 1, 9]) @pytest.mark.parametrize( ("size", "extent", "match"), [(-1, 100, "must be >= 0"), (10, -1, "must be >= 0")], ids=["negative-size", "negative-extent"], ) def test_fixed_dimension_rejects_negative(size: int, extent: int, match: str) -> None: """FixedDimension raises ValueError for negative size or extent""" with pytest.raises(ValueError, match=match): FixedDimension(size=size, extent=extent) # --------------------------------------------------------------------------- # VaryingDimension tests # --------------------------------------------------------------------------- def test_varying_dimension_construction() -> None: """VaryingDimension stores edges, cumulative sums, nchunks, and extent correctly""" d = VaryingDimension([10, 20, 30], extent=60) assert d.edges == (10, 20, 30) assert d.cumulative == (10, 30, 60) assert d.nchunks == 3 assert d.extent == 60 @pytest.mark.parametrize( ( "chunk_idx", "expected_offset", "expected_size", "expected_data", "expected_chunk_for_first_idx", ), [ (0, 0, 10, 10, 0), (1, 10, 20, 20, 1), (2, 30, 30, 30, 2), ], ) def test_varying_dimension( chunk_idx: int, expected_offset: int, expected_size: int, expected_data: int, expected_chunk_for_first_idx: int, ) -> None: """VaryingDimension chunk_offset, chunk_size, data_size, and index_to_chunk return correct values""" d = VaryingDimension([10, 20, 30], extent=60) assert d.chunk_offset(chunk_idx) == expected_offset assert d.chunk_size(chunk_idx) == expected_size assert d.data_size(chunk_idx) == expected_data assert d.index_to_chunk(expected_offset) == expected_chunk_for_first_idx def test_varying_dimension_indices_to_chunks() -> None: """VaryingDimension.indices_to_chunks vectorizes index-to-chunk mapping over an array""" d = VaryingDimension([10, 20, 30], extent=60) indices = np.array([0, 9, 10, 29, 30, 59]) np.testing.assert_array_equal(d.indices_to_chunks(indices), [0, 0, 1, 1, 2, 2]) @pytest.mark.parametrize( ("edges", "extent", "match"), [ ([], 0, "must not be empty"), ([10, 0, 5], 15, "must be > 0"), ], ids=["empty", "zero-edge"], ) def test_varying_dimension_rejects_invalid(edges: list[int], extent: int, match: str) -> None: """VaryingDimension raises ValueError for empty edges or zero-length edges""" with pytest.raises(ValueError, match=match): VaryingDimension(edges, extent=extent) # --------------------------------------------------------------------------- # ChunkSpec tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("slices", "codec_shape", "expected_shape", "expected_boundary"), [ ((slice(0, 10), slice(0, 20)), (10, 20), (10, 20), False), ((slice(90, 95), slice(0, 20)), (10, 20), (5, 20), True), ((slice(10, 10),), (0,), (0,), False), ((slice(0, 10), slice(0, 5)), (10, 10), (10, 5), True), ], ids=["basic", "boundary", "empty-slices", "multidim-boundary"], ) def test_chunk_spec( slices: tuple[slice, ...], codec_shape: tuple[int, ...], expected_shape: tuple[int, ...], expected_boundary: bool, ) -> None: """ChunkSpec reports correct shape and boundary status from slices and codec_shape""" spec = ChunkSpec(slices=slices, codec_shape=codec_shape) assert spec.shape == expected_shape assert spec.is_boundary == expected_boundary # --------------------------------------------------------------------------- # ChunkGrid construction tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("array_shape", "chunk_sizes", "expected_regular", "expected_ndim", "expected_chunk_shape"), [ ((100, 200), (10, 20), True, 2, (10, 20)), ((), (), True, 0, ()), ((60, 100), [[10, 20, 30], [25, 25, 25, 25]], False, 2, None), ((30, 50), [[10, 10, 10], [25, 25]], True, 2, (10, 25)), # uniform edges → regular ], ids=["regular", "zero-dim", "rectilinear", "uniform-becomes-regular"], ) def test_chunk_grid_construction( array_shape: tuple[int, ...], chunk_sizes: Any, expected_regular: bool, expected_ndim: int, expected_chunk_shape: tuple[int, ...] | None, ) -> None: """ChunkGrid.from_sizes produces grids with correct regularity, ndim, and chunk_shape""" g = ChunkGrid.from_sizes(array_shape, chunk_sizes) assert g.is_regular == expected_regular assert g.ndim == expected_ndim if expected_chunk_shape is not None: assert g.chunk_shape == expected_chunk_shape else: with pytest.raises(ValueError, match="only available for regular"): _ = g.chunk_shape def test_chunk_grid_rectilinear_uniform_dim_is_fixed() -> None: """A rectilinear grid with all-same sizes in one dim stores it as Fixed.""" g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [25, 25, 25, 25]]) assert isinstance(g._dimensions[0], VaryingDimension) assert isinstance(g._dimensions[1], FixedDimension) # --------------------------------------------------------------------------- # ChunkGrid query tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("shape", "chunks", "expected_grid_shape"), [ ((100, 200), (10, 20), (10, 10)), ((95, 200), (10, 20), (10, 10)), ((60, 100), [[10, 20, 30], [25, 25, 25, 25]], (3, 4)), ], ids=["regular", "regular-boundary", "rectilinear"], ) def test_chunk_grid_shape( shape: tuple[int, ...], chunks: Any, expected_grid_shape: tuple[int, ...], ) -> None: """ChunkGrid.grid_shape returns the expected number of chunks per dimension""" g = ChunkGrid.from_sizes(shape, chunks) assert g.grid_shape == expected_grid_shape @pytest.mark.parametrize( ( "array_shape", "chunk_sizes", "coords", "expected_shape", "expected_codec_shape", "expected_boundary", ), [ # regular interior ((100, 200), (10, 20), (0, 0), (10, 20), (10, 20), False), # regular boundary ((95, 200), (10, 20), (9, 0), (5, 20), (10, 20), True), # rectilinear ((60, 100), [[10, 20, 30], [25, 25, 25, 25]], (0, 0), (10, 25), (10, 25), False), ((60, 100), [[10, 20, 30], [25, 25, 25, 25]], (1, 0), (20, 25), (20, 25), False), ((60, 100), [[10, 20, 30], [25, 25, 25, 25]], (2, 3), (30, 25), (30, 25), False), ], ids=["regular", "regular-boundary", "rectilinear-0,0", "rectilinear-1,0", "rectilinear-2,3"], ) def test_chunk_grid_getitem( array_shape: tuple[int, ...], chunk_sizes: Any, coords: tuple[int, ...], expected_shape: tuple[int, ...], expected_codec_shape: tuple[int, ...], expected_boundary: bool, ) -> None: """ChunkGrid.__getitem__ returns a ChunkSpec with correct shape, codec_shape, and boundary flag""" g = ChunkGrid.from_sizes(array_shape, chunk_sizes) spec = g[coords] assert spec is not None assert spec.shape == expected_shape assert spec.codec_shape == expected_codec_shape assert spec.is_boundary == expected_boundary @pytest.mark.parametrize( ("array_shape", "chunk_sizes", "coords"), [ ((100, 200), (10, 20), (99, 0)), ((60, 100), [[10, 20, 30], [25, 25, 25, 25]], (3, 0)), ], ids=["regular-oob", "rectilinear-oob"], ) def test_chunk_grid_getitem_oob( array_shape: tuple[int, ...], chunk_sizes: Any, coords: tuple[int, ...] ) -> None: """Out-of-bounds chunk coordinates return None""" g = ChunkGrid.from_sizes(array_shape, chunk_sizes) assert g[coords] is None def test_chunk_grid_getitem_slices() -> None: """ChunkSpec.slices reflect the correct start/stop for a rectilinear chunk""" g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [25, 25, 25, 25]]) spec = g[(1, 2)] assert spec is not None assert spec.slices == (slice(10, 30, 1), slice(50, 75, 1)) # -- all_chunk_coords tests -- @pytest.mark.parametrize( ("array_shape", "chunk_sizes", "origin", "selection_shape", "expected_coords"), [ # rectilinear grid ( (60, 100), [[10, 20, 30], [50, 50]], None, None, [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)], ), ((60, 100), [[10, 20, 30], [50, 50]], (1, 0), None, [(1, 0), (1, 1), (2, 0), (2, 1)]), ((60, 100), [[10, 20, 30], [50, 50]], None, (2, 1), [(0, 0), (1, 0)]), ((60, 100), [[10, 20, 30], [50, 50]], (1, 1), (2, 1), [(1, 1), (2, 1)]), # regular grid ((30, 40), (10, 20), (2, 1), None, [(2, 1)]), ((30, 40), (10, 20), None, (0, 0), []), ((60, 80), (20, 20), (0, 2), (3, 1), [(0, 2), (1, 2), (2, 2)]), ], ids=[ "all", "with-origin", "with-sel-shape", "origin+sel", "last-chunk", "zero-sel", "single-dim", ], ) def test_all_chunk_coords( array_shape: tuple[int, ...], chunk_sizes: Any, origin: tuple[int, ...] | None, selection_shape: tuple[int, ...] | None, expected_coords: list[tuple[int, ...]], ) -> None: """all_chunk_coords yields the expected coordinates with optional origin and selection_shape""" g = ChunkGrid.from_sizes(array_shape, chunk_sizes) kwargs: dict[str, Any] = {} if origin is not None: kwargs["origin"] = origin if selection_shape is not None: kwargs["selection_shape"] = selection_shape assert list(g.all_chunk_coords(**kwargs)) == expected_coords def test_chunk_grid_get_nchunks() -> None: """get_nchunks returns the total number of chunks across all dimensions""" g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) assert g.get_nchunks() == 6 def test_chunk_grid_iter() -> None: """Iterating a ChunkGrid yields the correct number of ChunkSpec objects""" g = ChunkGrid.from_sizes((30, 40), (10, 20)) specs = list(g) assert len(specs) == 6 assert all(isinstance(s, ChunkSpec) for s in specs) # --------------------------------------------------------------------------- # RLE tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("compressed", "expected"), [ ([[10, 3]], [10, 10, 10]), ([[10, 2], [20, 1]], [10, 10, 20]), ], ) def test_rle_expand(compressed: list[Any], expected: list[int]) -> None: """RLE-encoded edges expand correctly""" assert expand_rle(compressed) == expected @pytest.mark.parametrize( ("original", "expected"), [ ([10, 10, 10], [[10, 3]]), ([10, 10, 20], [[10, 2], 20]), ([5], [5]), ([10, 20, 30], [10, 20, 30]), ], ) def test_rle_compress(original: list[int], expected: list[Any]) -> None: """compress_rle produces the expected RLE encoding for various input sequences""" assert compress_rle(original) == expected def test_rle_roundtrip() -> None: """compress_rle followed by expand_rle recovers the original sequence""" original = [10, 10, 10, 20, 20, 30] compressed = compress_rle(original) assert expand_rle(compressed) == original @pytest.mark.parametrize( ("rle_input", "match"), [ ([0], "Chunk edge length must be >= 1"), ([-5], "Chunk edge length must be >= 1"), ([[0, 3]], "Chunk edge length must be >= 1"), ([[-10, 2]], "Chunk edge length must be >= 1"), ([[5, 0]], "RLE repeat count must be >= 1"), ([[5, -1]], "RLE repeat count must be >= 1"), ], ids=[ "zero-edge", "negative-edge", "zero-rle-size", "negative-rle-size", "zero-rle-count", "negative-rle-count", ], ) def test_rle_expand_rejects_invalid(rle_input: list[Any], match: str) -> None: """expand_rle raises ValueError for zero/negative edge lengths or repeat counts""" with pytest.raises(ValueError, match=match): expand_rle(rle_input) # -- expand_rle handles JSON floats -- def test_expand_rle_bare_integer_floats_accepted() -> None: """JSON parsers may emit 10.0 for the integer 10; expand_rle should handle it.""" result = expand_rle([10.0, 20.0]) # type: ignore[list-item] assert result == [10, 20] def test_expand_rle_pair_with_float_count() -> None: """expand_rle accepts float repeat counts that are integer-valued""" result = expand_rle([[10, 3.0]]) # type: ignore[list-item] assert result == [10, 10, 10] # --------------------------------------------------------------------------- # _is_rectilinear_chunks tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("value", "expected"), [ ([[10, 20], [5, 5]], True), (((10, 20), (5, 5)), True), ((10, 20), False), ([10, 20], False), (10, False), ("auto", False), ([], False), ([[]], True), (ChunkGrid.from_sizes((10,), (5,)), False), (None, False), (3.14, False), ], ids=[ "nested-lists", "nested-tuples", "flat-tuple", "flat-list", "single-int", "string", "empty-list", "empty-nested-list", "chunk-grid-instance", "none", "float", ], ) def test_is_rectilinear_chunks(value: Any, expected: bool) -> None: """_is_rectilinear_chunks correctly identifies nested sequences as rectilinear""" assert _is_rectilinear_chunks(value) is expected def test_is_rectilinear_chunks_handles_broken_iterable() -> None: """_is_rectilinear_chunks returns False for objects that raise on iteration.""" class BrokenIter: def __iter__(self) -> Any: raise TypeError("cannot iterate") assert _is_rectilinear_chunks(BrokenIter()) is False # --------------------------------------------------------------------------- # Serialization tests # --------------------------------------------------------------------------- def test_serialization_error_non_regular_chunk_shape() -> None: """Accessing chunk_shape on a non-regular grid raises ValueError.""" grid = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [25, 25, 25, 25]]) with pytest.raises(ValueError, match="only available for regular"): grid.chunk_shape # noqa: B018 def test_serialization_error_zero_extent_rectilinear() -> None: """RectilinearChunkGridMetadata rejects empty edge tuples.""" with pytest.raises(ValueError, match="has no chunk edges"): RectilinearChunkGridMetadata(chunk_shapes=((),)) def test_serialization_unknown_name_parse() -> None: """Parsing metadata with an unknown chunk grid name raises ValueError""" with pytest.raises(ValueError, match="Unknown chunk grid"): parse_chunk_grid({"name": "hexagonal", "configuration": {}}) def test_from_metadata_unknown_chunk_grid_type() -> None: """ChunkGrid.from_metadata raises TypeError for unrecognised chunk grid metadata.""" from unittest.mock import MagicMock from zarr.core.metadata.v3 import ArrayV3Metadata mock_meta = MagicMock(spec=ArrayV3Metadata) mock_meta.chunk_grid = MagicMock() # not Regular or Rectilinear with pytest.raises(TypeError, match="Unknown chunk grid metadata type"): ChunkGrid.from_metadata(mock_meta) def test_from_sizes_rejects_empty_edge_list() -> None: """ChunkGrid.from_sizes raises ValueError when a dimension has an empty edge list.""" with pytest.raises(ValueError, match="at least one chunk"): ChunkGrid.from_sizes((10,), ([],)) # --------------------------------------------------------------------------- # Spec compliance tests # --------------------------------------------------------------------------- def test_spec_kind_inline_required_on_deserialize() -> None: """Deserialization requires kind: 'inline'.""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"chunk_shapes": [[10, 20], [15, 15]]}, } with pytest.raises(ValueError, match="requires a 'kind' field"): parse_chunk_grid(data) def test_spec_kind_unknown_rejected() -> None: """Unsupported rectilinear chunk grid kind raises ValueError on parse""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "reference", "chunk_shapes": [[10, 20], [15, 15]]}, } with pytest.raises(ValueError, match="Unsupported rectilinear chunk grid kind"): parse_chunk_grid(data) def test_spec_integer_shorthand_per_dimension() -> None: """A bare integer in chunk_shapes means repeat until >= extent.""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [4, [1, 2, 3]]}, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) g = ChunkGrid.from_sizes((6, 6), meta.chunk_shapes) assert _edges(g, 0) == (4, 4) assert _edges(g, 1) == (1, 2, 3) def test_spec_mixed_rle_and_bare_integers() -> None: """An array can mix bare integers and [value, count] RLE pairs.""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[[1, 3], 3]]}, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) g = ChunkGrid.from_sizes((6,), meta.chunk_shapes) assert _edges(g, 0) == (1, 1, 1, 3) def test_spec_overflow_chunks_allowed() -> None: """Edge sum >= extent is valid (overflow chunks permitted).""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[4, 4, 4]]}, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) g = ChunkGrid.from_sizes((6,), meta.chunk_shapes) assert _edges(g, 0) == (4, 4, 4) def test_spec_example() -> None: """The full example from the spec README.""" data: dict[str, Any] = { "name": "rectilinear", "configuration": { "kind": "inline", "chunk_shapes": [ 4, [1, 2, 3], [[4, 2]], [[1, 3], 3], [4, 4, 4], ], }, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) g = ChunkGrid.from_sizes((6, 6, 6, 6, 6), meta.chunk_shapes) assert _edges(g, 0) == (4, 4) assert _edges(g, 1) == (1, 2, 3) assert _edges(g, 2) == (4, 4) assert _edges(g, 3) == (1, 1, 1, 3) assert _edges(g, 4) == (4, 4, 4) # --------------------------------------------------------------------------- # parse_chunk_grid validation tests # --------------------------------------------------------------------------- def test_parse_chunk_grid_varying_extent_mismatch_raises() -> None: """Reconstructing a ChunkGrid with mismatched extents raises ValueError""" g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) with pytest.raises(ValueError, match="extent"): ChunkGrid( dimensions=tuple( dim.with_extent(ext) for dim, ext in zip(g._dimensions, (100, 100), strict=True) ) ) def test_parse_chunk_grid_varying_extent_match_ok() -> None: """Reconstructing a ChunkGrid with matching extents succeeds""" g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) g2 = ChunkGrid( dimensions=tuple( dim.with_extent(ext) for dim, ext in zip(g._dimensions, (60, 100), strict=True) ) ) assert g2._dimensions[0].extent == 60 @pytest.mark.parametrize( ("chunk_shapes", "array_shape", "match"), [ ([[10, 20, 30], [25, 25]], (100, 50), "extent 100 exceeds sum of edges 60"), ([[50, 50], [10, 20]], (100, 50), "extent 50 exceeds sum of edges 30"), ], ids=["first-dim-mismatch", "second-dim-mismatch"], ) def test_parse_chunk_grid_rectilinear_extent_mismatch_raises( chunk_shapes: list[list[int]], array_shape: tuple[int, ...], match: str ) -> None: """Rectilinear grid raises ValueError when array extent exceeds sum of edges""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": chunk_shapes}, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) with pytest.raises(ValueError, match=match): ChunkGrid.from_sizes(array_shape, meta.chunk_shapes) def test_parse_chunk_grid_rectilinear_extent_match_passes() -> None: """Rectilinear grid with matching extents parses and builds successfully""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[10, 20, 30], [25, 25]]}, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) g = ChunkGrid.from_sizes((60, 50), meta.chunk_shapes) assert g.grid_shape == (3, 2) def test_parse_chunk_grid_rectilinear_ndim_mismatch_raises() -> None: """Mismatched ndim between array shape and chunk_sizes raises ValueError""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[10, 20], [25, 25]]}, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) with pytest.raises(ValueError, match="3 dimensions but chunk_sizes has 2"): ChunkGrid.from_sizes((30, 50, 100), meta.chunk_shapes) def test_parse_chunk_grid_rectilinear_rle_extent_validated() -> None: """RLE-encoded edges are expanded before validation.""" data: dict[str, Any] = { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[[10, 5]], [[25, 2]]]}, } meta = parse_chunk_grid(data) assert isinstance(meta, RectilinearChunkGridMetadata) g = ChunkGrid.from_sizes((50, 50), meta.chunk_shapes) assert g.grid_shape == (5, 2) with pytest.raises(ValueError, match="extent 100 exceeds sum of edges 50"): ChunkGrid.from_sizes((100, 50), meta.chunk_shapes) def test_parse_chunk_grid_varying_dimension_extent_mismatch_on_chunkgrid_input() -> None: """ChunkGrid constructor rejects VaryingDimension with extent exceeding sum of edges""" g = ChunkGrid.from_sizes((60, 50), [[10, 20, 30], [25, 25]]) with pytest.raises(ValueError, match="less than"): ChunkGrid( dimensions=tuple( dim.with_extent(ext) for dim, ext in zip(g._dimensions, (100, 50), strict=True) ) ) # --------------------------------------------------------------------------- # Rectilinear indexing tests # --------------------------------------------------------------------------- def test_basic_indexer_rectilinear() -> None: """BasicIndexer produces correct projections for a full-slice rectilinear selection""" from zarr.core.indexing import BasicIndexer g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) indexer = BasicIndexer( selection=(slice(None), slice(None)), shape=(60, 100), chunk_grid=g, ) projections = list(indexer) assert len(projections) == 6 p0 = projections[0] assert p0.chunk_coords == (0, 0) assert p0.chunk_selection == (slice(0, 10, 1), slice(0, 50, 1)) p1 = projections[2] assert p1.chunk_coords == (1, 0) assert p1.chunk_selection == (slice(0, 20, 1), slice(0, 50, 1)) def test_basic_indexer_int_selection() -> None: """BasicIndexer with integer selection maps to the correct chunk and local offset""" from zarr.core.indexing import BasicIndexer g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) indexer = BasicIndexer( selection=(15, slice(None)), shape=(60, 100), chunk_grid=g, ) projections = list(indexer) assert len(projections) == 2 assert projections[0].chunk_coords == (1, 0) assert projections[0].chunk_selection == (5, slice(0, 50, 1)) def test_basic_indexer_slice_subset() -> None: """BasicIndexer with partial slices spans the expected chunk dimensions""" from zarr.core.indexing import BasicIndexer g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) indexer = BasicIndexer( selection=(slice(5, 35), slice(0, 50)), shape=(60, 100), chunk_grid=g, ) projections = list(indexer) chunk_coords_dim0 = sorted({p.chunk_coords[0] for p in projections}) assert chunk_coords_dim0 == [0, 1, 2] def test_orthogonal_indexer_rectilinear() -> None: """OrthogonalIndexer produces the expected number of projections for a rectilinear grid""" from zarr.core.indexing import OrthogonalIndexer g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) indexer = OrthogonalIndexer( selection=(slice(None), slice(None)), shape=(60, 100), chunk_grid=g, ) projections = list(indexer) assert len(projections) == 6 def test_oob_block_raises_bounds_check_error() -> None: """Out-of-bounds block index should raise BoundsCheckError, not IndexError.""" store = MemoryStore() a = zarr.create_array(store, shape=(30,), chunks=[[10, 20]], dtype="int32") with pytest.raises(BoundsCheckError): a.get_block_selection((2,)) # --------------------------------------------------------------------------- # End-to-end tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("shape", "chunks", "expected_regular"), [ ((100, 200), (10, 20), True), ((60, 100), [[10, 20, 30], [50, 50]], False), ], ids=["regular", "rectilinear"], ) def test_e2e_create_array( tmp_path: Path, shape: tuple[int, ...], chunks: Any, expected_regular: bool ) -> None: """End-to-end array creation sets correct regularity and ndim on chunk_grid""" arr = zarr.create_array( store=tmp_path / "arr.zarr", shape=shape, chunks=chunks, dtype="float32", ) assert ChunkGrid.from_metadata(arr.metadata).is_regular == expected_regular assert ChunkGrid.from_metadata(arr.metadata).ndim == len(shape) @pytest.mark.parametrize( ("shape", "chunks", "grid_type_name", "grid_name"), [ ((100, 200), (10, 20), "RegularChunkGridMetadata", "regular"), ((60, 100), [[10, 20, 30], [50, 50]], "RectilinearChunkGridMetadata", "rectilinear"), ], ids=["regular", "rectilinear"], ) def test_e2e_chunk_grid_serializes( tmp_path: Path, shape: tuple[int, ...], chunks: Any, grid_type_name: str, grid_name: str ) -> None: """Array metadata serializes chunk_grid with the correct type and name""" from zarr.core.metadata.v3 import ( ArrayV3Metadata, RectilinearChunkGridMetadata, RegularChunkGridMetadata, ) grid_type = ( RegularChunkGridMetadata if grid_type_name == "RegularChunkGridMetadata" else RectilinearChunkGridMetadata ) arr = zarr.create_array( store=tmp_path / "arr.zarr", shape=shape, chunks=chunks, dtype="float32", ) assert isinstance(arr.metadata, ArrayV3Metadata) assert isinstance(arr.metadata.chunk_grid, grid_type) d = arr.metadata.to_dict() chunk_grid_dict = d["chunk_grid"] assert isinstance(chunk_grid_dict, dict) assert chunk_grid_dict["name"] == grid_name def test_e2e_chunk_grid_name_roundtrip_preserves_rectilinear(tmp_path: Path) -> None: """A rectilinear grid with uniform edges stays 'rectilinear' through to_dict/from_dict.""" from zarr.core.metadata.v3 import ArrayV3Metadata, RectilinearChunkGridMetadata meta_dict: dict[str, Any] = { "zarr_format": 3, "node_type": "array", "shape": [100, 100], "chunk_grid": { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[[50, 2]], [[25, 4]]]}, }, "chunk_key_encoding": {"name": "default"}, "data_type": "float32", "fill_value": 0.0, "codecs": [{"name": "bytes", "configuration": {"endian": "little"}}], } meta = ArrayV3Metadata.from_dict(meta_dict) assert isinstance(meta.chunk_grid, RectilinearChunkGridMetadata) d = meta.to_dict() chunk_grid_dict = d["chunk_grid"] assert isinstance(chunk_grid_dict, dict) assert chunk_grid_dict["name"] == "rectilinear" def test_e2e_chunk_grid_name_regular_from_dict(tmp_path: Path) -> None: """A 'regular' chunk grid name is preserved through from_dict.""" from zarr.core.metadata.v3 import ArrayV3Metadata, RegularChunkGridMetadata meta_dict: dict[str, Any] = { "zarr_format": 3, "node_type": "array", "shape": [100, 100], "chunk_grid": { "name": "regular", "configuration": {"chunk_shape": [50, 25]}, }, "chunk_key_encoding": {"name": "default"}, "data_type": "float32", "fill_value": 0.0, "codecs": [{"name": "bytes", "configuration": {"endian": "little"}}], } meta = ArrayV3Metadata.from_dict(meta_dict) assert isinstance(meta.chunk_grid, RegularChunkGridMetadata) d = meta.to_dict() chunk_grid_dict = d["chunk_grid"] assert isinstance(chunk_grid_dict, dict) assert chunk_grid_dict["name"] == "regular" # --------------------------------------------------------------------------- # Sharding compatibility tests # --------------------------------------------------------------------------- def test_sharding_accepts_rectilinear_outer_grid() -> None: """ShardingCodec.validate should not reject rectilinear outer grids.""" from zarr.codecs.sharding import ShardingCodec from zarr.core.dtype import Float32 from zarr.core.metadata.v3 import RectilinearChunkGridMetadata codec = ShardingCodec(chunk_shape=(5, 5)) grid_meta = RectilinearChunkGridMetadata(chunk_shapes=((10, 20, 30), (50, 50))) codec.validate( shape=(60, 100), dtype=Float32(), chunk_grid=grid_meta, ) def test_sharding_rejects_non_divisible_rectilinear() -> None: """Rectilinear shard sizes not divisible by inner chunk_shape should raise.""" from zarr.codecs.sharding import ShardingCodec from zarr.core.dtype import Float32 from zarr.core.metadata.v3 import RectilinearChunkGridMetadata codec = ShardingCodec(chunk_shape=(5, 5)) grid_meta = RectilinearChunkGridMetadata(chunk_shapes=((10, 20, 17), (50, 50))) with pytest.raises(ValueError, match="divisible"): codec.validate( shape=(47, 100), dtype=Float32(), chunk_grid=grid_meta, ) def test_sharding_accepts_divisible_rectilinear() -> None: """Rectilinear shard sizes all divisible by inner chunk_shape should pass.""" from zarr.codecs.sharding import ShardingCodec from zarr.core.dtype import Float32 from zarr.core.metadata.v3 import RectilinearChunkGridMetadata codec = ShardingCodec(chunk_shape=(5, 5)) grid_meta = RectilinearChunkGridMetadata(chunk_shapes=((10, 20, 30), (50, 50))) codec.validate( shape=(60, 100), dtype=Float32(), chunk_grid=grid_meta, ) def test_sharding_rejects_non_divisible_among_repeated_edges() -> None: """Shard validation catches a non-divisible edge even among many repeated valid ones.""" from zarr.codecs.sharding import ShardingCodec from zarr.core.dtype import Float32 from zarr.core.metadata.v3 import RectilinearChunkGridMetadata # edges (10, 10, 7) — 7 is not divisible by 5 codec = ShardingCodec(chunk_shape=(5,)) grid_meta = RectilinearChunkGridMetadata(chunk_shapes=((10, 10, 7),)) with pytest.raises(ValueError, match="divisible"): codec.validate(shape=(27,), dtype=Float32(), chunk_grid=grid_meta) def test_sharding_accepts_all_repeated_divisible_edges() -> None: """Shard validation passes when all distinct edges are divisible by inner chunk size.""" from zarr.codecs.sharding import ShardingCodec from zarr.core.dtype import Float32 from zarr.core.metadata.v3 import RectilinearChunkGridMetadata # edges (10, 10, 20, 10) — unique values {10, 20}, both divisible by 5 codec = ShardingCodec(chunk_shape=(5,)) grid_meta = RectilinearChunkGridMetadata(chunk_shapes=((10, 10, 20, 10),)) codec.validate(shape=(50,), dtype=Float32(), chunk_grid=grid_meta) # --------------------------------------------------------------------------- # Edge cases # --------------------------------------------------------------------------- def test_edge_case_chunk_grid_boundary_getitem() -> None: """ChunkGrid with boundary FixedDimension via direct construction.""" g = ChunkGrid(dimensions=(FixedDimension(10, 95), FixedDimension(20, 40))) spec = g[(9, 1)] assert spec is not None assert spec.shape == (5, 20) assert spec.codec_shape == (10, 20) assert spec.is_boundary def test_edge_case_chunk_grid_boundary_iter() -> None: """Iterating a boundary grid yields correct boundary ChunkSpecs.""" g = ChunkGrid(dimensions=(FixedDimension(10, 25),)) specs = list(g) assert len(specs) == 3 assert specs[0].shape == (10,) assert specs[1].shape == (10,) assert specs[2].shape == (5,) assert specs[2].is_boundary assert not specs[0].is_boundary def test_edge_case_chunk_grid_boundary_shape() -> None: """shape property with boundary extent.""" g = ChunkGrid(dimensions=(FixedDimension(10, 95),)) assert g.grid_shape == (10,) # -- Zero-size and zero-extent -- @pytest.mark.parametrize( ("size", "extent"), [(0, 0), (0, 5), (10, 0)], ids=["zero-size-zero-extent", "zero-size-nonzero-extent", "zero-extent-nonzero-size"], ) def test_edge_case_zero_size_or_extent(size: int, extent: int) -> None: """FixedDimension with zero size or extent has zero chunks and getitem returns None""" d = FixedDimension(size=size, extent=extent) assert d.nchunks == 0 g = ChunkGrid(dimensions=(d,)) assert g[0] is None def test_edge_case_zero_size_data_and_indices() -> None: """FixedDimension(size=0) handles data_size, index_to_chunk, and indices_to_chunks safely.""" d = FixedDimension(size=0, extent=0) # Zero-sized chunks have zero data assert d.data_size(0) == 0 # Vectorized lookup maps every index to chunk 0 (avoids division by zero) indices = np.array([0, 0, 0], dtype=np.intp) np.testing.assert_array_equal(d.indices_to_chunks(indices), np.zeros(3, dtype=np.intp)) def test_edge_case_zero_size_nonzero_extent_index() -> None: """FixedDimension(size=0, extent>0) maps valid indices to chunk 0 without dividing by zero.""" d = FixedDimension(size=0, extent=5) assert d.nchunks == 0 # index_to_chunk avoids division by zero and returns 0 assert d.index_to_chunk(0) == 0 assert d.index_to_chunk(4) == 0 def test_edge_case_zero_size_data_and_index() -> None: """FixedDimension(size=0) returns zero for data_size and maps indices to chunk 0.""" d = FixedDimension(size=0, extent=0) # data_size returns 0 for a zero-sized chunk assert d.data_size(0) == 0 # vectorized indices_to_chunks returns zeros indices = np.array([0, 0, 0], dtype=np.intp) np.testing.assert_array_equal(d.indices_to_chunks(indices), np.zeros(3, dtype=np.intp)) # -- 0-d grid -- def test_0d_grid_getitem() -> None: """0-d grid has exactly one chunk at coords ().""" g = ChunkGrid.from_sizes((), ()) spec = g[()] assert spec is not None assert spec.shape == () assert spec.codec_shape == () assert not spec.is_boundary def test_0d_grid_iter() -> None: """0-d grid iteration yields a single ChunkSpec.""" g = ChunkGrid.from_sizes((), ()) specs = list(g) assert len(specs) == 1 def test_0d_grid_all_chunk_coords() -> None: """0-d grid has one chunk coord: the empty tuple.""" g = ChunkGrid.from_sizes((), ()) coords = list(g.all_chunk_coords()) assert coords == [()] def test_0d_grid_nchunks() -> None: """0-d grid reports exactly one chunk""" g = ChunkGrid.from_sizes((), ()) assert g.get_nchunks() == 1 # -- parse_chunk_grid edge cases -- def test_parse_chunk_grid_preserves_varying_extent() -> None: """parse_chunk_grid does not overwrite VaryingDimension extent.""" g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) assert isinstance(g._dimensions[0], VaryingDimension) assert g._dimensions[0].extent == 60 g2 = ChunkGrid( dimensions=tuple( dim.with_extent(ext) for dim, ext in zip(g._dimensions, (60, 100), strict=True) ) ) assert isinstance(g2._dimensions[0], VaryingDimension) assert g2._dimensions[0].extent == 60 def test_parse_chunk_grid_rebinds_fixed_extent() -> None: """parse_chunk_grid updates FixedDimension extent from array shape.""" g = ChunkGrid.from_sizes((100, 200), (10, 20)) assert g._dimensions[0].extent == 100 g2 = ChunkGrid( dimensions=tuple( dim.with_extent(ext) for dim, ext in zip(g._dimensions, (50, 100), strict=True) ) ) assert isinstance(g2._dimensions[0], FixedDimension) assert g2._dimensions[0].extent == 50 assert g2.grid_shape == (5, 5) # -- ChunkGrid.__getitem__ validation -- def test_getitem_int_1d_regular() -> None: """Integer indexing works for 1-d regular grids.""" g = ChunkGrid.from_sizes((100,), (10,)) spec = g[0] assert spec is not None assert spec.shape == (10,) assert spec.slices == (slice(0, 10, 1),) spec = g[9] assert spec is not None assert spec.shape == (10,) def test_getitem_int_1d_rectilinear() -> None: """Integer indexing works for 1-d rectilinear grids.""" g = ChunkGrid.from_sizes((100,), [[20, 30, 50]]) spec = g[0] assert spec is not None assert spec.shape == (20,) spec = g[1] assert spec is not None assert spec.shape == (30,) spec = g[2] assert spec is not None assert spec.shape == (50,) @pytest.mark.parametrize( ("shape", "chunks", "match"), [ ((), (), "Expected 0 coordinate.*got 1"), ((100, 200), (10, 20), "Expected 2 coordinate.*got 1"), ], ids=["0d", "2d"], ) def test_getitem_int_ndim_mismatch_raises( shape: tuple[int, ...], chunks: tuple[int, ...], match: str ) -> None: """Integer indexing on a multi-dim or 0-d grid raises ValueError for ndim mismatch""" g = ChunkGrid.from_sizes(shape, chunks) with pytest.raises(ValueError, match=match): g[0] @pytest.mark.parametrize( "index", [(10,), (99,), (-1,)], ids=["oob-10", "oob-99", "negative"], ) def test_getitem_oob_returns_none(index: tuple[int, ...]) -> None: """Out-of-bounds or negative chunk indices return None""" g = ChunkGrid.from_sizes((100,), (10,)) assert g[index] is None # -- Rectilinear with zero-nchunks FixedDimension -- def test_zero_nchunks_fixed_dim_in_rectilinear() -> None: """A rectilinear grid with a 0-extent FixedDimension still has valid size.""" g = ChunkGrid( dimensions=( VaryingDimension([10, 20], extent=30), FixedDimension(size=10, extent=0), ) ) assert g.grid_shape == (2, 0) # -- VaryingDimension data_size -- def test_varying_dim_data_size_equals_chunk_size() -> None: """For VaryingDimension, data_size == chunk_size (no padding).""" d = VaryingDimension([10, 20, 5], extent=35) for i in range(3): assert d.data_size(i) == d.chunk_size(i) # --------------------------------------------------------------------------- # OrthogonalIndexer rectilinear tests # --------------------------------------------------------------------------- def test_orthogonal_int_array_selection_rectilinear() -> None: """Integer array selection with rectilinear grid must produce correct chunk-local selections.""" from zarr.core.indexing import OrthogonalIndexer g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) indexer = OrthogonalIndexer( selection=(np.array([5, 15, 35]), slice(None)), shape=(60, 100), chunk_grid=g, ) projections = list(indexer) chunk_coords = [p.chunk_coords for p in projections] assert chunk_coords == [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)] def test_orthogonal_bool_array_selection_rectilinear() -> None: """Boolean array selection with rectilinear grid produces correct chunk projections.""" from zarr.core.indexing import OrthogonalIndexer g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) mask = np.zeros(60, dtype=bool) mask[5] = True mask[15] = True mask[35] = True indexer = OrthogonalIndexer( selection=(mask, slice(None)), shape=(60, 100), chunk_grid=g, ) projections = list(indexer) assert len(projections) == 6 chunk_coords = [p.chunk_coords for p in projections] assert (0, 0) in chunk_coords assert (1, 0) in chunk_coords assert (2, 0) in chunk_coords assert (0, 1) in chunk_coords assert (1, 1) in chunk_coords assert (2, 1) in chunk_coords def test_orthogonal_advanced_indexing_produces_correct_projections() -> None: """Verify OrthogonalIndexer produces correct chunk projections for advanced indexing with VaryingDimension.""" from zarr.core.indexing import OrthogonalIndexer g = ChunkGrid.from_sizes((60, 100), [[10, 20, 30], [50, 50]]) indexer = OrthogonalIndexer( selection=(np.array([5, 15]), slice(None)), shape=(60, 100), chunk_grid=g, ) projections = list(indexer) assert len(projections) == 4 coords = [p.chunk_coords for p in projections] assert (0, 0) in coords assert (0, 1) in coords assert (1, 0) in coords assert (1, 1) in coords # --------------------------------------------------------------------------- # Full pipeline rectilinear tests (helpers) # --------------------------------------------------------------------------- def _make_1d(tmp_path: Path) -> tuple[zarr.Array[Any], np.ndarray[Any, Any]]: a = np.arange(30, dtype="int32") z = zarr.create_array( store=tmp_path / "arr1d.zarr", shape=(30,), chunks=[[5, 10, 15]], dtype="int32", ) z[:] = a return z, a def _make_2d(tmp_path: Path) -> tuple[zarr.Array[Any], np.ndarray[Any, Any]]: a = np.arange(6000, dtype="int32").reshape(60, 100) z = zarr.create_array( store=tmp_path / "arr2d.zarr", shape=(60, 100), chunks=[[10, 20, 30], [25, 25, 25, 25]], dtype="int32", ) z[:] = a return z, a # --- Basic selection --- def test_pipeline_basic_selection_1d(tmp_path: Path) -> None: """1D rectilinear basic selections match numpy for ints, slices, and full-array reads""" z, a = _make_1d(tmp_path) sels: list[Any] = [0, 4, 5, 14, 15, 29, -1, slice(None), slice(3, 18), slice(0, 0)] for sel in sels: np.testing.assert_array_equal(z[sel], a[sel], err_msg=f"sel={sel}") def test_pipeline_basic_selection_1d_strided(tmp_path: Path) -> None: """1D rectilinear strided slice selections match numpy""" z, a = _make_1d(tmp_path) for sel in [slice(None, None, 2), slice(1, 25, 3), slice(0, 30, 7)]: np.testing.assert_array_equal(z[sel], a[sel], err_msg=f"sel={sel}") def test_pipeline_basic_selection_2d(tmp_path: Path) -> None: """2D rectilinear basic selections match numpy across chunk boundaries""" z, a = _make_2d(tmp_path) selections: list[Any] = [ 42, -1, (9, 24), (10, 25), (30, 50), (59, 99), slice(None), (slice(5, 35), slice(20, 80)), (slice(0, 10), slice(0, 25)), (slice(10, 10), slice(None)), (slice(None, None, 3), slice(None, None, 7)), ] for sel in selections: np.testing.assert_array_equal(z[sel], a[sel], err_msg=f"sel={sel}") # --- Orthogonal selection --- def test_pipeline_orthogonal_selection_1d_bool(tmp_path: Path) -> None: """1D boolean orthogonal indexing on rectilinear arrays matches numpy""" z, a = _make_1d(tmp_path) ix = np.zeros(30, dtype=bool) ix[[0, 4, 5, 14, 15, 29]] = True np.testing.assert_array_equal(z.oindex[ix], a[ix]) def test_pipeline_orthogonal_selection_1d_int(tmp_path: Path) -> None: """1D integer and negative-index orthogonal selection on rectilinear arrays matches numpy""" z, a = _make_1d(tmp_path) ix = np.array([0, 4, 5, 14, 15, 29]) np.testing.assert_array_equal(z.oindex[ix], a[ix]) ix_neg = np.array([0, -1, -15, -25]) np.testing.assert_array_equal(z.oindex[ix_neg], a[ix_neg]) def test_pipeline_orthogonal_selection_2d_bool(tmp_path: Path) -> None: """2D boolean orthogonal selection on rectilinear arrays matches numpy""" z, a = _make_2d(tmp_path) ix0 = np.zeros(60, dtype=bool) ix0[[0, 9, 10, 29, 30, 59]] = True ix1 = np.zeros(100, dtype=bool) ix1[[0, 24, 25, 49, 50, 99]] = True np.testing.assert_array_equal(z.oindex[ix0, ix1], a[np.ix_(ix0, ix1)]) def test_pipeline_orthogonal_selection_2d_int(tmp_path: Path) -> None: """2D integer orthogonal selection on rectilinear arrays matches numpy""" z, a = _make_2d(tmp_path) ix0 = np.array([0, 9, 10, 29, 30, 59]) ix1 = np.array([0, 24, 25, 49, 50, 99]) np.testing.assert_array_equal(z.oindex[ix0, ix1], a[np.ix_(ix0, ix1)]) def test_pipeline_orthogonal_selection_2d_mixed(tmp_path: Path) -> None: """2D mixed int-array and slice orthogonal selection on rectilinear arrays matches numpy""" z, a = _make_2d(tmp_path) ix = np.array([0, 9, 10, 29, 30, 59]) np.testing.assert_array_equal(z.oindex[ix, slice(25, 75)], a[np.ix_(ix, np.arange(25, 75))]) np.testing.assert_array_equal( z.oindex[slice(10, 30), ix[:4]], a[np.ix_(np.arange(10, 30), ix[:4])] ) # --- Coordinate (vindex) selection --- def test_pipeline_coordinate_selection_1d(tmp_path: Path) -> None: """1D coordinate (vindex) selection on rectilinear arrays matches numpy""" z, a = _make_1d(tmp_path) ix = np.array([0, 4, 5, 14, 15, 29]) np.testing.assert_array_equal(z.vindex[ix], a[ix]) def test_pipeline_coordinate_selection_2d(tmp_path: Path) -> None: """2D coordinate (vindex) selection on rectilinear arrays matches numpy""" z, a = _make_2d(tmp_path) r = np.array([0, 9, 10, 29, 30, 59]) c = np.array([0, 24, 25, 49, 50, 99]) np.testing.assert_array_equal(z.vindex[r, c], a[r, c]) def test_pipeline_coordinate_selection_2d_bool_mask(tmp_path: Path) -> None: """2D boolean mask vindex selection on rectilinear arrays matches numpy""" z, a = _make_2d(tmp_path) mask = a > 3000 np.testing.assert_array_equal(z.vindex[mask], a[mask]) # --- Block selection --- def test_pipeline_block_selection_1d(tmp_path: Path) -> None: """1D block selection on rectilinear arrays returns correct chunk data""" z, a = _make_1d(tmp_path) np.testing.assert_array_equal(z.blocks[0], a[0:5]) np.testing.assert_array_equal(z.blocks[1], a[5:15]) np.testing.assert_array_equal(z.blocks[2], a[15:30]) np.testing.assert_array_equal(z.blocks[-1], a[15:30]) np.testing.assert_array_equal(z.blocks[0:2], a[0:15]) np.testing.assert_array_equal(z.blocks[1:3], a[5:30]) np.testing.assert_array_equal(z.blocks[:], a[:]) def test_pipeline_block_selection_2d(tmp_path: Path) -> None: """2D block selection on rectilinear arrays returns correct chunk data""" z, a = _make_2d(tmp_path) np.testing.assert_array_equal(z.blocks[0, 0], a[0:10, 0:25]) np.testing.assert_array_equal(z.blocks[1, 2], a[10:30, 50:75]) np.testing.assert_array_equal(z.blocks[2, 3], a[30:60, 75:100]) np.testing.assert_array_equal(z.blocks[-1, -1], a[30:60, 75:100]) np.testing.assert_array_equal(z.blocks[0:2, 1:3], a[0:30, 25:75]) np.testing.assert_array_equal(z.blocks[:, :], a[:, :]) def test_pipeline_set_block_selection_1d(tmp_path: Path) -> None: """Writing via 1D block selection on rectilinear arrays persists correctly""" z, a = _make_1d(tmp_path) val = np.full(10, -1, dtype="int32") z.blocks[1] = val a[5:15] = val np.testing.assert_array_equal(z[:], a) def test_pipeline_set_block_selection_2d(tmp_path: Path) -> None: """Writing via 2D block selection on rectilinear arrays persists correctly""" z, a = _make_2d(tmp_path) val = np.full((30, 50), -99, dtype="int32") z.blocks[0:2, 1:3] = val a[0:30, 25:75] = val np.testing.assert_array_equal(z[:], a) def test_pipeline_block_selection_slice_stop_at_nchunks(tmp_path: Path) -> None: """Block slice with stop == nchunks exercises the dim_len fallback.""" z, a = _make_1d(tmp_path) np.testing.assert_array_equal(z.blocks[1:3], a[5:30]) np.testing.assert_array_equal(z.blocks[0:10], a[:]) def test_pipeline_block_selection_slice_stop_at_nchunks_2d(tmp_path: Path) -> None: """Same fallback test for 2D rectilinear arrays.""" z, a = _make_2d(tmp_path) np.testing.assert_array_equal(z.blocks[2:3, 3:4], a[30:60, 75:100]) np.testing.assert_array_equal(z.blocks[0:99, 0:99], a[:, :]) # --- Set coordinate selection --- def test_pipeline_set_coordinate_selection_1d(tmp_path: Path) -> None: """Writing via 1D coordinate selection on rectilinear arrays persists correctly""" z, a = _make_1d(tmp_path) ix = np.array([0, 4, 5, 14, 15, 29]) val = np.full(len(ix), -7, dtype="int32") z.vindex[ix] = val a[ix] = val np.testing.assert_array_equal(z[:], a) def test_pipeline_set_coordinate_selection_2d(tmp_path: Path) -> None: """Writing via 2D coordinate selection on rectilinear arrays persists correctly""" z, a = _make_2d(tmp_path) r = np.array([0, 9, 10, 29, 30, 59]) c = np.array([0, 24, 25, 49, 50, 99]) val = np.full(len(r), -42, dtype="int32") z.vindex[r, c] = val a[r, c] = val np.testing.assert_array_equal(z[:], a) # --- Set selection --- def test_pipeline_set_basic_selection(tmp_path: Path) -> None: """Writing via basic slice selection on rectilinear arrays persists correctly""" z, a = _make_2d(tmp_path) new_data = np.full((20, 50), -1, dtype="int32") z[5:25, 10:60] = new_data a[5:25, 10:60] = new_data np.testing.assert_array_equal(z[:], a) def test_pipeline_set_orthogonal_selection(tmp_path: Path) -> None: """Writing via orthogonal selection on rectilinear arrays persists correctly""" z, a = _make_2d(tmp_path) rows = np.array([0, 10, 30]) cols = np.array([0, 25, 50, 75]) val = np.full((3, 4), -99, dtype="int32") z.oindex[rows, cols] = val a[np.ix_(rows, cols)] = val np.testing.assert_array_equal(z[:], a) # --- Higher dimensions --- def test_pipeline_3d_array(tmp_path: Path) -> None: """3D rectilinear array write and read-back match numpy""" shape = (12, 20, 15) chunk_shapes = [[4, 8], [5, 5, 10], [5, 10]] a = np.arange(int(np.prod(shape)), dtype="int32").reshape(shape) z = zarr.create_array( store=tmp_path / "arr3d.zarr", shape=shape, chunks=chunk_shapes, dtype="int32", ) z[:] = a np.testing.assert_array_equal(z[:], a) np.testing.assert_array_equal(z[2:10, 3:18, 4:14], a[2:10, 3:18, 4:14]) def test_pipeline_1d_single_chunk(tmp_path: Path) -> None: """Single-chunk rectilinear array write and read-back match numpy""" a = np.arange(20, dtype="int32") z = zarr.create_array( store=tmp_path / "arr1c.zarr", shape=(20,), chunks=[[20]], dtype="int32", ) z[:] = a np.testing.assert_array_equal(z[:], a) # --- Persistence roundtrip --- def test_pipeline_persistence_roundtrip(tmp_path: Path) -> None: """Rectilinear array survives close and reopen with correct data""" _, a = _make_2d(tmp_path) z2 = zarr.open_array(store=tmp_path / "arr2d.zarr", mode="r") assert not ChunkGrid.from_metadata(z2.metadata).is_regular np.testing.assert_array_equal(z2[:], a) # --- Highly irregular chunks --- def test_pipeline_highly_irregular_chunks(tmp_path: Path) -> None: """Highly irregular chunk sizes produce correct write and partial-read results""" shape = (100, 100) chunk_shapes = [[5, 10, 15, 20, 50], [100]] a = np.arange(10000, dtype="int32").reshape(shape) z = zarr.create_array( store=tmp_path / "irreg.zarr", shape=shape, chunks=chunk_shapes, dtype="int32", ) z[:] = a np.testing.assert_array_equal(z[:], a) np.testing.assert_array_equal(z[3:97, 10:90], a[3:97, 10:90]) # --- API validation --- def test_pipeline_v2_rejects_rectilinear(tmp_path: Path) -> None: """Creating a rectilinear array with zarr_format=2 raises ValueError""" with pytest.raises(ValueError, match="Zarr format 2"): zarr.create_array( store=tmp_path / "v2.zarr", shape=(30,), chunks=[[10, 20]], dtype="int32", zarr_format=2, ) def test_pipeline_sharding_rejects_rectilinear_chunks_with_shards(tmp_path: Path) -> None: """Rectilinear chunks (inner) with sharding is not supported.""" with pytest.raises(ValueError, match="Rectilinear chunks with sharding"): zarr.create_array( store=tmp_path / "shard.zarr", shape=(60, 100), chunks=[[10, 20, 30], [25, 25, 25, 25]], shards=(30, 50), dtype="int32", ) def test_pipeline_rectilinear_shards_roundtrip(tmp_path: Path) -> None: """Rectilinear shards with uniform inner chunks: full write/read roundtrip.""" data = np.arange(120 * 100, dtype="int32").reshape(120, 100) arr = zarr.create_array( store=tmp_path / "rect_shards.zarr", shape=(120, 100), chunks=(10, 10), shards=[[60, 40, 20], [50, 50]], dtype="int32", ) arr[:] = data result = arr[:] np.testing.assert_array_equal(result, data) def test_pipeline_rectilinear_shards_partial_read(tmp_path: Path) -> None: """Partial reads across rectilinear shard boundaries.""" data = np.arange(120 * 100, dtype="float64").reshape(120, 100) arr = zarr.create_array( store=tmp_path / "rect_shards.zarr", shape=(120, 100), chunks=(10, 10), shards=[[60, 40, 20], [50, 50]], dtype="float64", ) arr[:] = data result = arr[50:70, 40:60] np.testing.assert_array_equal(result, data[50:70, 40:60]) def test_pipeline_rectilinear_shards_validates_divisibility(tmp_path: Path) -> None: """Inner chunk_shape must divide every shard's dimensions.""" with pytest.raises(ValueError, match="divisible"): zarr.create_array( store=tmp_path / "bad.zarr", shape=(120, 100), chunks=(10, 10), shards=[[60, 45, 15], [50, 50]], dtype="int32", ) def test_pipeline_nchunks(tmp_path: Path) -> None: """Rectilinear array reports the correct total number of chunks""" z, _ = _make_2d(tmp_path) assert ChunkGrid.from_metadata(z.metadata).get_nchunks() == 12 def test_pipeline_parse_chunk_grid_regular_from_dict() -> None: """parse_chunk_grid constructs a regular grid from a metadata dict.""" d: dict[str, Any] = {"name": "regular", "configuration": {"chunk_shape": [10, 20]}} meta = parse_chunk_grid(d) assert isinstance(meta, RegularChunkGridMetadata) g = ChunkGrid.from_sizes((100, 200), tuple(meta.chunk_shape)) assert g.is_regular assert g.chunk_shape == (10, 20) assert g.grid_shape == (10, 10) assert g.get_nchunks() == 100 # --------------------------------------------------------------------------- # VaryingDimension boundary tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("edges", "extent", "chunk_idx", "expected_data_size"), [ ([10, 20, 30], 50, 0, 10), ([10, 20, 30], 50, 1, 20), ([10, 20, 30], 50, 2, 20), ([10, 20, 30], 60, 2, 30), ([10, 20, 30], 31, 0, 10), ([10, 20, 30], 31, 1, 20), ([10, 20, 30], 31, 2, 1), ], ids=[ "interior-0", "interior-1", "boundary-clipped", "exact-no-clip", "single-element-boundary-0", "single-element-boundary-1", "single-element-boundary-2", ], ) def test_varying_dimension_boundary_data_size( edges: list[int], extent: int, chunk_idx: int, expected_data_size: int ) -> None: """VaryingDimension.data_size clips correctly at boundary chunks""" d = VaryingDimension(edges, extent=extent) assert d.data_size(chunk_idx) == expected_data_size def test_varying_dimension_boundary_extent_parameter() -> None: """VaryingDimension preserves extent and full chunk_size even when extent < sum of edges""" d = VaryingDimension([10, 20, 30], extent=50) assert d.extent == 50 assert d.chunk_size(2) == 30 def test_varying_dimension_extent_exceeds_sum_rejected() -> None: """VaryingDimension rejects extent greater than sum of edges""" with pytest.raises(ValueError, match="exceeds sum of edges"): VaryingDimension([10, 20], extent=50) def test_varying_dimension_negative_extent_rejected() -> None: """VaryingDimension rejects negative extent""" with pytest.raises(ValueError, match="must be >= 0"): VaryingDimension([10, 20], extent=-1) def test_varying_dimension_zero_extent() -> None: """VaryingDimension with extent=0 has zero active chunks but retains all grid cells.""" d = VaryingDimension([10, 20], extent=0) assert d.nchunks == 0 assert d.ngridcells == 2 # No chunks overlap [0, 0), so the grid is structurally non-empty but logically empty g = ChunkGrid(dimensions=(d,)) assert g.grid_shape == (0,) assert list(g) == [] def test_varying_dimension_boundary_chunk_spec() -> None: """ChunkGrid with a boundary VaryingDimension produces correct ChunkSpec.""" g = ChunkGrid(dimensions=(VaryingDimension([10, 20, 30], extent=50),)) spec = g[(2,)] assert spec is not None assert spec.codec_shape == (30,) assert spec.shape == (20,) assert spec.is_boundary is True def test_varying_dimension_interior_chunk_spec() -> None: """Interior VaryingDimension chunk has matching codec_shape and shape with no boundary""" g = ChunkGrid(dimensions=(VaryingDimension([10, 20, 30], extent=50),)) spec = g[(0,)] assert spec is not None assert spec.codec_shape == (10,) assert spec.shape == (10,) assert spec.is_boundary is False # --------------------------------------------------------------------------- # Multiple overflow chunks tests # --------------------------------------------------------------------------- def test_overflow_multiple_chunks_past_extent() -> None: """Edges past extent are structural; nchunks counts active only.""" g = ChunkGrid.from_sizes((50,), [[10, 20, 30, 40]]) d = g._dimensions[0] assert d.ngridcells == 4 assert d.nchunks == 3 assert d.data_size(0) == 10 assert d.data_size(1) == 20 assert d.data_size(2) == 20 assert d.chunk_size(2) == 30 def test_overflow_chunk_spec_past_extent_is_oob() -> None: """Chunk entirely past the extent is out of bounds (not active).""" g = ChunkGrid.from_sizes((50,), [[10, 20, 30, 40]]) spec = g[(3,)] assert spec is None def test_overflow_chunk_spec_partial() -> None: """ChunkSpec for a partially-overflowing chunk clips correctly.""" g = ChunkGrid.from_sizes((50,), [[10, 20, 30, 40]]) spec = g[(2,)] assert spec is not None assert spec.shape == (20,) assert spec.codec_shape == (30,) assert spec.is_boundary is True assert spec.slices == (slice(30, 50, 1),) def test_overflow_chunk_sizes() -> None: """chunk_sizes only includes active chunks.""" g = ChunkGrid.from_sizes((50,), [[10, 20, 30, 40]]) assert g.chunk_sizes == ((10, 20, 20),) def test_overflow_multidim() -> None: """Overflow in multiple dimensions simultaneously.""" g = ChunkGrid.from_sizes((45, 100), [[10, 20, 30], [40, 40, 40]]) assert g.chunk_sizes == ((10, 20, 15), (40, 40, 20)) spec = g[(2, 2)] assert spec is not None assert spec.shape == (15, 20) assert spec.codec_shape == (30, 40) def test_overflow_uniform_edges_collapses_to_fixed() -> None: """Uniform edges where len == ceildiv(extent, edge) collapse to FixedDimension.""" g = ChunkGrid.from_sizes((35,), [[10, 10, 10, 10]]) assert isinstance(g._dimensions[0], FixedDimension) assert g.is_regular assert g.chunk_sizes == ((10, 10, 10, 5),) assert g._dimensions[0].nchunks == 4 def test_overflow_index_to_chunk_near_extent() -> None: """Index lookup near and at the extent boundary.""" d = VaryingDimension([10, 20, 30, 40], extent=50) assert d.index_to_chunk(29) == 1 assert d.index_to_chunk(30) == 2 assert d.index_to_chunk(49) == 2 # --------------------------------------------------------------------------- # Boundary indexing tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ( "dim", "mask", "dim_len", "expected_chunk_ix", "expected_sel_len", "expected_first_two", "expected_third", ), [ ( FixedDimension(size=5, extent=7), np.array([False, False, False, False, False, True, True]), 7, 1, 5, (np.True_, np.True_), np.False_, ), ( VaryingDimension([5, 10], extent=7), np.array([False, False, False, False, False, True, True]), 7, 1, 10, (np.True_, np.True_), np.False_, ), ], ids=["fixed-boundary", "varying-boundary"], ) def test_bool_indexer_boundary( dim: FixedDimension | VaryingDimension, mask: np.ndarray[Any, Any], dim_len: int, expected_chunk_ix: int, expected_sel_len: int, expected_first_two: tuple[Any, Any], expected_third: Any, ) -> None: """BoolArrayDimIndexer pads to codec size for boundary chunks.""" from zarr.core.indexing import BoolArrayDimIndexer indexer = BoolArrayDimIndexer(mask, dim_len, dim) projections = list(indexer) assert len(projections) == 1 p = projections[0] assert p.dim_chunk_ix == expected_chunk_ix sel = p.dim_chunk_sel assert isinstance(sel, np.ndarray) assert sel.shape[0] == expected_sel_len assert sel[0] is expected_first_two[0] assert sel[1] is expected_first_two[1] assert sel[2] is expected_third def test_bool_indexer_no_padding_interior() -> None: """No padding needed for interior chunks.""" from zarr.core.indexing import BoolArrayDimIndexer dim = FixedDimension(size=5, extent=10) mask = np.array([True, False, False, False, False, False, False, False, False, False]) indexer = BoolArrayDimIndexer(mask, 10, dim) projections = list(indexer) assert len(projections) == 1 p = projections[0] assert p.dim_chunk_ix == 0 sel = p.dim_chunk_sel assert isinstance(sel, np.ndarray) assert sel.shape[0] == 5 def test_slice_indexer_varying_boundary() -> None: """SliceDimIndexer clips to data_size at boundary for VaryingDimension.""" from zarr.core.indexing import SliceDimIndexer dim = VaryingDimension([5, 10], extent=7) indexer = SliceDimIndexer(slice(None), 7, dim) projections = list(indexer) assert len(projections) == 2 assert projections[0].dim_chunk_sel == slice(0, 5, 1) assert projections[1].dim_chunk_sel == slice(0, 2, 1) def test_int_array_indexer_varying_boundary() -> None: """IntArrayDimIndexer handles indices near boundary correctly.""" from zarr.core.indexing import IntArrayDimIndexer dim = VaryingDimension([5, 10], extent=7) indices = np.array([6]) indexer = IntArrayDimIndexer(indices, 7, dim) projections = list(indexer) assert len(projections) == 1 assert projections[0].dim_chunk_ix == 1 sel = projections[0].dim_chunk_sel assert isinstance(sel, np.ndarray) np.testing.assert_array_equal(sel, [1]) @pytest.mark.parametrize( "dim", [FixedDimension(size=2, extent=10), VaryingDimension([5, 5], extent=10)], ids=["fixed", "varying"], ) def test_slice_indexer_empty_slice_at_boundary(dim: FixedDimension | VaryingDimension) -> None: """SliceDimIndexer yields no projections for an empty slice at the dimension boundary.""" from zarr.core.indexing import SliceDimIndexer indexer = SliceDimIndexer(slice(10, 10), 10, dim) projections = list(indexer) assert len(projections) == 0 def test_orthogonal_indexer_varying_boundary_advanced() -> None: """OrthogonalIndexer with advanced indexing uses per-chunk chunk_size.""" from zarr.core.indexing import OrthogonalIndexer g = ChunkGrid( dimensions=( VaryingDimension([5, 10], extent=7), FixedDimension(size=4, extent=8), ) ) indexer = OrthogonalIndexer( selection=(np.array([0, 6]), slice(None)), shape=(7, 8), chunk_grid=g, ) projections = list(indexer) assert len(projections) == 4 coords = {p.chunk_coords for p in projections} assert coords == {(0, 0), (0, 1), (1, 0), (1, 1)} # --------------------------------------------------------------------------- # update_shape tests # --------------------------------------------------------------------------- def test_update_shape_no_change() -> None: """update_shape with the same shape preserves edges unchanged""" grid = ChunkGrid.from_sizes((60, 50), [[10, 20, 30], [25, 25]]) new_grid = grid.update_shape((60, 50)) assert _edges(new_grid, 0) == (10, 20, 30) assert _edges(new_grid, 1) == (25, 25) def test_update_shape_grow_single_dim() -> None: """Growing a single dimension appends a new edge chunk""" grid = ChunkGrid.from_sizes((60, 50), [[10, 20, 30], [25, 25]]) new_grid = grid.update_shape((80, 50)) assert _edges(new_grid, 0) == (10, 20, 30, 20) assert _edges(new_grid, 1) == (25, 25) def test_update_shape_grow_multiple_dims() -> None: """Growing multiple dimensions appends correctly sized edge chunks""" grid = ChunkGrid.from_sizes((30, 50), [[10, 20], [20, 30]]) new_grid = grid.update_shape((45, 65)) assert _edges(new_grid, 0) == (10, 20, 15) assert _edges(new_grid, 1) == (20, 30, 15) def test_update_shape_shrink_single_dim() -> None: """Shrinking a single dimension reduces nchunks while preserving edges""" grid = ChunkGrid.from_sizes((100, 50), [[10, 20, 30, 40], [25, 25]]) new_grid = grid.update_shape((35, 50)) assert _edges(new_grid, 0) == (10, 20, 30, 40) assert new_grid._dimensions[0].nchunks == 3 assert _edges(new_grid, 1) == (25, 25) def test_update_shape_shrink_to_single_chunk() -> None: """Shrinking to fit within the first chunk reduces nchunks to 1""" grid = ChunkGrid.from_sizes((60, 50), [[10, 20, 30], [25, 25]]) new_grid = grid.update_shape((5, 50)) assert _edges(new_grid, 0) == (10, 20, 30) assert new_grid._dimensions[0].nchunks == 1 assert _edges(new_grid, 1) == (25, 25) def test_update_shape_shrink_multiple_dims() -> None: """Shrinking multiple dimensions reduces nchunks in each dimension""" grid = ChunkGrid.from_sizes((40, 60), [[10, 10, 15, 5], [20, 25, 15]]) new_grid = grid.update_shape((25, 35)) assert _edges(new_grid, 0) == (10, 10, 15, 5) assert new_grid._dimensions[0].nchunks == 3 assert _edges(new_grid, 1) == (20, 25, 15) assert new_grid._dimensions[1].nchunks == 2 def test_update_shape_dimension_mismatch_error() -> None: """update_shape raises ValueError when new shape has different ndim""" grid = ChunkGrid.from_sizes((30, 70), [[10, 20], [30, 40]]) with pytest.raises(ValueError, match="dimensions"): grid.update_shape((30, 70, 100)) def test_update_shape_boundary_cases() -> None: """update_shape handles grow-one-dim and shrink-both-dims edge cases correctly""" grid = ChunkGrid.from_sizes((60, 40), [[10, 20, 30], [15, 25]]) new_grid = grid.update_shape((60, 65)) assert _edges(new_grid, 0) == (10, 20, 30) assert _edges(new_grid, 1) == (15, 25, 25) grid2 = ChunkGrid.from_sizes((60, 50), [[10, 20, 30], [15, 25, 10]]) new_grid2 = grid2.update_shape((30, 40)) assert _edges(new_grid2, 0) == (10, 20, 30) assert new_grid2._dimensions[0].nchunks == 2 assert _edges(new_grid2, 1) == (15, 25, 10) assert new_grid2._dimensions[1].nchunks == 2 def test_update_shape_regular_preserves_extents(tmp_path: Path) -> None: """Resize a regular array -- chunk_grid extents must match new shape.""" z = zarr.create_array( store=tmp_path / "regular.zarr", shape=(100,), chunks=(10,), dtype="int32", ) z[:] = np.arange(100, dtype="int32") z.resize(50) assert z.shape == (50,) assert ChunkGrid.from_metadata(z.metadata)._dimensions[0].extent == 50 # --------------------------------------------------------------------------- # update_shape boundary tests # --------------------------------------------------------------------------- def test_update_shape_shrink_creates_boundary() -> None: """Shrinking extent into a chunk creates a boundary with clipped data_size""" grid = ChunkGrid.from_sizes((60,), [[10, 20, 30]]) new_grid = grid.update_shape((45,)) dim = new_grid._dimensions[0] assert isinstance(dim, VaryingDimension) assert dim.edges == (10, 20, 30) assert dim.extent == 45 assert dim.chunk_size(2) == 30 assert dim.data_size(2) == 15 def test_update_shape_shrink_to_exact_boundary() -> None: """Shrinking to an exact chunk boundary reduces nchunks without partial data""" grid = ChunkGrid.from_sizes((60,), [[10, 20, 30]]) new_grid = grid.update_shape((30,)) dim = new_grid._dimensions[0] assert isinstance(dim, VaryingDimension) assert dim.edges == (10, 20, 30) assert dim.nchunks == 2 assert dim.ngridcells == 3 assert dim.extent == 30 assert dim.data_size(1) == 20 def test_update_shape_shrink_chunk_spec() -> None: """After shrink, ChunkSpec reflects boundary correctly.""" grid = ChunkGrid.from_sizes((60,), [[10, 20, 30]]) new_grid = grid.update_shape((45,)) spec = new_grid[(2,)] assert spec is not None assert spec.codec_shape == (30,) assert spec.shape == (15,) assert spec.is_boundary is True def test_update_shape_parse_chunk_grid_rebinds_extent() -> None: """parse_chunk_grid re-binds VaryingDimension extent to array shape.""" g = ChunkGrid.from_sizes((60,), [[10, 20, 30]]) g2 = ChunkGrid( dimensions=tuple( dim.with_extent(ext) for dim, ext in zip(g._dimensions, (50,), strict=True) ) ) dim = g2._dimensions[0] assert isinstance(dim, VaryingDimension) assert dim.extent == 50 assert dim.data_size(2) == 20 # --------------------------------------------------------------------------- # Resize rectilinear tests # --------------------------------------------------------------------------- async def test_async_resize_grow() -> None: """Async resize grow appends new edge chunks and preserves existing data""" store = zarr.storage.MemoryStore() arr = await zarr.api.asynchronous.create_array( store=store, shape=(30, 40), chunks=[[10, 20], [20, 20]], dtype="i4", zarr_format=3, ) data = np.arange(30 * 40, dtype="i4").reshape(30, 40) await arr.setitem(slice(None), data) await arr.resize((50, 60)) assert arr.shape == (50, 60) assert _edges(ChunkGrid.from_metadata(arr.metadata), 0) == (10, 20, 20) assert _edges(ChunkGrid.from_metadata(arr.metadata), 1) == (20, 20, 20) result = await arr.getitem((slice(0, 30), slice(0, 40))) np.testing.assert_array_equal(result, data) async def test_async_resize_shrink() -> None: """Async resize shrink truncates data to the new shape""" store = zarr.storage.MemoryStore() arr = await zarr.api.asynchronous.create_array( store=store, shape=(60, 50), chunks=[[10, 20, 30], [25, 25]], dtype="f4", zarr_format=3, ) data = np.arange(60 * 50, dtype="f4").reshape(60, 50) await arr.setitem(slice(None), data) await arr.resize((25, 30)) assert arr.shape == (25, 30) result = await arr.getitem(slice(None)) np.testing.assert_array_equal(result, data[:25, :30]) def test_sync_resize_grow() -> None: """Sync resize grow expands the array and preserves existing data""" store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(20, 30), chunks=[[8, 12], [10, 20]], dtype="u1", zarr_format=3, ) data = np.arange(20 * 30, dtype="u1").reshape(20, 30) arr[:] = data arr.resize((35, 45)) assert arr.shape == (35, 45) np.testing.assert_array_equal(arr[:20, :30], data) def test_sync_resize_shrink() -> None: """Sync resize shrink truncates the array and returns correct data""" store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(40, 50), chunks=[[10, 15, 15], [20, 30]], dtype="i2", zarr_format=3, ) data = np.arange(40 * 50, dtype="i2").reshape(40, 50) arr[:] = data arr.resize((15, 30)) assert arr.shape == (15, 30) np.testing.assert_array_equal(arr[:], data[:15, :30]) # --------------------------------------------------------------------------- # Append rectilinear tests # --------------------------------------------------------------------------- async def test_append_first_axis() -> None: """Appending along axis 0 grows the array and concatenates data correctly""" store = zarr.storage.MemoryStore() arr = await zarr.api.asynchronous.create_array( store=store, shape=(30, 20), chunks=[[10, 20], [10, 10]], dtype="i4", zarr_format=3, ) initial = np.arange(30 * 20, dtype="i4").reshape(30, 20) await arr.setitem(slice(None), initial) append_data = np.arange(30 * 20, 45 * 20, dtype="i4").reshape(15, 20) await arr.append(append_data, axis=0) assert arr.shape == (45, 20) result = await arr.getitem(slice(None)) np.testing.assert_array_equal(result, np.vstack([initial, append_data])) async def test_append_second_axis() -> None: """Appending along axis 1 grows the array and concatenates data correctly""" store = zarr.storage.MemoryStore() arr = await zarr.api.asynchronous.create_array( store=store, shape=(20, 30), chunks=[[10, 10], [10, 20]], dtype="f4", zarr_format=3, ) initial = np.arange(20 * 30, dtype="f4").reshape(20, 30) await arr.setitem(slice(None), initial) append_data = np.arange(20 * 30, 20 * 45, dtype="f4").reshape(20, 15) await arr.append(append_data, axis=1) assert arr.shape == (20, 45) result = await arr.getitem(slice(None)) np.testing.assert_array_equal(result, np.hstack([initial, append_data])) def test_sync_append() -> None: """Sync append grows the array and preserves both initial and appended data""" store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(20, 20), chunks=[[8, 12], [7, 13]], dtype="u2", zarr_format=3, ) initial = np.arange(20 * 20, dtype="u2").reshape(20, 20) arr[:] = initial append_data = np.arange(20 * 20, 25 * 20, dtype="u2").reshape(5, 20) arr.append(append_data, axis=0) assert arr.shape == (25, 20) np.testing.assert_array_equal(arr[:20, :], initial) np.testing.assert_array_equal(arr[20:, :], append_data) async def test_multiple_appends() -> None: """Multiple sequential appends accumulate data correctly""" store = zarr.storage.MemoryStore() arr = await zarr.api.asynchronous.create_array( store=store, shape=(10, 10), chunks=[[3, 7], [4, 6]], dtype="i4", zarr_format=3, ) initial = np.arange(10 * 10, dtype="i4").reshape(10, 10) await arr.setitem(slice(None), initial) all_data = [initial] for i in range(3): chunk = np.full((5, 10), i + 100, dtype="i4") await arr.append(chunk, axis=0) all_data.append(chunk) assert arr.shape == (25, 10) result = await arr.getitem(slice(None)) np.testing.assert_array_equal(result, np.vstack(all_data)) async def test_append_with_partial_edge_chunks() -> None: """Appending data that creates partial edge chunks preserves all data""" store = zarr.storage.MemoryStore() arr = await zarr.api.asynchronous.create_array( store=store, shape=(25, 30), chunks=[[10, 15], [12, 18]], dtype="f8", zarr_format=3, ) initial = np.random.default_rng(42).random((25, 30)) await arr.setitem(slice(None), initial) append_data = np.random.default_rng(43).random((10, 30)) await arr.append(append_data, axis=0) assert arr.shape == (35, 30) result = np.asarray(await arr.getitem(slice(None))) np.testing.assert_array_almost_equal(result, np.vstack([initial, append_data])) async def test_append_small_data() -> None: """Appending a small amount of data smaller than a chunk works correctly""" store = zarr.storage.MemoryStore() arr = await zarr.api.asynchronous.create_array( store=store, shape=(20, 20), chunks=[[8, 12], [7, 13]], dtype="i4", zarr_format=3, ) data = np.arange(20 * 20, dtype="i4").reshape(20, 20) await arr.setitem(slice(None), data) small = np.full((3, 20), 999, dtype="i4") await arr.append(small, axis=0) assert arr.shape == (23, 20) result = await arr.getitem((slice(20, 23), slice(None))) np.testing.assert_array_equal(result, small) # --------------------------------------------------------------------------- # V2 regression tests # --------------------------------------------------------------------------- def test_v2_create_and_readback(tmp_path: Path) -> None: """Basic V2 array: create, write, read back.""" data = np.arange(60, dtype="float64").reshape(6, 10) a = zarr.create_array( store=tmp_path / "v2.zarr", shape=data.shape, chunks=(3, 5), dtype=data.dtype, zarr_format=2, ) a[:] = data np.testing.assert_array_equal(a[:], data) def test_v2_chunk_grid_is_regular(tmp_path: Path) -> None: """V2 chunk_grid produces a regular ChunkGrid with FixedDimensions.""" a = zarr.create_array( store=tmp_path / "v2.zarr", shape=(20, 30), chunks=(10, 15), dtype="int32", zarr_format=2, ) grid = ChunkGrid.from_metadata(a.metadata) assert grid.is_regular assert grid.chunk_shape == (10, 15) assert grid.grid_shape == (2, 2) assert all(isinstance(d, FixedDimension) for d in grid._dimensions) def test_v2_boundary_chunks(tmp_path: Path) -> None: """V2 boundary chunks: codec buffer size stays full, data is clipped.""" a = zarr.create_array( store=tmp_path / "v2.zarr", shape=(25,), chunks=(10,), dtype="int32", zarr_format=2, ) grid = ChunkGrid.from_metadata(a.metadata) assert grid._dimensions[0].nchunks == 3 assert grid._dimensions[0].chunk_size(2) == 10 assert grid._dimensions[0].data_size(2) == 5 def test_v2_slicing_with_boundary(tmp_path: Path) -> None: """V2 array slicing across boundary chunks returns correct data.""" data = np.arange(25, dtype="int32") a = zarr.create_array( store=tmp_path / "v2.zarr", shape=(25,), chunks=(10,), dtype="int32", zarr_format=2, ) a[:] = data np.testing.assert_array_equal(a[18:25], data[18:25]) np.testing.assert_array_equal(a[:], data) def test_v2_metadata_roundtrip(tmp_path: Path) -> None: """V2 metadata survives store close and reopen.""" store_path = tmp_path / "v2.zarr" data = np.arange(12, dtype="float32").reshape(3, 4) a = zarr.create_array( store=store_path, shape=data.shape, chunks=(2, 2), dtype=data.dtype, zarr_format=2, ) a[:] = data b = zarr.open_array(store=store_path, mode="r") assert b.metadata.zarr_format == 2 assert b.chunks == (2, 2) assert ChunkGrid.from_metadata(b.metadata).chunk_shape == (2, 2) np.testing.assert_array_equal(b[:], data) def test_v2_chunk_spec_via_grid(tmp_path: Path) -> None: """ChunkSpec from V2 grid has correct slices and codec_shape.""" a = zarr.create_array( store=tmp_path / "v2.zarr", shape=(15, 20), chunks=(10, 10), dtype="int32", zarr_format=2, ) grid = ChunkGrid.from_metadata(a.metadata) spec = grid[(0, 0)] assert spec is not None assert spec.shape == (10, 10) assert spec.codec_shape == (10, 10) spec = grid[(1, 1)] assert spec is not None assert spec.shape == (5, 10) assert spec.codec_shape == (10, 10) # --------------------------------------------------------------------------- # ChunkSizes tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("shape", "chunks", "expected"), [ ((100, 80), (30, 40), ((30, 30, 30, 10), (40, 40))), ((90, 80), (30, 40), ((30, 30, 30), (40, 40))), ((60, 100), [[10, 20, 30], [50, 50]], ((10, 20, 30), (50, 50))), ((10,), (10,), ((10,),)), ], ids=["regular", "regular-exact", "rectilinear", "single-chunk"], ) def test_chunk_sizes( shape: tuple[int, ...], chunks: Any, expected: tuple[tuple[int, ...], ...] ) -> None: """chunk_sizes returns the per-dimension tuple of actual data sizes""" grid = ChunkGrid.from_sizes(shape, chunks) assert grid.chunk_sizes == expected def test_array_read_chunk_sizes_regular() -> None: """Regular array exposes correct read_chunk_sizes and write_chunk_sizes""" store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(100, 80), chunks=(30, 40), dtype="i4", zarr_format=3 ) assert arr.read_chunk_sizes == ((30, 30, 30, 10), (40, 40)) assert arr.write_chunk_sizes == ((30, 30, 30, 10), (40, 40)) def test_array_read_chunk_sizes_rectilinear() -> None: """Rectilinear array exposes correct read_chunk_sizes and write_chunk_sizes""" store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(60, 100), chunks=[[10, 20, 30], [50, 50]], dtype="i4", zarr_format=3 ) assert arr.read_chunk_sizes == ((10, 20, 30), (50, 50)) assert arr.write_chunk_sizes == ((10, 20, 30), (50, 50)) def test_array_sharded_chunk_sizes() -> None: """Sharded array read_chunk_sizes reflects inner chunks and write_chunk_sizes reflects shards""" store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(120, 80), chunks=(60, 40), shards=(120, 80), dtype="i4", zarr_format=3, ) assert arr.read_chunk_sizes == ((60, 60), (40, 40)) assert arr.write_chunk_sizes == ((120,), (80,)) # --------------------------------------------------------------------------- # Info display test # --------------------------------------------------------------------------- def test_chunk_grid_repr_regular() -> None: """ChunkGrid repr shows uniform chunk sizes and array shape for regular grids.""" grid = ChunkGrid.from_sizes((100, 200), (10, 20)) r = repr(grid) assert r == "ChunkGrid(chunk_sizes=(10, 20), array_shape=(100, 200))" def test_chunk_grid_repr_rectilinear() -> None: """ChunkGrid repr shows per-chunk edge tuples for rectilinear dimensions.""" grid = ChunkGrid.from_sizes((30,), ([10, 20],)) r = repr(grid) assert "(10, 20)" in r assert "(30,)" in r def test_info_display_rectilinear() -> None: """Array.info should not crash for rectilinear grids.""" store = zarr.storage.MemoryStore() arr = zarr.create_array( store=store, shape=(30,), chunks=[[10, 20]], dtype="i4", zarr_format=3, ) info = arr.info text = repr(info) assert "" in text assert "Array" in text # --------------------------------------------------------------------------- # nchunks tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("shape", "chunks", "expected"), [ ((30,), [[10, 20]], 2), ((30, 40), [[10, 20], [15, 25]], 4), ], ids=["1d", "2d"], ) def test_nchunks_rectilinear( shape: tuple[int, ...], chunks: list[list[int]], expected: int ) -> None: """Array.nchunks reports correct total chunk count for rectilinear arrays""" store = MemoryStore() a = zarr.create_array(store, shape=shape, chunks=chunks, dtype="int32") assert a.nchunks == expected # --------------------------------------------------------------------------- # iter_chunk_regions test # --------------------------------------------------------------------------- def test_iter_chunk_regions_rectilinear() -> None: """_iter_chunk_regions should work for rectilinear arrays.""" from zarr.core.array import _iter_chunk_regions store = MemoryStore() a = zarr.create_array(store, shape=(30,), chunks=[[10, 20]], dtype="int32") regions = list(_iter_chunk_regions(a)) assert len(regions) == 2 assert regions[0] == (slice(0, 10, 1),) assert regions[1] == (slice(10, 30, 1),) # --------------------------------------------------------------------------- # RectilinearChunkGridMetadata metadata object tests (already parametrized) # --------------------------------------------------------------------------- @pytest.mark.parametrize( ("json_input", "expected_chunk_shapes"), [ ( { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [4, 8]}, }, (4, 8), ), ( { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[1, 2, 3], [10, 20]]}, }, ((1, 2, 3), (10, 20)), ), ( { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[[4, 3]], [10, 20]]}, }, ((4, 4, 4), (10, 20)), ), ( { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[[1, 3], 3], [5]]}, }, ((1, 1, 1, 3), (5,)), ), ( { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [4, [10, 20]]}, }, (4, (10, 20)), ), ], ) def test_rectilinear_from_dict( json_input: RectilinearChunkGridMetadataJSON, expected_chunk_shapes: tuple[int | tuple[int, ...], ...], ) -> None: """RectilinearChunkGridMetadata.from_dict correctly parses all spec forms.""" grid = RectilinearChunkGridMetadata.from_dict(json_input) assert grid.chunk_shapes == expected_chunk_shapes @pytest.mark.parametrize( ("chunk_shapes", "expected_json_shapes"), [ ((4, 8), [4, 8]), (((4,), (8,)), [[4], [8]]), (((10, 20), (5, 5)), [[10, 20], [[5, 2]]]), (((4, 4, 4), (10, 20)), [[[4, 3]], [10, 20]]), ((4, (10, 20)), [4, [10, 20]]), ], ) def test_rectilinear_to_dict( chunk_shapes: tuple[int | tuple[int, ...], ...], expected_json_shapes: list[Any], ) -> None: """RectilinearChunkGridMetadata.to_dict serializes back to spec-compliant JSON.""" grid = RectilinearChunkGridMetadata(chunk_shapes=chunk_shapes) result = grid.to_dict() assert result["name"] == "rectilinear" assert result["configuration"]["kind"] == "inline" assert list(result["configuration"]["chunk_shapes"]) == expected_json_shapes @pytest.mark.parametrize( "json_input", [ {"name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [4, 8]}}, { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[1, 2, 3], [10, 20]]}, }, { "name": "rectilinear", "configuration": {"kind": "inline", "chunk_shapes": [[[4, 3]], [[5, 2]]]}, }, ], ) def test_rectilinear_roundtrip(json_input: RectilinearChunkGridMetadataJSON) -> None: """from_dict -> to_dict -> from_dict produces the same grid.""" grid1 = RectilinearChunkGridMetadata.from_dict(json_input) grid2 = RectilinearChunkGridMetadata.from_dict(grid1.to_dict()) assert grid1.chunk_shapes == grid2.chunk_shapes # --------------------------------------------------------------------------- # Hypothesis property tests # --------------------------------------------------------------------------- pytest.importorskip("hypothesis") import hypothesis.strategies as st # noqa: E402 from hypothesis import event, given, settings # noqa: E402 @st.composite def rectilinear_chunks_st(draw: st.DrawFn, *, shape: tuple[int, ...]) -> list[list[int]]: """Generate valid rectilinear chunk shapes for a given array shape.""" chunk_shapes: list[list[int]] = [] for size in shape: assert size > 0 max_chunks = min(size, 10) nchunks = draw(st.integers(min_value=1, max_value=max_chunks)) if nchunks == 1: chunk_shapes.append([size]) else: dividers = sorted( draw( st.lists( st.integers(min_value=1, max_value=size - 1), min_size=nchunks - 1, max_size=nchunks - 1, unique=True, ) ) ) chunk_shapes.append( [a - b for a, b in zip(dividers + [size], [0] + dividers, strict=False)] ) return chunk_shapes @st.composite def rectilinear_arrays_st(draw: st.DrawFn) -> tuple[zarr.Array[Any], np.ndarray[Any, Any]]: """Generate a rectilinear zarr array with random data, shape, and chunks.""" from zarr.storage import MemoryStore ndim = draw(st.integers(min_value=1, max_value=3)) shape = draw(st.tuples(*[st.integers(min_value=2, max_value=20) for _ in range(ndim)])) chunk_shapes = draw(rectilinear_chunks_st(shape=shape)) event(f"ndim={ndim}, shape={shape}") a = np.arange(int(np.prod(shape)), dtype="int32").reshape(shape) store = MemoryStore() z = zarr.create_array(store=store, shape=shape, chunks=chunk_shapes, dtype="int32") z[:] = a return z, a @settings(deadline=None, max_examples=50) @given(data=st.data()) def test_property_block_indexing_rectilinear(data: st.DataObject) -> None: """Property test: block indexing on rectilinear arrays matches numpy.""" z, a = data.draw(rectilinear_arrays_st()) grid = ChunkGrid.from_metadata(z.metadata) for dim in range(a.ndim): dim_grid = grid._dimensions[dim] block_ix = data.draw(st.integers(min_value=0, max_value=dim_grid.nchunks - 1)) sel = [slice(None)] * a.ndim start = dim_grid.chunk_offset(block_ix) stop = start + dim_grid.data_size(block_ix) sel[dim] = slice(start, stop) block_sel: list[slice | int] = [slice(None)] * a.ndim block_sel[dim] = block_ix np.testing.assert_array_equal( z.blocks[tuple(block_sel)], a[tuple(sel)], err_msg=f"dim={dim}, block={block_ix}", ) zarr-python-3.2.1/tests/test_v2.py000066400000000000000000000234041517635743000171320ustar00rootroot00000000000000import json from pathlib import Path from typing import Any, Literal import numpy as np import pytest from numcodecs import Delta, Zlib from numcodecs.blosc import Blosc from numcodecs.zstd import Zstd import zarr import zarr.core.buffer import zarr.storage from zarr import config from zarr.abc.store import Store from zarr.core.buffer.core import default_buffer_prototype from zarr.core.dtype import FixedLengthUTF32, VariableLengthUTF8 from zarr.core.dtype.npy.bytes import NullTerminatedBytes from zarr.core.dtype.npy.structured import Struct from zarr.core.dtype.wrapper import ZDType from zarr.core.group import Group from zarr.core.sync import sync from zarr.errors import ZarrDeprecationWarning from zarr.storage import MemoryStore, StorePath @pytest.fixture async def store() -> StorePath: return StorePath(await MemoryStore.open()) def test_simple(store: StorePath) -> None: data = np.arange(0, 256, dtype="uint16").reshape((16, 16)) a = zarr.create_array( store / "simple_v2", zarr_format=2, shape=data.shape, chunks=(16, 16), dtype=data.dtype, fill_value=0, ) a[:, :] = data assert np.array_equal(data, a[:, :]) def test_codec_pipeline() -> None: # https://github.com/zarr-developers/zarr-python/issues/2243 store = MemoryStore() array = zarr.create( store=store, shape=(1,), dtype="i4", zarr_format=2, filters=[Delta(dtype="i4").get_config()], compressor=Blosc().get_config(), ) array[:] = 1 result = array[:] expected = np.ones(1) np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize( ("dtype", "expected_dtype", "fill_value", "fill_value_json"), [ ("|S1", "|S1", b"X", "WA=="), ("|V1", "|V1", b"X", "WA=="), ("|V10", "|V10", b"X", "WAAAAAAAAAAAAA=="), ], ) async def test_v2_encode_decode( dtype: str, expected_dtype: str, fill_value: bytes, fill_value_json: str ) -> None: store = zarr.storage.MemoryStore() g = zarr.group(store=store, zarr_format=2) g.create_array( name="foo", shape=(3,), chunks=(3,), dtype=dtype, fill_value=fill_value, compressor=None ) result = await store.get("foo/.zarray", zarr.core.buffer.default_buffer_prototype()) assert result is not None serialized = json.loads(result.to_bytes()) expected = { "chunks": [3], "compressor": None, "dtype": expected_dtype, "fill_value": fill_value_json, "filters": None, "order": "C", "shape": [3], "zarr_format": 2, "dimension_separator": ".", } assert serialized == expected data = zarr.open_array(store=store, path="foo")[:] np.testing.assert_equal(data, np.full((3,), b"X", dtype=dtype)) data = zarr.open_array(store=store, path="foo")[:] np.testing.assert_equal(data, np.full((3,), b"X", dtype=dtype)) @pytest.mark.parametrize( ("dtype", "value"), [ (NullTerminatedBytes(length=1), b"Y"), (FixedLengthUTF32(length=1), "Y"), (VariableLengthUTF8(), "Y"), ], ) def test_v2_encode_decode_with_data(dtype: ZDType[Any, Any], value: str) -> None: expected = np.full((3,), value, dtype=dtype.to_native_dtype()) a = zarr.create( shape=(3,), zarr_format=2, dtype=dtype, ) a[:] = expected data = a[:] np.testing.assert_equal(data, expected) @pytest.mark.parametrize("filters", [[], [Delta(dtype=" None: array_fixture = [42] with config.set({"array.order": order}): arr = zarr.create(shape=1, dtype=" None: """ Test that passing compressor=None results in no compressor. Also test that the default value of the compressor parameter does produce a compressor. """ g = zarr.open(store, mode="w", zarr_format=2) assert isinstance(g, Group) arr = g.create_array("one", dtype="i8", shape=(1,), chunks=(1,), compressor=None) assert arr.async_array.compressor is None assert not (arr.filters) arr = g.create_array("two", dtype="i8", shape=(1,), chunks=(1,)) assert arr.async_array.compressor is not None assert not (arr.filters) arr = g.create_array("three", dtype="i8", shape=(1,), chunks=(1,), compressor=Zstd()) assert arr.async_array.compressor is not None assert not (arr.filters) with pytest.raises(ValueError): g.create_array( "four", dtype="i8", shape=(1,), chunks=(1,), compressor=None, compressors=None ) @pytest.mark.parametrize("numpy_order", ["C", "F"]) @pytest.mark.parametrize("zarr_order", ["C", "F"]) def test_v2_non_contiguous(numpy_order: Literal["C", "F"], zarr_order: Literal["C", "F"]) -> None: """ Make sure zarr v2 arrays save data using the memory order given to the zarr array, not the memory order of the original numpy array. """ store = MemoryStore() arr = zarr.create_array( store, shape=(10, 8), chunks=(3, 3), fill_value=np.nan, dtype="float64", zarr_format=2, filters=None, compressors=None, overwrite=True, order=zarr_order, ) # Non-contiguous write, using numpy memory order a = np.arange(arr.shape[0] * arr.shape[1]).reshape(arr.shape, order=numpy_order) arr[6:9, 3:6] = a[6:9, 3:6] # The slice on the RHS is important np.testing.assert_array_equal(arr[6:9, 3:6], a[6:9, 3:6]) buf = sync(store.get("2.1", default_buffer_prototype())) assert buf is not None np.testing.assert_array_equal( a[6:9, 3:6], np.frombuffer(buf.to_bytes(), dtype="float64").reshape((3, 3), order=zarr_order), ) # After writing and reading from zarr array, order should be same as zarr order sub_arr = arr[6:9, 3:6] assert isinstance(sub_arr, np.ndarray) if zarr_order == "F": assert (sub_arr).flags.f_contiguous else: assert (sub_arr).flags.c_contiguous # Contiguous write store = MemoryStore() arr = zarr.create_array( store, shape=(10, 8), chunks=(3, 3), fill_value=np.nan, dtype="float64", zarr_format=2, compressors=None, filters=None, overwrite=True, order=zarr_order, ) a = np.arange(9).reshape((3, 3), order=numpy_order) arr[6:9, 3:6] = a np.testing.assert_array_equal(arr[6:9, 3:6], a) # After writing and reading from zarr array, order should be same as zarr order sub_arr = arr[6:9, 3:6] assert isinstance(sub_arr, np.ndarray) if zarr_order == "F": assert (sub_arr).flags.f_contiguous else: assert (sub_arr).flags.c_contiguous def test_default_compressor_deprecation_warning() -> None: with pytest.warns(ZarrDeprecationWarning, match="default_compressor is deprecated"): zarr.storage.default_compressor = "zarr.codecs.zstd.ZstdCodec()" # type: ignore[attr-defined] @pytest.mark.parametrize("fill_value", [None, (b"", 0, 0.0)], ids=["no_fill", "fill"]) def test_structured_dtype_roundtrip(fill_value: float | bytes, tmp_path: Path) -> None: a = np.array( [(b"aaa", 1, 4.2), (b"bbb", 2, 8.4), (b"ccc", 3, 12.6)], dtype=[("foo", "S3"), ("bar", "i4"), ("baz", "f8")], ) array_path = tmp_path / "data.zarr" za = zarr.create( shape=(3,), store=array_path, chunks=(2,), fill_value=fill_value, zarr_format=2, dtype=a.dtype, ) if fill_value is not None: assert (np.array([fill_value] * a.shape[0], dtype=a.dtype) == za[:]).all() za[...] = a za = zarr.open_array(store=array_path) assert (a == za[:]).all() @pytest.mark.parametrize( ( "fill_value", "dtype", "expected_result", ), [ ( ("Alice", 30), np.dtype([("name", "U10"), ("age", "i4")]), np.array([("Alice", 30)], dtype=[("name", "U10"), ("age", "i4")])[0], ), ( ["Bob", 25], np.dtype([("name", "U10"), ("age", "i4")]), np.array([("Bob", 25)], dtype=[("name", "U10"), ("age", "i4")])[0], ), ( b"\x01\x00\x00\x00\x02\x00\x00\x00", np.dtype([("x", "i4"), ("y", "i4")]), np.array([(1, 2)], dtype=[("x", "i4"), ("y", "i4")])[0], ), ], ids=[ "tuple_input", "list_input", "bytes_input", ], ) def test_parse_structured_fill_value_valid( fill_value: Any, dtype: np.dtype[Any], expected_result: Any ) -> None: zdtype = Struct.from_native_dtype(dtype) result = zdtype.cast_scalar(fill_value) assert result.dtype == expected_result.dtype assert result == expected_result if isinstance(expected_result, np.void): for name in expected_result.dtype.names or []: assert result[name] == expected_result[name] @pytest.mark.parametrize("fill_value", [None, b"x"], ids=["no_fill", "fill"]) def test_other_dtype_roundtrip(fill_value: None | bytes, tmp_path: Path) -> None: a = np.array([b"a\0\0", b"bb", b"ccc"], dtype="V7") array_path = tmp_path / "data.zarr" za = zarr.create( shape=(3,), store=array_path, chunks=(2,), fill_value=fill_value, zarr_format=2, dtype=a.dtype, ) if fill_value is not None: assert (np.array([fill_value] * a.shape[0], dtype=a.dtype) == za[:]).all() za[...] = a za = zarr.open_array(store=array_path) assert (a == za[:]).all() zarr-python-3.2.1/tests/test_zarr.py000066400000000000000000000012621517635743000175570ustar00rootroot00000000000000import pytest import zarr def test_exports() -> None: """ Ensure that everything in __all__ can be imported. """ from zarr import __all__ for export in __all__: getattr(zarr, export) def test_print_debug_info(capsys: pytest.CaptureFixture[str]) -> None: """ Ensure that print_debug_info does not raise an error """ from importlib.metadata import version from zarr import __version__, print_debug_info print_debug_info() captured = capsys.readouterr() # test that at least some of what we expect is # printed out assert f"zarr: {__version__}" in captured.out assert f"numpy: {version('numpy')}" in captured.out zarr-python-3.2.1/uv.lock000066400000000000000000023136771517635743000153540ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.12" [[package]] name = "aiobotocore" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "aioitertools" }, { name = "botocore" }, { name = "jmespath" }, { name = "multidict" }, { name = "python-dateutil" }, { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/50/a48ed11b15f926ce3dbb33e7fb0f25af17dbb99bcb7ae3b30c763723eca7/aiobotocore-3.4.0.tar.gz", hash = "sha256:a918b5cb903f81feba7e26835aed4b5e6bb2d0149d7f42bb2dd7d8089e3d9000", size = 122360, upload-time = "2026-04-07T06:12:24.884Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/df/d8/ce9386e6d76ea79e61dee15e62aa48cff6be69e89246b0ac4a11857cb02c/aiobotocore-3.4.0-py3-none-any.whl", hash = "sha256:26290eb6830ea92d8a6f5f90b56e9f5cedd6d126074d5db63b195e281d982465", size = 88018, upload-time = "2026-04-07T06:12:22.684Z" }, ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] name = "aiohttp" version = "3.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] [[package]] name = "aioitertools" version = "0.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, ] [[package]] name = "aiosignal" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] 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 = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "antlr4-python3-runtime" version = "4.13.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" }, ] [[package]] name = "appnope" version = "0.1.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] [[package]] name = "astor" version = "0.8.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" }, ] [[package]] name = "astroid" version = "3.3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, ] [[package]] name = "asttokens" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "attrs" version = "26.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "aws-sam-translator" version = "1.103.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, { name = "jsonschema" }, { name = "pydantic" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/e3/82cc7240504b1c0d2d7ed7028b05ccceedb02932b8638c61a8372a5d875f/aws_sam_translator-1.103.0.tar.gz", hash = "sha256:8317b72ef412db581dc7846932a44dfc1729adea578d9307a3e6ece46a7882ca", size = 344881, upload-time = "2025-11-21T19:50:51.818Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/86/6414c215ff0a10b33bf89622951e7d4413106320657535d2ba0e4f634661/aws_sam_translator-1.103.0-py3-none-any.whl", hash = "sha256:d4eb4a1efa62f00b253ee5f8c0084bd4b7687186c6a12338f900ebe07ff74dad", size = 403100, upload-time = "2025-11-21T19:50:50.528Z" }, ] [[package]] name = "aws-xray-sdk" version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/14/25/0cbd7a440080def5e6f063720c3b190a25f8aa2938c1e34415dc18241596/aws_xray_sdk-2.15.0.tar.gz", hash = "sha256:794381b96e835314345068ae1dd3b9120bd8b4e21295066c37e8814dbb341365", size = 76315, upload-time = "2025-10-29T20:59:45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/c3/f30a7a63e664acc7c2545ca0491b6ce8264536e0e5cad3965f1d1b91e960/aws_xray_sdk-2.15.0-py2.py3-none-any.whl", hash = "sha256:422d62ad7d52e373eebb90b642eb1bb24657afe03b22a8df4a8b2e5108e278a3", size = 103228, upload-time = "2025-10-29T21:00:24.12Z" }, ] [[package]] name = "babel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backrefs" version = "6.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] [[package]] name = "beautifulsoup4" version = "4.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] name = "bleach" version = "6.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, ] [package.optional-dependencies] css = [ { name = "tinycss2" }, ] [[package]] name = "blinker" version = "1.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, ] [[package]] name = "boto3" version = "1.42.84" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/89/2d647bd717da55a8cc68602b197f53a5fa36fb95a2f9e76c4aff11a9cfd1/boto3-1.42.84.tar.gz", hash = "sha256:6a84b3293a5d8b3adf827a54588e7dcffcf0a85410d7dadca615544f97d27579", size = 112816, upload-time = "2026-04-06T19:39:07.585Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2d/31/cdf4326841613d1d181a77b3038a988800fb3373ca50de1639fba9fa87de/boto3-1.42.84-py3-none-any.whl", hash = "sha256:4d03ad3211832484037337292586f71f48707141288d9ac23049c04204f4ab03", size = 140555, upload-time = "2026-04-06T19:39:06.009Z" }, ] [[package]] name = "botocore" version = "1.42.84" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/b7/1c03423843fb0d1795b686511c00ee63fed1234c2400f469aeedfd42212f/botocore-1.42.84.tar.gz", hash = "sha256:234064604c80d9272a5e9f6b3566d260bcaa053a5e05246db90d7eca1c2cf44b", size = 15148615, upload-time = "2026-04-06T19:38:56.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e3/37/0c0c90361c8a1b9e6c75222ca24ae12996a298c0e18822a72ab229c37207/botocore-1.42.84-py3-none-any.whl", hash = "sha256:15f3fe07dfa6545e46a60c4b049fe2bdf63803c595ae4a4eec90e8f8172764f3", size = 14827061, upload-time = "2026-04-06T19:38:53.613Z" }, ] [[package]] name = "cairocffi" version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, ] [[package]] name = "cairosvg" version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cairocffi" }, { name = "cssselect2" }, { name = "defusedxml" }, { name = "pillow" }, { name = "tinycss2" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/07/e8412a13019b3f737972dea23a2c61ca42becafc16c9338f4ca7a0caa993/cairosvg-2.9.0.tar.gz", hash = "sha256:1debb00cd2da11350d8b6f5ceb739f1b539196d71d5cf5eb7363dbd1bfbc8dc5", size = 40877, upload-time = "2026-03-13T15:42:00.564Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/e0/5011747466414c12cac8a8df77aa235068669a6a5a5df301a96209db6054/cairosvg-2.9.0-py3-none-any.whl", hash = "sha256:4b82d07d145377dffdfc19d9791bd5fb65539bb4da0adecf0bdbd9cd4ffd7c68", size = 45962, upload-time = "2026-03-14T13:56:33.512Z" }, ] [[package]] name = "certifi" version = "2026.2.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "cfn-lint" version = "1.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aws-sam-translator" }, { name = "jsonpatch" }, { name = "networkx" }, { name = "pyyaml" }, { name = "regex" }, { name = "sympy" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/436c192cdf8dbddd8e09a591384f126c5a47937c14953d87b1dacacd0543/cfn_lint-1.41.0.tar.gz", hash = "sha256:6feca1cf57f9ed2833bab68d9b1d38c8033611e571fa792e45ab4a39e2b8ab57", size = 3408534, upload-time = "2025-11-18T20:03:33.431Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cf/5e/81ef8f87894543210d783a495c8880cfb0b5baa0ee3bcc6d852f1b343863/cfn_lint-1.41.0-py3-none-any.whl", hash = "sha256:cd43f76f59a664b2bad580840827849fac0d56a3b80e9a41315d8ab5ff6b563a", size = 5674429, upload-time = "2025-11-18T20:03:31.083Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" version = "8.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "comm" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] [[package]] name = "coverage" version = "7.13.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [[package]] name = "cryptography" version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] name = "cssselect2" version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tinycss2" }, { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, ] [[package]] name = "cuda-pathfinder" version = "1.5.3" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/d6/ac63065d33dd700fee7ebd7d287332401b54e31b9346e142f871e1f0b116/cuda_pathfinder-1.5.3-py3-none-any.whl", hash = "sha256:dff021123aedbb4117cc7ec81717bbfe198fb4e8b5f1ee57e0e084fec5c8577d", size = 49991, upload-time = "2026-04-14T20:09:27.037Z" }, ] [[package]] name = "cupy-cuda12x" version = "14.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-pathfinder" }, { name = "numpy" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/38/ca/b93ef9fca1471a65f136a73e10819634c0b83427362fc08fc9f29f935bf0/cupy_cuda12x-14.0.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f244bc14fad6f1ef0c74abd98afa4b82d2534aecdba911197810ec0047f0d1f3", size = 145578614, upload-time = "2026-02-20T10:22:49.108Z" }, { url = "https://files.pythonhosted.org/packages/5a/a6/944406223a190815d9df156a1d66f3b0352bd8827dc4a8c752196d616dbc/cupy_cuda12x-14.0.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:9f0c81c3509f77be3ae8444759d5b314201b2dfcbbf2ae0d0b5fb7a61f20893c", size = 134613763, upload-time = "2026-02-20T10:22:56.792Z" }, { url = "https://files.pythonhosted.org/packages/11/fd/62e6e3f3c0c9f785b2dbdc2bff01bc375f5c6669d52e5e151f7aeb577801/cupy_cuda12x-14.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:63dc8a3a88d2ffd0386796b915d27acc7f2332c2291efd1ff4f0021b96f02051", size = 96267167, upload-time = "2026-02-20T10:23:02.263Z" }, { url = "https://files.pythonhosted.org/packages/99/67/f967c5aff77bd6ae6765faf20580db80bb8a7e2574e999166de1d4e50146/cupy_cuda12x-14.0.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:9d9b1bdcf9fa777593017867e8733192c071b94639a1b3e8b2ee99eb3f3ea760", size = 145128055, upload-time = "2026-02-20T10:23:08.765Z" }, { url = "https://files.pythonhosted.org/packages/80/53/037c931731151c504cfc00069eb295c903927c92145115623f13bd2ea076/cupy_cuda12x-14.0.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:21fcb4e917e43237edcc5e3a1a1241e2a2946ba9e577ce36fd580bd9856f91e8", size = 134227269, upload-time = "2026-02-20T10:23:16.147Z" }, { url = "https://files.pythonhosted.org/packages/a3/70/ce8344426effda22152bf30cfb8f9b6477645d0f41df784674369af8f422/cupy_cuda12x-14.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:b7399e7fe4e2be3b5c3974fc892a661e10082836a4c78d0152b39cb483608a89", size = 96250134, upload-time = "2026-02-20T10:23:22.631Z" }, { url = "https://files.pythonhosted.org/packages/5d/cb/ba61bcd602856aeabf362280cb3c17ed5fe03ae23e84578eb99f5245546c/cupy_cuda12x-14.0.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:3be87da86d808d9fec23b0a1df001f15f8f145698bc4bebc6d6938fa7e11519f", size = 144976386, upload-time = "2026-02-20T10:23:29.877Z" }, { url = "https://files.pythonhosted.org/packages/ba/73/34e5f334f6b1e5c5dff80af8109979fb0e8461b27e4454517e0e47486455/cupy_cuda12x-14.0.1-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:fa356384760e01498d010af2d96de536ef3dad19db1d3a1ad0764e4323fb919f", size = 133521354, upload-time = "2026-02-20T10:23:37.063Z" }, { url = "https://files.pythonhosted.org/packages/e5/a3/80ff83dcad1ac61741714d97fce5a3ef42c201bb40005ec5cc413e34d75f/cupy_cuda12x-14.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:cafe62131caef63b5e90b71b617bb4bf47d7bd9e11cccabea8104db1e01db02e", size = 96822848, upload-time = "2026-02-20T10:23:42.684Z" }, ] [[package]] name = "debugpy" version = "1.8.20" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] name = "docker" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "requests" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" }, ] [[package]] name = "docutils" version = "0.22.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] name = "donfig" version = "0.8.1.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/71/80cc718ff6d7abfbabacb1f57aaa42e9c1552bfdd01e64ddd704e4a03638/donfig-0.8.1.post1.tar.gz", hash = "sha256:3bef3413a4c1c601b585e8d297256d0c1470ea012afa6e8461dc28bfb7c23f52", size = 19506, upload-time = "2024-05-23T14:14:31.513Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/d5/c5db1ea3394c6e1732fb3286b3bd878b59507a8f77d32a2cebda7d7b7cd4/donfig-0.8.1.post1-py3-none-any.whl", hash = "sha256:2a3175ce74a06109ff9307d90a230f81215cbac9a751f4d1c6194644b8204f9d", size = 21592, upload-time = "2024-05-23T14:13:55.283Z" }, ] [[package]] name = "execnet" version = "2.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] name = "executing" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] [[package]] name = "fastjsonschema" version = "2.21.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] [[package]] name = "flask" version = "3.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, { name = "click" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, ] [[package]] name = "flask-cors" version = "6.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flask" }, { name = "werkzeug" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, ] [[package]] name = "frozenlist" version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] name = "fsspec" version = "2026.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] [[package]] name = "ghp-import" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "google-crc32c" version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, ] [[package]] name = "graphql-core" version = "3.2.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/c5/36aa96205c3ecbb3d34c7c24189e4553c7ca2ebc7e1dd07432339b980272/graphql_core-3.2.8.tar.gz", hash = "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", size = 513181, upload-time = "2026-03-05T19:55:37.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-py3-none-any.whl", hash = "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c", size = 207349, upload-time = "2026-03-05T19:55:35.911Z" }, ] [[package]] name = "griffe-inherited-docstrings" version = "1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/da/fd002dc5f215cd896bfccaebe8b4aa1cdeed8ea1d9d60633685bd61ff933/griffe_inherited_docstrings-1.1.3.tar.gz", hash = "sha256:cd1f937ec9336a790e5425e7f9b92f5a5ab17f292ba86917f1c681c0704cb64e", size = 26738, upload-time = "2026-02-21T09:38:44.312Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/16/20/4bc15f242181daad1c104e0a7d33be49e712461ea89e548152be0365b9ea/griffe_inherited_docstrings-1.1.3-py3-none-any.whl", hash = "sha256:aa7f6e624515c50d9325a5cfdf4b2acac547f1889aca89092d5da7278f739695", size = 6710, upload-time = "2026-02-20T11:06:38.75Z" }, ] [[package]] name = "griffelib" version = "2.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] [[package]] name = "hypothesis" version = "6.152.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/b1/c32bcddb9aab9e3abc700f1f56faf14e7655c64a16ca47701a57362276ea/hypothesis-6.152.1.tar.gz", hash = "sha256:4f4ed934eee295dd84ee97592477d23e8dc03e9f12ae0ee30a4e7c9ef3fca3b0", size = 465029, upload-time = "2026-04-14T22:29:24.062Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/83/860fb3075e00b0fc19a22a2301bc3c96f00437558c3911bdd0a3573a4a53/hypothesis-6.152.1-py3-none-any.whl", hash = "sha256:40a3619d9e0cb97b018857c7986f75cf5de2e5ec0fa8a0b172d00747758f749e", size = 530752, upload-time = "2026-04-14T22:29:20.893Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "imagesize" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "ipykernel" version = "7.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, { name = "ipython" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, { name = "nest-asyncio" }, { name = "packaging" }, { name = "psutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, ] [[package]] name = "ipython" version = "9.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "decorator" }, { name = "ipython-pygments-lexers" }, { name = "jedi" }, { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, ] [[package]] name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] [[package]] name = "itsdangerous" version = "2.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, ] [[package]] name = "jedi" version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jmespath" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "joserfc" version = "1.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" }, ] [[package]] name = "jsonpatch" version = "1.33" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpointer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, ] [[package]] name = "jsonpath-ng" version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/32/58/250751940d75c8019659e15482d548a4aa3b6ce122c515102a4bfdac50e3/jsonpath_ng-1.8.0.tar.gz", hash = "sha256:54252968134b5e549ea5b872f1df1168bd7defe1a52fed5a358c194e1943ddc3", size = 74513, upload-time = "2026-02-24T14:42:06.182Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/99/33c7d78a3fb70d545fd5411ac67a651c81602cc09c9cf0df383733f068c5/jsonpath_ng-1.8.0-py3-none-any.whl", hash = "sha256:b8dde192f8af58d646fc031fac9c99fe4d00326afc4148f1f043c601a8cfe138", size = 67844, upload-time = "2026-02-28T00:53:19.637Z" }, ] [[package]] name = "jsonpointer" version = "3.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] name = "jsonschema" version = "4.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/6e/35174c1d3f30560848c82d3c233c01420e047d70925c897a4d6e932b4898/jsonschema-4.24.1.tar.gz", hash = "sha256:fe45a130cc7f67cd0d67640b4e7e3e2e666919462ae355eda238296eafeb4b5d", size = 356635, upload-time = "2025-07-17T14:40:01.05Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/7f/ea48ffb58f9791f9d97ccb35e42fea1ebc81c67ce36dc4b8b2eee60e8661/jsonschema-4.24.1-py3-none-any.whl", hash = "sha256:6b916866aa0b61437785f1277aa2cbd63512e8d4b47151072ef13292049b4627", size = 89060, upload-time = "2025-07-17T14:39:59.471Z" }, ] [[package]] name = "jsonschema-path" version = "0.4.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pathable" }, { name = "pyyaml" }, { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" }, ] [[package]] name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] [[package]] name = "jupyter-client" version = "8.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, { name = "python-dateutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, ] [[package]] name = "jupyter-core" version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] [[package]] name = "jupyterlab-pygments" version = "0.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, ] [[package]] name = "jupytext" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "mdit-py-plugins" }, { name = "nbformat" }, { name = "packaging" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/a5/80c02f307c8ce863cb33e27daf049315e9d96979e14eead700923b5ec9cc/jupytext-1.19.1.tar.gz", hash = "sha256:82587c07e299173c70ed5e8ec7e75183edf1be289ed518bab49ad0d4e3d5f433", size = 4307829, upload-time = "2026-01-25T21:35:13.276Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/16/5a/736dd2f4535dbf3bf26523f9158c011389ef88dd06ec2eef67fd744f1c7b/jupytext-1.19.1-py3-none-any.whl", hash = "sha256:d8975035155d034bdfde5c0c37891425314b7ea8d3a6c4b5d18c294348714cd9", size = 170478, upload-time = "2026-01-25T21:35:11.17Z" }, ] [[package]] name = "lazy-object-proxy" version = "1.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" }, { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" }, { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" }, { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" }, { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" }, { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" }, ] [[package]] name = "librt" version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] [[package]] name = "markdown" version = "3.10.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] name = "markdown-exec" version = "1.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/73/1f20927d075c83c0e2bc814d3b8f9bd254d919069f78c5423224b4407944/markdown_exec-1.12.1.tar.gz", hash = "sha256:eee8ba0df99a5400092eeda80212ba3968f3cbbf3a33f86f1cd25161538e6534", size = 78105, upload-time = "2025-11-11T19:25:05.44Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ea/22/7b684ddb01b423b79eaba9726954bbe559540d510abc7a72a84d8eee1b26/markdown_exec-1.12.1-py3-none-any.whl", hash = "sha256:a645dce411fee297f5b4a4169c245ec51e20061d5b71e225bef006e87f3e465f", size = 38046, upload-time = "2025-11-11T19:25:03.878Z" }, ] [package.optional-dependencies] ansi = [ { name = "pygments-ansi-color" }, ] [[package]] name = "markdown-it-py" version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] name = "matplotlib-inline" version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] [[package]] name = "mdit-py-plugins" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] name = "mike" version = "2.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "mkdocs" }, { name = "pyparsing" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "verspec" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/47/fa87e9d56bef16cdfe34b059a437e8c6f7ec6f1b9c378871c3cf95ebea9c/mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e", size = 38450, upload-time = "2026-04-14T04:59:03.944Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040", size = 34026, upload-time = "2026-04-14T04:59:02.602Z" }, ] [[package]] name = "mistune" version = "3.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-autorefs" version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, ] [[package]] name = "mkdocs-get-deps" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } 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-jupyter" version = "0.26.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipykernel" }, { name = "jupytext" }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "nbconvert" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/aa/f8d15409a9a3112486994a80d5a975694c7d145c4f8b5b484aeb383420ef/mkdocs_jupyter-0.26.3.tar.gz", hash = "sha256:e1e8bd48a1b96542e84e3028e3066112bac7b94d95ab69f8b91305c84003ca26", size = 1628353, upload-time = "2026-04-17T18:56:31.517Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl", hash = "sha256:cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82", size = 1459740, upload-time = "2026-04-17T18:56:30.031Z" }, ] [[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.optional-dependencies] imaging = [ { name = "cairosvg" }, { name = "pillow" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] name = "mkdocs-redirects" version = "1.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "properdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/73/25/49725f78ca5d3026b09973f7a2b3a8b179cc2e8c15e43d5a13bc79f6b274/mkdocs_redirects-1.2.3.tar.gz", hash = "sha256:5e980330999299729a2d6a125347d1af78023d68a23681a4de3053ce7dfe2e51", size = 7712, upload-time = "2026-03-28T13:57:41.766Z" } 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" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] 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-python" version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffelib" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, ] [[package]] name = "moto" version = "5.1.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boto3" }, { name = "botocore" }, { name = "cryptography" }, { name = "jinja2" }, { name = "python-dateutil" }, { name = "requests" }, { name = "responses" }, { name = "werkzeug" }, { name = "xmltodict" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/3d/1765accbf753dc1ae52f26a2e2ed2881d78c2eb9322c178e45312472e4a0/moto-5.1.22.tar.gz", hash = "sha256:e5b2c378296e4da50ce5a3c355a1743c8d6d396ea41122f5bb2a40f9b9a8cc0e", size = 8547792, upload-time = "2026-03-08T21:06:43.731Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/4f/8812a01e3e0bd6be3e13b90432fb5c696af9a720af3f00e6eba5ad748345/moto-5.1.22-py3-none-any.whl", hash = "sha256:d9f20ae3cf29c44f93c1f8f06c8f48d5560e5dc027816ef1d0d2059741ffcfbe", size = 6617400, upload-time = "2026-03-08T21:06:41.093Z" }, ] [package.optional-dependencies] s3 = [ { name = "py-partiql-parser" }, { name = "pyyaml" }, ] server = [ { name = "antlr4-python3-runtime" }, { name = "aws-sam-translator" }, { name = "aws-xray-sdk" }, { name = "cfn-lint" }, { name = "docker" }, { name = "flask" }, { name = "flask-cors" }, { name = "graphql-core" }, { name = "joserfc" }, { name = "jsonpath-ng" }, { name = "openapi-spec-validator" }, { name = "py-partiql-parser" }, { name = "pydantic" }, { name = "pyparsing" }, { name = "pyyaml" }, { name = "setuptools" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] name = "msgpack" version = "1.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, ] [[package]] name = "multidict" version = "6.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] name = "mypy" version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" }, { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" }, { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" }, { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" }, { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" }, { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" }, { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" }, { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" }, { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" }, { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" }, { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" }, { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" }, { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" }, { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" }, { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" }, { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" }, { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" }, { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" }, { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" }, { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" }, { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" }, { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nbclient" version = "0.10.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "nbformat" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, ] [[package]] name = "nbconvert" version = "7.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "bleach", extra = ["css"] }, { name = "defusedxml" }, { name = "jinja2" }, { name = "jupyter-core" }, { name = "jupyterlab-pygments" }, { name = "markupsafe" }, { name = "mistune" }, { name = "nbclient" }, { name = "nbformat" }, { name = "packaging" }, { name = "pandocfilters" }, { name = "pygments" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, ] [[package]] name = "nbformat" version = "5.10.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastjsonschema" }, { name = "jsonschema" }, { name = "jupyter-core" }, { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, ] [[package]] name = "nest-asyncio" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "numcodecs" version = "0.16.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/bd/8a391e7c356366224734efd24da929cc4796fff468bfb179fe1af6548535/numcodecs-0.16.5.tar.gz", hash = "sha256:0d0fb60852f84c0bd9543cc4d2ab9eefd37fc8efcc410acd4777e62a1d300318", size = 6276387, upload-time = "2025-11-21T02:49:48.986Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/75/cc/55420f3641a67f78392dc0bc5d02cb9eb0a9dcebf2848d1ac77253ca61fa/numcodecs-0.16.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:24e675dc8d1550cd976a99479b87d872cb142632c75cc402fea04c08c4898523", size = 1656287, upload-time = "2025-11-21T02:49:25.755Z" }, { url = "https://files.pythonhosted.org/packages/f5/6c/86644987505dcb90ba6d627d6989c27bafb0699f9fd00187e06d05ea8594/numcodecs-0.16.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94ddfa4341d1a3ab99989d13b01b5134abb687d3dab2ead54b450aefe4ad5bd6", size = 1148899, upload-time = "2025-11-21T02:49:26.87Z" }, { url = "https://files.pythonhosted.org/packages/97/1e/98aaddf272552d9fef1f0296a9939d1487914a239e98678f6b20f8b0a5c8/numcodecs-0.16.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b554ab9ecf69de7ca2b6b5e8bc696bd9747559cb4dd5127bd08d7a28bec59c3a", size = 8534814, upload-time = "2025-11-21T02:49:28.547Z" }, { url = "https://files.pythonhosted.org/packages/fb/53/78c98ef5c8b2b784453487f3e4d6c017b20747c58b470393e230c78d18e8/numcodecs-0.16.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad1a379a45bd3491deab8ae6548313946744f868c21d5340116977ea3be5b1d6", size = 9173471, upload-time = "2025-11-21T02:49:30.444Z" }, { url = "https://files.pythonhosted.org/packages/1c/20/2fdec87fc7f8cec950d2b0bea603c12dc9f05b4966dc5924ba5a36a61bf6/numcodecs-0.16.5-cp312-cp312-win_amd64.whl", hash = "sha256:845a9857886ffe4a3172ba1c537ae5bcc01e65068c31cf1fce1a844bd1da050f", size = 801412, upload-time = "2025-11-21T02:49:32.123Z" }, { url = "https://files.pythonhosted.org/packages/38/38/071ced5a5fd1c85ba0e14ba721b66b053823e5176298c2f707e50bed11d9/numcodecs-0.16.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25be3a516ab677dad890760d357cfe081a371d9c0a2e9a204562318ac5969de3", size = 1654359, upload-time = "2025-11-21T02:49:33.673Z" }, { url = "https://files.pythonhosted.org/packages/d1/c0/5f84ba7525577c1b9909fc2d06ef11314825fc4ad4378f61d0e4c9883b4a/numcodecs-0.16.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0107e839ef75b854e969cb577e140b1aadb9847893937636582d23a2a4c6ce50", size = 1144237, upload-time = "2025-11-21T02:49:35.294Z" }, { url = "https://files.pythonhosted.org/packages/0b/00/787ea5f237b8ea7bc67140c99155f9c00b5baf11c49afc5f3bfefa298f95/numcodecs-0.16.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:015a7c859ecc2a06e2a548f64008c0ec3aaecabc26456c2c62f4278d8fc20597", size = 8483064, upload-time = "2025-11-21T02:49:36.454Z" }, { url = "https://files.pythonhosted.org/packages/c4/e6/d359fdd37498e74d26a167f7a51e54542e642ea47181eb4e643a69a066c3/numcodecs-0.16.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84230b4b9dad2392f2a84242bd6e3e659ac137b5a1ce3571d6965fca673e0903", size = 9126063, upload-time = "2025-11-21T02:49:38.018Z" }, { url = "https://files.pythonhosted.org/packages/27/72/6663cc0382ddbb866136c255c837bcb96cc7ce5e83562efec55e1b995941/numcodecs-0.16.5-cp313-cp313-win_amd64.whl", hash = "sha256:5088145502ad1ebf677ec47d00eb6f0fd600658217db3e0c070c321c85d6cf3d", size = 799275, upload-time = "2025-11-21T02:49:39.558Z" }, { url = "https://files.pythonhosted.org/packages/3c/9e/38e7ca8184c958b51f45d56a4aeceb1134ecde2d8bd157efadc98502cc42/numcodecs-0.16.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b05647b8b769e6bc8016e9fd4843c823ce5c9f2337c089fb5c9c4da05e5275de", size = 1654721, upload-time = "2025-11-21T02:49:40.602Z" }, { url = "https://files.pythonhosted.org/packages/a1/37/260fa42e7b2b08e6e00ad632f8dd620961a60a459426c26cea390f8c68d0/numcodecs-0.16.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3832bd1b5af8bb3e413076b7d93318c8e7d7b68935006b9fa36ca057d1725a8f", size = 1146887, upload-time = "2025-11-21T02:49:41.721Z" }, { url = "https://files.pythonhosted.org/packages/4e/15/e2e1151b5a8b14a15dfd4bb4abccce7fff7580f39bc34092780088835f3a/numcodecs-0.16.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49f7b7d24f103187f53135bed28bb9f0ed6b2e14c604664726487bb6d7c882e1", size = 8476987, upload-time = "2025-11-21T02:49:43.363Z" }, { url = "https://files.pythonhosted.org/packages/6d/30/16a57fc4d9fb0ba06c600408bd6634f2f1753c54a7a351c99c5e09b51ee2/numcodecs-0.16.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aec9736d81b70f337d89c4070ee3ffeff113f386fd789492fa152d26a15043e4", size = 9102377, upload-time = "2025-11-21T02:49:45.508Z" }, { url = "https://files.pythonhosted.org/packages/31/a5/a0425af36c20d55a3ea884db4b4efca25a43bea9214ba69ca7932dd997b4/numcodecs-0.16.5-cp314-cp314-win_amd64.whl", hash = "sha256:b16a14303800e9fb88abc39463ab4706c037647ac17e49e297faa5f7d7dbbf1d", size = 819022, upload-time = "2025-11-21T02:49:47.39Z" }, ] [package.optional-dependencies] msgpack = [ { name = "msgpack" }, ] [[package]] name = "numpy" version = "2.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, ] [[package]] name = "numpydoc" version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/5e/3a6a3e90f35cea3853c45e5d5fb9b7192ce4384616f932cf7591298ab6e1/numpydoc-1.10.0-py3-none-any.whl", hash = "sha256:3149da9874af890bcc2a82ef7aae5484e5aa81cb2778f08e3c307ba6d963721b", size = 69255, upload-time = "2025-12-02T16:39:11.561Z" }, ] [[package]] name = "obstore" version = "0.9.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/96/b4e0d466715daec9e221cccb8ac57dc91ba08830a68d7ed5a2729ab21a32/obstore-0.9.3.tar.gz", hash = "sha256:0f56e7efd53c22e7eaf14ccce931c678b01a016ceb2226cd4bb01c741a58f5a2", size = 124143, upload-time = "2026-04-15T18:16:56.812Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/c0/202a30127786169e74516ae700de8f153c430d349985bf6d09b33ab7f481/obstore-0.9.3-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e56bd948c87e0b16211ea0363913529c1cda67d9ada809c28e70a9b22c385433", size = 4112171, upload-time = "2026-04-15T18:15:09.478Z" }, { url = "https://files.pythonhosted.org/packages/df/8e/fc4995a82b53cdd8e61f5ebfe5007deeeda4f0746b0e46e1dbbd293d9d3d/obstore-0.9.3-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b65c3d17ff6fa7a239b99df05e6f2badb1852a1cc56ebcd24f9347d88b5cc629", size = 3880522, upload-time = "2026-04-15T18:15:11.744Z" }, { url = "https://files.pythonhosted.org/packages/86/82/5f6c3ff5b25e6bd0b97c6d5459b91edcbc4ab71e66a2df1b9a6884a8a4bc/obstore-0.9.3-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7178850a492a3bd534bf6d178d20aee4a171e6b4a4fead46f9879a630db06353", size = 4042745, upload-time = "2026-04-15T18:15:13.712Z" }, { url = "https://files.pythonhosted.org/packages/b8/b1/5df6a6b2f1a8039b3dc06e9a0d990fe26471f153529a8e92dad4167f505a/obstore-0.9.3-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fdc75d11d4f17dd79fe47bb7576dd404675d52f99e0d998319f4bf032541171", size = 4145646, upload-time = "2026-04-15T18:15:15.829Z" }, { url = "https://files.pythonhosted.org/packages/54/3a/a238ad9e1c4f3aba8a7acff04eba7d7e143aa07f7a26435acf5655372953/obstore-0.9.3-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1943511b022ce49010059b4b274f1608daf041827e9a8e0ae2311c70fcea921b", size = 4427486, upload-time = "2026-04-15T18:15:17.635Z" }, { url = "https://files.pythonhosted.org/packages/0e/be/adffdbec3edb4cf2bea2f856ebf14efc0e398843a477823040c95a39c754/obstore-0.9.3-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c4ed947b1cb5da6d8a8af8d2378b038dab34deb86e679c940a8a09f2ad72a4", size = 4340837, upload-time = "2026-04-15T18:15:19.711Z" }, { url = "https://files.pythonhosted.org/packages/4c/cf/92593c3981e38e9f682d858aff251f6731517d682a9b389cb1e2c94ed6a4/obstore-0.9.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab427bec57b8b5084eccd78d7592cf28860a1cf631bd0e0b8c0e0b793efdc033", size = 4231679, upload-time = "2026-04-15T18:15:21.473Z" }, { url = "https://files.pythonhosted.org/packages/ee/8b/920f726117180e57c51bf6bc503f286782a3bbb6fb8f70ed9bff4f674796/obstore-0.9.3-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:48f58c44c9a5c7b82346fe5e3469bae3109c8b168c1c096c41b80a9f0bc8d5a6", size = 4105350, upload-time = "2026-04-15T18:15:23.566Z" }, { url = "https://files.pythonhosted.org/packages/48/5c/6abd29ae02f64f677a67cab6ee5492d14f004b483bafbf397c3ddef4f4bf/obstore-0.9.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:591db6375f90a7528f902d3cff8738d93219d6162d517e4d464bd333133daa1d", size = 4296104, upload-time = "2026-04-15T18:15:25.388Z" }, { url = "https://files.pythonhosted.org/packages/21/92/a1eacdf5bf0ca2d2d7fc6d6430e1f305782abaa0409fe120fcfc3083dea4/obstore-0.9.3-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e42c3caf9f77a2a5aaddfd6a2356c3ad317884f188cfec5b000c48c21362af79", size = 4278210, upload-time = "2026-04-15T18:15:27.43Z" }, { url = "https://files.pythonhosted.org/packages/48/c6/8e62cab73552f5491f01bffbb279b7d25ec148fb89ca4ad4192ac00c6d95/obstore-0.9.3-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:bb509ea1fec9dcee996151f2c5d6664b1516da12675d2c27583ef9fff81c43ec", size = 4266039, upload-time = "2026-04-15T18:15:29.179Z" }, { url = "https://files.pythonhosted.org/packages/b0/3f/36b1823574277d3a494b7280883a0d78ed46e71d3e8e4de2b8135b15792a/obstore-0.9.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6eec5a7a917ec6a5148d4dd6a0e53ce87949c520de96b1822bbbc4aa79b65610", size = 4451602, upload-time = "2026-04-15T18:15:31.468Z" }, { url = "https://files.pythonhosted.org/packages/20/4c/8f4650d2e29472998bbcbfb1f0a67e865fea419108faa43f5d180bdb166a/obstore-0.9.3-cp311-abi3-win_amd64.whl", hash = "sha256:87a1d5db9804df06f0ed5cd25b209c527fc2ec54a91c56ae6ff62d27db680b92", size = 4190181, upload-time = "2026-04-15T18:15:33.235Z" }, { url = "https://files.pythonhosted.org/packages/22/9f/8df3b10646a3b90c318f85f8e89feca337c6cf56cb50f003e03124186dd2/obstore-0.9.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d738d976a15cca90fb6a900c4f546544644b38a559bb99f6372030bb80e058d5", size = 4084824, upload-time = "2026-04-15T18:15:35.038Z" }, { url = "https://files.pythonhosted.org/packages/f7/ce/e5729f8504e48ab88586138a0311ae7800a2f61a3fc989444ee29a1dbdb6/obstore-0.9.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:663ed5b534d3896e703725d2b4f11959fbbb3e499b39e7f2fb9044aad658c568", size = 3867311, upload-time = "2026-04-15T18:15:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/30/c96619b6faf1337edd0ba82bf06cbf6d20124152d9848a20bbe4ec8da046/obstore-0.9.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c3d84f21424dbb9d7acd0111d3f281e61ca0f8a21f7b7f2e5599d515fff5b02", size = 4036006, upload-time = "2026-04-15T18:15:39.071Z" }, { url = "https://files.pythonhosted.org/packages/f2/ab/48e5caf2bac13deba9d4e87dda28850f68b39b69a0431e5254200bfed786/obstore-0.9.3-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d535037d5c4240a849716b69e98aa7e7e5cea1b116558ede08a9962f501c23b3", size = 4135380, upload-time = "2026-04-15T18:15:41.783Z" }, { url = "https://files.pythonhosted.org/packages/4c/d4/11f6a74df6d0b891be9538129c61cee50518835face031d0138a2505e371/obstore-0.9.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71cdbebbcb78f63ffe4042e86e1ad41e723245bad42cf79b4c750592cb01aa57", size = 4414425, upload-time = "2026-04-15T18:15:43.586Z" }, { url = "https://files.pythonhosted.org/packages/14/c3/d299c174875490bbe9110d68887acbf4e197dd13becda75f1b8e13033f46/obstore-0.9.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d196a009870151d8c1bf8a550e3ce0c15c6a48af9bb36bb8954e70e52136863d", size = 4338358, upload-time = "2026-04-15T18:15:45.898Z" }, { url = "https://files.pythonhosted.org/packages/01/ab/29b55e1536ea0891062d0f2109f5c81804cb36976590418700bf8ec287b9/obstore-0.9.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da32f949a21dfa15739ce6286572a67ce167b827e0a28cbcbb956c25d29162fa", size = 4220536, upload-time = "2026-04-15T18:15:47.822Z" }, { url = "https://files.pythonhosted.org/packages/0d/08/3ee0302ccb106c82600e9e0c1985c426f9906116ef68ecb0a0547ad0e728/obstore-0.9.3-cp313-cp313t-manylinux_2_24_aarch64.whl", hash = "sha256:a97b7ba1211c39ec58821c52a8719fde710e17ee17c40067e360f45c1e444a0b", size = 4102754, upload-time = "2026-04-15T18:15:49.855Z" }, { url = "https://files.pythonhosted.org/packages/17/04/fa33f40d8f96ecce24bcd1c92989a95885c78509e587748960b0299df696/obstore-0.9.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f5051b250080f739b53ff0c09a16b369206aa0aa904449cd8eadae8f51b03d0f", size = 4290696, upload-time = "2026-04-15T18:15:52.003Z" }, { url = "https://files.pythonhosted.org/packages/8e/43/30e1d06f1c037a2e032d016ba381950ba8589432f012b88c3866af00f6ea/obstore-0.9.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:9a7251b2ffbb945cf19daf05c074850a79661ee667ba1e95cb53891cb706b9aa", size = 4272490, upload-time = "2026-04-15T18:15:53.969Z" }, { url = "https://files.pythonhosted.org/packages/b4/d6/2b4fe6979a4f2b0c53b0caea2fa7ceaf1494a0662cd8fa1d127fdc3d659d/obstore-0.9.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b8db25033f71e17e996ca05043118e64cf2067190081912dee85cb4c0ab192e5", size = 4254455, upload-time = "2026-04-15T18:15:56.313Z" }, { url = "https://files.pythonhosted.org/packages/dc/a2/6974c786022f5339243d44a10f53708a8a17c8b7f7b5ad7cf7d9236c9e36/obstore-0.9.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e5f07ca479133dd1768b763cddcd1f3990a22ca4f2f071f5fd77d2bdf7d2baec", size = 4440612, upload-time = "2026-04-15T18:15:58.219Z" }, { url = "https://files.pythonhosted.org/packages/c0/72/9efbd4331a7c58b520a15bf11086ef483c2f86859fa08788741442bb0b76/obstore-0.9.3-cp313-cp313t-win_amd64.whl", hash = "sha256:48e4debe9e0f2efc89208b95cc72dbc666debafd453fe6e9e71e6a24260fb4f6", size = 4177664, upload-time = "2026-04-15T18:16:00.642Z" }, { url = "https://files.pythonhosted.org/packages/fb/cb/5128b750bf529a56f9723561add27035bcbdd7b5fd74e157d08f7bf4527b/obstore-0.9.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4100e2e811a692a67fccbce5bab357c3a430e0243e1c894a49543b2d8dcfa203", size = 4085201, upload-time = "2026-04-15T18:16:03.2Z" }, { url = "https://files.pythonhosted.org/packages/8e/93/deefdc898239f777066ba98fe1a58f706aed5e1e5c7c14b6503880383f14/obstore-0.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:95eedc2b47640ce892e39244775bbf59a73a75fae8d2d2ed4446ba24ffba1e50", size = 3868333, upload-time = "2026-04-15T18:16:04.981Z" }, { url = "https://files.pythonhosted.org/packages/19/eb/68059d436055fc3378bf66bcfcb7df19a0f3a544fed4d327d325d0b1db51/obstore-0.9.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:17a0eb8a812d3f229e989547bb28e50dbc8bb5ff7f98b0cd58353e1b5ef9ca22", size = 4036100, upload-time = "2026-04-15T18:16:07.098Z" }, { url = "https://files.pythonhosted.org/packages/37/b1/772a84fa919530a25c1b55b5f493ef3be965b9e8fbef2c8d158dee2105dd/obstore-0.9.3-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7245fbd83633c4c6c75e3f359ef249b679dd8f7d0aa7cf825fc09c70bd1b14fc", size = 4135329, upload-time = "2026-04-15T18:16:09.14Z" }, { url = "https://files.pythonhosted.org/packages/61/78/247c3fcf50674a477ebdcdc265aa93dc06bf197b700f0dd99967f1239ccb/obstore-0.9.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e003515ba8605d31ce7110f7238b0f5963784e02b936c932f35d7723bfcd7bc4", size = 4414818, upload-time = "2026-04-15T18:16:11.552Z" }, { url = "https://files.pythonhosted.org/packages/46/ca/becd3c0534e1d2433c33ac1a7342d2984282c41619eda3fa32d9a1190a57/obstore-0.9.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13f74d0c81104f7fecab017a2df23a1f7ed049088d7d5aff179edaac88d99a81", size = 4337670, upload-time = "2026-04-15T18:16:13.458Z" }, { url = "https://files.pythonhosted.org/packages/36/90/57e4d351f89ac779783a4d80f22f7aa6a332d0196a0db4412ae23d0743ca/obstore-0.9.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df9a86804d4135ff81e7bd7a2622e098c8b11b7bdf5df66164906069df2cd4cf", size = 4220838, upload-time = "2026-04-15T18:16:15.679Z" }, { url = "https://files.pythonhosted.org/packages/6e/dc/9d05879a5f0e4d2ef13738a4a0db5f83ec36e8f13a85df613148a40fd5d1/obstore-0.9.3-cp314-cp314t-manylinux_2_24_aarch64.whl", hash = "sha256:976d167998ac05aff47ced10c5e1603b05dad509f06a74368609fef956f5c129", size = 4103173, upload-time = "2026-04-15T18:16:17.997Z" }, { url = "https://files.pythonhosted.org/packages/35/92/21a0406c5aef9681b8290c80478bf777c3d63413e5c8928490fd760d5fdc/obstore-0.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:543f4a13f8cdf8d70a4b2283c58f6bb8e77f1b083567ca147187876b66870065", size = 4291176, upload-time = "2026-04-15T18:16:19.961Z" }, { url = "https://files.pythonhosted.org/packages/81/56/0ca8ad32a68c1e496ae9e6958638f38789c8549128bc0bed4980ca24db9e/obstore-0.9.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:32c1db99f6c0c6d12e8c53bc6961a6564925492dc035d069e53c6045f291ad0f", size = 4273148, upload-time = "2026-04-15T18:16:22.274Z" }, { url = "https://files.pythonhosted.org/packages/11/75/d7c424569fbfad9edcb009b7fac19b1433d971a3fd7a882e7b7a7db84d1d/obstore-0.9.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cc8af837d139bc92d32cdd8eb78119ae936689bc25fe67ac5d19f8d0c2666370", size = 4254697, upload-time = "2026-04-15T18:16:24.102Z" }, { url = "https://files.pythonhosted.org/packages/4f/7d/615aaf24ea0f0de38fb8ccbc9923637f1e7564946539fc19709aaea43907/obstore-0.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac2d51451500e2cdbec196470ddd5e3f6eea686f3fbd3702c9270254e8f41d39", size = 4441326, upload-time = "2026-04-15T18:16:25.931Z" }, { url = "https://files.pythonhosted.org/packages/ac/e2/7fafcd7c09d6a6eb689c1293f8a61e9c6965f5c8753c4f1fb779216c221d/obstore-0.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:ddcfc72d68d26782ce6f4ef382125beb0609e29047240e19be4407fc3be1871e", size = 4178356, upload-time = "2026-04-15T18:16:27.91Z" }, ] [[package]] name = "openapi-schema-validator" version = "0.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema" }, { name = "jsonschema-specifications" }, { name = "pydantic" }, { name = "pydantic-settings" }, { name = "referencing" }, { name = "rfc3339-validator" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/4b/67b24b2b23d96ea862be2cca3632a546f67a22461200831213e80c3c6011/openapi_schema_validator-0.8.1.tar.gz", hash = "sha256:4c57266ce8cbfa37bb4eb4d62cdb7d19356c3a468e3535743c4562863e1790da", size = 23134, upload-time = "2026-03-02T08:46:29.807Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6f/87/e9f29f463b230d4b47d65e17858c595153a8ca8c1775f16e406aa82d455d/openapi_schema_validator-0.8.1-py3-none-any.whl", hash = "sha256:0f5859794c5bfa433d478dc5ac5e5768d50adc56b14380c8a6fd3a8113e89c9b", size = 19211, upload-time = "2026-03-02T08:46:28.154Z" }, ] [[package]] name = "openapi-spec-validator" version = "0.8.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema" }, { name = "jsonschema-path" }, { name = "lazy-object-proxy" }, { name = "openapi-schema-validator" }, { name = "pydantic" }, { name = "pydantic-settings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/de/0199b15f5dde3ca61df6e6b3987420bfd424db077998f0162e8ffe12e4f5/openapi_spec_validator-0.8.4.tar.gz", hash = "sha256:8bb324b9b08b9b368b1359dec14610c60a8f3a3dd63237184eb04456d4546f49", size = 1756847, upload-time = "2026-03-01T15:48:19.499Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/70/52310f9ece5f4eb02e0b31d538b51f729169517767a8d0100a25db31d67f/openapi_spec_validator-0.8.4-py3-none-any.whl", hash = "sha256:cf905117063d7c4d495c8a5a167a1f2a8006da6ffa8ba234a7ed0d0f11454d51", size = 50330, upload-time = "2026-03-01T15:48:17.668Z" }, ] [[package]] name = "packaging" version = "26.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] name = "pandocfilters" version = "1.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, ] [[package]] name = "parso" version = "0.8.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] [[package]] name = "pathable" version = "0.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, ] [[package]] name = "pathlib-abc" version = "0.5.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/cb/448649d7f25d228bf0be3a04590ab7afa77f15e056f8fa976ed05ec9a78f/pathlib_abc-0.5.2.tar.gz", hash = "sha256:fcd56f147234645e2c59c7ae22808b34c364bb231f685ddd9f96885aed78a94c", size = 33342, upload-time = "2025-10-10T18:37:20.524Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/29/c028a0731e202035f0e2e0bfbf1a3e46ad6c628cbb17f6f1cc9eea5d9ff1/pathlib_abc-0.5.2-py3-none-any.whl", hash = "sha256:4c9d94cf1b23af417ce7c0417b43333b06a106c01000b286c99de230d95eefbb", size = 19070, upload-time = "2025-10-10T18:37:19.437Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "pillow" version = "12.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] [[package]] name = "platformdirs" version = "4.9.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] [[package]] name = "propcache" version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] [[package]] name = "properdocs" version = "1.6.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ec/29/f27a4e1eddf72ed3db6e47818fbafe6debbf09fd7051f9c1a007239b46ef/properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e", size = 276141, upload-time = "2026-03-20T20:07:48.167Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, ] [[package]] name = "psutil" version = "7.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] [[package]] name = "py-cpuinfo" version = "9.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, ] [[package]] name = "py-partiql-parser" version = "0.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/56/7a/a0f6bda783eb4df8e3dfd55973a1ac6d368a89178c300e1b5b91cd181e5e/py_partiql_parser-0.6.3.tar.gz", hash = "sha256:09cecf916ce6e3da2c050f0cb6106166de42c33d34a078ec2eb19377ea70389a", size = 17456, upload-time = "2025-10-18T13:56:13.441Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c9/33/a7cbfccc39056a5cf8126b7aab4c8bafbedd4f0ca68ae40ecb627a2d2cd3/py_partiql_parser-0.6.3-py2.py3-none-any.whl", hash = "sha256:deb0769c3346179d2f590dcbde556f708cdb929059fb654bad75f4cf6e07f582", size = 23752, upload-time = "2025-10-18T13:56:12.256Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] name = "pydantic" version = "2.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, ] [[package]] name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] [[package]] name = "pydantic-settings" version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pygments-ansi-color" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/f9/7f417aaee98a74b4f757f2b72971245181fcf25d824d2e7a190345669eaf/pygments-ansi-color-0.3.0.tar.gz", hash = "sha256:7018954cf5b11d1e734383a1bafab5af613213f246109417fee3f76da26d5431", size = 7317, upload-time = "2023-05-18T22:44:35.792Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/17/8306a0bcd8c88d7761c2e73e831b0be026cd6873ce1f12beb3b4c9a03ffa/pygments_ansi_color-0.3.0-py3-none-any.whl", hash = "sha256:7eb063feaecadad9d4d1fd3474cbfeadf3486b64f760a8f2a00fc25392180aba", size = 10242, upload-time = "2023-05-18T22:44:34.287Z" }, ] [[package]] name = "pymdown-extensions" version = "10.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, ] [[package]] name = "pyparsing" version = "3.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pytest" version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "pytest-accept" version = "0.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astor" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/15/37f660ba2b40875324b41d343976962f09c8bef5ba668544236afb424bd7/pytest_accept-0.2.3.tar.gz", hash = "sha256:c747d92ef0bcac0dc20e46f3dfb73b8e9aee970de11b98985868560ca508d06e", size = 25990, upload-time = "2026-03-01T05:00:45.561Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/29/50a8582f90c7d31a9df2ecafb345f6cd3f6a9eaad1b4a94a50ce83eb6ee2/pytest_accept-0.2.3-py3-none-any.whl", hash = "sha256:dad6934349fcd78d31d2f4e0daa372d47f2c11525c7c0802f12c3efe422c8d89", size = 35642, upload-time = "2026-03-01T05:00:44.047Z" }, ] [[package]] name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] [[package]] name = "pytest-benchmark" version = "5.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "py-cpuinfo" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/34/9f732b76456d64faffbef6232f1f9dbec7a7c4999ff46282fa418bd1af66/pytest_benchmark-5.2.3.tar.gz", hash = "sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779", size = 341340, upload-time = "2025-11-09T18:48:43.215Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/33/29/e756e715a48959f1c0045342088d7ca9762a2f509b945f362a316e9412b7/pytest_benchmark-5.2.3-py3-none-any.whl", hash = "sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803", size = 45255, upload-time = "2025-11-09T18:48:39.765Z" }, ] [[package]] name = "pytest-codspeed" version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, { name = "pytest" }, { name = "rich" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/bc/9070fdbfb479a0e92a12652a68875de157dc9be7dc4865a06a519e3a1877/pytest_codspeed-4.4.0.tar.gz", hash = "sha256:edb7c101d9c50439a42cf02cfa9c0ac92da618841636bbebf87c3fa54669442a", size = 201093, upload-time = "2026-04-14T15:13:20.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3e/70/4a401b37f80aaebbcbfb2803b0fab75331af554cd75755bc2059f7809bb4/pytest_codspeed-4.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a5c1d51e7ca72ffe247c99b9a97a54191185e8f7a27528e2200d7416da2a68b", size = 820334, upload-time = "2026-04-14T15:13:03.605Z" }, { url = "https://files.pythonhosted.org/packages/16/52/beb46293d414d65163f8f3218aaa2f05e53bdc5cf64f24cc3843c31d3ca4/pytest_codspeed-4.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:215170441e57bfcbefd179dfd86ccd54ed0ee235e0602a068ce4448b35f13cb2", size = 829269, upload-time = "2026-04-14T15:13:05.197Z" }, { url = "https://files.pythonhosted.org/packages/78/53/031793dab3a0edbbcbbd8755648ace0853f4cfb92a0e09e620f301f9ef5d/pytest_codspeed-4.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee3e1964446011ca192eebf0350227df231a5b88af57e518f2a4328fc8ca5131", size = 820300, upload-time = "2026-04-14T15:13:06.791Z" }, { url = "https://files.pythonhosted.org/packages/e7/66/0c3530c0dd9959b7f0930551b3de296db391040e5e8ad3e0cab917736980/pytest_codspeed-4.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340dbb1cc5a21434e0e29bd68ab03c7dc7ad9bfde09d1980b7161352c4c2f048", size = 829201, upload-time = "2026-04-14T15:13:08Z" }, { url = "https://files.pythonhosted.org/packages/f2/8a/24c7997d95f8bda081b8d4346750a5db0d9d8405183ee5cb9062f7381476/pytest_codspeed-4.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:413666266762f9cef1321ba971a9e127b97a1f1dad40ddfd2184c2bc5ac157f9", size = 820242, upload-time = "2026-04-14T15:13:09.191Z" }, { url = "https://files.pythonhosted.org/packages/8b/7f/3912bf6c2bcddb69189d23213f28e5bc058fd4c78fca15dd0010938154b0/pytest_codspeed-4.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e258e6c3d5a8a02ae02a64831be3acd44c19210ffbf13321bdbb8c111c5c6fe4", size = 829190, upload-time = "2026-04-14T15:13:10.762Z" }, { url = "https://files.pythonhosted.org/packages/d8/f4/2cc5e10847aee4233690aa511df6b6f1c2c09f9d8ae506628a138f4ba201/pytest_codspeed-4.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d5dd94dcb69460f916acb9c69865d0171b98acec3ce256645d0c0275b553d7", size = 827557, upload-time = "2026-04-14T15:13:12.553Z" }, { url = "https://files.pythonhosted.org/packages/7f/57/982ce8aa81089b285730dca8404c76af648af41e46d95012be54452913e6/pytest_codspeed-4.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33c38e0e797c74506004f231fc53eab0e412987de281755f714018334381aa3a", size = 835388, upload-time = "2026-04-14T15:13:14.232Z" }, { url = "https://files.pythonhosted.org/packages/99/36/9e84323c6be426728e897133f8e9f3e65a90c26c137e190ca9b27bf304c3/pytest_codspeed-4.4.0-py3-none-any.whl", hash = "sha256:a6aab2fa73523f538e7729c20ccf4a1e8e921324c9877a816b05334135950fd9", size = 203809, upload-time = "2026-04-14T15:13:18.72Z" }, ] [[package]] name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] name = "pytest-xdist" version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "execnet" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] name = "pyyaml-env-tag" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "pyzmq" version = "27.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, ] [[package]] name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "regex" version = "2026.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, ] [[package]] name = "requests" version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] name = "responses" version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303, upload-time = "2026-02-19T14:38:05.574Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099, upload-time = "2026-02-19T14:38:03.847Z" }, ] [[package]] name = "rfc3339-validator" version = "0.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, ] [[package]] name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "roman-numerals" version = "4.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] [[package]] name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, { 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.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] name = "s3fs" version = "2026.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiobotocore" }, { name = "aiohttp" }, { name = "fsspec" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/93/093972862fb9c2fdc24ecf8d6d2212853df1945eddf26ba2625e8eaeee66/s3fs-2026.3.0.tar.gz", hash = "sha256:ce8b30a9dc5e01c5127c96cb7377290243a689a251ef9257336ac29d72d7b0d8", size = 85986, upload-time = "2026-03-27T19:28:20.963Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/52/5ccdc01f7a8a61357d15a66b5d8a6580aa8529cb33f32e6cbb71c52622c5/s3fs-2026.3.0-py3-none-any.whl", hash = "sha256:2fa40a64c03003cfa5ae0e352788d97aa78ae8f9e25ea98b28ce9d21ba10c1b8", size = 32399, upload-time = "2026-03-27T19:28:19.702Z" }, ] [[package]] name = "s3transfer" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, ] [[package]] name = "setuptools" version = "82.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "soupsieve" version = "2.8.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "alabaster" }, { name = "babel" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "docutils" }, { name = "imagesize" }, { name = "jinja2" }, { name = "packaging" }, { name = "pygments" }, { name = "requests" }, { name = "roman-numerals" }, { name = "snowballstemmer" }, { name = "sphinxcontrib-applehelp" }, { name = "sphinxcontrib-devhelp" }, { name = "sphinxcontrib-htmlhelp" }, { name = "sphinxcontrib-jsmath" }, { name = "sphinxcontrib-qthelp" }, { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asttokens" }, { name = "executing" }, { name = "pure-eval" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] [[package]] name = "sympy" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "tinycss2" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] [[package]] name = "tomlkit" version = "0.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, ] [[package]] name = "tornado" version = "6.5.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, ] [[package]] name = "towncrier" version = "25.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "jinja2" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] [[package]] name = "typer" version = "0.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "universal-pathlib" version = "0.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fsspec" }, { name = "pathlib-abc" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/6e/d997a70ee8f4c61f9a7e2f4f8af721cf072a3326848fc881b05187e52558/universal_pathlib-0.3.10.tar.gz", hash = "sha256:4487cbc90730a48cfb64f811d99e14b6faed6d738420cd5f93f59f48e6930bfb", size = 261110, upload-time = "2026-02-22T14:40:58.87Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dd/1a/5d9a402b39ec892d856bbdd9db502ff73ce28cdf4aff72eb1ce1d6843506/universal_pathlib-0.3.10-py3-none-any.whl", hash = "sha256:dfaf2fb35683d2eb1287a3ed7b215e4d6016aa6eaf339c607023d22f90821c66", size = 83528, upload-time = "2026-02-22T14:40:57.316Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uv" version = "0.11.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9b/7d/17750123a8c8e324627534fe1ae2e7a46689db8492f1a834ab4fd229a7d8/uv-0.11.7.tar.gz", hash = "sha256:46d971489b00bdb27e0aa715e4a5cd4ef2c28ea5b6ef78f2b67bf861eb44b405", size = 4083385, upload-time = "2026-04-15T21:42:55.474Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/5b/2bb2ab6fe6c78c2be10852482ef0cae5f3171460a6e5e24c32c9a0843163/uv-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:f422d39530516b1dfb28bb6e90c32bb7dacd50f6a383cd6e40c1a859419fbc8c", size = 23757265, upload-time = "2026-04-15T21:43:14.494Z" }, { url = "https://files.pythonhosted.org/packages/b2/f5/36ff27b01e60a88712628c8a5a6003b8e418883c24e084e506095844a797/uv-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8b2fe1ec6775dad10183e3fdce430a5b37b7857d49763c884f3a67eaa8ca6f8a", size = 23184529, upload-time = "2026-04-15T21:42:30.225Z" }, { url = "https://files.pythonhosted.org/packages/8a/fa/f379be661316698f877e78f4c51e5044be0b6f390803387237ad92c4057f/uv-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:162fa961a9a081dcea6e889c79f738a5ae56507047e4672964972e33c301bea9", size = 21780167, upload-time = "2026-04-15T21:42:44.942Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/fbed29775b0612f4f5679d3226268f1a347161abc1727b4080fb41d9f46f/uv-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5985a15a92bd9a170fc1947abb1fbc3e9828c5a430ad85b5bed8356c20b67a71", size = 23609640, upload-time = "2026-04-15T21:42:22.57Z" }, { url = "https://files.pythonhosted.org/packages/ad/de/989a69634a869a22322770120557c2d8cbba5b77ec7cfad326b4ec0f0547/uv-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:fab0bb43fbbc0ee5b5fee212078d2300c371b725faff7cf72eeaafa0bff0606b", size = 23322484, upload-time = "2026-04-15T21:43:26.52Z" }, { url = "https://files.pythonhosted.org/packages/24/08/c1af05ea602eb4eb75d86badb6b0594cc104c3ca83ccf06d9ed4dd2186ad/uv-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23d457d6731ebdb83f1bffebe4894edab2ef43c1ec5488433c74300db4958924", size = 23326385, upload-time = "2026-04-15T21:42:41.32Z" }, { url = "https://files.pythonhosted.org/packages/68/99/e246962da06383e992ecab55000c62a50fb36efef855ea7264fad4816bf4/uv-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d6a17507b8139b8803f445a03fd097f732ce8356b1b7b13cdb4dd8ef7f4b2e0", size = 24985751, upload-time = "2026-04-15T21:42:37.777Z" }, { url = "https://files.pythonhosted.org/packages/45/2d/b0b68083859579ce811996c1480765ec6a2442b44c451eaef53e6218fbae/uv-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd48823ca4b505124389f49ae50626ba9f57212b9047738efc95126ed5f3844d", size = 25724160, upload-time = "2026-04-15T21:43:18.762Z" }, { url = "https://files.pythonhosted.org/packages/4e/19/5970e89d9e458fd3c4966bbc586a685a1c0ab0a8bf334503f63fa20b925b/uv-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb91f52ee67e10d5290f2c2897e2171357f1a10966de38d83eefa93d96843b0c", size = 25028512, upload-time = "2026-04-15T21:43:02.721Z" }, { url = "https://files.pythonhosted.org/packages/83/eb/4e1557daf6693cb446ed28185664ad6682fd98c6dbac9e433cbc35df450a/uv-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e4d5e31bea86e1b6e0f5a0f95e14e80018e6f6c0129256d2915a4b3d793644d", size = 24933975, upload-time = "2026-04-15T21:42:18.828Z" }, { url = "https://files.pythonhosted.org/packages/68/55/3b517ec8297f110d6981f525cccf26f86e30883fbb9c282769cffbcdcfca/uv-0.11.7-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ceae53b202ea92bc954759bc7c7570cdcd5c3512fce15701198c19fd2dfb8605", size = 23706403, upload-time = "2026-04-15T21:43:10.664Z" }, { url = "https://files.pythonhosted.org/packages/dc/30/7d93a0312d60e147722967036dc8ea37baab4802784bddc22464cb707deb/uv-0.11.7-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:f97e9f4e4d44fb5c4dfaa05e858ef3414a96416a2e4af270ecd88a3e5fb049a9", size = 24495797, upload-time = "2026-04-15T21:42:26.538Z" }, { url = "https://files.pythonhosted.org/packages/8c/89/d49480bdab7725d36982793857e461d471bde8e1b7f438ffccee677a7bf8/uv-0.11.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:750ee5b96959b807cf442b73dd8b55111862d63f258f896787ea5f06b68aaca9", size = 24580471, upload-time = "2026-04-15T21:42:52.871Z" }, { url = "https://files.pythonhosted.org/packages/b6/9f/c57dc03b48be17b564e304eb9ff982890c12dfb888b1ce370788733329ab/uv-0.11.7-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f394331f0507e80ee732cb3df737589de53bed999dd02a6d24682f08c2f8ac4f", size = 24113637, upload-time = "2026-04-15T21:42:34.094Z" }, { url = "https://files.pythonhosted.org/packages/13/ba/b87e358b629a68258527e3490e73b7b148770f4d2257842dea3b7981d4e8/uv-0.11.7-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0df59ab0c6a4b14a763e8445e1c303af9abeb53cdfa4428daf9ff9642c0a3cce", size = 25119850, upload-time = "2026-04-15T21:43:22.529Z" }, { url = "https://files.pythonhosted.org/packages/4b/74/16d229e1d8574bcbafa6dc643ac20b70c3e581f42ac31a6f4fd53035ffe3/uv-0.11.7-py3-none-win32.whl", hash = "sha256:553e67cc766d013ce24353fecd4ea5533d2aedcfd35f9fac430e07b1d1f23ed4", size = 22918454, upload-time = "2026-04-15T21:42:58.702Z" }, { url = "https://files.pythonhosted.org/packages/a6/1d/b73e473da616ac758b8918fb218febcc46ddf64cba9e03894dfa226b28bd/uv-0.11.7-py3-none-win_amd64.whl", hash = "sha256:5674dfb5944513f4b3735b05c2deba6b1b01151f46729d533d413a9a905f8c5d", size = 25447744, upload-time = "2026-04-15T21:42:48.813Z" }, { url = "https://files.pythonhosted.org/packages/1b/bb/e6bfdea92ed270f3445a5a3c17599d041b3f2dbc5026c09e02830a03bbaf/uv-0.11.7-py3-none-win_arm64.whl", hash = "sha256:6158b7e39464f1aa1e040daa0186cae4749a78b5cd80ac769f32ca711b8976b1", size = 23941816, upload-time = "2026-04-15T21:43:06.732Z" }, ] [[package]] name = "verspec" version = "0.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "wcwidth" version = "0.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] [[package]] name = "webencodings" version = "0.5.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] [[package]] name = "werkzeug" version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] name = "wrapt" version = "2.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] [[package]] name = "xmltodict" version = "1.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, ] [[package]] name = "yarl" version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] [[package]] name = "zarr" source = { editable = "." } dependencies = [ { name = "donfig" }, { name = "google-crc32c" }, { name = "numcodecs" }, { name = "numpy" }, { name = "packaging" }, { name = "typing-extensions" }, { name = "zarr-metadata" }, ] [package.optional-dependencies] cli = [ { name = "typer" }, ] gpu = [ { name = "cupy-cuda12x" }, ] optional = [ { name = "universal-pathlib" }, ] remote = [ { name = "fsspec" }, { name = "obstore" }, ] [package.dev-dependencies] dev = [ { name = "astroid" }, { name = "botocore" }, { name = "coverage" }, { name = "fsspec" }, { name = "griffe-inherited-docstrings" }, { name = "hypothesis" }, { name = "markdown-exec", extra = ["ansi"] }, { name = "mike" }, { name = "mkdocs" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-material", extra = ["imaging"] }, { name = "mkdocs-redirects" }, { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, { name = "moto", extra = ["s3", "server"] }, { name = "mypy" }, { name = "numcodecs", extra = ["msgpack"] }, { name = "numpydoc" }, { name = "obstore" }, { name = "pytest" }, { name = "pytest-accept" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-codspeed" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "requests" }, { name = "ruff" }, { name = "s3fs" }, { name = "tomlkit" }, { name = "towncrier" }, { name = "universal-pathlib" }, { name = "uv" }, ] docs = [ { name = "astroid" }, { name = "griffe-inherited-docstrings" }, { name = "markdown-exec", extra = ["ansi"] }, { name = "mike" }, { name = "mkdocs" }, { name = "mkdocs-jupyter" }, { name = "mkdocs-material", extra = ["imaging"] }, { name = "mkdocs-redirects" }, { name = "mkdocstrings" }, { name = "mkdocstrings-python" }, { name = "numcodecs", extra = ["msgpack"] }, { name = "pytest" }, { name = "ruff" }, { name = "s3fs" }, { name = "towncrier" }, ] remote-tests = [ { name = "botocore" }, { name = "coverage" }, { name = "fsspec" }, { name = "hypothesis" }, { name = "moto", extra = ["s3", "server"] }, { name = "numpydoc" }, { name = "obstore" }, { name = "pytest" }, { name = "pytest-accept" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-codspeed" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "requests" }, { name = "s3fs" }, { name = "tomlkit" }, { name = "uv" }, ] test = [ { name = "coverage" }, { name = "hypothesis" }, { name = "numpydoc" }, { name = "pytest" }, { name = "pytest-accept" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-codspeed" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "tomlkit" }, { name = "uv" }, ] [package.metadata] requires-dist = [ { name = "cupy-cuda12x", marker = "extra == 'gpu'" }, { name = "donfig", specifier = ">=0.8" }, { name = "fsspec", marker = "extra == 'remote'", specifier = ">=2023.10.0" }, { name = "google-crc32c", specifier = ">=1.5" }, { name = "numcodecs", specifier = ">=0.14" }, { name = "numpy", specifier = ">=2" }, { name = "obstore", marker = "extra == 'remote'", specifier = ">=0.5.1" }, { name = "packaging", specifier = ">=22.0" }, { name = "typer", marker = "extra == 'cli'" }, { name = "typing-extensions", specifier = ">=4.13" }, { name = "universal-pathlib", marker = "extra == 'optional'" }, { name = "zarr-metadata", editable = "packages/zarr-metadata" }, ] provides-extras = ["cli", "gpu", "optional", "remote"] [package.metadata.requires-dev] dev = [ { name = "astroid", specifier = "<4" }, { name = "botocore" }, { name = "coverage", specifier = ">=7.10" }, { name = "fsspec", specifier = ">=2023.10.0" }, { name = "griffe-inherited-docstrings" }, { name = "hypothesis" }, { name = "markdown-exec", extras = ["ansi"] }, { name = "mike", specifier = ">=2.1.3" }, { name = "mkdocs", specifier = ">=1.6.1,<2" }, { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.6.14" }, { name = "mkdocs-redirects", specifier = ">=1.2.0" }, { name = "mkdocstrings", specifier = ">=0.29.1" }, { name = "mkdocstrings-python", specifier = ">=1.16.10" }, { name = "moto", extras = ["s3", "server"] }, { name = "mypy" }, { name = "numcodecs", extras = ["msgpack"] }, { name = "numpydoc" }, { name = "obstore", specifier = ">=0.5.1" }, { name = "pytest" }, { name = "pytest-accept" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-codspeed" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "requests" }, { name = "ruff" }, { name = "s3fs", specifier = ">=2023.10.0" }, { name = "tomlkit" }, { name = "towncrier" }, { name = "universal-pathlib" }, { name = "uv" }, ] docs = [ { name = "astroid", specifier = "<4" }, { name = "griffe-inherited-docstrings" }, { name = "markdown-exec", extras = ["ansi"] }, { name = "mike", specifier = ">=2.1.3" }, { name = "mkdocs", specifier = ">=1.6.1,<2" }, { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.6.14" }, { name = "mkdocs-redirects", specifier = ">=1.2.0" }, { name = "mkdocstrings", specifier = ">=0.29.1" }, { name = "mkdocstrings-python", specifier = ">=1.16.10" }, { name = "numcodecs", extras = ["msgpack"] }, { name = "pytest" }, { name = "ruff" }, { name = "s3fs", specifier = ">=2023.10.0" }, { name = "towncrier" }, ] remote-tests = [ { name = "botocore" }, { name = "coverage", specifier = ">=7.10" }, { name = "fsspec", specifier = ">=2023.10.0" }, { name = "hypothesis" }, { name = "moto", extras = ["s3", "server"] }, { name = "numpydoc" }, { name = "obstore", specifier = ">=0.5.1" }, { name = "pytest" }, { name = "pytest-accept" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-codspeed" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "requests" }, { name = "s3fs", specifier = ">=2023.10.0" }, { name = "tomlkit" }, { name = "uv" }, ] test = [ { name = "coverage", specifier = ">=7.10" }, { name = "hypothesis" }, { name = "numpydoc" }, { name = "pytest" }, { name = "pytest-accept" }, { name = "pytest-asyncio" }, { name = "pytest-benchmark" }, { name = "pytest-codspeed" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "tomlkit" }, { name = "uv" }, ] [[package]] name = "zarr-metadata" version = "0.1.0" source = { editable = "packages/zarr-metadata" } dependencies = [ { name = "typing-extensions" }, ] [package.metadata] requires-dist = [ { name = "pytest", marker = "extra == 'test'" }, { name = "typing-extensions", specifier = ">=4.13" }, ] provides-extras = ["test"]