-
Notifications
You must be signed in to change notification settings - Fork 67
Add ManifestStore for loading data from ManifestArrays #490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
d53e426
5ad7d37
ac64c16
e9f213f
1079607
b27dec9
266fbed
480c175
04a2e60
81b13c8
540b0a8
29111c2
147682b
2475e6d
cc1b2f8
5dee76d
e3195fd
ac37f15
58fc240
f15a52a
52272d0
756c3c4
c14e67a
e8b995c
52287e9
aadb2e8
cc93bfd
5f4b51e
1c373ab
c39b335
f04ce07
5d799f2
faa0e67
ae7597a
aee346b
939d243
5231800
fdc8c41
28255e7
4a1b4ab
759e365
29d34e3
1f8f54b
3c35d8e
73e83dd
595fbb8
2246d64
e492564
091a4dd
4a99b15
7d67b74
6bfdf4d
709788b
ed0c1a5
dfba447
9e0bb9b
48e32a4
f221c93
2bea8b6
61168f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
maxrjones marked this conversation as resolved.
Outdated
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import pickle | ||
| from collections.abc import Iterable | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from xarray import DataArray, Dataset | ||
| from zarr.abc.store import ( | ||
| ByteRequest, | ||
| OffsetByteRequest, | ||
| RangeByteRequest, | ||
| Store, | ||
| SuffixByteRequest, | ||
| ) | ||
| from zarr.core.buffer import Buffer | ||
| from zarr.core.buffer.core import BufferPrototype | ||
|
|
||
| from virtualizarr.storage.common import ( | ||
| find_matching_store, | ||
| get_zarr_metadata, | ||
| list_dir_from_xr_obj, | ||
| parse_manifest_index, | ||
| ) | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import AsyncGenerator, Iterable | ||
| from typing import Any | ||
|
|
||
| from obstore.store import ObjectStore as _UpstreamObjectStore | ||
|
maxrjones marked this conversation as resolved.
Outdated
|
||
| from zarr.core.buffer import BufferPrototype | ||
| from zarr.core.common import BytesLike | ||
|
|
||
|
|
||
| __all__ = ["VirtualObjectStore"] | ||
|
|
||
| _ALLOWED_EXCEPTIONS: tuple[type[Exception], ...] = ( | ||
| FileNotFoundError, | ||
| IsADirectoryError, | ||
| NotADirectoryError, | ||
| ) | ||
|
|
||
|
|
||
| class VirtualObjectStore(Store): | ||
| """A Zarr store that uses obstore for fast read/write from AWS, GCP, Azure. | ||
|
maxrjones marked this conversation as resolved.
Outdated
|
||
|
|
||
| Parameters | ||
| ---------- | ||
| stores : dict[prefix, obstore.store.ObjectStore] | ||
| A mapping of url prefixes to obstore Store instances set up with the proper credentials. | ||
|
|
||
| Warnings | ||
| -------- | ||
| VirtualObjectStore is experimental and subject to API changes without notice. Please | ||
| raise an issue with any comments/concerns about the store. | ||
|
|
||
| Notes | ||
| ----- | ||
| Modified from https://github.com/zarr-developers/zarr-python/pull/1661 | ||
| """ | ||
|
|
||
| def __eq__(self, value: object): | ||
| NotImplementedError | ||
|
|
||
| def __init__( | ||
| self, | ||
| xr_obj: DataArray | Dataset, | ||
|
maxrjones marked this conversation as resolved.
Outdated
|
||
| stores: dict[str, _UpstreamObjectStore], | ||
| ) -> None: | ||
| import obstore as obs | ||
|
|
||
| # TODO: Support DataArray, Dataset, or DataTree across all methods | ||
| for store in stores.values(): | ||
| if not isinstance( | ||
| store, | ||
| ( | ||
| obs.store.AzureStore, | ||
| obs.store.GCSStore, | ||
| obs.store.HTTPStore, | ||
| obs.store.S3Store, | ||
| obs.store.LocalStore, | ||
| obs.store.MemoryStore, | ||
| ), | ||
| ): | ||
| raise TypeError(f"expected ObjectStore class, got {store!r}") | ||
| super().__init__(read_only=True) | ||
| self.stores = stores | ||
| self.xr_obj: DataArray | Dataset = xr_obj | ||
|
maxrjones marked this conversation as resolved.
Outdated
maxrjones marked this conversation as resolved.
Outdated
|
||
|
|
||
| def __str__(self) -> str: | ||
| return f"ManifesStore({self.xr_obj})" | ||
|
|
||
| def __getstate__(self) -> dict[Any, Any]: | ||
| state = self.__dict__.copy() | ||
| stores = state["stores"] | ||
| for k, v in stores: | ||
| stores[k] = pickle.dumps(v) | ||
| state["stores"] = stores | ||
| return state | ||
|
|
||
| def __setstate__(self, state: dict[Any, Any]) -> None: | ||
| stores = state["stores"] | ||
| for k, v in stores: | ||
| stores[k] = pickle.loads(v) | ||
| state["stores"] = stores | ||
| 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 | ||
|
|
||
| if "zarr.json" in key: | ||
| return get_zarr_metadata(self.xr_obj, key) | ||
| manifest_index = parse_manifest_index(key) | ||
|
maxrjones marked this conversation as resolved.
Outdated
|
||
| # Get path, offset, and length matching this key from the ChunkManifest | ||
| if manifest_index.variable == "__xarray_dataarray_variable__": | ||
| path = self.xr_obj.data.manifest._paths[*manifest_index.indexes] | ||
| offset = self.xr_obj.data.manifest._offsets[*manifest_index.indexes] | ||
| length = self.xr_obj.data.manifest._lengths[*manifest_index.indexes] | ||
| else: | ||
| path = self.xr_obj[manifest_index.variable].data.manifest._paths[ | ||
| *manifest_index.indexes | ||
| ] | ||
| offset = self.xr_obj[manifest_index.variable].data.manifest._offsets[ | ||
| *manifest_index.indexes | ||
| ] | ||
| length = self.xr_obj[manifest_index.variable].data.manifest._lengths[ | ||
| *manifest_index.indexes | ||
| ] | ||
| store_request = find_matching_store(stores=self.stores, request_key=path) | ||
| # Transform the input byte range to account for the chunk location in the file | ||
| chunk_end_exclusive = offset + length | ||
| byte_range = _transform_byte_range( | ||
| byte_range, chunk_start=offset, chunk_end_exclusive=chunk_end_exclusive | ||
| ) | ||
| # Actually get the bytes | ||
| try: | ||
| bytes = await obs.get_range_async( | ||
| self.stores[store_request.store_id], | ||
|
maxrjones marked this conversation as resolved.
Outdated
|
||
| store_request.key, | ||
| start=byte_range.start, | ||
| end=byte_range.end, | ||
| ) | ||
| return prototype.buffer.from_bytes(bytes) # type: ignore[arg-type] | ||
| except _ALLOWED_EXCEPTIONS: | ||
| return None | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are any of these exceptions allowed? Don't we want to raise them?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is how Zarr handles empty chunks (xref #16). I'll need to put some more thought into this and return with a more complete answer later on.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you read this? #427 (comment) Seems related. |
||
|
|
||
| async def get_partial_values( | ||
| self, | ||
| prototype: BufferPrototype, | ||
| key_ranges: Iterable[tuple[str, ByteRequest | None]], | ||
| ) -> list[Buffer | None]: | ||
| # docstring inherited | ||
| raise NotImplementedError | ||
|
|
||
| async def exists(self, key: str) -> bool: | ||
| # docstring inherited | ||
| raise NotImplementedError | ||
|
|
||
| @property | ||
| def supports_writes(self) -> bool: | ||
| # docstring inherited | ||
| return False | ||
|
|
||
| async def set(self, key: str, value: Buffer) -> None: | ||
| # docstring inherited | ||
| raise NotImplementedError | ||
|
|
||
| async def set_if_not_exists(self, key: str, value: Buffer) -> None: | ||
| # docstring inherited | ||
| raise NotImplementedError | ||
|
|
||
| @property | ||
| def supports_deletes(self) -> bool: | ||
| # docstring inherited | ||
| return False | ||
|
|
||
| async def delete(self, key: str) -> None: | ||
| raise NotImplementedError | ||
|
|
||
| @property | ||
| def supports_partial_writes(self) -> bool: | ||
| # docstring inherited | ||
| return False | ||
|
|
||
| async def set_partial_values( | ||
| self, key_start_values: Iterable[tuple[str, int, BytesLike]] | ||
| ) -> None: | ||
| # docstring inherited | ||
| raise NotImplementedError | ||
|
|
||
| @property | ||
| def supports_listing(self) -> bool: | ||
| # docstring inherited | ||
| return True | ||
|
|
||
| def list(self) -> AsyncGenerator[str, None]: | ||
| # docstring inherited | ||
| raise NotImplementedError | ||
|
|
||
| def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]: | ||
| # docstring inherited | ||
| raise NotImplementedError | ||
|
|
||
| def list_dir(self, prefix: str) -> AsyncGenerator[str, None]: | ||
| # docstring inherited | ||
| if isinstance(self.xr_obj, (DataArray, Dataset)): | ||
| return list_dir_from_xr_obj(self.xr_obj, prefix) | ||
| else: | ||
| raise NotImplementedError( | ||
| "Only DataArray and Datasets are currently supported" | ||
| ) | ||
|
|
||
|
|
||
| def _transform_byte_range( | ||
| byte_range: ByteRequest | None, *, chunk_start: int, chunk_end_exclusive: int | ||
| ) -> RangeByteRequest: | ||
| """ | ||
| Convert an incoming byte_range which assumes one chunk per file to a | ||
| virtual byte range that accounts for the location of a chunk within a file. | ||
| """ | ||
| if byte_range is None: | ||
| byte_range = RangeByteRequest(chunk_start, chunk_end_exclusive) | ||
| elif isinstance(byte_range, RangeByteRequest): | ||
| if byte_range.end > chunk_end_exclusive: | ||
| raise ValueError( | ||
| f"Chunk ends before byte {chunk_end_exclusive} but request end was {byte_range.end}" | ||
| ) | ||
| byte_range = RangeByteRequest( | ||
| chunk_start + byte_range.start, chunk_start + byte_range.end | ||
| ) | ||
| elif isinstance(byte_range, OffsetByteRequest): | ||
| byte_range = RangeByteRequest( | ||
| chunk_start + byte_range.offset, chunk_end_exclusive | ||
| ) # type: ignore[arg-type] | ||
| elif isinstance(byte_range, SuffixByteRequest): | ||
| byte_range = RangeByteRequest( | ||
| chunk_end_exclusive - byte_range.suffix, chunk_end_exclusive | ||
| ) # type: ignore[arg-type] | ||
| else: | ||
| raise ValueError(f"Unexpected byte_range, got {byte_range}") | ||
| return byte_range | ||
Uh oh!
There was an error while loading. Please reload this page.