Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
85a8e82
Add open_virtual() top-level API with force_external, load, and codec…
betolink May 5, 2026
cba74a3
Add virtual API docs page, link virtual_data.ipynb tutorial in nav
betolink May 5, 2026
1ccd8bf
update wording
betolink May 5, 2026
c780bab
Merge branch 'main' into virtual-open
betolink May 18, 2026
a9cf363
fix lint
betolink May 18, 2026
019b1da
fix typing defs
betolink May 18, 2026
06f4fb7
fix mypy for manifest patching
betolink May 18, 2026
bb16701
override direct access for virtual stores, todo: find a way to test i…
betolink May 18, 2026
4d3b57d
update warning rules
betolink May 18, 2026
98b3183
Merge remote-tracking branch 'origin/main' into virtual-open
betolink May 18, 2026
da93349
fix ruff
betolink May 18, 2026
4077ca2
pinning vz to > 2.x
betolink May 19, 2026
823c144
Merge branch 'main' into virtual-open
betolink May 19, 2026
bfb5170
parallelize virtualizarr tests
betolink May 19, 2026
56609bc
merging kerchunk into virtualizarr deps
betolink May 19, 2026
2a57863
update icechunk opener
betolink May 20, 2026
2eeb773
Fix test deprecation warning for store (#1351)
betolink May 20, 2026
338e434
Merge branch 'main' into virtual-open
betolink May 26, 2026
1541b00
Merge branch 'main' into virtual-open
betolink May 30, 2026
33a2d92
Merge branch 'main' into virtual-open
betolink Jun 1, 2026
9873ada
Merge branch 'main' into virtual-open
betolink Jun 4, 2026
e616aba
simplify uri resolution for virtual_open
betolink Jun 10, 2026
3238c24
upgrade VCRpy to v8.0.0
betolink Jun 10, 2026
de15c4b
fix linting
betolink Jun 10, 2026
8272649
update min deps
betolink Jun 10, 2026
5330b6f
update min deps again
betolink Jun 10, 2026
eb6421a
update diagream
betolink Jun 23, 2026
4a8e2dc
resolv merge conflict
betolink Jun 23, 2026
2603859
Merge branch 'main' into virtual-open
betolink Jul 1, 2026
79e37db
fix lint
betolink Jul 1, 2026
ca7a0ee
return true if the url has icechunk on it
betolink Jul 1, 2026
a7927fd
mock store for unit test
betolink Jul 1, 2026
91ee595
remove diagram from vds notebook
betolink Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codespellignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ges
cach
fo
2 changes: 1 addition & 1 deletion .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ jobs:
EARTHDATA_USERNAME: ${{ secrets.EDL_USERNAME }}
EARTHDATA_PASSWORD: ${{ secrets.EDL_PASSWORD }}
run: |
# -rxXs: Show provided (r)eason in summary for (x)fail, (X)pass, and (s)kipped tests
uv run pytest tests/integration \
-n logical \
-rxXs \
--cov=earthaccess \
--cov-report=term-missing \
Expand Down
2 changes: 2 additions & 0 deletions docs/.nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ nav:
- "Store": "api/store/store.md"
- Auth:
- "Auth": "api/auth/auth.md"
- Virtual:
- "Virtual": "api/virtual/virtual.md"

- User guide:
- "user/index.md"
Expand Down
16 changes: 16 additions & 0 deletions docs/api/virtual/virtual.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Documentation for `earthaccess.virtual`

::: earthaccess.virtual
options:
inherited_members: true
show_root_heading: true
show_source: false

---

## Internal helpers

::: earthaccess.virtual._parser.homogenize_dataset_codec_level
options:
show_root_heading: true
show_source: false
Binary file added docs/tutorials/virtual-diagram.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3,753 changes: 3,753 additions & 0 deletions docs/tutorials/virtual_data.ipynb

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions earthaccess/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import threading

from .api import (
auth_environ,
Expand All @@ -24,7 +23,7 @@
from .services import DataServices
from .store import Store
from .system import PROD, UAT
from .virtual import virtualize
from .virtual import open_virtual, virtualize

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -55,11 +54,12 @@
"granule_query",
"login",
"open",
# virtual
"open_virtual",
"search_data",
"search_datasets",
"search_services",
"status",
# virtual
"virtualize",
]

Expand All @@ -72,7 +72,6 @@

_auth = Auth()
_store: Store | None = None
_lock = threading.Lock()


def __getattr__(name): # type: ignore[no-untyped-def]
Expand Down
1 change: 0 additions & 1 deletion earthaccess/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ def __init__(self, hostname: str, auth: tuple[str, str]) -> None:
self.auth = auth

def __call__(self, r: requests.Response, **kwargs: Any) -> requests.Response:

# If the response's URL is not for the EDL system we're authenticating
# against, then simply return the response unchanged. Otherwise, we'll
# prepare a new request below with the user's EDL credentials.
Expand Down
49 changes: 49 additions & 0 deletions earthaccess/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, ClassVar

import requests
import s3fs

import earthaccess

Expand Down Expand Up @@ -290,6 +291,54 @@ def s3_bucket(self) -> dict[str, Any]:

return self["umm"].get("DirectDistributionInformation", {})

def virtual_collection_url(self) -> str | None:
"""Return the VIRTUAL COLLECTION URL from the collection's RelatedUrls.

Returns:
The URL of the virtual store reference file (e.g. a .json or
.icechunk file), or ``None`` if no such link exists.
"""
for link in self["umm"].get("RelatedUrls", []):
if (
link.get("Type") == "GET DATA"
and link.get("Subtype") == "VIRTUAL COLLECTION"
):
return link["URL"]
return None

def get_s3_credentials(self) -> dict[str, str]:
"""Return temporary S3 credentials for this collection.

Returns:
A dictionary with ``accessKeyId``, ``secretAccessKey``,
and ``sessionToken``.

Raises:
ValueError: If the collection has no ``S3CredentialsAPIEndpoint``.
"""
dd = self["umm"].get("DirectDistributionInformation", {})
endpoint = dd.get("S3CredentialsAPIEndpoint")
if not endpoint:
msg = "This collection does not provide an S3CredentialsAPIEndpoint."
raise ValueError(
msg,
)
return earthaccess.__auth__.get_s3_credentials(endpoint=endpoint)

def get_s3_filesystem(self) -> s3fs.S3FileSystem:
"""Return an authenticated ``s3fs.S3FileSystem`` for this collection.

Returns:
An ``s3fs.S3FileSystem`` configured with temporary S3 credentials
from the collection's credentials endpoint.
"""
creds = self.get_s3_credentials()
return s3fs.S3FileSystem(
key=creds["accessKeyId"],
secret=creds["secretAccessKey"],
token=creds["sessionToken"],
)

def services(self) -> dict[Any, list[dict[str, Any]]]:
"""Return list of services available for this collection."""
warnings.warn(
Expand Down
2 changes: 1 addition & 1 deletion earthaccess/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _is_interactive() -> bool:
Interactive sessions include Jupyter Notebooks, IPython REPL, and default Python REPL.
"""
try:
from IPython import ( # type: ignore[import-not-found] # noqa: PLC0415
from IPython import ( # type: ignore[import-not-found]
get_ipython,
)

Expand Down
16 changes: 13 additions & 3 deletions earthaccess/virtual/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
"""Virtual dataset utilities for cloud-native access to NASA Earthdata granules.

This subpackage provides tools for creating virtual xarray Datasets from
NASA Earthdata granules without downloading data, using VirtualiZarr parsers.
NASA Earthdata granules without downloading data, using VirtualiZarr parsers,
and for opening existing virtual stores (Icechunk / kerchunk references).

Public API
----------
- ``virtualize`` — create a virtual (or loaded) xarray Dataset from granules.
- ``open_virtual`` — open an existing virtual store (Icechunk, kerchunk .parquet
or .json references) from a URI.
- ``homogenize_dataset_codec_level`` — patch Zlib codec levels on all
``ManifestArray`` objects in a virtual dataset (power-users).
- ``SUPPORTED_PARSERS`` — frozenset of recognised parser name strings.
- ``get_granule_credentials_endpoint_and_region`` — resolve S3 credentials
for a granule (useful for advanced direct-access workflows).
Expand All @@ -18,11 +23,16 @@
from earthaccess.virtual._credentials import (
get_granule_credentials_endpoint_and_region,
)
from earthaccess.virtual._parser import SUPPORTED_PARSERS
from earthaccess.virtual.core import virtualize
from earthaccess.virtual._parser import (
SUPPORTED_PARSERS,
homogenize_dataset_codec_level,
)
from earthaccess.virtual.core import open_virtual, virtualize

__all__ = [
"SUPPORTED_PARSERS",
"get_granule_credentials_endpoint_and_region",
"homogenize_dataset_codec_level",
"open_virtual",
"virtualize",
]
6 changes: 3 additions & 3 deletions earthaccess/virtual/_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,11 @@ def build_obstore_registry(
ImportError: If ``earthaccess[virtualizarr]`` is not installed.
"""
try:
from obstore.auth.earthdata import ( # noqa: PLC0415
from obstore.auth.earthdata import (
NasaEarthdataCredentialProvider,
)
from obstore.store import HTTPStore, S3Store # noqa: PLC0415
from virtualizarr.registry import ObjectStoreRegistry # noqa: PLC0415
from obstore.store import HTTPStore, S3Store
from virtualizarr.registry import ObjectStoreRegistry
except ImportError:
msg = (
"earthaccess.virtualize() requires `pip install earthaccess[virtualizarr]`"
Expand Down
76 changes: 75 additions & 1 deletion earthaccess/virtual/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from earthaccess.virtual._types import AccessType, ParserType

if TYPE_CHECKING:
import xarray as xr

import earthaccess
from earthaccess.virtual._types import AccessType, ParserType

Expand Down Expand Up @@ -70,7 +72,7 @@ def resolve_parser(
return parser

try:
from virtualizarr.parsers import ( # noqa: PLC0415
from virtualizarr.parsers import (
DMRPPParser,
HDFParser,
KerchunkJSONParser,
Expand Down Expand Up @@ -133,3 +135,75 @@ def get_urls_for_parser(
url = url + ".dmrpp"
urls.append(url)
return urls


def homogenize_dataset_codec_level(ds: xr.Dataset, target_level: int = 7) -> xr.Dataset:
"""Patch Zlib codec levels on all ManifestArrays in *ds* to *target_level*."""
try:
import xarray as xr
from virtualizarr.manifests import ManifestArray
except ImportError:
msg = (
"earthaccess.virtualize() requires `pip install earthaccess[virtualizarr]`"
)
raise ImportError(
msg,
) from None

def _patch_ma(ma: ManifestArray, level: int) -> ManifestArray:
if not isinstance(ma, ManifestArray):
return ma

meta = ma.metadata
codecs = list(meta.codecs)
modified = False

for i, codec in enumerate(codecs):
if not hasattr(codec, "to_dict"):
continue
cd = codec.to_dict()
if cd.get("name") != "numcodecs.zlib":
continue
cfg = cd.get("configuration", {})
if isinstance(cfg, dict):
if cfg.get("level") == level:
continue
cd["configuration"] = {**cfg, "level": level}
else:
cd["configuration"] = {"level": level}
codecs[i] = type(codec).from_dict(cd)
modified = True

if not modified:
return ma

import zarr

new_meta = zarr.core.metadata.v3.ArrayV3Metadata(
shape=meta.shape,
data_type=meta.data_type,
chunk_grid=meta.chunk_grid,
chunk_key_encoding=meta.chunk_key_encoding,
fill_value=meta.fill_value,
codecs=tuple(codecs),
attributes=meta.attributes,
dimension_names=meta.dimension_names,
storage_transformers=meta.storage_transformers,
)
return ManifestArray(new_meta, ma.manifest)

new_vars = {
name: (
xr.DataArray(
_patch_ma(var.data, target_level),
dims=var.dims,
attrs=var.attrs,
name=name,
)
if isinstance(var.data, ManifestArray)
else var
)
for name, var in ds.data_vars.items()
}

return xr.Dataset(new_vars, coords=ds.coords, attrs=ds.attrs)
Loading
Loading