Skip to content

STAC Subpackage#

The pyramids.stac subpackage connects pyramids to the SpatioTemporal Asset Catalog ecosystem — searching STAC APIs, reading assets as pyramids Dataset / NetCDF objects, building cubes, signing cloud credentials, and writing STAC Items back out. Everything is GDAL-native (no rasterio, no xarray) and duck-typed over the STAC Item/Asset contract, so pystac is never imported by pyramids itself — raw STAC JSON dicts work as well as pystac.Item objects.

Module layout#

Hold "Ctrl" to enable pan & zoom
classDiagram
    class Signer {
        <<protocol>>
        +name
        +sign_request(request)
        +sign_item(item)
        +sign_href(href) str
        +gdal_env() dict
    }
    class AnonymousSigner
    class AWSRequesterPaysSigner
    class BearerTokenSigner
    class PlanetaryComputerSigner
    class EarthdataSigner
    class CDSESigner
    Signer <|.. AnonymousSigner
    Signer <|.. AWSRequesterPaysSigner
    Signer <|.. BearerTokenSigner
    Signer <|.. PlanetaryComputerSigner
    Signer <|.. EarthdataSigner
    Signer <|.. CDSESigner

Public API at a glance#

Concern Symbol Page
Open a STAC API client open_client Client & search
Typed item search search (bbox/intersects/datetime/query/CQL2) Client & search
Read one asset → Dataset/NetCDF load_asset · which_engine · resolved_href Assets
Read proj/raster/eo metadata (no file open) read_extension_metadata Assets
Mosaic one asset across items (lazy VRT) build_vrt_from_stac Assets
Download assets to local files ([stac] extra) download_item Assets
Serialize items ↔ GeoParquet ([parquet] extra) to_geoparquet · from_geoparquet Assets
Cloud credentials (6 signers) Signer protocol + concrete signers Signers

Two STAC entry points live on the raster classes (documented there):

  • DatasetCollection.from_stac(items, asset, *, signer, align, skip_missing, groupby, like, crs, resolution, bounds) — build a time-stacked cube from STAC items (single asset → time stack, or a list of assets → band axis; groupby="solar_day" mosaics same-overpass tiles; like=/crs+resolution+bounds matches a target grid). See DatasetCollection.
  • DatasetCollection.from_point(lat, lon, *, collection, bands, start_date, end_date, edge_size, resolution) — a cubo-style point + edge-size convenience cube. See DatasetCollection.
  • Dataset.to_stac_item(item_id, *, asset_href, …) — describe a raster as a STAC Item dict (proj + raster extensions). See I/O.

Install#

open_client / search need the [stac] extra (which also bundles the optional asset downloader stac-asset); read_extension_metadata, load_asset, build_vrt_from_stac, to_stac_item, and the signers need only core pyramids:

pip install 'pyramids-gis[stac]'           # client/search + download_item
pip install 'pyramids-gis[stac,parquet]'   # + GeoParquet round-trip

See the STAC tutorial for an end-to-end walkthrough, the offline STAC notebook, and the live-endpoint notebooks for Earth Search (anonymous) and the Planetary Computer (signed).

pyramids.stac.client #

Open a pystac-client Client with a pyramids signer wired in.

This thin wrapper attaches a :class:~pyramids.stac.signers.Signer to both pystac-client hooks at once — modifier (post-response Item rewrite) and request_modifier (pre-send HTTP request signing) — so the caller never has to remember which boundary a given signer cares about.

pystac-client is an optional dependency. Install with one of:

  • PyPI: pip install 'pyramids-gis[stac]'
  • conda-forge: conda install -c conda-forge pyramids-stac

open_client(url, *, signer=None, headers=None, timeout=30) #

Open a STAC API client with a pyramids signer wired into both hooks.

Parameters:

Name Type Description Default
url str

STAC API root URL (e.g. an .../stac/v1 endpoint).

required
signer Signer | None

A :class:~pyramids.stac.signers.Signer. Defaults to :class:~pyramids.stac.signers.AnonymousSigner (no credentials). The signer's sign_item is passed as the client modifier and its sign_request as the request_modifier.

None
headers dict[str, str] | None

Optional static HTTP headers added to every request.

None
timeout float | None

Per-request timeout in seconds.

30

Returns:

Type Description
Any

A pystac_client.Client instance.

Raises:

Type Description
OptionalPackageDoesNotExist

When pystac-client is not installed.

Examples:

  • Open a public catalog anonymously (requires the [stac] extra):
    >>> from pyramids.stac import open_client  # doctest: +SKIP
    >>> client = open_client("https://earth-search.aws.element84.com/v1")  # doctest: +SKIP
    >>> search = client.search(collections=["sentinel-2-l2a"], max_items=1)  # doctest: +SKIP
    >>> next(search.items()).collection_id  # doctest: +SKIP
    'sentinel-2-l2a'
    
  • Wire a bearer-token signer into both hooks:
    >>> from pyramids.stac import open_client, BearerTokenSigner  # doctest: +SKIP
    >>> client = open_client(  # doctest: +SKIP
    ...     "https://stac.dataspace.copernicus.eu/v1",
    ...     signer=BearerTokenSigner(token="my-token"),
    ... )
    
Source code in src/pyramids/stac/client.py
def open_client(
    url: str,
    *,
    signer: Signer | None = None,
    headers: dict[str, str] | None = None,
    timeout: float | None = 30,
) -> Any:
    """Open a STAC API client with a pyramids signer wired into both hooks.

    Args:
        url: STAC API root URL (e.g. an `.../stac/v1` endpoint).
        signer: A :class:`~pyramids.stac.signers.Signer`. Defaults to
            :class:`~pyramids.stac.signers.AnonymousSigner` (no credentials).
            The signer's `sign_item` is passed as the client `modifier` and
            its `sign_request` as the `request_modifier`.
        headers: Optional static HTTP headers added to every request.
        timeout: Per-request timeout in seconds.

    Returns:
        A `pystac_client.Client` instance.

    Raises:
        OptionalPackageDoesNotExist: When `pystac-client` is not installed.

    Examples:
        - Open a public catalog anonymously (requires the `[stac]` extra):
            ```python
            >>> from pyramids.stac import open_client  # doctest: +SKIP
            >>> client = open_client("https://earth-search.aws.element84.com/v1")  # doctest: +SKIP
            >>> search = client.search(collections=["sentinel-2-l2a"], max_items=1)  # doctest: +SKIP
            >>> next(search.items()).collection_id  # doctest: +SKIP
            'sentinel-2-l2a'

            ```
        - Wire a bearer-token signer into both hooks:
            ```python
            >>> from pyramids.stac import open_client, BearerTokenSigner  # doctest: +SKIP
            >>> client = open_client(  # doctest: +SKIP
            ...     "https://stac.dataspace.copernicus.eu/v1",
            ...     signer=BearerTokenSigner(token="my-token"),
            ... )

            ```
    """
    import_pystac_client(_STAC_INSTALL_HINT)
    from pystac_client import Client

    signer = signer or AnonymousSigner()
    # Both hooks are wired unconditionally. For AnonymousSigner sign_item /
    # sign_request are no-ops, so the wiring is harmless; keeping it uniform
    # avoids a branch and means a custom signer never has to be "registered".
    return Client.open(
        url,
        headers=headers,
        timeout=timeout,
        modifier=signer.sign_item,
        request_modifier=signer.sign_request,
    )

pyramids.stac.search #

Typed STAC item-search helper (PB-3).

A thin, typed wrapper over pystac_client.Client.search that makes the common AOI / time / cloud query one call and returns an ItemCollection ready for :meth:pyramids.dataset.DatasetCollection.from_stac. It:

  • opens a client from a URL (or accepts an already-open Client);
  • gates a CQL2 filter on the endpoint advertising the FILTER conformance class, raising a clear error instead of pystac-client's opaque one;
  • accepts a shapely geometry or a GeoJSON dict for intersects;
  • bounds the query at the APIbbox / datetime / max_items / limit are forwarded to client.search so the server (and paging) does the work (M3). This contrasts with :func:pyramids.dataset._stac.from_stac, whose own bbox / max_items are client-side post-filters over an already-materialised item list.

pystac-client is an optional dependency. Install with one of:

  • PyPI: pip install 'pyramids-gis[stac]'
  • conda-forge: conda install -c conda-forge pyramids-stac

search(client_or_url, collections, *, bbox=None, intersects=None, datetime=None, query=None, filter=None, sortby=None, max_items=None, limit=None, signer=None) #

Run a STAC item search and return the matched ItemCollection.

Parameters:

Name Type Description Default
client_or_url Any

An open pystac_client.Client, or a STAC API root URL (opened via :func:pyramids.stac.open_client, wiring signer).

required
collections str | Sequence[str]

Collection id, or a sequence of collection ids, to search.

required
bbox Sequence[float] | None

Optional (minx, miny, maxx, maxy) lon/lat box, forwarded to the API. Mutually exclusive with intersects (STAC API rule).

None
intersects Any

Optional AOI geometry — a shapely geometry (anything with a __geo_interface__) or a GeoJSON-geometry dict. A shapely geometry is converted to GeoJSON before the request.

None
datetime Any

Optional RFC 3339 datetime or interval string (e.g. "2023-06/2023-08"), forwarded to the API.

None
query Any

Optional query extension dict (e.g. {"eo:cloud_cover": {"lt": 20}}).

None
filter Any

Optional CQL2 filter (cql2-json dict or cql2-text string). When given, the endpoint must advertise the FILTER conformance class.

None
sortby Any

Optional sort specification forwarded to the API.

None
max_items int | None

Optional cap on the total number of items returned (bounds paging at the API).

None
limit int | None

Optional page size forwarded to the API.

None
signer Any

Optional signer used only when client_or_url is a URL (to open the client). Ignored when an open client is passed.

None

Returns:

Type Description
Any

The pystac.ItemCollection of matched items, ready to hand to

Any

DatasetCollection.from_stac.

Raises:

Type Description
OptionalPackageDoesNotExist

When pystac-client is not installed.

ValueError

When both bbox and intersects are given (mutually exclusive per the STAC API spec), or when a filter is given but the endpoint does not advertise the CQL2 FILTER conformance class.

Examples:

  • Search a collection over an AOI and time window, then build a cube (requires the [stac] extra and network access):
    >>> from pyramids.stac import search  # doctest: +SKIP
    >>> from pyramids.dataset import DatasetCollection  # doctest: +SKIP
    >>> items = search(  # doctest: +SKIP
    ...     "https://earth-search.aws.element84.com/v1",
    ...     "sentinel-2-l2a",
    ...     bbox=(11.0, 46.0, 11.2, 46.2),
    ...     datetime="2023-06/2023-08",
    ...     query={"eo:cloud_cover": {"lt": 20}},
    ...     max_items=10,
    ... )
    >>> cube = DatasetCollection.from_stac(items, asset=["red", "green", "blue"])  # doctest: +SKIP
    
Source code in src/pyramids/stac/search.py
def search(
    client_or_url: Any,
    collections: str | Sequence[str],
    *,
    bbox: Sequence[float] | None = None,
    intersects: Any = None,
    datetime: Any = None,
    query: Any = None,
    filter: Any = None,
    sortby: Any = None,
    max_items: int | None = None,
    limit: int | None = None,
    signer: Any = None,
) -> Any:
    """Run a STAC item search and return the matched ``ItemCollection``.

    Args:
        client_or_url: An open ``pystac_client.Client``, or a STAC API root URL
            (opened via :func:`pyramids.stac.open_client`, wiring `signer`).
        collections: Collection id, or a sequence of collection ids, to search.
        bbox: Optional `(minx, miny, maxx, maxy)` lon/lat box, forwarded to the
            API. Mutually exclusive with `intersects` (STAC API rule).
        intersects: Optional AOI geometry — a shapely geometry (anything with a
            ``__geo_interface__``) or a GeoJSON-geometry dict. A shapely geometry
            is converted to GeoJSON before the request.
        datetime: Optional RFC 3339 datetime or interval string
            (e.g. ``"2023-06/2023-08"``), forwarded to the API.
        query: Optional ``query`` extension dict (e.g.
            ``{"eo:cloud_cover": {"lt": 20}}``).
        filter: Optional CQL2 filter (cql2-json dict or cql2-text string). When
            given, the endpoint must advertise the ``FILTER`` conformance class.
        sortby: Optional sort specification forwarded to the API.
        max_items: Optional cap on the total number of items returned (bounds
            paging at the API).
        limit: Optional page size forwarded to the API.
        signer: Optional signer used only when `client_or_url` is a URL (to open
            the client). Ignored when an open client is passed.

    Returns:
        The ``pystac.ItemCollection`` of matched items, ready to hand to
        ``DatasetCollection.from_stac``.

    Raises:
        OptionalPackageDoesNotExist: When `pystac-client` is not installed.
        ValueError: When both `bbox` and `intersects` are given (mutually
            exclusive per the STAC API spec), or when a `filter` is given but
            the endpoint does not advertise the CQL2 ``FILTER`` conformance
            class.

    Examples:
        - Search a collection over an AOI and time window, then build a cube
          (requires the `[stac]` extra and network access):
            ```python
            >>> from pyramids.stac import search  # doctest: +SKIP
            >>> from pyramids.dataset import DatasetCollection  # doctest: +SKIP
            >>> items = search(  # doctest: +SKIP
            ...     "https://earth-search.aws.element84.com/v1",
            ...     "sentinel-2-l2a",
            ...     bbox=(11.0, 46.0, 11.2, 46.2),
            ...     datetime="2023-06/2023-08",
            ...     query={"eo:cloud_cover": {"lt": 20}},
            ...     max_items=10,
            ... )
            >>> cube = DatasetCollection.from_stac(items, asset=["red", "green", "blue"])  # doctest: +SKIP

            ```
    """
    if bbox is not None and intersects is not None:
        raise ValueError(
            "bbox and intersects are mutually exclusive (STAC API spec); pass "
            "only one."
        )

    import_pystac_client(_STAC_INSTALL_HINT)
    from pystac_client import ConformanceClasses

    client = (
        open_client(client_or_url, signer=signer)
        if isinstance(client_or_url, str)
        else client_or_url
    )

    if filter is not None and not client.conforms_to(ConformanceClasses.FILTER):
        raise ValueError(
            "the STAC endpoint does not advertise the CQL2 FILTER conformance "
            "class, so a `filter` cannot be used against it."
        )

    if intersects is not None and hasattr(intersects, "__geo_interface__"):
        intersects = intersects.__geo_interface__

    return client.search(
        collections=collections,
        bbox=bbox,
        intersects=intersects,
        datetime=datetime,
        query=query,
        filter=filter,
        sortby=sortby,
        max_items=max_items,
        limit=limit,
    ).item_collection()