Skip to content

Assets: read, metadata, VRT, download, GeoParquet#

The asset-level surface of pyramids.stac: open a single asset, read its extension metadata without touching the file, mosaic an asset across items into a lazy VRT, download assets locally, and round-trip Items through GeoParquet.

  • Read one asset — load_asset dispatches by media type (COG/GeoTIFF → Dataset, NetCDF/Zarr → NetCDF, GRIB → open_grib, JPEG2000 → Dataset); which_engine previews the reader without opening; resolved_href returns the (optionally signed) href without opening.
  • Extension metadata — read_extension_metadata turns a STAC Item's proj / raster / eo fields into a grid + band-metadata dict (CRS, geotransform, shape, nodata/scale/offset, band names) without opening the asset, the way stackstac / odc-stac / rio-tiler do.
  • VRT mosaic — build_vrt_from_stac stitches one asset across many items into a lazy GDAL VRT read on demand via /vsicurl/.
  • Alternate hrefs — preferred_asset_href / asset_alternate_href resolve the alternate-assets extension, so a caller can prefer an s3:// mirror over the canonical https:// href and fall back automatically when it is absent. preferred_asset_source makes the same choice but reports the selected location's media type alongside its href, and asset_field reads one field off an asset whether it arrives as a raw dict or a pystac.Asset.
  • Verification — verify_asset (and load_asset(verify=True)) probes an HTTP asset for reachability and a content-type consistent with its declared type, before GDAL is asked to open it. Off by default, so no extra request is made.
  • Physical units & overrides — load_asset(rescale=True) applies the raster:bands scale/offset so values come back in physical units instead of raw DN; cfg= supplies data_type / nodata / unit that a thin catalog omits and maps band aliases ("rededge" → "B05").
  • Windowed reads from an Item — read_item_part / read_item_preview / read_item_point / read_item_feature resolve an item + asset and delegate to the COG engine's overview-decimated reads, so a window costs a partial read, not a whole scene.
  • Download — download_item, download_item_collection and download_collection copy assets to local files (optional stac-asset, shipped in the [stac] extra).
  • GeoParquet — to_geoparquet / from_geoparquet serialize an ItemCollection to a single columnar file and back, in either pyramids' lossless JSON-blob layout or the interoperable stac-geoparquet 1.1 layout (to_geoparquet_spec / from_geoparquet_spec, or spec=True) that DuckDB and pyarrow can query directly (optional pyarrow, the [parquet] extra).

Reading assets#

pyramids.stac._loader #

Open a STAC asset as a pyramids Dataset / NetCDF, dispatched by type.

Takes a STAC Item + asset_key (or an Asset directly), resolves the asset href, and opens it with the right GDAL-backed reader chosen by the asset's media_type (with the href extension as a fallback):

media_type / extension reader
image/tiff... / .tif .tiff :meth:Dataset.read_file
image/jp2 / .jp2 .jpx :meth:Dataset.read_file
application/x-netcdf / .nc .nc4 .cdf :meth:NetCDF.read_file
application/wmo-grib2 / .grib2 .grb :func:pyramids.grib.open_grib
application/vnd+zarr / .zarr :meth:NetCDF.read_file (GDAL Zarr)

Four opt-ins sit on top of that dispatch, all off by default so an existing read behaves exactly as before:

  • alternate= prefers an alternate-assets href (an s3:// mirror over the public HTTPS copy, say), silently falling back to the canonical href.
  • verify= pre-flights the href with one HEAD (:func:verify_asset) and warns when it is unreachable or serves a content type contradicting the declared one. HTTP(S) only, and never requested unless asked for.
  • rescale= returns physical units instead of stored counts, applying the asset's raster:bands scale / offset with no-data masked first. GDAL (GeoTIFF/COG/JPEG2000) assets only — netCDF, Zarr and GRIB carry their own CF packing, which the readers already unpack.
  • cfg= supplies per-asset metadata a thin catalog omits (data_type, nodata, unit) and resolves band aliases to real asset keys. See :mod:pyramids.stac._config for the schema. The metadata half is GDAL-only too, but unlike rescale nothing about a CF reader makes it redundant — so a cfg override resolving to a netCDF / Zarr / GRIB asset is reported with an AssetMetadataWarning rather than silently dropped. Aliases apply on every engine, since they only choose which asset key is read.

The last two cannot be stamped onto the opened handle — an asset is opened read-only, and a remote /vsicurl COG rejects every metadata setter — so they materialise a writable in-memory copy instead (:func:pyramids.stac._config.materialise).

Everything is duck-typed — pyramids does not import or depend on pystac; the Item / Asset contract is read via getattr + dict lookup (pystac.Asset has .href / .media_type; raw STAC JSON uses {"href":..., "type":...}). Assets resolve to pyramids' GDAL-backed wrappers.

which_engine(item_or_asset, asset_key=None, *, alternate=None) #

Return the reader name :func:load_asset would use, without opening.

Parameters:

Name Type Description Default
item_or_asset Any

A STAC Item or Asset (pystac object or raw dict).

required
asset_key str | None

Asset name when passing an Item; None for an Asset.

None
alternate str | Sequence[str] | None

alternate-assets key (or keys in preference order) to prefer over the canonical href, so the extension fallback sees the href that would actually be opened. None keeps the canonical href.

None

Returns:

Type Description
str

One of "gdal", "netcdf", "grib", "zarr".

Examples:

  • A COG asset dispatches to the GDAL reader:
    >>> from pyramids.stac import which_engine
    >>> asset = {
    ...     "href": "s3://bucket/scene.tif",
    ...     "type": "image/tiff; application=geotiff; profile=cloud-optimized",
    ... }
    >>> which_engine(asset)
    'gdal'
    
  • A GRIB2 asset (recognised by extension when type is absent):
    >>> which_engine({"href": "https://host/gfs.t00z.pgrb2.f000.grib2"})
    'grib'
    
  • An Item + asset key resolves the named asset:
    >>> item = {"assets": {"data": {"href": "x.nc", "type": "application/x-netcdf"}}}
    >>> which_engine(item, "data")
    'netcdf'
    
Source code in src/pyramids/stac/_loader.py
def which_engine(
    item_or_asset: Any,
    asset_key: str | None = None,
    *,
    alternate: str | Sequence[str] | None = None,
) -> str:
    """Return the reader name :func:`load_asset` would use, without opening.

    Args:
        item_or_asset: A STAC Item or Asset (pystac object or raw dict).
        asset_key: Asset name when passing an Item; `None` for an Asset.
        alternate: `alternate-assets` key (or keys in preference order) to
            prefer over the canonical href, so the extension fallback sees the
            href that would actually be opened. `None` keeps the canonical href.

    Returns:
        One of `"gdal"`, `"netcdf"`, `"grib"`, `"zarr"`.

    Examples:
        - A COG asset dispatches to the GDAL reader:
            ```python
            >>> from pyramids.stac import which_engine
            >>> asset = {
            ...     "href": "s3://bucket/scene.tif",
            ...     "type": "image/tiff; application=geotiff; profile=cloud-optimized",
            ... }
            >>> which_engine(asset)
            'gdal'

            ```
        - A GRIB2 asset (recognised by extension when type is absent):
            ```python
            >>> which_engine({"href": "https://host/gfs.t00z.pgrb2.f000.grib2"})
            'grib'

            ```
        - An Item + asset key resolves the named asset:
            ```python
            >>> item = {"assets": {"data": {"href": "x.nc", "type": "application/x-netcdf"}}}
            >>> which_engine(item, "data")
            'netcdf'

            ```
    """
    href, media_type = _resolve_asset(item_or_asset, asset_key, alternate)
    return _engine_for(media_type, href)

resolved_href(item_or_asset, asset_key=None, *, signer=None, alternate=None, verify=False, verify_strict=False) #

Return an asset's resolved (optionally signed) href without opening it.

The read-free companion to :func:load_asset: it resolves the asset href and, when a signer is given, applies signer.sign_href — but never opens the asset. Useful for building a VRT over many assets (:func:pyramids.stac.build_vrt_from_stac), pre-flighting URLs, or debugging what load_asset would open.

Parameters:

Name Type Description Default
item_or_asset Any

A STAC Item (pystac object or raw dict) or an Asset.

required
asset_key str | None

Asset name when passing an Item; None for an Asset.

None
signer Any

Optional signer; when given, its sign_href rewrites the href (e.g. grafting a SAS token). gdal_env() is not applied — no read happens here.

None
alternate str | Sequence[str] | None

alternate-assets key (or keys in preference order) to prefer over the canonical href — e.g. "s3" to read a bucket mirror instead of the public HTTPS copy. An asset that publishes no such alternate falls back to its canonical href. None (the default) keeps today's behaviour. The signer always runs on the chosen href, alternate or not.

None
verify bool

Pre-flight the chosen href with :func:verify_asset (one HEAD, HTTP(S) only) and warn when it is unreachable or serves a content type contradicting the declared one. False (the default) issues no request at all.

False
verify_strict bool

With verify=True, raise :class:~pyramids.base._errors.StacError instead of warning.

False

Returns:

Type Description
str

The resolved asset href, signed when a signer is supplied.

Raises:

Type Description
StacAssetError

The asset is missing or has no href (subclasses :class:KeyError).

StacError

verify_strict=True and verification failed.

Examples:

  • Resolve a plain asset href:
    >>> from pyramids.stac import resolved_href
    >>> resolved_href({"href": "s3://b/scene.tif", "type": "image/tiff"})
    's3://b/scene.tif'
    
  • Resolve an item's asset and sign it with a simple signer:
    >>> class _S:
    ...     def sign_href(self, href):
    ...         return href + "?sig=tok"
    >>> item = {"assets": {"B04": {"href": "https://h/B04.tif"}}}
    >>> resolved_href(item, "B04", signer=_S())
    'https://h/B04.tif?sig=tok'
    
  • Prefer the asset's s3 mirror, falling back when it has none:
    >>> asset = {
    ...     "href": "https://h/a.tif",
    ...     "type": "image/tiff",
    ...     "alternate": {"s3": {"href": "s3://b/a.tif"}},
    ... }
    >>> resolved_href(asset, alternate="s3")
    's3://b/a.tif'
    >>> resolved_href(asset, alternate="gs")
    'https://h/a.tif'
    
Source code in src/pyramids/stac/_loader.py
def resolved_href(
    item_or_asset: Any,
    asset_key: str | None = None,
    *,
    signer: Any = None,
    alternate: str | Sequence[str] | None = None,
    verify: bool = False,
    verify_strict: bool = False,
) -> str:
    """Return an asset's resolved (optionally signed) href without opening it.

    The read-free companion to :func:`load_asset`: it resolves the asset href
    and, when a `signer` is given, applies `signer.sign_href` — but never opens
    the asset. Useful for building a VRT over many assets
    (:func:`pyramids.stac.build_vrt_from_stac`), pre-flighting URLs, or
    debugging what `load_asset` would open.

    Args:
        item_or_asset: A STAC Item (pystac object or raw dict) or an Asset.
        asset_key: Asset name when passing an Item; `None` for an Asset.
        signer: Optional signer; when given, its `sign_href` rewrites the href
            (e.g. grafting a SAS token). `gdal_env()` is **not** applied — no
            read happens here.
        alternate: `alternate-assets` key (or keys in preference order) to
            prefer over the canonical href — e.g. `"s3"` to read a bucket mirror
            instead of the public HTTPS copy. An asset that publishes no such
            alternate falls back to its canonical href. `None` (the default)
            keeps today's behaviour. The signer always runs on the chosen href,
            alternate or not.
        verify: Pre-flight the chosen href with :func:`verify_asset` (one HEAD,
            HTTP(S) only) and warn when it is unreachable or serves a content
            type contradicting the declared one. `False` (the default) issues no
            request at all.
        verify_strict: With `verify=True`, raise
            :class:`~pyramids.base._errors.StacError` instead of warning.

    Returns:
        The resolved asset href, signed when a `signer` is supplied.

    Raises:
        StacAssetError: The asset is missing or has no href (subclasses
            :class:`KeyError`).
        StacError: `verify_strict=True` and verification failed.

    Examples:
        - Resolve a plain asset href:
            ```python
            >>> from pyramids.stac import resolved_href
            >>> resolved_href({"href": "s3://b/scene.tif", "type": "image/tiff"})
            's3://b/scene.tif'

            ```
        - Resolve an item's asset and sign it with a simple signer:
            ```python
            >>> class _S:
            ...     def sign_href(self, href):
            ...         return href + "?sig=tok"
            >>> item = {"assets": {"B04": {"href": "https://h/B04.tif"}}}
            >>> resolved_href(item, "B04", signer=_S())
            'https://h/B04.tif?sig=tok'

            ```
        - Prefer the asset's `s3` mirror, falling back when it has none:
            ```python
            >>> asset = {
            ...     "href": "https://h/a.tif",
            ...     "type": "image/tiff",
            ...     "alternate": {"s3": {"href": "s3://b/a.tif"}},
            ... }
            >>> resolved_href(asset, alternate="s3")
            's3://b/a.tif'
            >>> resolved_href(asset, alternate="gs")
            'https://h/a.tif'

            ```
    """
    href, media_type = _resolve_asset(item_or_asset, asset_key, alternate)
    if signer is not None:
        href = signer.sign_href(href)
    if verify:
        verify_asset(href, media_type, strict=verify_strict)
    return href

load_asset(item_or_asset, asset_key=None, *, signer=None, vsi=None, alternate=None, verify=False, verify_strict=False, rescale=False, cfg=None, collection_id=None) #

Open a STAC asset as a pyramids Dataset / NetCDF.

Resolves the asset href, optionally rewrites it through a signer (a :class:~pyramids.stac.signers.Signer), then opens it with the GDAL-backed reader chosen by media_type / extension. When a signer is given, both of its hooks are applied: signer.sign_href rewrites the href, and signer.gdal_env is installed as GDAL config for the duration of the open (via :class:~pyramids.base.remote.CloudConfig), so the underlying VSI handle is created with the right credentials / requester-pays knobs.

Parameters:

Name Type Description Default
item_or_asset Any

A STAC Item (pystac.Item or raw dict) or an Asset.

required
asset_key str | None

Asset name when passing an Item; None for an Asset.

None
signer Any

Optional signer. signer.sign_href(href) rewrites the href (e.g. grafting a SAS token) and signer.gdal_env() supplies GDAL config applied while the asset is opened (e.g. AWS_REQUEST_PAYER=requester for an :class:~pyramids.stac.signers.AWSRequesterPaysSigner, or an Authorization header for a :class:~pyramids.stac.signers.BearerTokenSigner). None leaves the href unchanged and applies no extra config.

None
vsi str | None

Optional explicit archive kind forwarded to the reader (e.g. a GeoTIFF/GRIB inside a .zip).

None
alternate str | Sequence[str] | None

alternate-assets key (or keys in preference order) to prefer over the canonical href — e.g. "s3" to read a bucket mirror instead of the public HTTPS copy, which inside the same cloud region is both cheaper and faster. An asset that publishes no such alternate falls back to its canonical href, so one call site works across a mixed collection. None (the default) keeps today's behaviour. The signer still runs on the chosen href.

None
verify bool

Pre-flight the href with :func:verify_asset before opening it: one HEAD (HTTP(S) hrefs only) that warns when the asset is unreachable or serves a content type contradicting its declared type. False (the default) issues no extra request, so a normal read never pays for it.

False
verify_strict bool

With verify=True, raise :class:~pyramids.base._errors.StacError on a failed verification instead of emitting an :class:AssetVerificationWarning.

False
rescale bool

Return physical units rather than stored counts, applying real = stored * scale + offset from the asset's raster:bands scale / offset. No-data is masked before scaling and the result's no-data becomes NaN, so a sentinel is never scaled into a plausible-looking value; the returned raster declares identity packing, so a later read_array(unpack=True) cannot apply the same factor twice. Costs a materialised (in-memory, float32) copy — the opened handle is read-only, so the factor cannot be stamped onto it — and is therefore a no-op when the asset declares no raster:bands, or when every band is the identity. GDAL assets only: netCDF / Zarr / GRIB are left untouched, because their readers already unpack the CF packing their own metadata declares, and applying the STAC factor on top would scale them twice.

False
cfg Any

Optional stac_cfg-style mapping supplying per-asset metadata the item omits (data_type, nodata, unit) and band aliases ({"aliases": {"rededge": "B05"}} makes asset_key="rededge" read the B05 asset). Keyed by collection id, with a "*" section for cross-collection defaults; :mod:pyramids.stac._config documents the full schema. Overrides fill gaps only — a value the asset already declares is kept, and the skip warns unless the collection sets warnings: "ignore". Applying one materialises a writable copy (the same read-only constraint as rescale), so nothing is paid when no override actually applies. The metadata overrides are GDAL assets only; a nodata / data_type / unit resolving to a netCDF / Zarr / GRIB asset cannot be applied and is reported with an :class:~pyramids.stac._config.AssetMetadataWarning instead of being dropped in silence. Aliases are honoured on every engine.

None
collection_id str | None

The collection cfg is read under. None (the default) takes it from the item (item["collection"] / item.collection_id), leaving cfg's "*" section as the only one that can apply to a bare asset dict.

None

Returns:

Name Type Description
A Dataset

class:~pyramids.dataset.Dataset for COG/GeoTIFF assets, or a

Dataset

class:~pyramids.netcdf.NetCDF (a Dataset subclass) for

Dataset

NetCDF / Zarr / GRIB assets.

Raises:

Type Description
KeyError

The asset is missing or has no href.

ValueError

The asset's type/extension matches no supported reader, or cfg configures a data_type numpy does not know.

StacError

verify_strict=True and verification failed.

Examples:

  • Open a COG asset from a STAC Item (requires network access):
    >>> from pyramids.stac import load_asset  # doctest: +SKIP
    >>> item = {"assets": {"B04": {"href": "s3://.../B04.tif",
    ...                            "type": "image/tiff; application=geotiff"}}}
    >>> ds = load_asset(item, "B04")  # doctest: +SKIP
    >>> ds.band_count  # doctest: +SKIP
    1
    
  • Sign the href with an MPC/CDSE-style bearer signer before opening (the token is installed as a GDAL Authorization header for the open):
    >>> from pyramids.stac import load_asset, BearerTokenSigner  # doctest: +SKIP
    >>> ds = load_asset(item, "B04", signer=BearerTokenSigner("tok"))  # doctest: +SKIP
    
  • Read a Requester-Pays bucket: the signer's gdal_env opts into AWS_REQUEST_PAYER=requester for the duration of the open:
    >>> from pyramids.stac import load_asset, AWSRequesterPaysSigner  # doctest: +SKIP
    >>> asset = {"href": "s3://usgs-landsat/collection02/.../B4.TIF",
    ...          "type": "image/tiff; application=geotiff"}
    >>> ds = load_asset(asset, signer=AWSRequesterPaysSigner(region="us-west-2"))  # doctest: +SKIP
    
  • Read a packed asset in physical units (raster:bands scale 0.0001):
    >>> from pyramids.stac import load_asset  # doctest: +SKIP
    >>> ds = load_asset(item, "B04", rescale=True)  # doctest: +SKIP
    
  • Name an asset by alias and supply the nodata the catalog omits:
    >>> cfg = {"sentinel-2-l2a": {  # doctest: +SKIP
    ...     "assets": {"*": {"nodata": 0}},
    ...     "aliases": {"red": "B04"},
    ... }}
    >>> ds = load_asset(item, "red", cfg=cfg)  # doctest: +SKIP
    
Source code in src/pyramids/stac/_loader.py
def load_asset(
    item_or_asset: Any,
    asset_key: str | None = None,
    *,
    signer: Any = None,
    vsi: str | None = None,
    alternate: str | Sequence[str] | None = None,
    verify: bool = False,
    verify_strict: bool = False,
    rescale: bool = False,
    cfg: Any = None,
    collection_id: str | None = None,
) -> Dataset:
    """Open a STAC asset as a pyramids `Dataset` / `NetCDF`.

    Resolves the asset href, optionally rewrites it through a `signer`
    (a :class:`~pyramids.stac.signers.Signer`), then opens it with the
    GDAL-backed reader chosen by `media_type` / extension. When a signer is
    given, **both** of its hooks are applied: `signer.sign_href` rewrites the
    href, and `signer.gdal_env` is installed as GDAL config for the duration
    of the open (via :class:`~pyramids.base.remote.CloudConfig`), so the
    underlying VSI handle is created with the right credentials / requester-pays
    knobs.

    Args:
        item_or_asset: A STAC Item (pystac.Item or raw dict) or an Asset.
        asset_key: Asset name when passing an Item; `None` for an Asset.
        signer: Optional signer. `signer.sign_href(href)` rewrites the href
            (e.g. grafting a SAS token) and `signer.gdal_env()` supplies GDAL
            config applied while the asset is opened (e.g.
            `AWS_REQUEST_PAYER=requester` for an
            :class:`~pyramids.stac.signers.AWSRequesterPaysSigner`, or an
            `Authorization` header for a
            :class:`~pyramids.stac.signers.BearerTokenSigner`). `None` leaves
            the href unchanged and applies no extra config.
        vsi: Optional explicit archive kind forwarded to the reader (e.g. a
            GeoTIFF/GRIB inside a `.zip`).
        alternate: `alternate-assets` key (or keys in preference order) to
            prefer over the canonical href — e.g. `"s3"` to read a bucket mirror
            instead of the public HTTPS copy, which inside the same cloud region
            is both cheaper and faster. An asset that publishes no such
            alternate falls back to its canonical href, so one call site works
            across a mixed collection. `None` (the default) keeps today's
            behaviour. The signer still runs on the chosen href.
        verify: Pre-flight the href with :func:`verify_asset` before opening it:
            one HEAD (HTTP(S) hrefs only) that warns when the asset is
            unreachable or serves a content type contradicting its declared
            `type`. `False` (the default) issues no extra request, so a normal
            read never pays for it.
        verify_strict: With `verify=True`, raise
            :class:`~pyramids.base._errors.StacError` on a failed verification
            instead of emitting an :class:`AssetVerificationWarning`.
        rescale: Return **physical units** rather than stored counts, applying
            `real = stored * scale + offset` from the asset's `raster:bands`
            `scale` / `offset`. No-data is masked *before* scaling and the
            result's no-data becomes `NaN`, so a sentinel is never scaled into a
            plausible-looking value; the returned raster declares identity
            packing, so a later `read_array(unpack=True)` cannot apply the same
            factor twice. Costs a **materialised** (in-memory, `float32`) copy —
            the opened handle is read-only, so the factor cannot be stamped onto
            it — and is therefore a no-op when the asset declares no
            `raster:bands`, or when every band is the identity. **GDAL assets
            only**: netCDF / Zarr / GRIB are left untouched, because their
            readers already unpack the CF packing their own metadata declares,
            and applying the STAC factor on top would scale them twice.
        cfg: Optional `stac_cfg`-style mapping supplying per-asset metadata the
            item omits (`data_type`, `nodata`, `unit`) and **band aliases**
            (`{"aliases": {"rededge": "B05"}}` makes `asset_key="rededge"` read
            the `B05` asset). Keyed by collection id, with a `"*"` section for
            cross-collection defaults; :mod:`pyramids.stac._config` documents
            the full schema. Overrides fill gaps only — a value the asset
            already declares is kept, and the skip warns unless the collection
            sets `warnings: "ignore"`. Applying one materialises a writable copy
            (the same read-only constraint as `rescale`), so nothing is paid
            when no override actually applies. The metadata overrides are
            **GDAL assets only**; a `nodata` / `data_type` / `unit` resolving to
            a netCDF / Zarr / GRIB asset cannot be applied and is reported with
            an :class:`~pyramids.stac._config.AssetMetadataWarning` instead of
            being dropped in silence. Aliases are honoured on every engine.
        collection_id: The collection `cfg` is read under. `None` (the default)
            takes it from the item (`item["collection"]` /
            `item.collection_id`), leaving `cfg`'s `"*"` section as the only one
            that can apply to a bare asset dict.

    Returns:
        A :class:`~pyramids.dataset.Dataset` for COG/GeoTIFF assets, or a
        :class:`~pyramids.netcdf.NetCDF` (a `Dataset` subclass) for
        NetCDF / Zarr / GRIB assets.

    Raises:
        KeyError: The asset is missing or has no href.
        ValueError: The asset's type/extension matches no supported reader, or
            `cfg` configures a `data_type` numpy does not know.
        StacError: `verify_strict=True` and verification failed.

    Examples:
        - Open a COG asset from a STAC Item (requires network access):
            ```python
            >>> from pyramids.stac import load_asset  # doctest: +SKIP
            >>> item = {"assets": {"B04": {"href": "s3://.../B04.tif",
            ...                            "type": "image/tiff; application=geotiff"}}}
            >>> ds = load_asset(item, "B04")  # doctest: +SKIP
            >>> ds.band_count  # doctest: +SKIP
            1

            ```
        - Sign the href with an MPC/CDSE-style bearer signer before opening
          (the token is installed as a GDAL `Authorization` header for the
          open):
            ```python
            >>> from pyramids.stac import load_asset, BearerTokenSigner  # doctest: +SKIP
            >>> ds = load_asset(item, "B04", signer=BearerTokenSigner("tok"))  # doctest: +SKIP

            ```
        - Read a Requester-Pays bucket: the signer's `gdal_env` opts into
          `AWS_REQUEST_PAYER=requester` for the duration of the open:
            ```python
            >>> from pyramids.stac import load_asset, AWSRequesterPaysSigner  # doctest: +SKIP
            >>> asset = {"href": "s3://usgs-landsat/collection02/.../B4.TIF",
            ...          "type": "image/tiff; application=geotiff"}
            >>> ds = load_asset(asset, signer=AWSRequesterPaysSigner(region="us-west-2"))  # doctest: +SKIP

            ```
        - Read a packed asset in physical units (`raster:bands` scale 0.0001):
            ```python
            >>> from pyramids.stac import load_asset  # doctest: +SKIP
            >>> ds = load_asset(item, "B04", rescale=True)  # doctest: +SKIP

            ```
        - Name an asset by alias and supply the nodata the catalog omits:
            ```python
            >>> cfg = {"sentinel-2-l2a": {  # doctest: +SKIP
            ...     "assets": {"*": {"nodata": 0}},
            ...     "aliases": {"red": "B04"},
            ... }}
            >>> ds = load_asset(item, "red", cfg=cfg)  # doctest: +SKIP

            ```
    """
    if cfg is not None:
        if collection_id is None:
            collection_id = item_collection_id(item_or_asset)
        if asset_key is not None:
            # Before _resolve_asset, or the aliased key is never looked up.
            asset_key = resolve_alias(cfg, collection_id, asset_key)
    href, media_type = _resolve_asset(item_or_asset, asset_key, alternate)
    if signer is not None:
        href = signer.sign_href(href)
    if verify:
        verify_asset(href, media_type, strict=verify_strict)
    engine = _engine_for(media_type, href)
    if cfg is not None and engine != "gdal":
        _warn_cfg_outside_gdal(item_or_asset, asset_key, cfg, collection_id, engine)
    signer_env = signer.gdal_env() if signer is not None else None
    with _open_config(href, engine, signer_env):
        if engine == "gdal":
            result: Any = Dataset.read_file(href, vsi=vsi, gdal_env=signer_env)
            result = _apply_overrides(
                result, item_or_asset, asset_key, rescale, cfg, collection_id
            )
        elif engine == "zarr":
            # Read through zarr/fsspec, which never consults GDAL config — so
            # nothing is captured on the result either (see _persist_gdal_env).
            result = _load_zarr(href)
        else:
            result = (
                open_grib(href, vsi=vsi) if engine == "grib" else NetCDF.read_file(href)
            )
            # Unlike Dataset.read_file these readers take no gdal_env=, so the
            # signer env is attached to the opened object instead.
            _persist_gdal_env(result, signer_env)
    return cast(Dataset, result)

Extension metadata (proj / raster / eo)#

pyramids.stac._extensions #

Read STAC proj / raster / eo extension metadata (PB-1).

This module builds a cube/grid skeleton — CRS, geotransform, shape, per-band nodata / scale / offset, band names — directly from the STAC Item JSON, without opening any asset: pure dict reads (via :func:pyramids.stac._item.asset_field, no pystac dependency) that yield a grid/band-metadata dict downstream code can use to build a VRT (PB-5), a multi-asset cube (PB-2), or a grid match (PC-2) without a header open.

Scope note: these are readers only. They deliberately do not stamp the metadata onto a :class:~pyramids.dataset.Dataset returned by :func:pyramids.stac.load_asset, because that reader opens assets read-only (remote /vsicurl COGs cannot be opened for write), and mutating a read-only GDAL handle (SetProjection / SetNoDataValue / SetScale) raises under gdal.UseExceptions(). Writable consumers (VRT/stack builders) apply the metadata themselves from the dict this module returns.

parse_number(value, default=None) #

Coerce a STAC numeric field to a float, honouring nan/inf strings.

The raster extension allows non-finite nodata values to be encoded as the strings "nan" / "inf" / "-inf".

Parameters:

Name Type Description Default
value Any

The raw field value (number, numeric string, nan/inf string, or None).

required
default Any

Returned when value is None or cannot be parsed.

None

Returns:

Type Description
Any

A float for numeric / nan-inf inputs, otherwise default.

Examples:

  • A plain number passes through as a float:
    >>> from pyramids.stac._extensions import parse_number
    >>> parse_number(-9999)
    -9999.0
    
  • The string "-inf" becomes negative infinity:
    >>> parse_number("-inf")
    -inf
    
  • An unparseable value falls back to the default:
    >>> parse_number("n/a", default=0.0)
    0.0
    
Source code in src/pyramids/stac/_extensions.py
def parse_number(value: Any, default: Any = None) -> Any:
    """Coerce a STAC numeric field to a float, honouring nan/inf strings.

    The ``raster`` extension allows non-finite nodata values to be encoded as
    the strings ``"nan"`` / ``"inf"`` / ``"-inf"``.

    Args:
        value: The raw field value (number, numeric string, nan/inf string,
            or ``None``).
        default: Returned when `value` is ``None`` or cannot be parsed.

    Returns:
        A float for numeric / nan-inf inputs, otherwise `default`.

    Examples:
        - A plain number passes through as a float:
            ```python
            >>> from pyramids.stac._extensions import parse_number
            >>> parse_number(-9999)
            -9999.0

            ```
        - The string ``"-inf"`` becomes negative infinity:
            ```python
            >>> parse_number("-inf")
            -inf

            ```
        - An unparseable value falls back to the default:
            ```python
            >>> parse_number("n/a", default=0.0)
            0.0

            ```
    """
    if value is None:
        return default
    if isinstance(value, bool):
        return default
    if isinstance(value, (int, float)):
        return float(value)
    if isinstance(value, str):
        token = value.strip().lower()
        if token in _NODATA_STRINGS:
            return _NODATA_STRINGS[token]
        try:
            return float(value)
        except ValueError:
            return default
    return default

affine_to_geotransform(transform) #

Convert a STAC proj:transform affine to a GDAL geotransform.

proj:transform is the affine ordering [a, b, c, d, e, f] (mapping (col, row) to (x, y): x = a*col + b*row + c, y = d*col + e*row + f). GDAL's geotransform is the reordering (c, a, b, f, d, e) — i.e. (x_origin, x_res, x_rot, y_origin, y_rot, y_res). A 9-element affine (with a trailing [0, 0, 1] row) is accepted; only the first six coefficients are used.

Parameters:

Name Type Description Default
transform Any

A 6- or 9-element proj:transform sequence.

required

Returns:

Type Description
tuple[float, ...]

The 6-tuple GDAL geotransform.

Raises:

Type Description
ValueError

When transform has fewer than six coefficients.

Examples:

  • A north-up 30 m grid reorders to the GDAL geotransform:
    >>> from pyramids.stac._extensions import affine_to_geotransform
    >>> affine_to_geotransform([30.0, 0.0, 224985.0, 0.0, -30.0, 6790215.0])
    (224985.0, 30.0, 0.0, 6790215.0, 0.0, -30.0)
    
  • The trailing [0, 0, 1] row of a 9-element affine is ignored:
    >>> affine_to_geotransform([10.0, 0.0, 100.0, 0.0, -10.0, 200.0, 0.0, 0.0, 1.0])
    (100.0, 10.0, 0.0, 200.0, 0.0, -10.0)
    
Source code in src/pyramids/stac/_extensions.py
def affine_to_geotransform(transform: Any) -> tuple[float, ...]:
    """Convert a STAC ``proj:transform`` affine to a GDAL geotransform.

    ``proj:transform`` is the affine ordering ``[a, b, c, d, e, f]``
    (mapping ``(col, row)`` to ``(x, y)``: ``x = a*col + b*row + c``,
    ``y = d*col + e*row + f``). GDAL's geotransform is the reordering
    ``(c, a, b, f, d, e)`` — i.e. ``(x_origin, x_res, x_rot, y_origin, y_rot,
    y_res)``. A 9-element affine (with a trailing ``[0, 0, 1]`` row) is
    accepted; only the first six coefficients are used.

    Args:
        transform: A 6- or 9-element ``proj:transform`` sequence.

    Returns:
        The 6-tuple GDAL geotransform.

    Raises:
        ValueError: When `transform` has fewer than six coefficients.

    Examples:
        - A north-up 30 m grid reorders to the GDAL geotransform:
            ```python
            >>> from pyramids.stac._extensions import affine_to_geotransform
            >>> affine_to_geotransform([30.0, 0.0, 224985.0, 0.0, -30.0, 6790215.0])
            (224985.0, 30.0, 0.0, 6790215.0, 0.0, -30.0)

            ```
        - The trailing ``[0, 0, 1]`` row of a 9-element affine is ignored:
            ```python
            >>> affine_to_geotransform([10.0, 0.0, 100.0, 0.0, -10.0, 200.0, 0.0, 0.0, 1.0])
            (100.0, 10.0, 0.0, 200.0, 0.0, -10.0)

            ```
    """
    coeffs = list(transform)
    if len(coeffs) < 6:
        raise ValueError(
            f"proj:transform must have at least 6 coefficients, got {len(coeffs)}: "
            f"{coeffs!r}"
        )
    a, b, c, d, e, f = (float(x) for x in coeffs[:6])
    return (c, a, b, f, d, e)

geotransform_to_affine(geotransform) #

Convert a GDAL geotransform to a STAC proj:transform affine.

The inverse of :func:affine_to_geotransform. GDAL's geotransform is (c, a, b, f, d, e) — (x_origin, x_res, x_rot, y_origin, y_rot, y_res) — and proj:transform is the affine ordering [a, b, c, d, e, f].

Parameters:

Name Type Description Default
geotransform Any

A 6-element GDAL geotransform.

required

Returns:

Type Description
list[float]

The 6-element proj:transform affine.

Raises:

Type Description
ValueError

When geotransform has fewer than six coefficients.

Examples:

  • A north-up 30 m grid maps back to the affine order:
    >>> from pyramids.stac._extensions import geotransform_to_affine
    >>> geotransform_to_affine((224985.0, 30.0, 0.0, 6790215.0, 0.0, -30.0))
    [30.0, 0.0, 224985.0, 0.0, -30.0, 6790215.0]
    
Source code in src/pyramids/stac/_extensions.py
def geotransform_to_affine(geotransform: Any) -> list[float]:
    """Convert a GDAL geotransform to a STAC ``proj:transform`` affine.

    The inverse of :func:`affine_to_geotransform`. GDAL's geotransform is
    ``(c, a, b, f, d, e)`` — ``(x_origin, x_res, x_rot, y_origin, y_rot,
    y_res)`` — and ``proj:transform`` is the affine ordering
    ``[a, b, c, d, e, f]``.

    Args:
        geotransform: A 6-element GDAL geotransform.

    Returns:
        The 6-element ``proj:transform`` affine.

    Raises:
        ValueError: When `geotransform` has fewer than six coefficients.

    Examples:
        - A north-up 30 m grid maps back to the affine order:
            ```python
            >>> from pyramids.stac._extensions import geotransform_to_affine
            >>> geotransform_to_affine((224985.0, 30.0, 0.0, 6790215.0, 0.0, -30.0))
            [30.0, 0.0, 224985.0, 0.0, -30.0, 6790215.0]

            ```
    """
    gt = list(geotransform)
    if len(gt) < 6:
        raise ValueError(
            f"geotransform must have at least 6 coefficients, got {len(gt)}: {gt!r}"
        )
    c, a, b, f, d, e = (float(x) for x in gt[:6])
    return [a, b, c, d, e, f]

read_extension_metadata(item, asset_key=None) #

Read proj / raster / eo extension fields for a STAC asset.

Item-level fields (under properties) are read first and an asset-level value of the same key overrides them, matching the STAC convention that an asset narrows item-level metadata. No asset file is opened.

Parameters:

Name Type Description Default
item Any

A STAC Item (pystac object or raw dict). When asset_key is None the item is treated as the asset itself.

required
asset_key str | None

The asset key whose metadata to read, or None to read a bare asset.

None

Returns:

Type Description
dict[str, Any]

A dict with keys:

dict[str, Any]
  • epsg — proj:epsg (int) or None.
dict[str, Any]
  • crs — proj:code (e.g. "EPSG:32633") when present, else derived from proj:epsg, else None.
dict[str, Any]
  • transform — raw proj:transform list or None.
dict[str, Any]
  • geotransform — the GDAL geotransform derived from transform, or None when no transform is present.
dict[str, Any]
  • shape — proj:shape [rows, cols] or None.
dict[str, Any]
  • raster_bands — raster:bands list or None.
dict[str, Any]
  • eo_bands — eo:bands list or None.
dict[str, Any]
  • band_names — names derived from eo:bands (name or common_name) when every band has one, else None.

Raises:

Type Description
StacAssetError

When asset_key is given but absent from the item.

Examples:

  • Read a Sentinel-2-style asset's projection metadata from raw JSON:
    >>> from pyramids.stac._extensions import read_extension_metadata
    >>> item = {
    ...     "properties": {"proj:epsg": 32633},
    ...     "assets": {"B04": {
    ...         "href": "s3://b/B04.tif",
    ...         "proj:shape": [10980, 10980],
    ...         "proj:transform": [10.0, 0.0, 600000.0, 0.0, -10.0, 5300040.0],
    ...         "raster:bands": [{"nodata": 0, "scale": 0.0001}],
    ...         "eo:bands": [{"name": "B04", "common_name": "red"}],
    ...     }},
    ... }
    >>> meta = read_extension_metadata(item, "B04")
    >>> meta["crs"]
    'EPSG:32633'
    >>> meta["geotransform"]
    (600000.0, 10.0, 0.0, 5300040.0, 0.0, -10.0)
    >>> meta["band_names"]
    ['B04']
    
  • An asset-level proj:epsg overrides the item-level value:
    >>> item = {
    ...     "properties": {"proj:epsg": 4326},
    ...     "assets": {"dem": {"href": "x.tif", "proj:epsg": 3857}},
    ... }
    >>> read_extension_metadata(item, "dem")["epsg"]
    3857
    
  • A bare asset with no extension fields yields all-empty metadata:
    >>> meta = read_extension_metadata({"href": "x.tif"})
    >>> (meta["crs"], meta["geotransform"], meta["raster_bands"])
    (None, None, None)
    
Source code in src/pyramids/stac/_extensions.py
def read_extension_metadata(item: Any, asset_key: str | None = None) -> dict[str, Any]:
    """Read ``proj`` / ``raster`` / ``eo`` extension fields for a STAC asset.

    Item-level fields (under ``properties``) are read first and an asset-level
    value of the same key overrides them, matching the STAC convention that an
    asset narrows item-level metadata. No asset file is opened.

    Args:
        item: A STAC Item (pystac object or raw dict). When `asset_key` is
            ``None`` the `item` is treated as the asset itself.
        asset_key: The asset key whose metadata to read, or ``None`` to read
            a bare asset.

    Returns:
        A dict with keys:

        * ``epsg`` — ``proj:epsg`` (int) or ``None``.
        * ``crs`` — ``proj:code`` (e.g. ``"EPSG:32633"``) when present, else
          derived from ``proj:epsg``, else ``None``.
        * ``transform`` — raw ``proj:transform`` list or ``None``.
        * ``geotransform`` — the GDAL geotransform derived from ``transform``,
          or ``None`` when no transform is present.
        * ``shape`` — ``proj:shape`` ``[rows, cols]`` or ``None``.
        * ``raster_bands`` — ``raster:bands`` list or ``None``.
        * ``eo_bands`` — ``eo:bands`` list or ``None``.
        * ``band_names`` — names derived from ``eo:bands`` (``name`` or
          ``common_name``) when every band has one, else ``None``.

    Raises:
        StacAssetError: When `asset_key` is given but absent from the item.

    Examples:
        - Read a Sentinel-2-style asset's projection metadata from raw JSON:
            ```python
            >>> from pyramids.stac._extensions import read_extension_metadata
            >>> item = {
            ...     "properties": {"proj:epsg": 32633},
            ...     "assets": {"B04": {
            ...         "href": "s3://b/B04.tif",
            ...         "proj:shape": [10980, 10980],
            ...         "proj:transform": [10.0, 0.0, 600000.0, 0.0, -10.0, 5300040.0],
            ...         "raster:bands": [{"nodata": 0, "scale": 0.0001}],
            ...         "eo:bands": [{"name": "B04", "common_name": "red"}],
            ...     }},
            ... }
            >>> meta = read_extension_metadata(item, "B04")
            >>> meta["crs"]
            'EPSG:32633'
            >>> meta["geotransform"]
            (600000.0, 10.0, 0.0, 5300040.0, 0.0, -10.0)
            >>> meta["band_names"]
            ['B04']

            ```
        - An asset-level ``proj:epsg`` overrides the item-level value:
            ```python
            >>> item = {
            ...     "properties": {"proj:epsg": 4326},
            ...     "assets": {"dem": {"href": "x.tif", "proj:epsg": 3857}},
            ... }
            >>> read_extension_metadata(item, "dem")["epsg"]
            3857

            ```
        - A bare asset with no extension fields yields all-empty metadata:
            ```python
            >>> meta = read_extension_metadata({"href": "x.tif"})
            >>> (meta["crs"], meta["geotransform"], meta["raster_bands"])
            (None, None, None)

            ```
    """
    props = item_properties(item)
    asset = get_asset(item, asset_key) if asset_key is not None else item

    def pick(key: str, default: Any = None) -> Any:
        return asset_field(asset, key, props.get(key, default))

    epsg = pick("proj:epsg")
    code = pick("proj:code")
    if code is None and epsg is not None:
        code = f"EPSG:{epsg}"

    transform = pick("proj:transform")
    geotransform = affine_to_geotransform(transform) if transform else None

    eo_bands = asset_field(asset, "eo:bands")
    band_names: list[str] | None = None
    if eo_bands:
        names = [b.get("name") or b.get("common_name") for b in eo_bands]
        if all(names):
            band_names = list(names)

    return {
        "epsg": epsg,
        "crs": code,
        "transform": transform,
        "geotransform": geotransform,
        "shape": pick("proj:shape"),
        "raster_bands": asset_field(asset, "raster:bands"),
        "eo_bands": eo_bands,
        "band_names": band_names,
    }

VRT mosaic#

pyramids.stac._vrt.build_vrt_from_stac(items, asset, *, signer=None, separate=False, strict=True) #

Mosaic one STAC asset across items into a lazy VRT-backed Dataset.

Parameters:

Name Type Description Default
items Any

Iterable of STAC Items (pystac objects, raw JSON dicts, or any duck-typed equivalent — same contract as :meth:pyramids.dataset.DatasetCollection.from_stac).

required
asset str

The asset key to mosaic (e.g. "visual", "B04").

required
signer Any

Optional signer (e.g. a :class:pyramids.stac.signers.Signer). Its sign_href rewrites every source href and its gdal_env() is installed while the VRT is built. See the module note on read-time credentials for env-based signers.

None
separate bool

When False (default) the assets are mosaicked spatially (overlapping/tiling sources compose into one image — the stac-vrt model). When True, each source becomes a separate band (a band-stack VRT), which requires the sources to share a grid.

False
strict bool

What to do when GDAL skips a requested source. True (the default) raises, so a partially-built mosaic is never mistaken for a complete one. False warns and returns the partial mosaic — use it only when an incomplete footprint is genuinely acceptable, and check what came back.

True

Returns:

Name Type Description
Dataset Dataset

A lazy Dataset over an in-memory .vrt; GDAL reads the

Dataset

underlying sources on demand (/vsicurl/ range requests for remote

Dataset

hrefs).

Raises:

Type Description
ValueError

When items yields no items.

RuntimeError

When gdal.BuildVRT fails outright (every source unreadable), or — with the default strict=True — when it silently skipped some of them (unreadable href, mismatched band count or CRS).

Warns:

Type Description
UserWarning

When some sources were skipped and strict is False.

Examples:

  • Mosaic the visual asset of several items into one lazy Dataset (requires network for remote hrefs):
    >>> from pyramids.stac import build_vrt_from_stac  # doctest: +SKIP
    >>> ds = build_vrt_from_stac(items, asset="visual")  # doctest: +SKIP
    >>> arr = ds.read_array()  # GDAL pulls source pixels lazily  # doctest: +SKIP
    
  • Accept a partial mosaic when some hrefs are known to be missing:
    >>> ds = build_vrt_from_stac(items, "visual", strict=False)  # doctest: +SKIP
    
See Also
  • :func:pyramids.stac.load_asset: open a single asset instead of mosaicking one asset across items.
  • :meth:pyramids.dataset.DatasetCollection.from_stac: stack the same asset across items along a time axis rather than mosaicking it into one image.
  • :func:pyramids.dataset.merge.merge_rasters: the eager, file-writing counterpart to this lazy VRT.
Source code in src/pyramids/stac/_vrt.py
def build_vrt_from_stac(
    items: Any,
    asset: str,
    *,
    signer: Any = None,
    separate: bool = False,
    strict: bool = True,
) -> Dataset:
    """Mosaic one STAC asset across items into a lazy VRT-backed `Dataset`.

    Args:
        items: Iterable of STAC Items (pystac objects, raw JSON dicts, or any
            duck-typed equivalent — same contract as
            :meth:`pyramids.dataset.DatasetCollection.from_stac`).
        asset: The asset key to mosaic (e.g. `"visual"`, `"B04"`).
        signer: Optional signer (e.g. a :class:`pyramids.stac.signers.Signer`).
            Its `sign_href` rewrites every source href and its `gdal_env()` is
            installed while the VRT is built. See the module note on read-time
            credentials for env-based signers.
        separate: When `False` (default) the assets are mosaicked spatially
            (overlapping/tiling sources compose into one image — the stac-vrt
            model). When `True`, each source becomes a separate band (a
            band-stack VRT), which requires the sources to share a grid.
        strict: What to do when GDAL skips a requested source. `True` (the
            default) raises, so a partially-built mosaic is never mistaken for a
            complete one. `False` warns and returns the partial mosaic — use it
            only when an incomplete footprint is genuinely acceptable, and check
            what came back.

    Returns:
        Dataset: A lazy `Dataset` over an in-memory `.vrt`; GDAL reads the
        underlying sources on demand (`/vsicurl/` range requests for remote
        hrefs).

    Raises:
        ValueError: When `items` yields no items.
        RuntimeError: When `gdal.BuildVRT` fails outright (every source
            unreadable), or — with the default `strict=True` — when it silently
            skipped some of them (unreadable href, mismatched band count or CRS).

    Warns:
        UserWarning: When some sources were skipped and `strict` is `False`.

    Examples:
        - Mosaic the `visual` asset of several items into one lazy Dataset
          (requires network for remote hrefs):
            ```python
            >>> from pyramids.stac import build_vrt_from_stac  # doctest: +SKIP
            >>> ds = build_vrt_from_stac(items, asset="visual")  # doctest: +SKIP
            >>> arr = ds.read_array()  # GDAL pulls source pixels lazily  # doctest: +SKIP

            ```
        - Accept a partial mosaic when some hrefs are known to be missing:
            ```python
            >>> ds = build_vrt_from_stac(items, "visual", strict=False)  # doctest: +SKIP

            ```

    See Also:
        - :func:`pyramids.stac.load_asset`: open a *single* asset instead of
          mosaicking one asset across items.
        - :meth:`pyramids.dataset.DatasetCollection.from_stac`: stack the same
          asset across items along a **time** axis rather than mosaicking it
          into one image.
        - :func:`pyramids.dataset.merge.merge_rasters`: the eager, file-writing
          counterpart to this lazy VRT.
    """
    item_list = list(items)
    if not item_list:
        raise ValueError("build_vrt_from_stac received no items.")

    gdal_env = signer.gdal_env() if signer is not None else None
    hrefs = [resolved_href(item, asset, signer=signer) for item in item_list]
    # Header credentials are baked into each source path so they survive into
    # the VRT's lazy, read-time source opens (see _embed_source_options). The
    # hrefs are kept alongside: an embedded path carries a live credential and
    # must never be the thing a message, warning or log names.
    sources = [(href, _embed_source_options(href, gdal_env)) for href in hrefs]
    vsi_paths = [vsi_path for _href, vsi_path in sources]
    _warn_unembeddable_credentials(gdal_env, hrefs)
    vrt_path = f"/vsimem/pyramids_scratch_{uuid.uuid4().hex}.vrt"

    # BuildVRT opens every source. Over `/vsicurl/` that costs a directory
    # listing plus a fan of sidecar probes per source unless the fast-read
    # preset is installed, so pay for it once here — but only when a source is
    # actually remote: the preset's readdir skip would also hide a *local*
    # source's `.aux.xml` / world-file sidecars. Remoteness is decided on the
    # hrefs, not the rewritten paths, which `is_remote` does not classify.
    with _source_config(hrefs, gdal_env):
        vrt_ds = _build_vrt(vrt_path, vsi_paths, separate, asset)
        # GDAL lists exactly the sources it kept, so the difference against what
        # was requested is what it silently skipped.
        dropped = _dropped_sources(sources, vrt_ds.GetFileList() or ())
        vrt_ds.FlushCache()
        vrt_ds = None
        # Track the in-memory VRT *before* anything else can raise, so a failure
        # below cannot orphan it beyond the process-exit sweep (M1).
        register_vsimem(vrt_path)
        try:
            _check_dropped_sources(dropped, len(vsi_paths), asset, strict)
            # Persist the signer env on the returned Dataset: the VRT opens its
            # sources lazily on the first pixel read, i.e. after this block has
            # exited, so without it an env-credentialed signer's reads 401.
            dataset = Dataset.read_file(vrt_path, gdal_env=gdal_env)
        except BaseException:
            # Nothing references the VRT on this path — reclaim it now rather
            # than leaving it in /vsimem until interpreter shutdown. BaseException
            # rather than Exception because a KeyboardInterrupt mid-open orphans
            # the artefact just as surely as a read failure does.
            _reclaim_vsimem(vrt_path)
            raise
    # Reclaim the VRT when the Dataset that needs it is collected, mirroring
    # `Dataset.from_bytes`. Without this a service building one mosaic per
    # request accumulates a /vsimem VRT per request for the life of the process;
    # the `register_vsimem` above is only the process-exit backstop.
    weakref.finalize(dataset, _reclaim_vsimem, vrt_path)
    return dataset

Alternate asset hrefs#

pyramids.stac._item #

Duck-typed accessors for STAC Items and Assets (no pystac dependency).

Both :mod:pyramids.stac._loader (asset → reader dispatch) and :mod:pyramids.dataset._stac (ItemCollection → :class:DatasetCollection) need to read the same fields off a STAC Item / Asset that may be a :class:pystac.Item / :class:pystac.Asset object or a raw STAC JSON dict. Before this module those two readers each carried their own getattr-then-dict-fallback resolver, which duplicated the contract and risked drifting apart.

Centralising the access here gives one duck-typed contract:

  • :class:pystac.Item / :class:pystac.Asset objects (attribute access: item.assets, asset.href, asset.media_type, asset.extra_fields).
  • Raw STAC JSON dicts (item["assets"], asset["href"], asset["type"], and extension fields as top-level keys on the asset dict).

pyramids does not import or depend on pystac. The extension-field accessor (:func:asset_field) is the extension point for reading proj / raster / eo metadata (PB-1).

asset_alternate_href(asset, name) #

Return asset["alternate"][name]["href"], or None when absent.

Reads the STAC alternate-assets extension, which publishes the same data at several locations — typically a public https href next to an s3 mirror that is cheaper (or the only readable one) from inside the cloud region hosting it.

Resolution goes through :func:asset_field, so the block is found both as a top-level "alternate" key on a raw asset dict and in a pystac.Asset's extra_fields.

Parameters:

Name Type Description Default
asset Any

A STAC Asset (pystac object or raw dict).

required
name str

The alternate key to look up (e.g. "s3", "gs").

required

Returns:

Type Description
str | None

The alternate href as a string, or None when the asset carries no

str | None

alternate block, no entry under name, or an entry without an href.

Examples:

  • Read an s3 mirror published beside the canonical href:
    >>> from pyramids.stac._item import asset_alternate_href
    >>> asset = {
    ...     "href": "https://h/a.tif",
    ...     "alternate": {"s3": {"href": "s3://b/a.tif"}},
    ... }
    >>> asset_alternate_href(asset, "s3")
    's3://b/a.tif'
    
  • An absent alternate answers None rather than raising:
    >>> asset_alternate_href(asset, "gs") is None
    True
    
See Also
  • :func:preferred_asset_href: picks an alternate with the canonical href as the fallback.
Source code in src/pyramids/stac/_item.py
def asset_alternate_href(asset: Any, name: str) -> str | None:
    """Return `asset["alternate"][name]["href"]`, or `None` when absent.

    Reads the STAC `alternate-assets` extension, which publishes the same data
    at several locations — typically a public `https` href next to an `s3`
    mirror that is cheaper (or the only readable one) from inside the cloud
    region hosting it.

    Resolution goes through :func:`asset_field`, so the block is found both as a
    top-level `"alternate"` key on a raw asset dict and in a `pystac.Asset`'s
    `extra_fields`.

    Args:
        asset: A STAC Asset (pystac object or raw dict).
        name: The alternate key to look up (e.g. `"s3"`, `"gs"`).

    Returns:
        The alternate href as a string, or `None` when the asset carries no
        `alternate` block, no entry under `name`, or an entry without an href.

    Examples:
        - Read an `s3` mirror published beside the canonical href:
            ```python
            >>> from pyramids.stac._item import asset_alternate_href
            >>> asset = {
            ...     "href": "https://h/a.tif",
            ...     "alternate": {"s3": {"href": "s3://b/a.tif"}},
            ... }
            >>> asset_alternate_href(asset, "s3")
            's3://b/a.tif'

            ```
        - An absent alternate answers `None` rather than raising:
            ```python
            >>> asset_alternate_href(asset, "gs") is None
            True

            ```

    See Also:
        - :func:`preferred_asset_href`: picks an alternate with the canonical
          href as the fallback.
    """
    entry = asset_alternate_entry(asset, name)
    href = None if entry is None else entry.get("href")
    return str(href) if href else None

preferred_asset_href(asset, alternate=None, *, item=None, asset_key=None) #

Return the first available alternate href, else the canonical href.

alternate is a preference, never a requirement: an asset that publishes no alternate block, or none under the requested keys, falls back to its canonical href silently. That keeps a single call site working across a mixed item collection where only some assets carry the extension.

pyramids accepts one key or an ordered sequence of them; stac-asset's Config.alternate_assets is always a list. Passing a bare string here is the single-key spelling of a one-element list, not a different contract.

Parameters:

Name Type Description Default
asset Any

A STAC Asset (pystac object or raw dict).

required
alternate str | Sequence[str] | None

An alternate key ("s3") or keys in preference order (["s3", "gs"]). None (the default) uses the canonical href.

None
item Any

The owning Item, used only to enrich the error message.

None
asset_key str | None

The asset key, used only to enrich the error message.

None

Returns:

Type Description
str

The preferred href as a string.

Raises:

Type Description
StacAssetError

No alternate matched and the asset has no canonical href (subclasses :class:KeyError).

Examples:

  • A requested alternate wins over the canonical href:
    >>> from pyramids.stac._item import preferred_asset_href
    >>> asset = {
    ...     "href": "https://h/a.tif",
    ...     "alternate": {"s3": {"href": "s3://b/a.tif"}},
    ... }
    >>> preferred_asset_href(asset, "s3")
    's3://b/a.tif'
    
  • The first key that resolves wins, and an unmatched preference falls back to the canonical href:
    >>> preferred_asset_href(asset, ["gs", "s3"])
    's3://b/a.tif'
    >>> preferred_asset_href(asset, "gs")
    'https://h/a.tif'
    
  • No preference at all keeps today's behaviour:
    >>> preferred_asset_href(asset)
    'https://h/a.tif'
    
See Also
  • :func:preferred_asset_source: the same choice, reported together with the media type of the chosen href — which is what a caller picking a reader needs, since an alternate may declare its own type.
Source code in src/pyramids/stac/_item.py
def preferred_asset_href(
    asset: Any,
    alternate: str | Sequence[str] | None = None,
    *,
    item: Any = None,
    asset_key: str | None = None,
) -> str:
    """Return the first available alternate href, else the canonical `href`.

    `alternate` is a *preference*, never a requirement: an asset that publishes
    no `alternate` block, or none under the requested keys, falls back to its
    canonical href silently. That keeps a single call site working across a
    mixed item collection where only some assets carry the extension.

    pyramids accepts one key or an ordered sequence of them; stac-asset's
    `Config.alternate_assets` is always a list. Passing a bare string here is
    the single-key spelling of a one-element list, not a different contract.

    Args:
        asset: A STAC Asset (pystac object or raw dict).
        alternate: An alternate key (`"s3"`) or keys in preference order
            (`["s3", "gs"]`). `None` (the default) uses the canonical href.
        item: The owning Item, used only to enrich the error message.
        asset_key: The asset key, used only to enrich the error message.

    Returns:
        The preferred href as a string.

    Raises:
        StacAssetError: No alternate matched and the asset has no canonical
            `href` (subclasses :class:`KeyError`).

    Examples:
        - A requested alternate wins over the canonical href:
            ```python
            >>> from pyramids.stac._item import preferred_asset_href
            >>> asset = {
            ...     "href": "https://h/a.tif",
            ...     "alternate": {"s3": {"href": "s3://b/a.tif"}},
            ... }
            >>> preferred_asset_href(asset, "s3")
            's3://b/a.tif'

            ```
        - The first key that resolves wins, and an unmatched preference falls
          back to the canonical href:
            ```python
            >>> preferred_asset_href(asset, ["gs", "s3"])
            's3://b/a.tif'
            >>> preferred_asset_href(asset, "gs")
            'https://h/a.tif'

            ```
        - No preference at all keeps today's behaviour:
            ```python
            >>> preferred_asset_href(asset)
            'https://h/a.tif'

            ```

    See Also:
        - :func:`preferred_asset_source`: the same choice, reported together
          with the media type of the chosen href — which is what a caller
          picking a reader needs, since an alternate may declare its own `type`.
    """
    href, _media_type = preferred_asset_source(
        asset, alternate, item=item, asset_key=asset_key
    )
    return href

preferred_asset_source(asset, alternate=None, *, item=None, asset_key=None) #

Return the (href, media_type) pair describing the location to read.

The companion of :func:preferred_asset_href for callers that then have to pick a reader: an alternate may declare its own type, and the alternate-assets extension does not require it to match the canonical asset's. An s3:// mirror published as a Zarr store beside an HTTPS COG is a legitimate pair, so the media type has to travel with the href that was chosen rather than being read off the asset afterwards.

An alternate that declares no type inherits the asset's, which is the common case (a mirror of the same bytes) and keeps the href-extension fallback working for a typeless asset.

Parameters:

Name Type Description Default
asset Any

A STAC Asset (pystac object or raw dict).

required
alternate str | Sequence[str] | None

An alternate key ("s3") or keys in preference order (["s3", "gs"]). None (the default) uses the canonical href.

None
item Any

The owning Item, used only to enrich the error message.

None
asset_key str | None

The asset key, used only to enrich the error message.

None

Returns:

Type Description
str

The chosen href, and the media type describing that href — the

str | None

alternate's own type when it declares one, else the asset's declared

tuple[str, str | None]

media type, else None.

Raises:

Type Description
StacAssetError

No alternate matched and the asset has no canonical href (subclasses :class:KeyError).

Examples:

  • A typed alternate reports its own media type:
    >>> from pyramids.stac._item import preferred_asset_source
    >>> asset = {
    ...     "href": "https://h/a.tif",
    ...     "type": "image/tiff",
    ...     "alternate": {"s3": {"href": "s3://b/a", "type": "application/vnd+zarr"}},
    ... }
    >>> preferred_asset_source(asset, "s3")
    ('s3://b/a', 'application/vnd+zarr')
    
  • A typeless alternate inherits the asset's media type:
    >>> asset["alternate"]["gs"] = {"href": "gs://b/a.tif"}
    >>> preferred_asset_source(asset, "gs")
    ('gs://b/a.tif', 'image/tiff')
    
  • No preference keeps the canonical pair:
    >>> preferred_asset_source(asset)
    ('https://h/a.tif', 'image/tiff')
    
Source code in src/pyramids/stac/_item.py
def preferred_asset_source(
    asset: Any,
    alternate: str | Sequence[str] | None = None,
    *,
    item: Any = None,
    asset_key: str | None = None,
) -> tuple[str, str | None]:
    """Return the `(href, media_type)` pair describing the location to read.

    The companion of :func:`preferred_asset_href` for callers that then have to
    pick a *reader*: an alternate may declare its own `type`, and the
    `alternate-assets` extension does not require it to match the canonical
    asset's. An `s3://` mirror published as a Zarr store beside an HTTPS COG is
    a legitimate pair, so the media type has to travel with the href that was
    chosen rather than being read off the asset afterwards.

    An alternate that declares no `type` inherits the asset's, which is the
    common case (a mirror of the same bytes) and keeps the href-extension
    fallback working for a typeless asset.

    Args:
        asset: A STAC Asset (pystac object or raw dict).
        alternate: An alternate key (`"s3"`) or keys in preference order
            (`["s3", "gs"]`). `None` (the default) uses the canonical href.
        item: The owning Item, used only to enrich the error message.
        asset_key: The asset key, used only to enrich the error message.

    Returns:
        The chosen href, and the media type describing *that* href — the
        alternate's own `type` when it declares one, else the asset's declared
        media type, else `None`.

    Raises:
        StacAssetError: No alternate matched and the asset has no canonical
            `href` (subclasses :class:`KeyError`).

    Examples:
        - A typed alternate reports its own media type:
            ```python
            >>> from pyramids.stac._item import preferred_asset_source
            >>> asset = {
            ...     "href": "https://h/a.tif",
            ...     "type": "image/tiff",
            ...     "alternate": {"s3": {"href": "s3://b/a", "type": "application/vnd+zarr"}},
            ... }
            >>> preferred_asset_source(asset, "s3")
            ('s3://b/a', 'application/vnd+zarr')

            ```
        - A typeless alternate inherits the asset's media type:
            ```python
            >>> asset["alternate"]["gs"] = {"href": "gs://b/a.tif"}
            >>> preferred_asset_source(asset, "gs")
            ('gs://b/a.tif', 'image/tiff')

            ```
        - No preference keeps the canonical pair:
            ```python
            >>> preferred_asset_source(asset)
            ('https://h/a.tif', 'image/tiff')

            ```
    """
    if alternate is None:
        names: tuple[str, ...] = ()
    elif isinstance(alternate, str):
        names = (alternate,)
    else:
        names = tuple(alternate)
    href: str | None = None
    declared: Any = None
    for name in names:
        entry = asset_alternate_entry(asset, name)
        href = None if entry is None else entry.get("href")
        if href:
            declared = entry.get("type") if entry is not None else None
            break
        href = None
    if href is None:
        href = asset_href(asset, item=item, asset_key=asset_key)
    media_type = asset_media_type(asset) if declared is None else str(declared)
    return str(href), media_type

asset_field(asset, key, default=None) #

Return an extension field from an asset (e.g. proj:transform).

Extension fields live as top-level keys on a raw asset dict, and in asset.extra_fields on a pystac.Asset. This is the building block for reading proj / raster / eo metadata (PB-1).

Parameters:

Name Type Description Default
asset Any

A STAC Asset (pystac object or raw dict).

required
key str

The extension field key (e.g. "proj:transform", "raster:bands").

required
default Any

Value returned when the field is absent.

None

Returns:

Type Description
Any

The field value, or default.

Examples:

  • Read a projection field from a dict asset:
    >>> from pyramids.stac._item import asset_field
    >>> asset_field({"href": "x.tif", "proj:epsg": 32633}, "proj:epsg")
    32633
    
  • Absent fields fall back to the default:
    >>> asset_field({"href": "x.tif"}, "proj:epsg", default="missing")
    'missing'
    
Source code in src/pyramids/stac/_item.py
def asset_field(asset: Any, key: str, default: Any = None) -> Any:
    """Return an extension field from an asset (e.g. `proj:transform`).

    Extension fields live as top-level keys on a raw asset dict, and in
    `asset.extra_fields` on a `pystac.Asset`. This is the building block for
    reading `proj` / `raster` / `eo` metadata (PB-1).

    Args:
        asset: A STAC Asset (pystac object or raw dict).
        key: The extension field key (e.g. `"proj:transform"`, `"raster:bands"`).
        default: Value returned when the field is absent.

    Returns:
        The field value, or `default`.

    Examples:
        - Read a projection field from a dict asset:
            ```python
            >>> from pyramids.stac._item import asset_field
            >>> asset_field({"href": "x.tif", "proj:epsg": 32633}, "proj:epsg")
            32633

            ```
        - Absent fields fall back to the default:
            ```python
            >>> asset_field({"href": "x.tif"}, "proj:epsg", default="missing")
            'missing'

            ```
    """
    if isinstance(asset, dict):
        return asset.get(key, default)
    extra = getattr(asset, "extra_fields", None)
    if isinstance(extra, dict) and key in extra:
        return extra[key]
    return default

Reachability & content-type verification#

pyramids.stac._loader.verify_asset(href, media_type=None, *, strict=False, timeout=VERIFY_TIMEOUT, opener=None) #

Pre-flight an asset href: is it reachable, and is it the type it claims?

The opt-in check behind verify= on :func:load_asset and :func:resolved_href. It costs one HEAD (or, where HEAD is refused, a 1-byte ranged GET) and catches the two failures that otherwise surface as an opaque GDAL error several seconds later: an expired or dead URL, and an asset whose bytes are not what the catalog says they are.

Verification is HTTP-only. An s3:// / gs:// / /vsi* / local href is skipped and answers None: those are read by GDAL's own VSI layer, whose credentials this check does not hold, so a probe here would report a permission failure that the real read does not have.

Hrefs are redacted (:func:pyramids.stac._vrt.redact) before they reach a message, so a signed URL's token cannot leak into a log handler.

Parameters:

Name Type Description Default
href str

The resolved (already signed) asset href.

required
media_type str | None

The asset's declared media type, compared leniently against the served Content-Type. None checks reachability only.

None
strict bool

Raise :class:~pyramids.base._errors.StacError instead of emitting an :class:AssetVerificationWarning.

False
timeout float

Per-attempt timeout in seconds.

VERIFY_TIMEOUT
opener Any

Optional opener (anything with .open(target, timeout=...)), for tests and for callers holding a configured :class:urllib.request.OpenerDirector.

None

Returns:

Type Description
str | None

The served content type, None when the href was skipped (non-HTTP) or

str | None

the response carried no Content-Type.

Raises:

Type Description
StacError

strict is True and the asset was unreachable or mislabeled.

Examples:

  • A matching content type verifies quietly and answers what was served:
    >>> import io
    >>> from pyramids.stac._loader import verify_asset
    >>> class _Opener:
    ...     def __init__(self, content_type):
    ...         self.headers = {"Content-Type": content_type}
    ...     def open(self, target, timeout=None):
    ...         response = io.BytesIO(b"")
    ...         response.headers = self.headers
    ...         return response
    >>> verify_asset("https://h/a.tif", "image/tiff", opener=_Opener("image/tiff"))
    'image/tiff'
    
  • A parameterised declaration still matches the bare served type:
    >>> verify_asset(
    ...     "https://h/a.tif",
    ...     "image/tiff; application=geotiff",
    ...     opener=_Opener("image/tiff"),
    ... )
    'image/tiff'
    
  • A contradiction raises under strict, with the query string redacted:
    >>> from pyramids.base._errors import StacError
    >>> try:
    ...     verify_asset(
    ...         "https://h/a.tif?sig=SECRET",
    ...         "image/tiff",
    ...         strict=True,
    ...         opener=_Opener("text/html"),
    ...     )
    ... except StacError as exc:
    ...     print(exc)
    STAC asset 'https://h/a.tif?<redacted>' is declared 'image/tiff' but the server served 'text/html'
    
  • A non-HTTP href is skipped rather than probed:
    >>> verify_asset("s3://b/a.tif", "image/tiff") is None
    True
    
Source code in src/pyramids/stac/_loader.py
def verify_asset(
    href: str,
    media_type: str | None = None,
    *,
    strict: bool = False,
    timeout: float = VERIFY_TIMEOUT,
    opener: Any = None,
) -> str | None:
    """Pre-flight an asset href: is it reachable, and is it the type it claims?

    The opt-in check behind `verify=` on :func:`load_asset` and
    :func:`resolved_href`. It costs one HEAD (or, where HEAD is refused, a
    1-byte ranged GET) and catches the two failures that otherwise surface as an
    opaque GDAL error several seconds later: an expired or dead URL, and an
    asset whose bytes are not what the catalog says they are.

    Verification is **HTTP-only**. An `s3://` / `gs://` / `/vsi*` / local href is
    skipped and answers `None`: those are read by GDAL's own VSI layer, whose
    credentials this check does not hold, so a probe here would report a
    permission failure that the real read does not have.

    Hrefs are redacted (:func:`pyramids.stac._vrt.redact`) before they reach a
    message, so a signed URL's token cannot leak into a log handler.

    Args:
        href: The resolved (already signed) asset href.
        media_type: The asset's declared media type, compared leniently against
            the served `Content-Type`. `None` checks reachability only.
        strict: Raise :class:`~pyramids.base._errors.StacError` instead of
            emitting an :class:`AssetVerificationWarning`.
        timeout: Per-attempt timeout in seconds.
        opener: Optional opener (anything with `.open(target, timeout=...)`),
            for tests and for callers holding a configured
            :class:`urllib.request.OpenerDirector`.

    Returns:
        The served content type, `None` when the href was skipped (non-HTTP) or
        the response carried no `Content-Type`.

    Raises:
        StacError: `strict` is `True` and the asset was unreachable or
            mislabeled.

    Examples:
        - A matching content type verifies quietly and answers what was served:
            ```python
            >>> import io
            >>> from pyramids.stac._loader import verify_asset
            >>> class _Opener:
            ...     def __init__(self, content_type):
            ...         self.headers = {"Content-Type": content_type}
            ...     def open(self, target, timeout=None):
            ...         response = io.BytesIO(b"")
            ...         response.headers = self.headers
            ...         return response
            >>> verify_asset("https://h/a.tif", "image/tiff", opener=_Opener("image/tiff"))
            'image/tiff'

            ```
        - A parameterised declaration still matches the bare served type:
            ```python
            >>> verify_asset(
            ...     "https://h/a.tif",
            ...     "image/tiff; application=geotiff",
            ...     opener=_Opener("image/tiff"),
            ... )
            'image/tiff'

            ```
        - A contradiction raises under `strict`, with the query string redacted:
            ```python
            >>> from pyramids.base._errors import StacError
            >>> try:
            ...     verify_asset(
            ...         "https://h/a.tif?sig=SECRET",
            ...         "image/tiff",
            ...         strict=True,
            ...         opener=_Opener("text/html"),
            ...     )
            ... except StacError as exc:
            ...     print(exc)
            STAC asset 'https://h/a.tif?<redacted>' is declared 'image/tiff' but the server served 'text/html'

            ```
        - A non-HTTP href is skipped rather than probed:
            ```python
            >>> verify_asset("s3://b/a.tif", "image/tiff") is None
            True

            ```
    """
    # Local import: `pyramids.stac._vrt` imports this module, so importing it at
    # module scope would close the `_loader` -> `_vrt` -> `_loader` cycle.
    from pyramids.stac._vrt import redact

    served: str | None = None
    scheme, separator, _rest = href.partition(_SCHEME_SEPARATOR)
    if separator and scheme.lower() in _HTTP_SCHEMES:
        safe = redact(href)
        try:
            served = _probe_content_type(href, timeout, opener)
        except urllib.error.HTTPError as exc:
            # The error *is* the response, and this is where its lifetime ends:
            # the caller is handed a warning (or a StacError), never the object,
            # so nothing downstream can release the socket. `closing` also runs
            # before `_report`, which raises under `strict`.
            with contextlib.closing(exc):
                detail = http_error_detail(exc)
            _report(
                f"STAC asset {safe!r} is unreachable: HTTP {exc.code} ({detail})",
                strict,
            )
        except OSError as exc:
            _report(f"STAC asset {safe!r} is unreachable: {exc}", strict)
        else:
            if not _media_types_agree(media_type, served):
                _report(
                    f"STAC asset {safe!r} is declared {media_type!r} but the "
                    f"server served {served!r}",
                    strict,
                )
    return served

Metadata overrides and band aliases#

Supply what a thin catalog omits, and name an asset by alias. Passed as cfg= to load_asset and DatasetCollection.from_stac:

cfg = {
    "sentinel-2-l2a": {
        "assets": {"*": {"nodata": 0}, "SCL": {"data_type": "uint8"}},
        "aliases": {"red": "B04", "rededge": "B05"},
    }
}

Most-specific wins: cfg["*"]["assets"]["*"] < cfg["*"]["assets"][key] < cfg[collection]["assets"]["*"] < cfg[collection]["assets"][key]. nodata and unit fill gaps only — a value the asset already declares is kept and the skip warns with AssetMetadataWarning (silence it with "warnings": "ignore").

Item-level windowed reads#

pyramids.stac._windowed #

Item-level windowed reads: pull a bbox / point / geometry window off an asset.

A caller who wants one window out of one asset of one STAC Item otherwise writes the same four lines every time — resolve the asset, sign it, open it, crop it. These helpers are that composition and nothing more: each resolves the asset through :func:pyramids.stac.load_asset (so signing, the /vsicurl fast-read preset and the alternate-assets preference all behave identically to a full read) and then delegates to the overview-decimated reads the COG engine already owns:

helper delegates to
:func:read_item_part :meth:pyramids.dataset.Dataset.read_part
:func:read_item_preview :meth:pyramids.dataset.Dataset.preview
:func:read_item_point :meth:pyramids.dataset.Dataset.point
:func:read_item_feature :meth:pyramids.dataset.Dataset.read_part

No windowing arithmetic lives here. The decimation, the pixel snapping, the partial-overlap padding and the CRS transform are the engine's, so an item-level read and a Dataset-level one answer the same array — and a COG over /vsicurl/ still fetches only the byte ranges its window needs, because the window is pushed into GDAL's read rather than applied to an array afterwards.

Scope. These are numpy-returning reads of a single asset. Deliberately absent, and not planned here: XYZ/TMS tile serving, PNG/JPEG rendering, colormaps, and cross-asset band-math expressions. That stack is a dynamic tile server (rio-tiler's territory), not pyramids' mission. The one XYZ primitive pyramids does own — :meth:pyramids.dataset.Dataset.read_tile, which is a read_part over Web-Mercator tile bounds — stays at the Dataset level, where it is a raster read rather than a serving layer.

read_item_part(item_or_asset, asset_key, bbox, *, bbox_crs=STAC_WINDOW_CRS, signer=None, alternate=None, **read_options) #

Read a bbox window out of one asset of a STAC Item.

Opens the asset through :func:~pyramids.stac.load_asset and delegates to :meth:~pyramids.dataset.Dataset.read_part, so the read is decimated from the nearest overview and a COG over /vsicurl/ fetches only the window's byte ranges.

Parameters:

Name Type Description Default
item_or_asset Any

A STAC Item (pystac object or raw dict) or an Asset.

required
asset_key str | None

Asset name when passing an Item; None for an Asset.

required
bbox Sequence[float]

(min_x, min_y, max_x, max_y) window, stated in bbox_crs.

required
bbox_crs int | str | None

CRS of bbox. Defaults to :data:STAC_WINDOW_CRS (EPSG:4326), the CRS STAC states item geometry in; pass None to give the bbox in the asset's own coordinates instead.

STAC_WINDOW_CRS
signer Any

Optional signer, forwarded to :func:~pyramids.stac.load_asset.

None
alternate str | Sequence[str] | None

alternate-assets key (or keys in preference order) to prefer over the canonical href.

None
**read_options Any

Forwarded verbatim to :meth:~pyramids.dataset.Dataset.read_part — dst_width, dst_height, resampling, band, return_transform.

{}

Returns:

Type Description
NDArray | tuple[NDArray, tuple[float, ...]]

numpy.ndarray: (rows, cols) for a single band or

NDArray | tuple[NDArray, tuple[float, ...]]

(bands, rows, cols) for all of them; an (array, geotransform) tuple

NDArray | tuple[NDArray, tuple[float, ...]]

when return_transform=True is forwarded.

Raises:

Type Description
StacAssetError

The asset is missing or has no href.

UnsupportedAssetError

The asset's type matches no reader, or opened as something with no windowed read (a 4-D Zarr cube).

CRSError

A bbox_crs was given but the asset has no CRS to transform into.

OutOfBoundsError

The window does not intersect the asset at all.

Examples:

  • Pull a lon/lat window out of a catalog's red band:
    >>> from pyramids.stac import read_item_part  # doctest: +SKIP
    >>> window = read_item_part(  # doctest: +SKIP
    ...     item, "B04", (12.4, 41.8, 12.6, 42.0), dst_width=256, dst_height=256
    ... )
    >>> window.shape  # doctest: +SKIP
    (256, 256)
    
Source code in src/pyramids/stac/_windowed.py
def read_item_part(
    item_or_asset: Any,
    asset_key: str | None,
    bbox: Sequence[float],
    *,
    bbox_crs: int | str | None = STAC_WINDOW_CRS,
    signer: Any = None,
    alternate: str | Sequence[str] | None = None,
    **read_options: Any,
) -> np.typing.NDArray | tuple[np.typing.NDArray, tuple[float, ...]]:
    """Read a bbox window out of one asset of a STAC Item.

    Opens the asset through :func:`~pyramids.stac.load_asset` and delegates to
    :meth:`~pyramids.dataset.Dataset.read_part`, so the read is decimated from
    the nearest overview and a COG over `/vsicurl/` fetches only the window's
    byte ranges.

    Args:
        item_or_asset: A STAC Item (pystac object or raw dict) or an Asset.
        asset_key: Asset name when passing an Item; `None` for an Asset.
        bbox: `(min_x, min_y, max_x, max_y)` window, stated in `bbox_crs`.
        bbox_crs: CRS of `bbox`. Defaults to :data:`STAC_WINDOW_CRS`
            (EPSG:4326), the CRS STAC states item geometry in; pass `None` to
            give the bbox in the asset's own coordinates instead.
        signer: Optional signer, forwarded to :func:`~pyramids.stac.load_asset`.
        alternate: `alternate-assets` key (or keys in preference order) to
            prefer over the canonical href.
        **read_options: Forwarded verbatim to
            :meth:`~pyramids.dataset.Dataset.read_part` — `dst_width`,
            `dst_height`, `resampling`, `band`, `return_transform`.

    Returns:
        numpy.ndarray: `(rows, cols)` for a single band or
        `(bands, rows, cols)` for all of them; an `(array, geotransform)` tuple
        when `return_transform=True` is forwarded.

    Raises:
        StacAssetError: The asset is missing or has no href.
        UnsupportedAssetError: The asset's type matches no reader, or opened as
            something with no windowed read (a 4-D Zarr cube).
        CRSError: A `bbox_crs` was given but the asset has no CRS to transform
            into.
        OutOfBoundsError: The window does not intersect the asset at all.

    Examples:
        - Pull a lon/lat window out of a catalog's red band:
            ```python
            >>> from pyramids.stac import read_item_part  # doctest: +SKIP
            >>> window = read_item_part(  # doctest: +SKIP
            ...     item, "B04", (12.4, 41.8, 12.6, 42.0), dst_width=256, dst_height=256
            ... )
            >>> window.shape  # doctest: +SKIP
            (256, 256)

            ```
    """
    dataset = _open_windowed(item_or_asset, asset_key, signer, alternate)
    return dataset.read_part(
        (float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3])),
        bbox_crs=bbox_crs,
        **read_options,
    )

read_item_preview(item_or_asset, asset_key=None, *, max_size=1024, signer=None, alternate=None, **read_options) #

Read a whole-asset thumbnail for one asset of a STAC Item.

Opens the asset through :func:~pyramids.stac.load_asset and delegates to :meth:~pyramids.dataset.Dataset.preview, which pulls from a coarse overview where one exists — so previewing a large COG costs a few ranged requests rather than the whole file.

Parameters:

Name Type Description Default
item_or_asset Any

A STAC Item (pystac object or raw dict) or an Asset.

required
asset_key str | None

Asset name when passing an Item; None for an Asset.

None
max_size int

Maximum pixels on the longer edge. Defaults to 1024.

1024
signer Any

Optional signer, forwarded to :func:~pyramids.stac.load_asset.

None
alternate str | Sequence[str] | None

alternate-assets key (or keys in preference order) to prefer over the canonical href.

None
**read_options Any

Forwarded verbatim to :meth:~pyramids.dataset.Dataset.preview — resampling, band.

{}

Returns:

Type Description
NDArray

numpy.ndarray: The downsampled array, (rows, cols) or

NDArray

(bands, rows, cols).

Raises:

Type Description
StacAssetError

The asset is missing or has no href.

UnsupportedAssetError

The asset's type matches no reader, or opened as something with no windowed read (a 4-D Zarr cube).

Examples:

  • Thumbnail an item's visual asset at 128 px:
    >>> from pyramids.stac import read_item_preview  # doctest: +SKIP
    >>> thumb = read_item_preview(item, "visual", max_size=128)  # doctest: +SKIP
    
Source code in src/pyramids/stac/_windowed.py
def read_item_preview(
    item_or_asset: Any,
    asset_key: str | None = None,
    *,
    max_size: int = 1024,
    signer: Any = None,
    alternate: str | Sequence[str] | None = None,
    **read_options: Any,
) -> np.typing.NDArray:
    """Read a whole-asset thumbnail for one asset of a STAC Item.

    Opens the asset through :func:`~pyramids.stac.load_asset` and delegates to
    :meth:`~pyramids.dataset.Dataset.preview`, which pulls from a coarse
    overview where one exists — so previewing a large COG costs a few ranged
    requests rather than the whole file.

    Args:
        item_or_asset: A STAC Item (pystac object or raw dict) or an Asset.
        asset_key: Asset name when passing an Item; `None` for an Asset.
        max_size: Maximum pixels on the longer edge. Defaults to 1024.
        signer: Optional signer, forwarded to :func:`~pyramids.stac.load_asset`.
        alternate: `alternate-assets` key (or keys in preference order) to
            prefer over the canonical href.
        **read_options: Forwarded verbatim to
            :meth:`~pyramids.dataset.Dataset.preview` — `resampling`, `band`.

    Returns:
        numpy.ndarray: The downsampled array, `(rows, cols)` or
        `(bands, rows, cols)`.

    Raises:
        StacAssetError: The asset is missing or has no href.
        UnsupportedAssetError: The asset's type matches no reader, or opened as
            something with no windowed read (a 4-D Zarr cube).

    Examples:
        - Thumbnail an item's visual asset at 128 px:
            ```python
            >>> from pyramids.stac import read_item_preview  # doctest: +SKIP
            >>> thumb = read_item_preview(item, "visual", max_size=128)  # doctest: +SKIP

            ```
    """
    dataset = _open_windowed(item_or_asset, asset_key, signer, alternate)
    # The `Dataset` facades forward through `*args, **kwargs` and so erase to
    # `Any`; cast back to the engine's declared return rather than widening this
    # signature.
    return cast("np.typing.NDArray", dataset.preview(max_size=max_size, **read_options))

read_item_point(item_or_asset, asset_key, point, *, point_crs=STAC_WINDOW_CRS, signer=None, alternate=None, **read_options) #

Sample one asset of a STAC Item at a single coordinate.

Opens the asset through :func:~pyramids.stac.load_asset and delegates to :meth:~pyramids.dataset.Dataset.point, which reads the single pixel covering the coordinate.

Parameters:

Name Type Description Default
item_or_asset Any

A STAC Item (pystac object or raw dict) or an Asset.

required
asset_key str | None

Asset name when passing an Item; None for an Asset.

required
point Sequence[float]

(x, y) — lon/lat by default — to sample.

required
point_crs int | str | None

CRS of point. Defaults to :data:STAC_WINDOW_CRS (EPSG:4326); pass None to give the coordinate in the asset's own coordinates instead.

STAC_WINDOW_CRS
signer Any

Optional signer, forwarded to :func:~pyramids.stac.load_asset.

None
alternate str | Sequence[str] | None

alternate-assets key (or keys in preference order) to prefer over the canonical href.

None
**read_options Any

Forwarded verbatim to :meth:~pyramids.dataset.Dataset.point — band.

{}

Returns:

Type Description
NDArray

numpy.ndarray: A 0-d array for a single band, or a (bands,) array

NDArray

when no band is given.

Raises:

Type Description
StacAssetError

The asset is missing or has no href.

UnsupportedAssetError

The asset's type matches no reader, or opened as something with no windowed read (a 4-D Zarr cube).

CRSError

A point_crs was given but the asset has no CRS to transform into.

OutOfBoundsError

The coordinate falls outside the asset's extent.

Examples:

  • Sample every band of an item's asset at one lon/lat:
    >>> from pyramids.stac import read_item_point  # doctest: +SKIP
    >>> read_item_point(item, "B04", (12.5, 41.9))  # doctest: +SKIP
    array([1234.])
    
Source code in src/pyramids/stac/_windowed.py
def read_item_point(
    item_or_asset: Any,
    asset_key: str | None,
    point: Sequence[float],
    *,
    point_crs: int | str | None = STAC_WINDOW_CRS,
    signer: Any = None,
    alternate: str | Sequence[str] | None = None,
    **read_options: Any,
) -> np.typing.NDArray:
    """Sample one asset of a STAC Item at a single coordinate.

    Opens the asset through :func:`~pyramids.stac.load_asset` and delegates to
    :meth:`~pyramids.dataset.Dataset.point`, which reads the single pixel
    covering the coordinate.

    Args:
        item_or_asset: A STAC Item (pystac object or raw dict) or an Asset.
        asset_key: Asset name when passing an Item; `None` for an Asset.
        point: `(x, y)` — lon/lat by default — to sample.
        point_crs: CRS of `point`. Defaults to :data:`STAC_WINDOW_CRS`
            (EPSG:4326); pass `None` to give the coordinate in the asset's own
            coordinates instead.
        signer: Optional signer, forwarded to :func:`~pyramids.stac.load_asset`.
        alternate: `alternate-assets` key (or keys in preference order) to
            prefer over the canonical href.
        **read_options: Forwarded verbatim to
            :meth:`~pyramids.dataset.Dataset.point` — `band`.

    Returns:
        numpy.ndarray: A 0-d array for a single band, or a `(bands,)` array
        when no `band` is given.

    Raises:
        StacAssetError: The asset is missing or has no href.
        UnsupportedAssetError: The asset's type matches no reader, or opened as
            something with no windowed read (a 4-D Zarr cube).
        CRSError: A `point_crs` was given but the asset has no CRS to transform
            into.
        OutOfBoundsError: The coordinate falls outside the asset's extent.

    Examples:
        - Sample every band of an item's asset at one lon/lat:
            ```python
            >>> from pyramids.stac import read_item_point  # doctest: +SKIP
            >>> read_item_point(item, "B04", (12.5, 41.9))  # doctest: +SKIP
            array([1234.])

            ```
    """
    dataset = _open_windowed(item_or_asset, asset_key, signer, alternate)
    return cast(
        "np.typing.NDArray",
        dataset.point(
            float(point[0]), float(point[1]), point_crs=point_crs, **read_options
        ),
    )

read_item_feature(item_or_asset, asset_key, geometry, *, geometry_crs=STAC_WINDOW_CRS, signer=None, alternate=None, **read_options) #

Read the window covering a geometry out of one asset of a STAC Item.

The geometry is reduced to its envelope (:func:geometry_bounds) and read through :func:read_item_part. The result is therefore the geometry's bounding window, not a masked cut-out: cells inside the window but outside the geometry keep their values. Masking is a separate operation — burn the geometry into a raster with :meth:pyramids.dataset.Dataset.from_features, wrapping it in a :class:~pyramids.feature.FeatureCollection, or clip the opened asset with :meth:pyramids.dataset.Dataset.clip, when the shape itself must be honoured.

Parameters:

Name Type Description Default
item_or_asset Any

A STAC Item (pystac object or raw dict) or an Asset.

required
asset_key str | None

Asset name when passing an Item; None for an Asset.

required
geometry Any

A shapely geometry, or a GeoJSON geometry / Feature / FeatureCollection / GeometryCollection mapping.

required
geometry_crs int | str | None

CRS of the geometry's coordinates. Defaults to :data:STAC_WINDOW_CRS (EPSG:4326), which is what GeoJSON states; pass None for a geometry already in the asset's coordinates.

STAC_WINDOW_CRS
signer Any

Optional signer, forwarded to :func:~pyramids.stac.load_asset.

None
alternate str | Sequence[str] | None

alternate-assets key (or keys in preference order) to prefer over the canonical href.

None
**read_options Any

Forwarded verbatim to :meth:~pyramids.dataset.Dataset.read_part — dst_width, dst_height, resampling, band, return_transform.

{}

Returns:

Type Description
NDArray | tuple[NDArray, tuple[float, ...]]

numpy.ndarray: The geometry's bounding window, shaped as

NDArray | tuple[NDArray, tuple[float, ...]]

func:read_item_part returns it.

Raises:

Type Description
ValueError

The geometry carries no coordinates.

StacAssetError

The asset is missing or has no href.

UnsupportedAssetError

The asset's type matches no reader, or opened as something with no windowed read (a 4-D Zarr cube).

OutOfBoundsError

The envelope does not intersect the asset at all.

Examples:

  • Read the window covering an AOI polygon:
    >>> from pyramids.stac import read_item_feature  # doctest: +SKIP
    >>> aoi = {"type": "Polygon", "coordinates": [[[12.4, 41.8], [12.6, 41.8],
    ...                                            [12.6, 42.0], [12.4, 41.8]]]}
    >>> window = read_item_feature(item, "B04", aoi)  # doctest: +SKIP
    
Source code in src/pyramids/stac/_windowed.py
def read_item_feature(
    item_or_asset: Any,
    asset_key: str | None,
    geometry: Any,
    *,
    geometry_crs: int | str | None = STAC_WINDOW_CRS,
    signer: Any = None,
    alternate: str | Sequence[str] | None = None,
    **read_options: Any,
) -> np.typing.NDArray | tuple[np.typing.NDArray, tuple[float, ...]]:
    """Read the window covering a geometry out of one asset of a STAC Item.

    The geometry is reduced to its envelope (:func:`geometry_bounds`) and read
    through :func:`read_item_part`. The result is therefore the geometry's
    **bounding window**, not a masked cut-out: cells inside the window but
    outside the geometry keep their values. Masking is a separate operation —
    burn the geometry into a raster with
    :meth:`pyramids.dataset.Dataset.from_features`, wrapping it in a
    :class:`~pyramids.feature.FeatureCollection`, or clip the opened asset with
    :meth:`pyramids.dataset.Dataset.clip`, when the shape itself must be
    honoured.

    Args:
        item_or_asset: A STAC Item (pystac object or raw dict) or an Asset.
        asset_key: Asset name when passing an Item; `None` for an Asset.
        geometry: A shapely geometry, or a GeoJSON geometry / `Feature` /
            `FeatureCollection` / `GeometryCollection` mapping.
        geometry_crs: CRS of the geometry's coordinates. Defaults to
            :data:`STAC_WINDOW_CRS` (EPSG:4326), which is what GeoJSON states;
            pass `None` for a geometry already in the asset's coordinates.
        signer: Optional signer, forwarded to :func:`~pyramids.stac.load_asset`.
        alternate: `alternate-assets` key (or keys in preference order) to
            prefer over the canonical href.
        **read_options: Forwarded verbatim to
            :meth:`~pyramids.dataset.Dataset.read_part` — `dst_width`,
            `dst_height`, `resampling`, `band`, `return_transform`.

    Returns:
        numpy.ndarray: The geometry's bounding window, shaped as
        :func:`read_item_part` returns it.

    Raises:
        ValueError: The geometry carries no coordinates.
        StacAssetError: The asset is missing or has no href.
        UnsupportedAssetError: The asset's type matches no reader, or opened as
            something with no windowed read (a 4-D Zarr cube).
        OutOfBoundsError: The envelope does not intersect the asset at all.

    Examples:
        - Read the window covering an AOI polygon:
            ```python
            >>> from pyramids.stac import read_item_feature  # doctest: +SKIP
            >>> aoi = {"type": "Polygon", "coordinates": [[[12.4, 41.8], [12.6, 41.8],
            ...                                            [12.6, 42.0], [12.4, 41.8]]]}
            >>> window = read_item_feature(item, "B04", aoi)  # doctest: +SKIP

            ```
    """
    return read_item_part(
        item_or_asset,
        asset_key,
        geometry_bounds(geometry),
        bbox_crs=geometry_crs,
        signer=signer,
        alternate=alternate,
        **read_options,
    )

Download to local files#

pyramids.stac.download #

Download STAC assets to local files via stac-asset (PC-3).

pyramids reads STAC assets lazily through GDAL /vsicurl/; some workflows want local copies instead (offline processing, repeated reads, archival). This module wraps stac_asset's synchronous download behind the optional [stac] extra, returning the local paths so they can feed :meth:pyramids.dataset.DatasetCollection.from_files.

Three entry points mirror the three stac_asset.blocking downloaders — one Item (:func:download_item), a whole ItemCollection (:func:download_item_collection, e.g. the result of :func:pyramids.stac.search), or a Collection (:func:download_collection). All three are blocking: the async stac_asset coroutines cannot run inside a live event loop, and pyramids exposes no async API.

stac-asset pulls heavy async dependencies (aiohttp, aiobotocore), so it is not a core dependency — it ships via the [stac] extra (alongside pystac-client). Install with one of:

  • PyPI: pip install 'pyramids-core[stac]'
  • conda-forge: conda install -c conda-forge pyramids-stac (stac-asset is not on conda-forge; install it alone with pip install stac-asset)

The per-protocol client (HTTP / S3 / Planetary Computer / Earthdata) is selected by stac_asset from each asset href.

download_item(item, directory, *, include=None, exclude=None, alternate_assets=None, s3_requester_pays=False, file_name_strategy=None, error_strategy=None, fail_fast=None, warn=None, max_concurrent=None) #

Download a STAC Item's assets to a local directory.

A thin, synchronous wrapper over stac_asset.blocking.download_item (the async download_item cannot run inside a live event loop). The per-protocol client is chosen by stac_asset from each asset href.

Parameters:

Name Type Description Default
item Any

A pystac.Item (stac-asset operates on pystac objects).

required
directory str | Path

Destination directory for the downloaded assets.

required
include list[str] | None

Optional asset keys to include (others skipped).

None
exclude list[str] | None

Optional asset keys to exclude.

None
alternate_assets list[str] | None

Keys of the alternate-assets extension to prefer over each asset's own href, in order (e.g. ["s3"] to pull from a bucket instead of the HTTPS mirror). Unset by default, which keeps the primary href.

None
s3_requester_pays bool

Opt into Requester-Pays for s3:// assets.

False
file_name_strategy Any | None

How downloaded files are named — a stac_asset.FileNameStrategy member or its name as a string ("file_name" keeps the href's file name, "key" names each file after its asset key). Defaults to stac-asset's FILE_NAME.

None
error_strategy Any | None

What happens to the already-downloaded files when a download fails — a stac_asset.ErrorStrategy member or its name ("delete" or "keep"). Defaults to stac-asset's DELETE.

None
fail_fast bool | None

Raise on the first asset error instead of gathering them all.

None
warn bool | None

Emit a warning per failed asset instead of raising.

None
max_concurrent int | None

Cap on concurrent asset downloads (stac-asset's max_concurrent_downloads, a downloader argument rather than a config field). Must be an integer >= 1; defaults to stac-asset's own limit.

None

Returns:

Type Description
Any

The downloaded pystac.Item (with asset hrefs rewritten to the local

Any

paths), as returned by stac_asset.

Raises:

Type Description
OptionalPackageDoesNotExist

When stac-asset is not installed.

ValueError

A strategy was given as a string naming no enum member, or max_concurrent is not an integer >= 1.

Examples:

  • Download an item's assets, then build a collection from the locals (requires the [stac] extra + network):
    >>> from pyramids.stac import download_item  # doctest: +SKIP
    >>> local = download_item(item, "scenes/")  # doctest: +SKIP
    >>> hrefs = [a.href for a in local.assets.values()]  # doctest: +SKIP
    
Source code in src/pyramids/stac/download.py
def download_item(
    item: Any,
    directory: str | Path,
    *,
    include: list[str] | None = None,
    exclude: list[str] | None = None,
    alternate_assets: list[str] | None = None,
    s3_requester_pays: bool = False,
    file_name_strategy: Any | None = None,
    error_strategy: Any | None = None,
    fail_fast: bool | None = None,
    warn: bool | None = None,
    max_concurrent: int | None = None,
) -> Any:
    """Download a STAC Item's assets to a local directory.

    A thin, synchronous wrapper over ``stac_asset.blocking.download_item`` (the
    async `download_item` cannot run inside a live event loop). The per-protocol
    client is chosen by `stac_asset` from each asset href.

    Args:
        item: A `pystac.Item` (stac-asset operates on pystac objects).
        directory: Destination directory for the downloaded assets.
        include: Optional asset keys to include (others skipped).
        exclude: Optional asset keys to exclude.
        alternate_assets: Keys of the `alternate-assets` extension to prefer
            over each asset's own href, in order (e.g. ``["s3"]`` to pull from a
            bucket instead of the HTTPS mirror). Unset by default, which keeps
            the primary href.
        s3_requester_pays: Opt into Requester-Pays for `s3://` assets.
        file_name_strategy: How downloaded files are named — a
            `stac_asset.FileNameStrategy` member or its name as a string
            (``"file_name"`` keeps the href's file name, ``"key"`` names each
            file after its asset key). Defaults to stac-asset's `FILE_NAME`.
        error_strategy: What happens to the already-downloaded files when a
            download fails — a `stac_asset.ErrorStrategy` member or its name
            (``"delete"`` or ``"keep"``). Defaults to stac-asset's `DELETE`.
        fail_fast: Raise on the first asset error instead of gathering them all.
        warn: Emit a warning per failed asset instead of raising.
        max_concurrent: Cap on concurrent asset downloads (stac-asset's
            `max_concurrent_downloads`, a downloader argument rather than a
            config field). Must be an integer >= 1; defaults to stac-asset's own limit.

    Returns:
        The downloaded `pystac.Item` (with asset hrefs rewritten to the local
        paths), as returned by `stac_asset`.

    Raises:
        OptionalPackageDoesNotExist: When `stac-asset` is not installed.
        ValueError: A strategy was given as a string naming no enum member,
            or `max_concurrent` is not an integer >= 1.

    Examples:
        - Download an item's assets, then build a collection from the locals
          (requires the `[stac]` extra + network):
            ```python
            >>> from pyramids.stac import download_item  # doctest: +SKIP
            >>> local = download_item(item, "scenes/")  # doctest: +SKIP
            >>> hrefs = [a.href for a in local.assets.values()]  # doctest: +SKIP

            ```
    """
    return _download(
        "download_item",
        item,
        directory,
        include=include,
        exclude=exclude,
        alternate_assets=alternate_assets,
        s3_requester_pays=s3_requester_pays,
        file_name_strategy=file_name_strategy,
        error_strategy=error_strategy,
        fail_fast=fail_fast,
        warn=warn,
        max_concurrent=max_concurrent,
    )

download_item_collection(item_collection, directory, *, include=None, exclude=None, alternate_assets=None, s3_requester_pays=False, file_name_strategy=None, error_strategy=None, fail_fast=None, warn=None, max_concurrent=None) #

Download every Item's assets from an ItemCollection to a local directory.

The many-item companion of :func:download_item, wrapping stac_asset.blocking.download_item_collection. Hand it the result of :func:pyramids.stac.search to take a whole search result offline; the returned ItemCollection can then feed :meth:pyramids.dataset.DatasetCollection.from_files.

Parameters:

Name Type Description Default
item_collection Any

A pystac.ItemCollection.

required
directory str | Path

Destination directory for the downloaded assets.

required
include list[str] | None

Optional asset keys to include (others skipped).

None
exclude list[str] | None

Optional asset keys to exclude.

None
alternate_assets list[str] | None

Keys of the alternate-assets extension to prefer over each asset's own href, in order.

None
s3_requester_pays bool

Opt into Requester-Pays for s3:// assets.

False
file_name_strategy Any | None

How downloaded files are named — a stac_asset.FileNameStrategy member or its name as a string ("file_name" or "key").

None
error_strategy Any | None

What happens to the already-downloaded files when a download fails — a stac_asset.ErrorStrategy member or its name ("delete" or "keep").

None
fail_fast bool | None

Raise on the first asset error instead of gathering them all.

None
warn bool | None

Emit a warning per failed asset instead of raising.

None
max_concurrent int | None

Cap on concurrent asset downloads (stac-asset's max_concurrent_downloads). Must be an integer >= 1; defaults to stac-asset's own limit.

None

Returns:

Type Description
Any

The downloaded pystac.ItemCollection, every asset href rewritten to

Any

its local path, as returned by stac_asset.

Raises:

Type Description
OptionalPackageDoesNotExist

When stac-asset is not installed.

ValueError

A strategy was given as a string naming no enum member, or max_concurrent is not an integer >= 1.

Examples:

  • Take a whole search result offline (requires the [stac] extra + network):
    >>> from pyramids.stac import download_item_collection  # doctest: +SKIP
    >>> local = download_item_collection(  # doctest: +SKIP
    ...     items, "scenes/", include=["B04"], max_concurrent=8
    ... )
    
Source code in src/pyramids/stac/download.py
def download_item_collection(
    item_collection: Any,
    directory: str | Path,
    *,
    include: list[str] | None = None,
    exclude: list[str] | None = None,
    alternate_assets: list[str] | None = None,
    s3_requester_pays: bool = False,
    file_name_strategy: Any | None = None,
    error_strategy: Any | None = None,
    fail_fast: bool | None = None,
    warn: bool | None = None,
    max_concurrent: int | None = None,
) -> Any:
    """Download every Item's assets from an ItemCollection to a local directory.

    The many-item companion of :func:`download_item`, wrapping
    ``stac_asset.blocking.download_item_collection``. Hand it the result of
    :func:`pyramids.stac.search` to take a whole search result offline; the
    returned ItemCollection can then feed
    :meth:`pyramids.dataset.DatasetCollection.from_files`.

    Args:
        item_collection: A `pystac.ItemCollection`.
        directory: Destination directory for the downloaded assets.
        include: Optional asset keys to include (others skipped).
        exclude: Optional asset keys to exclude.
        alternate_assets: Keys of the `alternate-assets` extension to prefer
            over each asset's own href, in order.
        s3_requester_pays: Opt into Requester-Pays for `s3://` assets.
        file_name_strategy: How downloaded files are named — a
            `stac_asset.FileNameStrategy` member or its name as a string
            (``"file_name"`` or ``"key"``).
        error_strategy: What happens to the already-downloaded files when a
            download fails — a `stac_asset.ErrorStrategy` member or its name
            (``"delete"`` or ``"keep"``).
        fail_fast: Raise on the first asset error instead of gathering them all.
        warn: Emit a warning per failed asset instead of raising.
        max_concurrent: Cap on concurrent asset downloads (stac-asset's
            `max_concurrent_downloads`). Must be an integer >= 1; defaults
            to stac-asset's own limit.

    Returns:
        The downloaded `pystac.ItemCollection`, every asset href rewritten to
        its local path, as returned by `stac_asset`.

    Raises:
        OptionalPackageDoesNotExist: When `stac-asset` is not installed.
        ValueError: A strategy was given as a string naming no enum member,
            or `max_concurrent` is not an integer >= 1.

    Examples:
        - Take a whole search result offline (requires the `[stac]` extra +
          network):
            ```python
            >>> from pyramids.stac import download_item_collection  # doctest: +SKIP
            >>> local = download_item_collection(  # doctest: +SKIP
            ...     items, "scenes/", include=["B04"], max_concurrent=8
            ... )

            ```
    """
    return _download(
        "download_item_collection",
        item_collection,
        directory,
        include=include,
        exclude=exclude,
        alternate_assets=alternate_assets,
        s3_requester_pays=s3_requester_pays,
        file_name_strategy=file_name_strategy,
        error_strategy=error_strategy,
        fail_fast=fail_fast,
        warn=warn,
        max_concurrent=max_concurrent,
    )

download_collection(collection, directory, *, include=None, exclude=None, alternate_assets=None, s3_requester_pays=False, file_name_strategy=None, error_strategy=None, fail_fast=None, warn=None, max_concurrent=None) #

Download a STAC Collection's own assets to a local directory.

Wraps stac_asset.blocking.download_collection, which fetches the assets hanging off the Collection itself (overviews, thumbnails, licences — not the assets of its Items; for those, search the Collection and call :func:download_item_collection).

Parameters:

Name Type Description Default
collection Any

A pystac.Collection.

required
directory str | Path

Destination directory for the downloaded assets.

required
include list[str] | None

Optional asset keys to include (others skipped).

None
exclude list[str] | None

Optional asset keys to exclude.

None
alternate_assets list[str] | None

Keys of the alternate-assets extension to prefer over each asset's own href, in order.

None
s3_requester_pays bool

Opt into Requester-Pays for s3:// assets.

False
file_name_strategy Any | None

How downloaded files are named — a stac_asset.FileNameStrategy member or its name as a string ("file_name" or "key").

None
error_strategy Any | None

What happens to the already-downloaded files when a download fails — a stac_asset.ErrorStrategy member or its name ("delete" or "keep").

None
fail_fast bool | None

Raise on the first asset error instead of gathering them all.

None
warn bool | None

Emit a warning per failed asset instead of raising.

None
max_concurrent int | None

Cap on concurrent asset downloads (stac-asset's max_concurrent_downloads). Must be an integer >= 1; defaults to stac-asset's own limit.

None

Returns:

Type Description
Any

The downloaded pystac.Collection, its asset hrefs rewritten to the

Any

local paths, as returned by stac_asset.

Raises:

Type Description
OptionalPackageDoesNotExist

When stac-asset is not installed.

ValueError

A strategy was given as a string naming no enum member, or max_concurrent is not an integer >= 1.

Examples:

  • Fetch a collection's own assets (requires the [stac] extra + network):
    >>> from pyramids.stac import download_collection  # doctest: +SKIP
    >>> local = download_collection(collection, "catalog/")  # doctest: +SKIP
    
Source code in src/pyramids/stac/download.py
def download_collection(
    collection: Any,
    directory: str | Path,
    *,
    include: list[str] | None = None,
    exclude: list[str] | None = None,
    alternate_assets: list[str] | None = None,
    s3_requester_pays: bool = False,
    file_name_strategy: Any | None = None,
    error_strategy: Any | None = None,
    fail_fast: bool | None = None,
    warn: bool | None = None,
    max_concurrent: int | None = None,
) -> Any:
    """Download a STAC Collection's own assets to a local directory.

    Wraps ``stac_asset.blocking.download_collection``, which fetches the assets
    hanging off the Collection itself (overviews, thumbnails, licences — not the
    assets of its Items; for those, search the Collection and call
    :func:`download_item_collection`).

    Args:
        collection: A `pystac.Collection`.
        directory: Destination directory for the downloaded assets.
        include: Optional asset keys to include (others skipped).
        exclude: Optional asset keys to exclude.
        alternate_assets: Keys of the `alternate-assets` extension to prefer
            over each asset's own href, in order.
        s3_requester_pays: Opt into Requester-Pays for `s3://` assets.
        file_name_strategy: How downloaded files are named — a
            `stac_asset.FileNameStrategy` member or its name as a string
            (``"file_name"`` or ``"key"``).
        error_strategy: What happens to the already-downloaded files when a
            download fails — a `stac_asset.ErrorStrategy` member or its name
            (``"delete"`` or ``"keep"``).
        fail_fast: Raise on the first asset error instead of gathering them all.
        warn: Emit a warning per failed asset instead of raising.
        max_concurrent: Cap on concurrent asset downloads (stac-asset's
            `max_concurrent_downloads`). Must be an integer >= 1; defaults
            to stac-asset's own limit.

    Returns:
        The downloaded `pystac.Collection`, its asset hrefs rewritten to the
        local paths, as returned by `stac_asset`.

    Raises:
        OptionalPackageDoesNotExist: When `stac-asset` is not installed.
        ValueError: A strategy was given as a string naming no enum member,
            or `max_concurrent` is not an integer >= 1.

    Examples:
        - Fetch a collection's own assets (requires the `[stac]` extra +
          network):
            ```python
            >>> from pyramids.stac import download_collection  # doctest: +SKIP
            >>> local = download_collection(collection, "catalog/")  # doctest: +SKIP

            ```
    """
    return _download(
        "download_collection",
        collection,
        directory,
        include=include,
        exclude=exclude,
        alternate_assets=alternate_assets,
        s3_requester_pays=s3_requester_pays,
        file_name_strategy=file_name_strategy,
        error_strategy=error_strategy,
        fail_fast=fail_fast,
        warn=warn,
        max_concurrent=max_concurrent,
    )

GeoParquet round-trip#

pyramids.stac._geoparquet #

Serialize STAC Items to/from GeoParquet (PD-3).

stac-geoparquet stores a STAC ItemCollection as one columnar GeoParquet file (geometry as WKB, WGS84) for bulk transfer + fast spatial filtering, avoiding thousands of per-item JSON requests. pyramids already has geopandas (core) and a FeatureCollection (a GeoDataFrame subclass) with GeoParquet I/O, plus the [parquet] extra (pyarrow) — so the round-trip needs no new dependency.

Two on-disk layouts live here, sharing one pair of entry points:

  • The JSON-blob layout (the default, spec=False): each row carries the item geometry (so the file is a valid, spatially-filterable GeoParquet) plus the full STAC Item as a stac_item JSON column, so :func:from_geoparquet reconstructs the exact item dicts — ready to feed :meth:pyramids.dataset.DatasetCollection.from_stac. Lossless, but only pyramids knows how to read the blob back.
  • The spec layout (spec=True, or :func:to_geoparquet_spec / :func:from_geoparquet_spec): the STAC-GeoParquet 1.1 shape — properties flattened into top-level typed columns, WKB geometry in OGC:CRS84, a bbox struct, and assets / links as Arrow structs. That is what lets DuckDB, pyarrow and the rest of the STAC tooling push spatial and attribute predicates down into the file. Written with pyarrow only — the stac-geoparquet package is deliberately not a dependency. It preserves every value, but the round trip is not an identity on the item dict: see the normalisations listed on :func:from_geoparquet_spec.

Requires the [parquet] extra (pyarrow) for the Parquet read/write itself.

to_geoparquet(items, path, *, spec=False) #

Write a sequence of STAC Items to a GeoParquet file.

With spec=False (the default) each item becomes a row carrying its geometry (a valid, spatially-filterable GeoParquet geometry in EPSG:4326) and the full item as a JSON column. With spec=True the whole write is delegated to :func:to_geoparquet_spec, which produces the columnar STAC-GeoParquet 1.1 layout instead — different columns, different metadata, and the extra guards listed under Raises.

Parameters:

Name Type Description Default
items Any

Iterable of STAC Items (pystac.Item objects with to_dict(), or raw STAC-JSON dicts — e.g. from :meth:pyramids.dataset.Dataset.to_stac_item).

required
path str | Path

Destination .parquet path.

required
spec bool

Write the interoperable STAC-GeoParquet 1.1 layout (flattened property columns, WKB geometry in OGC:CRS84, bbox struct, assets / links structs) by delegating to :func:to_geoparquet_spec, instead of the pyramids JSON-blob layout. Defaults to False, which keeps the historical behaviour byte for byte.

False

Raises:

Type Description
ValueError

When items is empty. With spec=True, also when a properties key collides with a reserved spec column name, or when an item's bbox is neither 4- nor 6-valued.

TypeError

When an item is neither a dict nor exposes to_dict(); with spec=True, also when a column's values can be stored neither as Arrow nor as JSON.

OptionalPackageDoesNotExist

When pyarrow (the [parquet] extra) is not installed (raised by FeatureCollection.to_parquet, or directly when spec=True).

Examples:

  • Round-trip a couple of item dicts through GeoParquet:
    >>> import tempfile, os
    >>> from pyramids.stac import to_geoparquet, from_geoparquet  # doctest: +SKIP
    >>> items = [{"id": "a", "geometry": {"type": "Point", "coordinates": [1.0, 2.0]},
    ...           "properties": {"datetime": "2023-01-01T00:00:00Z"}, "assets": {}}]
    >>> path = os.path.join(tempfile.mkdtemp(), "items.parquet")  # doctest: +SKIP
    >>> to_geoparquet(items, path)  # doctest: +SKIP
    >>> from_geoparquet(path)[0]["id"]  # doctest: +SKIP
    'a'
    
See Also

to_geoparquet_spec: The spec layout written directly, without the flag.

Source code in src/pyramids/stac/_geoparquet.py
def to_geoparquet(items: Any, path: str | Path, *, spec: bool = False) -> None:
    """Write a sequence of STAC Items to a GeoParquet file.

    With `spec=False` (the default) each item becomes a row carrying its
    geometry (a valid, spatially-filterable GeoParquet geometry in EPSG:4326)
    and the full item as a JSON column. With `spec=True` the whole write is
    delegated to :func:`to_geoparquet_spec`, which produces the columnar
    STAC-GeoParquet 1.1 layout instead — different columns, different
    metadata, and the extra guards listed under `Raises`.

    Args:
        items: Iterable of STAC Items (`pystac.Item` objects with `to_dict()`,
            or raw STAC-JSON dicts — e.g. from
            :meth:`pyramids.dataset.Dataset.to_stac_item`).
        path: Destination `.parquet` path.
        spec: Write the interoperable STAC-GeoParquet 1.1 layout (flattened
            property columns, WKB geometry in OGC:CRS84, `bbox` struct,
            `assets` / `links` structs) by delegating to
            :func:`to_geoparquet_spec`, instead of the pyramids JSON-blob
            layout. Defaults to `False`, which keeps the historical behaviour
            byte for byte.

    Raises:
        ValueError: When `items` is empty. With `spec=True`, also when a
            `properties` key collides with a reserved spec column name, or
            when an item's `bbox` is neither 4- nor 6-valued.
        TypeError: When an item is neither a dict nor exposes `to_dict()`;
            with `spec=True`, also when a column's values can be stored
            neither as Arrow nor as JSON.
        OptionalPackageDoesNotExist: When pyarrow (the `[parquet]` extra) is not
            installed (raised by `FeatureCollection.to_parquet`, or directly
            when `spec=True`).

    Examples:
        - Round-trip a couple of item dicts through GeoParquet:
            ```python
            >>> import tempfile, os
            >>> from pyramids.stac import to_geoparquet, from_geoparquet  # doctest: +SKIP
            >>> items = [{"id": "a", "geometry": {"type": "Point", "coordinates": [1.0, 2.0]},
            ...           "properties": {"datetime": "2023-01-01T00:00:00Z"}, "assets": {}}]
            >>> path = os.path.join(tempfile.mkdtemp(), "items.parquet")  # doctest: +SKIP
            >>> to_geoparquet(items, path)  # doctest: +SKIP
            >>> from_geoparquet(path)[0]["id"]  # doctest: +SKIP
            'a'

            ```

    See Also:
        to_geoparquet_spec: The spec layout written directly, without the flag.
    """
    if spec:
        to_geoparquet_spec(items, path)
        return

    from pyramids.feature import FeatureCollection

    rows = []
    geometries = []
    for item in items:
        as_dict = _item_to_dict(item)
        geometries.append(_item_geometry(as_dict))
        rows.append({"id": as_dict.get("id"), _ITEM_COLUMN: json.dumps(as_dict)})

    if not rows:
        raise _no_items_error("to_geoparquet")

    fc = FeatureCollection(rows, geometry=geometries, crs="EPSG:4326")
    fc.to_parquet(str(path))

from_geoparquet(path, *, spec=False) #

Read STAC Items back from a GeoParquet written by :func:to_geoparquet.

Both layouts are readable through this one entry point. With spec=False (the default) the file's metadata is probed first: a file carrying the stac-geoparquet key goes to the spec reader, so a spec file reads correctly even when the flag was not passed, and everything else takes the unchanged JSON-blob path. Passing spec=True selects the spec reader outright.

Parameters:

Name Type Description Default
path str | Path

Path to a .parquet file produced by :func:to_geoparquet (either layout).

required
spec bool

Read the file as the STAC-GeoParquet 1.1 layout by delegating to :func:from_geoparquet_spec. Defaults to False (JSON-blob, with the auto-detect fallback described above).

False

Returns:

Type Description
list[dict[str, Any]]

The list of STAC Item dicts (ready for

list[dict[str, Any]]

meth:pyramids.dataset.DatasetCollection.from_stac).

Raises:

Type Description
OptionalPackageDoesNotExist

When pyarrow (the [parquet] extra) is not installed (raised by FeatureCollection.read_parquet).

KeyError

When the file lacks the stac_item JSON column and carries no STAC-GeoParquet file metadata (so it was written by neither layout).

ValueError

When spec=True is passed for a file that is not in the spec layout (raised by :func:from_geoparquet_spec).

See Also

from_geoparquet_spec: The spec layout read directly, without the flag.

Source code in src/pyramids/stac/_geoparquet.py
def from_geoparquet(path: str | Path, *, spec: bool = False) -> list[dict[str, Any]]:
    """Read STAC Items back from a GeoParquet written by :func:`to_geoparquet`.

    Both layouts are readable through this one entry point. With `spec=False`
    (the default) the file's metadata is probed first: a file carrying the
    `stac-geoparquet` key goes to the spec reader, so a spec file reads
    correctly even when the flag was not passed, and everything else takes the
    unchanged JSON-blob path. Passing `spec=True` selects the spec reader
    outright.

    Args:
        path: Path to a `.parquet` file produced by :func:`to_geoparquet` (either
            layout).
        spec: Read the file as the STAC-GeoParquet 1.1 layout by delegating to
            :func:`from_geoparquet_spec`. Defaults to `False` (JSON-blob, with
            the auto-detect fallback described above).

    Returns:
        The list of STAC Item dicts (ready for
        :meth:`pyramids.dataset.DatasetCollection.from_stac`).

    Raises:
        OptionalPackageDoesNotExist: When pyarrow (the `[parquet]` extra) is not
            installed (raised by `FeatureCollection.read_parquet`).
        KeyError: When the file lacks the `stac_item` JSON column and carries no
            STAC-GeoParquet file metadata (so it was written by neither layout).
        ValueError: When `spec=True` is passed for a file that is not in the
            spec layout (raised by :func:`from_geoparquet_spec`).

    See Also:
        from_geoparquet_spec: The spec layout read directly, without the flag.
    """
    if spec or _is_spec_geoparquet(path):
        return from_geoparquet_spec(path)

    from pyramids.feature import FeatureCollection

    fc = FeatureCollection.read_parquet(str(path))
    return [json.loads(blob) for blob in fc[_ITEM_COLUMN]]

to_geoparquet_spec(items, path) #

Write STAC Items as a spec-compliant STAC-GeoParquet 1.1 file.

This is the interoperable, columnar layout — the one DuckDB, pyarrow and the wider STAC tooling can filter on without decoding a JSON blob per row. It is produced with pyarrow only; the stac-geoparquet package is not a dependency of pyramids.

On-disk columns, in order:

  • type, stac_version, stac_extensions, id — the item envelope.
  • geometry — WKB, OGC:CRS84 (written as PROJJSON in the GeoParquet geo file metadata, not left to the reader's default).
  • bbox — struct<xmin, ymin, xmax, ymax>, widened to struct<xmin, ymin, zmin, xmax, ymax, zmax> as soon as any item carries a 3D bbox. It is registered as the geometry column's covering only when every row has a bbox: a covering that does not span a row would make a spatial-predicate pushdown skip it.
  • collection — always emitted, null for items without one.
  • datetime — timestamp[us, UTC], always emitted (an all-null column included), unless a value is not an RFC 3339 timestamp at all.
  • One column per remaining properties key, alphabetically, Arrow-typed.
  • One column per extra item-root key, alphabetically.
  • links, assets — Arrow structs (assets keyed by asset name) when at least one item carries them; a null string column when no item does, since a struct has no fields to infer then.

A column Arrow cannot type — or would type only by widening an int to a float, or by inferring a zero-field struct from an empty dict such as the "assets": {} of an asset-less item — is JSON-encoded instead, which keeps the values intact at the cost of that column's queryability.

Three file-metadata keys are written: geo (GeoParquet 1.1), stac-geoparquet (the spec version) and pyramids:stac-geoparquet (which columns are JSON-encoded and which belong at the item root — read back by :func:from_geoparquet_spec, and ignorable by every other reader).

The file is written through a sibling temporary file and moved into place, so a failed write leaves neither a truncated new file nor a damaged previous one.

Parameters:

Name Type Description Default
items Any

Iterable of STAC Items (pystac.Item objects with to_dict(), or raw STAC-JSON dicts — e.g. from :meth:pyramids.dataset.Dataset.to_stac_item).

required
path str | Path

Destination .parquet path.

required

Raises:

Type Description
ValueError

When items is empty, when a properties key collides with one of the reserved spec column names (id, geometry, bbox, assets, …) — the item could not be reconstructed — or when an item's bbox holds neither 4 nor 6 values.

TypeError

When an item is neither a dict nor exposes to_dict(), or when a column's values can be stored neither as Arrow nor as JSON.

OptionalPackageDoesNotExist

When pyarrow (the [parquet] extra) is not installed.

Examples:

  • Write the columnar layout and read it with plain pyarrow:
    >>> import os, tempfile  # doctest: +SKIP
    >>> import pyarrow.parquet as pq  # doctest: +SKIP
    >>> from pyramids.stac import to_geoparquet_spec  # doctest: +SKIP
    >>> items = [{"id": "a", "geometry": {"type": "Point", "coordinates": [1.0, 2.0]},
    ...           "bbox": [1.0, 2.0, 1.0, 2.0], "assets": {},
    ...           "properties": {"datetime": "2023-01-01T00:00:00Z"}}]
    >>> path = os.path.join(tempfile.mkdtemp(), "spec.parquet")  # doctest: +SKIP
    >>> to_geoparquet_spec(items, path)  # doctest: +SKIP
    >>> "datetime" in pq.read_table(path).column_names  # doctest: +SKIP
    True
    
See Also

to_geoparquet: The pyramids JSON-blob layout (lossless, pyramids-only). from_geoparquet_spec: The matching reader.

Source code in src/pyramids/stac/_geoparquet.py
def to_geoparquet_spec(items: Any, path: str | Path) -> None:
    """Write STAC Items as a spec-compliant STAC-GeoParquet 1.1 file.

    This is the interoperable, columnar layout — the one DuckDB, pyarrow and the
    wider STAC tooling can filter on without decoding a JSON blob per row. It is
    produced with **pyarrow only**; the `stac-geoparquet` package is not a
    dependency of pyramids.

    On-disk columns, in order:

    - `type`, `stac_version`, `stac_extensions`, `id` — the item envelope.
    - `geometry` — WKB, OGC:CRS84 (written as PROJJSON in the GeoParquet `geo`
      file metadata, not left to the reader's default).
    - `bbox` — `struct<xmin, ymin, xmax, ymax>`, widened to
      `struct<xmin, ymin, zmin, xmax, ymax, zmax>` as soon as any item carries
      a 3D bbox. It is registered as the geometry column's `covering` only when
      *every* row has a bbox: a covering that does not span a row would make a
      spatial-predicate pushdown skip it.
    - `collection` — always emitted, null for items without one.
    - `datetime` — `timestamp[us, UTC]`, always emitted (an all-null column
      included), unless a value is not an RFC 3339 timestamp at all.
    - One column per remaining `properties` key, alphabetically, Arrow-typed.
    - One column per extra item-root key, alphabetically.
    - `links`, `assets` — Arrow structs (`assets` keyed by asset name) when at
      least one item carries them; a null string column when no item does,
      since a struct has no fields to infer then.

    A column Arrow cannot type — or would type only by widening an `int` to a
    `float`, or by inferring a zero-field struct from an empty dict such as the
    `"assets": {}` of an asset-less item — is JSON-encoded instead, which keeps
    the values intact at the cost of that column's queryability.

    Three file-metadata keys are written: `geo` (GeoParquet 1.1),
    `stac-geoparquet` (the spec version) and `pyramids:stac-geoparquet` (which
    columns are JSON-encoded and which belong at the item root — read back by
    :func:`from_geoparquet_spec`, and ignorable by every other reader).

    The file is written through a sibling temporary file and moved into place,
    so a failed write leaves neither a truncated new file nor a damaged
    previous one.

    Args:
        items: Iterable of STAC Items (`pystac.Item` objects with `to_dict()`,
            or raw STAC-JSON dicts — e.g. from
            :meth:`pyramids.dataset.Dataset.to_stac_item`).
        path: Destination `.parquet` path.

    Raises:
        ValueError: When `items` is empty, when a `properties` key collides
            with one of the reserved spec column names (`id`, `geometry`,
            `bbox`, `assets`, …) — the item could not be reconstructed — or
            when an item's `bbox` holds neither 4 nor 6 values.
        TypeError: When an item is neither a dict nor exposes `to_dict()`, or
            when a column's values can be stored neither as Arrow nor as JSON.
        OptionalPackageDoesNotExist: When pyarrow (the `[parquet]` extra) is not
            installed.

    Examples:
        - Write the columnar layout and read it with plain pyarrow:
            ```python
            >>> import os, tempfile  # doctest: +SKIP
            >>> import pyarrow.parquet as pq  # doctest: +SKIP
            >>> from pyramids.stac import to_geoparquet_spec  # doctest: +SKIP
            >>> items = [{"id": "a", "geometry": {"type": "Point", "coordinates": [1.0, 2.0]},
            ...           "bbox": [1.0, 2.0, 1.0, 2.0], "assets": {},
            ...           "properties": {"datetime": "2023-01-01T00:00:00Z"}}]
            >>> path = os.path.join(tempfile.mkdtemp(), "spec.parquet")  # doctest: +SKIP
            >>> to_geoparquet_spec(items, path)  # doctest: +SKIP
            >>> "datetime" in pq.read_table(path).column_names  # doctest: +SKIP
            True

            ```

    See Also:
        to_geoparquet: The pyramids JSON-blob layout (lossless, pyramids-only).
        from_geoparquet_spec: The matching reader.
    """
    arrow, parquet = _require_parquet()

    item_dicts = [_item_to_dict(item) for item in items]
    if not item_dicts:
        raise _no_items_error("to_geoparquet_spec")

    property_keys = _property_keys(item_dicts)
    root_extras = _root_extra_keys(item_dicts)
    reserved = _reserved_name_error(property_keys, root_extras)
    if reserved is not None:
        raise reserved

    geometries = [_item_geometry(item) for item in item_dicts]
    columns = _SpecColumns()
    for key in _ENVELOPE_COLUMNS:
        values = _column_values(item_dicts, key, property_keys)
        columns.add(key, *_spec_typed_column(arrow, key, values))

    columns.add(_GEOMETRY_COLUMN, _geometry_column(arrow, geometries))
    bbox_array = _bbox_column(arrow, item_dicts)
    columns.add(_BBOX_COLUMN, bbox_array)

    for key in ("collection", *property_keys, *root_extras, *_TRAILING_COLUMNS):
        values = _column_values(item_dicts, key, property_keys)
        columns.add(key, *_spec_body_column(arrow, key, values))

    table = columns.table(arrow)
    metadata = _spec_metadata(
        geometries, bbox_array.null_count == 0, columns.json_columns, root_extras
    )
    _write_table_atomically(parquet, table.replace_schema_metadata(metadata), path)

from_geoparquet_spec(path) #

Read STAC Items back from a spec-compliant STAC-GeoParquet file.

The inverse of :func:to_geoparquet_spec, and tolerant of files written by other STAC-GeoParquet writers: the reserved spec columns are recognised by name, every other column is read back as a properties entry, timestamp columns are rendered to their RFC 3339 strings and the WKB geometry is decoded to GeoJSON. Null cells become absent keys, which is what restores the original sparse items out of Arrow's rectangular structs.

Four normalisations are inherent to the columnar layout, and are the same ones the stac-geoparquet package applies — so the round trip preserves every value, but is not an identity on the item dict:

  • Timestamps come back in the Z spelling (...T00:00:00Z), whatever offset spelling they were written with, because the column stores a real instant rather than the original text.
  • Timestamps are stored with microsecond resolution, so a nanosecond timestamp loses its last three digits (...05.123456789Z → ...05.123456Z).
  • A key whose value was explicitly null comes back absent, since the column cannot tell "null" from "not set". That applies to a null geometry and to a key nested inside assets just as much as to a top-level property.
  • Every item gains a properties dict, empty when the row held no property values, even if the written item carried no properties key at all.

Parameters:

Name Type Description Default
path str | Path

Path to a .parquet file in the STAC-GeoParquet 1.1 layout.

required

Returns:

Type Description
list[dict[str, Any]]

The list of STAC Item dicts (ready for

list[dict[str, Any]]

meth:pyramids.dataset.DatasetCollection.from_stac). Every item

list[dict[str, Any]]

carries a properties dict, empty when the row held no property values.

Raises:

Type Description
OptionalPackageDoesNotExist

When pyarrow (the [parquet] extra) is not installed.

ValueError

When the file is not in the spec layout — the pyramids JSON-blob layout (read it with :func:from_geoparquet instead), or a Parquet file carrying neither the stac-geoparquet metadata nor any reserved spec column.

See Also

to_geoparquet_spec: The matching writer. from_geoparquet: Reads either layout, auto-detecting the spec one.

Source code in src/pyramids/stac/_geoparquet.py
def from_geoparquet_spec(path: str | Path) -> list[dict[str, Any]]:
    """Read STAC Items back from a spec-compliant STAC-GeoParquet file.

    The inverse of :func:`to_geoparquet_spec`, and tolerant of files written by
    other STAC-GeoParquet writers: the reserved spec columns are recognised by
    name, every other column is read back as a `properties` entry, timestamp
    columns are rendered to their RFC 3339 strings and the WKB geometry is
    decoded to GeoJSON. Null cells become absent keys, which is what restores
    the original sparse items out of Arrow's rectangular structs.

    Four normalisations are inherent to the columnar layout, and are the same
    ones the `stac-geoparquet` package applies — so the round trip preserves
    every *value*, but is **not** an identity on the item dict:

    - Timestamps come back in the `Z` spelling (`...T00:00:00Z`), whatever
      offset spelling they were written with, because the column stores a real
      instant rather than the original text.
    - Timestamps are stored with microsecond resolution, so a nanosecond
      timestamp loses its last three digits (`...05.123456789Z` →
      `...05.123456Z`).
    - A key whose value was explicitly `null` comes back absent, since the
      column cannot tell "null" from "not set". That applies to a `null`
      `geometry` and to a key nested inside `assets` just as much as to a
      top-level property.
    - Every item gains a `properties` dict, empty when the row held no property
      values, even if the written item carried no `properties` key at all.

    Args:
        path: Path to a `.parquet` file in the STAC-GeoParquet 1.1 layout.

    Returns:
        The list of STAC Item dicts (ready for
        :meth:`pyramids.dataset.DatasetCollection.from_stac`). Every item
        carries a `properties` dict, empty when the row held no property values.

    Raises:
        OptionalPackageDoesNotExist: When pyarrow (the `[parquet]` extra) is not
            installed.
        ValueError: When the file is not in the spec layout — the pyramids
            JSON-blob layout (read it with :func:`from_geoparquet` instead), or
            a Parquet file carrying neither the `stac-geoparquet` metadata nor
            any reserved spec column.

    See Also:
        to_geoparquet_spec: The matching writer.
        from_geoparquet: Reads either layout, auto-detecting the spec one.
    """
    _, parquet = _require_parquet()

    table = parquet.read_table(str(path))
    metadata = table.schema.metadata or {}
    _require_spec_layout(path, metadata, table.column_names)
    hints = _SpecHints.read(metadata)
    columns = {name: table.column(name).to_pylist() for name in table.column_names}
    return [_spec_item(columns, row, hints) for row in range(table.num_rows)]