|
47 | 47 | REDIRECT_HEAD_TIMEOUT, |
48 | 48 | RETRY_STATUSES, |
49 | 49 | VERSION_REGEX, |
| 50 | + ZARR_EXTENSIONS, |
50 | 51 | DandiInstance, |
51 | 52 | known_instances, |
52 | 53 | ) |
@@ -446,6 +447,72 @@ def get_assets( |
446 | 447 | ) |
447 | 448 |
|
448 | 449 |
|
| 450 | +def split_zarr_location(location: str) -> tuple[str, str] | None: |
| 451 | + """Split a location into ``(asset_path, zarr_subpath)`` if it crosses a zarr boundary. |
| 452 | +
|
| 453 | + Scans path components for segments ending with a Zarr extension |
| 454 | + (``.zarr``, ``.ngff``). If found **and** there are remaining components |
| 455 | + after the boundary, returns the split; otherwise returns ``None``. |
| 456 | +
|
| 457 | + Parameters |
| 458 | + ---------- |
| 459 | + location : str |
| 460 | + A POSIX-style path, e.g. ``"sub-1/file.ome.zarr/0/0/0"``. |
| 461 | +
|
| 462 | + Returns |
| 463 | + ------- |
| 464 | + tuple[str, str] | None |
| 465 | + ``(asset_path, zarr_subpath)`` when a zarr boundary with a subpath |
| 466 | + is detected, otherwise ``None``. |
| 467 | +
|
| 468 | + Examples |
| 469 | + -------- |
| 470 | + >>> split_zarr_location("sub-1/file.ome.zarr/0/0/0") |
| 471 | + ('sub-1/file.ome.zarr', '0/0/0') |
| 472 | + >>> split_zarr_location("sub-1/file.ome.zarr") # no subpath |
| 473 | + >>> split_zarr_location("sub-1/file.nwb") # no zarr extension |
| 474 | + """ |
| 475 | + parts = [p for p in location.split("/") if p] |
| 476 | + for i, part in enumerate(parts): |
| 477 | + if any(part.endswith(ext) for ext in ZARR_EXTENSIONS): |
| 478 | + asset_path = "/".join(parts[: i + 1]) |
| 479 | + zarr_subpath = "/".join(parts[i + 1 :]) |
| 480 | + return (asset_path, zarr_subpath) if zarr_subpath else None |
| 481 | + return None |
| 482 | + |
| 483 | + |
| 484 | +@dataclass |
| 485 | +class AssetZarrEntryURL(SingleAssetURL): |
| 486 | + """Parsed from a URL that points into entries within a Zarr asset. |
| 487 | +
|
| 488 | + For example, ``dandi://dandi/000108/sub-1/file.ome.zarr/0/0/0`` would |
| 489 | + produce ``asset_path="sub-1/file.ome.zarr"`` and ``zarr_subpath="0/0/0"``. |
| 490 | + """ |
| 491 | + |
| 492 | + asset_path: str # e.g., "sub-1/file.ome.zarr" |
| 493 | + zarr_subpath: str # e.g., "0/0/0" |
| 494 | + |
| 495 | + def get_assets( |
| 496 | + self, client: DandiAPIClient, order: str | None = None, strict: bool = False |
| 497 | + ) -> Iterator[BaseRemoteAsset]: |
| 498 | + """Yield the zarr asset whose path equals ``asset_path``. |
| 499 | +
|
| 500 | + If the asset does not exist, a `NotFoundError` is raised when |
| 501 | + ``strict`` is true; otherwise nothing is yielded. |
| 502 | + """ |
| 503 | + try: |
| 504 | + dandiset = self.get_dandiset(client, lazy=not strict) |
| 505 | + assert dandiset is not None |
| 506 | + dandiset.version_id # Force version evaluation |
| 507 | + except NotFoundError: |
| 508 | + if strict: |
| 509 | + raise |
| 510 | + else: |
| 511 | + return |
| 512 | + with _maybe_strict(strict): |
| 513 | + yield dandiset.get_asset_by_path(self.asset_path) |
| 514 | + |
| 515 | + |
449 | 516 | @dataclass |
450 | 517 | class AssetFolderURL(MultiAssetURL): |
451 | 518 | """ |
@@ -846,12 +913,24 @@ def parse( |
846 | 913 | path=location, |
847 | 914 | ) |
848 | 915 | else: |
849 | | - parsed_url = AssetItemURL( |
850 | | - instance=instance, |
851 | | - dandiset_id=dandiset_id, |
852 | | - version_id=version_id, |
853 | | - path=location, |
854 | | - ) |
| 916 | + # Check if location crosses a zarr boundary |
| 917 | + zarr_split = split_zarr_location(location) |
| 918 | + if zarr_split is not None: |
| 919 | + asset_path, zarr_subpath = zarr_split |
| 920 | + parsed_url = AssetZarrEntryURL( |
| 921 | + instance=instance, |
| 922 | + dandiset_id=dandiset_id, |
| 923 | + version_id=version_id, |
| 924 | + asset_path=asset_path, |
| 925 | + zarr_subpath=zarr_subpath, |
| 926 | + ) |
| 927 | + else: |
| 928 | + parsed_url = AssetItemURL( |
| 929 | + instance=instance, |
| 930 | + dandiset_id=dandiset_id, |
| 931 | + version_id=version_id, |
| 932 | + path=location, |
| 933 | + ) |
855 | 934 | elif asset_id: |
856 | 935 | if dandiset_id is None: |
857 | 936 | parsed_url = BaseAssetIDURL(instance=instance, asset_id=asset_id) |
|
0 commit comments