Zarr was designed for data that sits in object storage or on a local disk. This vignette covers how pizzarr reaches them — over HTTPS, and over the S3 API, including S3-compatible services that are not Amazon.
The worked example throughout is gridMET, a daily gridded meteorology dataset for the continental United States, republished by the USGS on an Open Storage Network pod. Its catalog entry advertises two addresses for the same data:
s3://mdmf/gdp/gridMET.zarr/ endpoint https://usgs.osn.mghpcc.org/
That pairing — a bucket URL plus a non-Amazon endpoint — is a case that requires extra documentation, so it is illustrated in this vignette.
In Python you would hand that catalog entry to xarray
and let fsspec sort out the connection:
import xarray as xr
ds = xr.open_dataset(
"s3://mdmf/gdp/gridMET.zarr/",
engine="zarr",
backend_kwargs={"storage_options": {
"anon": True,
"client_kwargs": {"endpoint_url": "https://usgs.osn.mghpcc.org/"},
}},
)However, pizzarr has no storage_options argument.
Connection settings come from environment variables that the Rust
object_store client reads when it first opens a store. The
reason is mechanical rather than principled: store handles are cached by
URL on the Rust side and shared across calls, so there is no per-call
place to pass options. The translation is:
| fsspec / xarray | pizzarr |
|---|---|
"anon": True |
the default — unsigned unless credentials are set |
"client_kwargs": {"endpoint_url": "https://host"} |
AWS_ENDPOINT=https://host |
"key", "secret" |
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY |
"token" |
AWS_SESSION_TOKEN |
"client_kwargs": {"region_name": "us-west-2"} |
AWS_REGION=us-west-2 |
"use_ssl": False |
AWS_ALLOW_HTTP=true |
"config_kwargs": {"s3": {"addressing_style": "path"}} |
AWS_VIRTUAL_HOSTED_STYLE_REQUEST=false |
gcsfs "token" |
GOOGLE_APPLICATION_CREDENTIALS |
gcsfs "token": "anon" |
GOOGLE_SKIP_SIGNATURE=true |
gcsfs "endpoint_url" |
GOOGLE_BASE_URL |
Set them with Sys.setenv() for the session, or in
.Renviron to persist them. Because the handle is cached,
changing a variable after a store is open does nothing until you call
zarrs_close_store() on that URL.
An S3 bucket read anonymously is, underneath, an HTTPS server that returns objects at predictable paths. pizzarr can take either view of it, and which one you want depends less on the data than on which build you installed (CRAN or r-universe).
The HTTPS route addresses objects directly. That is, an
s3://bucket/key address served by endpoint
https://host is reachable at
https://host/bucket/key, so gridMET becomes:
https://usgs.osn.mghpcc.org/mdmf/gdp/gridMET.zarr
This https route works on both distribution tiers — the pure-R CRAN build and the r-universe build with the zarrs backend. It needs no credentials and no configuration, and it is the only route the CRAN build has, so it is the one to reach for unless something stops you.
The S3 route uses the s3:// URL and the real S3 API by
way of object_store. It requires the r-universe build with
the s3 feature compiled in, and it is what you need for
buckets that are not anonymously readable over plain HTTPS, or that
require signed requests. Check what you have:
HttpStore takes the URL, and zarr_open()
gives back a group:
url <- "https://usgs.osn.mghpcc.org/mdmf/gdp/gridMET.zarr"
store <- HttpStore$new(url)
g <- zarr_open(store)
g
#> <ZarrGroup> /
#> Store type : HttpStore
#> Zarr format : 2
#> Read-only : TRUE
#> No. members : 12HTTP servers generally cannot be listed the way a directory can, so a
remote store is only browsable if it publishes consolidated metadata — a
.zmetadata key holding every array’s metadata in one
document. gridMET does, and listdir() reports its
contents:
store$listdir()
#> [1] "crs"
#> [2] "lat"
#> [3] "lon"
#> [4] "max_air_temperature"
#> [5] "max_relative_humidity"
#> [6] "min_air_temperature"
#> [7] "min_relative_humidity"
#> [8] "precipitation_amount"
#> [9] "specific_humidity"
#> [10] "surface_downwelling_shortwave_flux_in_air"
#> [11] "time"
#> [12] "wind_speed"Without consolidated metadata you have to know the array names in advance. This is why so many published Zarr stores are consolidated, and why the flag appears in the gridMET catalog entry alongside the connection settings.
gridMET’s arrays are Blosc-compressed, which the pure-R build handles
only if the blosc package is installed. The chunks below
need it:
lat <- g$get_item("lat")
lat
#> <ZarrArray> /lat
#> Shape : (585)
#> Chunks : (585)
#> Data type : <f8
#> Fill value : NaN
#> Order : C
#> Read-only : TRUE
#> Compressor : BloscCodec
#> Store type : HttpStore
#> Zarr format : 2
lat$get_item(list(slice(1, 5)))$data
#> [1] 49.40000 49.35833 49.31667 49.27500 49.23333Reaching the data variables is the same call. Their shape is worth a look before reading anything:
tmmx <- g$get_item("max_air_temperature")
tmmx
#> <ZarrArray> /max_air_temperature
#> Shape : (17369, 585, 1386)
#> Chunks : (2190, 150, 150)
#> Data type : <i2
#> Fill value :
#> Order : C
#> Read-only : TRUE
#> Compressor : BloscCodec
#> Store type : HttpStore
#> Zarr format : 2That is 17369 days by 585 rows by 1386 columns, in chunks of 2190 by 150 by 150. A chunk is the smallest unit the store will return, so asking for a single value still transfers the roughly 98 MB block that contains it — compressed in flight, but decompressed in full. Slicing a remote array cheaply means slicing along chunk boundaries and keeping the request small in the dimensions that are chunked finely.
The s3:// route needs the endpoint, and nothing else for
a public bucket:
Sys.setenv(AWS_ENDPOINT = "https://usgs.osn.mghpcc.org")
s3_url <- "s3://mdmf/gdp/gridMET.zarr"
zarrs_open_array_metadata(s3_url, "lat")Reads go through zarrs_get_subset(), which takes
zero-based, stop-exclusive ranges — one per dimension — and returns the
data along with its shape:
Blosc decompression happens in Rust here, so this path does not need
the blosc R package. When you are done with a store, drop
its cached handle:
S3Store and GcsStore exist to mark a URL as
cloud-backed so that dispatch picks the right backend. They are not full
stores — they carry a URL and nothing else, so calling
get_item() or listdir() on one raises an error
naming zarrs_get_subset(), HttpStore, and this
vignette. The functions above are the working entry point for
s3:// data.
The endpoint variable is the whole trick for MinIO, Ceph, Wasabi, Cloudflare R2, and Open Storage Network pods. A self-hosted MinIO over plain HTTP with path-style addressing needs three settings:
Sys.setenv(
AWS_ENDPOINT = "http://localhost:9000",
AWS_ALLOW_HTTP = "true",
AWS_VIRTUAL_HOSTED_STYLE_REQUEST = "false",
AWS_ACCESS_KEY_ID = "minioadmin",
AWS_SECRET_ACCESS_KEY = "minioadmin"
)
zarrs_get_subset("s3://my-bucket/data.zarr", "temperature",
list(c(0L, 10L)), NULL)Most managed services need only AWS_ENDPOINT; region is
often unnecessary outside AWS itself. Requests are signed as soon as any
credential variable is present and unsigned when none are, which is what
makes public buckets work with no setup. AWS_SKIP_SIGNATURE
overrides that decision in either direction — set it to
"true" to force anonymous access when stale credentials are
lying around in your environment, or "false" to insist on
signing.
GCS works the same way, with GOOGLE_ variables in place
of AWS_ ones, and it needs the gcs feature
compiled in. The one difference that matters: GCS does not infer
anonymous access from the absence of credentials the way S3 does. A
gs:// read against a world-readable bucket still tries to
authenticate — via GOOGLE_APPLICATION_CREDENTIALS,
GOOGLE_SERVICE_ACCOUNT, or the GCE metadata server — and
fails if none are available. Ask for anonymous access explicitly:
Sys.setenv(GOOGLE_SKIP_SIGNATURE = "true")
gs_url <- "gs://pangeo-data/ECCO_basins.zarr"
zarrs_open_array_metadata(gs_url, "basin_mask")$shapeGOOGLE_BASE_URL overrides the endpoint, which is what
you want for a local emulator such as fake-gcs-server.
The same data is also reachable over plain HTTPS at
https://storage.googleapis.com/bucket/path, which needs no
configuration and works on the CRAN tier as well:
Cloud stores are read-only. Writes to s3:// and
gs:// fall back to the R-native path, which has no cloud
implementation, so they fail — build locally and upload with another
tool. Azure (az://) is not implemented at all.
Requester-pays buckets have no way to signal the requester, and there is
no per-store credential object: configuration is process-global, so two
buckets needing different credentials in one session have to be read in
sequence with zarrs_close_store() and an environment change
between them.