Add support for partial Zarr download and upload#1816
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1816 +/- ##
==========================================
+ Coverage 76.88% 77.29% +0.40%
==========================================
Files 88 90 +2
Lines 12901 13234 +333
==========================================
+ Hits 9919 10229 +310
- Misses 2982 3005 +23
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Covers five areas:
- --zarr TYPE:PATTERN filtering for download (glob, path, regex)
- URL parsing with zarr boundary detection (AssetZarrEntryURL)
- --zarr-mode {full, patch} for upload
- Checksums and manifests (per-directory checksums are NOT
persisted on the archive; legacy .checksum files exist on S3
under zarr-checksums/ for ~72% of older zarrs but are orphaned)
- dandi ls for zarr contents
Related: #1462, #1474
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add --zarr CLI option for download to filter entries within zarr assets (glob/path/regex patterns with predefined 'metadata' alias), and --zarr-mode option for upload to support 'patch' mode (upload/update without deleting remote-only files). Key changes: - New dandi/zarr_filter.py: filter parsing, matching, and aliases - URL parsing: AssetZarrEntryURL for URLs pointing into zarr assets - Download pipeline: thread zarr_entry_filter through Downloader and _download_zarr, skip deletion and checksum when filter active - Upload pipeline: zarr_mode='patch' skips remote file deletion and client-side checksum verification - dandi ls: list zarr entries when URL points into a zarr Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pre-compile regex patterns at ZarrFilter construction time, catching
invalid patterns early instead of on every matches() call (B1)
- Remove redundant get_asset_download_path override in AssetZarrEntryURL
that was identical to the inherited SingleAssetURL method (H1)
- Use Literal["full", "patch"] for zarr_mode parameter instead of bare
str to prevent silent misbehavior on invalid values (H2)
- Collapse consecutive ** glob segments to avoid exponential
backtracking in _glob_match_parts (H3)
- Simplify split_zarr_location to use str.split instead of
PurePosixPath (M1)
- Add explanatory comment for type: ignore in parse_zarr_filter (M2)
- Yield {"status": "done"} when zarr filter matches zero entries (M4)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
click.Choice returns str at runtime, but upload() expects Literal["full", "patch"]. Add typing cast at the call site. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow existing codebase pattern (UploadExisting, DownloadExisting, etc.) to define ZarrMode as a str Enum in upload.py. Eliminates the duplicated Literal["full", "patch"] across three files and the cast() workaround in cmd_upload.py. Uses TYPE_CHECKING guard in files/zarr.py to avoid circular import (files/zarr.py -> upload.py -> .files). Co-Authored-By: Claude Code 2.1.63 / Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Code 2.1.63 / Claude Opus 4.6 <noreply@anthropic.com>
| ## Primary Use Case | ||
|
|
||
| From @kabilar (#1462): | ||
|
|
||
| ```bash | ||
| # 1. Download just the metadata files from a zarr | ||
| dandi download --zarr glob:'**/.z*' --zarr glob:'**/zarr.json' \ | ||
| dandi://dandi/001289/rawdata/.../PC.ome.zarr | ||
|
|
||
| # 2. Edit .zattrs locally | ||
| vim PC.ome.zarr/.zattrs | ||
|
|
||
| # 3. Upload changes without deleting remote data chunks | ||
| dandi upload --zarr-mode patch | ||
| ``` |
There was a problem hiding this comment.
Thanks @yarikoptic. An additional use case would be adding shards to an existing Zarr. For example, we have 355+ TB from a subject that were are stitching together into a single Zarr. We don't have enough space locally so will need to upload a shard, delete the local copy, and repeat the process.
There was a problem hiding this comment.
do you see currently proposed/implemented interface be sufficient for this use case?
There was a problem hiding this comment.
Yes, thanks. Should we add this as a use case in the design doc?
There was a problem hiding this comment.
we could -- just push it to this branch or send a PR against it if would like a review
There was a problem hiding this comment.
| ## Primary Use Case | |
| From @kabilar (#1462): | |
| ```bash | |
| # 1. Download just the metadata files from a zarr | |
| dandi download --zarr glob:'**/.z*' --zarr glob:'**/zarr.json' \ | |
| dandi://dandi/001289/rawdata/.../PC.ome.zarr | |
| # 2. Edit .zattrs locally | |
| vim PC.ome.zarr/.zattrs | |
| # 3. Upload changes without deleting remote data chunks | |
| dandi upload --zarr-mode patch | |
| ``` | |
| ## Use Cases | |
| ### Use Case 1 - Update metadata | |
| 1. Download just the metadata files from a Zarr. | |
| `dandi download --zarr glob:'**/.z*' --zarr glob:'**/zarr.json' \ | |
| dandi://dandi/001289/rawdata/.../PC.ome.zarr` | |
| 2. Edit metadata locally. | |
| `vim PC.ome.zarr/zarr.json` | |
| 3. Upload changes without deleting remote data chunks. | |
| `dandi upload --zarr-mode patch` | |
| ### Use Case 2 - Incrementally add shards to an existing Zarr | |
| We are generating a very large dataset for which there is not enough space to house the entire dataset locally. | |
| 1. Add shard(s) locally to a Zarr that exists on DANDI. | |
| 2. Upload shard(s) without deleting remote Zarr objects (that don't exist locally). | |
| 3. Remove local shard(s) and repeat the process. |
Co-authored-by: Kabilar Gunalan <kabilar.gunalan@gmail.com>
|
Hi @yarikoptic, thanks for this pull request. I was able to add objects to an existing zarr, delete objects, and modify metadata (i.e. I just have some minor feedback on the terminal output. When I run And then when I modify the zarr locally and run So I would propose the following changes to the terminal output:
|
|
Just explicitly noting that For For |
Six files had conflicts, all driven by three orthogonal upstream changes overlapping with the partial-zarr work: 1. --sync redesign (upstream PR "Redesign --sync as optional-value option", cb394e8): --sync switched from is_flag=True (bool) to an optional-value option resolving to None / "ask" / "do", with a SyncMode enum in dandi.consts. Function signatures for download() and upload() went from `sync: bool` to `sync: bool | SyncMode | None`, and the CLI now converts the string via `SyncMode(sync) if sync is not None else None`. 2. Zarr cleanup consolidation (upstream, commits db6bf4b..976122c): the "deleting extra files" phase in download.py was reworked into a single-pass tree scan with lazy "deleting extra files" status announcement (only yielded when something is actually deleted) and debug logging. 3. Validation companion file (upstream PR series around 3873319): upload() gained a `validation_log_path` parameter, cmd_upload.py imports SyncMode from ..consts, and computes a companion path from the CLI logfile. Partial-zarr's own additions that overlapped: - --zarr FILTER option on `dandi download` and a `zarr_filters` param threaded through to download(). - ZarrMode(str, Enum) in dandi.upload and --zarr-mode on `dandi upload`. - Skip extra-file deletion and whole-zarr checksum verification when a zarr filter is active (partial download semantics). Conflict resolutions: * dandi/cli/cmd_download.py Kept upstream's `sync: str | None` signature and `sync=SyncMode(sync) if sync is not None else None` conversion at the download() call site. Preserved partial-zarr's `zarr_filters` param and `zarr_filters=zarr_filters` kwarg. * dandi/cli/cmd_upload.py Merged imports: kept `SyncMode` from ..consts (upstream) alongside `ZarrMode` from ..upload (partial-zarr). In the upload_() call, combined upstream's `sync=SyncMode(sync) if sync is not None else None` and `validation_log_path=companion` with partial-zarr's `zarr_mode=zarr_mode`. * dandi/cli/tests/test_download.py Updated seven mock assertions to expect `sync=None` (upstream's new default) plus `zarr_filters=()` (partial-zarr's new kwarg). * dandi/download.py (signature) Signature is `sync: bool | SyncMode | None = False, zarr_filters: Sequence[str] = ()`. Bool default kept for backward compatibility with programmatic callers; upstream already handles `sync is True` by normalizing to SyncMode.ASK. * dandi/download.py (zarr cleanup block) Kept partial-zarr's outer guard `if zarr_entry_filter is not None: ... else:` so partial downloads skip both extra-file deletion and whole-zarr checksum verification. Inside the `else` branch, adopted upstream's single-pass scan with the `announced` flag, typed `dirs: deque[Path]`, and debug logging for both file deletion and empty-directory removal. The "skipped in final_out['message']" checksum block remains nested inside the same else, consistent with the pre-existing partial-zarr layout. * dandi/files/zarr.py Combined imports at the top of the file: `TYPE_CHECKING, Any, Optional` (partial-zarr, needed for the `ZarrMode` forward reference) plus `import urllib.parse` (upstream), and a TYPE_CHECKING guarded `from ..upload import ZarrMode` block to avoid a circular import. * dandi/upload.py Signature is `sync: bool | SyncMode | None = False, zarr_mode: ZarrMode = ZarrMode.FULL, validation_log_path: str | Path | None = None`. All three params are used later in the function (sync normalization at ~line 525, zarr_mode threaded into upload kwargs at ~line 415, validation_log_path at ~line 315). No conflicts in dandi/dandiarchive.py, dandi/tests/test_dandiarchive.py, dandi/tests/test_download.py, dandi/tests/test_upload.py — auto-merged. Co-Authored-By: Claude Code 2.1.138 / Claude Opus 4.7 <noreply@anthropic.com>
Restructure the design doc's "Primary Use Case" section into a "Use Cases" section covering: 1. Update metadata (the original scenario) 2. Incrementally add shards to an existing Zarr per @kabilar's review suggestion on PR #1816 (#1816 (comment)). The motivation for Use Case 2 is producing large datasets in shards when the full Zarr cannot fit locally. No source changes are needed -- the existing --zarr-mode patch code path already supports Use Case 2 by construction: new local files are uploaded, remote-only files are preserved, and whole-Zarr checksum verification is skipped so a pruned local checkout does not spuriously mismatch. Add a regression test test_upload_zarr_patch_mode_incremental_shards that explicitly exercises Use Case 2: prunes the fixture Zarr's local tree down to metadata files, drops in a synthetic shard, uploads with zarr_mode="patch", and asserts both that the new shard reached the remote and that every previously-pruned remote entry is still present. Co-Authored-By: Kabilar Gunalan <871137+kabilar@users.noreply.github.com> Co-Authored-By: Claude Code 2.1.138 / Claude Opus 4.7 <noreply@anthropic.com>
…o partial-zarr * 'partial-zarr' of https://github.com/dandi/dandi-cli: DOC: Minor tune up in phrasing of the design doc
Summary
Design and implementation for partial zarr download and upload support, addressing #1462, #1474, and related archive issues.
The PR covers five areas:
--zarr TYPE:PATTERNfiltering fordandi download— glob, path, and regex filters for selecting entries within zarr assets, with ametadataalias for common zarr metadata filesAssetZarrEntryURLto handle URLs likedandi://dandi/000108/.../file.ome.zarr/0/0/0--zarr-mode {full, patch}fordandi upload— patch mode uploads changed files without deleting remote files absent locallyzarr_checksumlibrary but are NOT persisted (only the root digest is stored in the DB); legacy.checksumfiles exist on S3 atzarr-checksums/for ~72% of older zarrs but are orphaned since Dec 2022dandi lsfor zarr contents — listing files within a zarr asset when URL points into a zarrKey findings from investigation
zarr_checksumalgorithm IS hierarchical (Merkle tree, bottom-up viaZarrChecksumTree)ingest_zarr_archivetask computes checksums entirely in memory and stores only the root digest.checksumfiles on S3 (zarr-checksums/prefix) were written byZarrChecksumFileUpdater, removed in dandi-archive PRs Testing started to fail due to an error in parsing (?) + new deprecationwarning #1390/[FEAT] dandi fsspec filesystem #1395 (Dec 2022). Legacy files remain for older zarrs but no API exposes themImplementation
New files
dandi/zarr_filter.py—ZarrFilterdataclass with glob/path/regex matching,parse_zarr_filter(),make_zarr_entry_filter(),ZARR_FILTER_ALIASES(includesmetadataalias)dandi/tests/test_zarr_filter.py— 52 unit tests covering all filter types, parsing, aliases, edge cases, invalid regex validationModified files
dandi/dandiarchive.py—split_zarr_location(),AssetZarrEntryURLclass, updatedparse_dandi_url()to detect zarr boundariesdandi/download.py—zarr_filtersparameter ondownload(), filter threading throughDownloaderand_download_zarr(), skip deletion/checksum when filter activedandi/files/zarr.py—zarr_mode: Literal["full", "patch"]oniter_upload(), patch mode skips remote file deletion and client-side checksum verificationdandi/upload.py—zarr_modeparameter onupload(), conditional pass-through forZarrAssetdandi/cli/cmd_download.py—--zarrclick option (multiple, OR logic)dandi/cli/cmd_upload.py—--zarr-modeclick optiondandi/cli/cmd_ls.py— list zarr entries when URL isAssetZarrEntryURLdandi/cli/tests/test_download.py— updated mock expectations for newzarr_filtersparameterIntegration tests
dandi/tests/test_download.py— 6 tests (glob filter, metadata alias, no-delete, path filter, nonexistent filter, sync conflict)dandi/tests/test_upload.py— 3 tests (patch no-delete, full delete, patch updates)dandi/tests/test_dandiarchive.py— 3 URL parsing cases + 8split_zarr_locationcasesReview checklist
Please review the design at
doc/design/partial-zarr.mdand comment on:--zarr TYPE:PATTERNsyntax — is the filter approach right? Are glob/path/regex the right types?metadataalias expansion — doesglob:**/.z*+glob:**/zarr.json+glob:**/.zmetadatacover all cases?--zarr-mode patchsemantics — is "upload without deleting" the right default for patch?Should subtree cleanup happen?AssetZarrEntryURLwith zarr boundary detection the right approach?--syncconflict raises error, server-side glob deferred)zarr-checksums/files on S3 be cleaned up as part of this or separately?TODO
dandi/zarr_filter.py— filter parsing and matchingAssetZarrEntryURLandsplit_zarr_location()indandi/dandiarchive.py--zarroption todandi downloadCLI_download_zarr()for partial download support--zarr-modeoption todandi uploadCLIiter_upload()(dandi/files/zarr.py)zarr_modethroughdandi/upload.pydandi lszarr contents supportzarr_filter.py(52 tests)split_zarr_location(11 test cases)Literaltyping,**collapse, redundant override removal)🤖 Generated with Claude Code