Browsing a STAC Catalog with the pystac_client

The pystac_client provide a python integration and command line tools to interact with a STAC catalog. The full documentation can be found here. We will focus on examples to illustrate access to data within the WarmWorld catalog.

Opening a known Dataset

For a known dataset, e.g. we have found the collection ID and item ID by browsing through the catalog with the STAC browser, we can quite easy get a way to open a dataset.

Afterwards we need to follow the required procedure to actually open the dataset, which will be part of a different guide.

import pystac_client
stac_url = "https://wwestac.cloud.dkrz.de/stac-fastapi-es/"
collection_id = "ngc3028"
item_id = "ngc3028_P1D_3"
catalog = pystac_client.Client.open(stac_url)
collection = catalog.get_collection(collection_id)
item = collection.get_item(item_id)

Now we want to find out, which asset we can use to access the data. Let’s therefore loop over the available entries and decide if there is something we can use.

import json
for name, asset in item.assets.items():
    print(f"asset name: {name}")
    print(  json.dumps(asset.to_dict(), indent=2))
    print()
asset name: disk
{
  "href": "file:///work/bm1235/k203123/nextgems_cycle3/experiments/ngc3028/outdata/ngc3028_P1D_3.zarr",
  "type": "application/vnd+zarr"
}

We only find a single asset called disk. The type indicated, that we would open a zarr dataset. Having a closer look at the href shows the prefix file://, so we have a local file. We know that this dataset is stored at DKRZ. So accessing the data is only possible, if we have access to the file system and the permissions to this particular directory and files.

A deeper look into a STAC catalog

This is a good point to inspect the underlying entries closer. Let’s print the content of the collection as well as the content of the item. For a nicer formatting, we weill use the json package.

import json
print( json.dumps(collection.to_dict(), indent=2))
{
  "type": "Collection",
  "id": "ngc3028",
  "stac_version": "1.1.0",
  "description": "ngc3028 collection",
  "links": [
    {
      "rel": "aggregate",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028/aggregate",
      "type": "application/json"
    },
    {
      "rel": "aggregations",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028/aggregations",
      "type": "application/json"
    },
    {
      "rel": "items",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028/items",
      "type": "application/geo+json"
    },
    {
      "rel": "parent",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/",
      "type": "application/json"
    },
    {
      "rel": "queryables",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028/queryables",
      "type": "application/schema+json"
    },
    {
      "rel": "root",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/",
      "type": "application/json",
      "title": "Warm World Data Catalog"
    },
    {
      "rel": "self",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028",
      "type": "application/json"
    }
  ],
  "title": "",
  "extent": {
    "spatial": {
      "bbox": [
        [
          -180,
          -90,
          180,
          90
        ]
      ]
    },
    "temporal": {
      "interval": [
        [
          "2024-07-22T11:50:29.668363Z",
          "2024-07-23T11:50:29.668363Z"
        ]
      ]
    }
  },
  "license": "CC-BY-SA-4.0"
}

We can see a clear structure on the output. This includes a nested tree with a number of different entries. A collection also has a number of links, to navigate through the catalog. For example, the child link gives all the items belonging to this collection.

The queryables link gives an overview on possible metadata we can use in the search function. This also includes detailed descriptions. The extent has a bounding box that covers all included items. The temporal extend covers the first and last date and time part of the dataset.

import json
print( json.dumps(item.to_dict(), indent=2))
{
  "type": "Feature",
  "stac_version": "1.1.0",
  "stac_extensions": [],
  "id": "ngc3028_P1D_3",
  "geometry": {
    "type": "Polygon",
    "coordinates": [
      [
        [
          -180.0,
          -90.0
        ],
        [
          -180.0,
          90.0
        ],
        [
          180.0,
          90.0
        ],
        [
          180.0,
          -90.0
        ],
        [
          -180.0,
          -90.0
        ]
      ]
    ]
  },
  "bbox": [
    -180.0,
    -90.0,
    180.0,
    90.0
  ],
  "properties": {
    "datetime": "2024-07-22T11:50:29.809918Z",
    "experiment": "ngc3028",
    "frequency": "P1D",
    "healpixLevel": "3",
    "variables": "-",
    "created": "2025-10-22T13:32:51.193696Z",
    "updated": "2025-10-22T13:32:51.193696Z"
  },
  "links": [
    {
      "rel": "self",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028/items/ngc3028_P1D_3",
      "type": "application/json"
    },
    {
      "rel": "parent",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028",
      "type": "application/json"
    },
    {
      "rel": "collection",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028",
      "type": "application/json"
    },
    {
      "rel": "root",
      "href": "https://wwestac.cloud.dkrz.de/stac-fastapi-es/collections/ngc3028",
      "type": "application/json",
      "title": ""
    }
  ],
  "assets": {
    "disk": {
      "href": "file:///work/bm1235/k203123/nextgems_cycle3/experiments/ngc3028/outdata/ngc3028_P1D_3.zarr",
      "type": "application/vnd+zarr"
    }
  },
  "collection": "ngc3028"
}

The general structure of the entry is similar to a collection, e.g. links, id, general information. An item has also additional entries. Most important is the assets block, with the possible ways to access the dataset. The geometry can be a polygon for a detailed description of the covered region.

A new part is the properties block, that contains fields for the creation and update of the item as well as general metadata, like the frequency of the data or the healpixLevel that corresponds to the spatial resolution.

Browsing through a Catalog

With this basic understanding of a STAC catalog, it should be clearer, that we can browse through the different layers, even without a package like the pystac_client. Nevertheless the pystac_clients simplifies a few things, so we will stick to it.

Next, lets browse through the full catalog and search if we find something interesting:

Browsing through Collections:

Let’s first have a look at all the available collections:

import pystac_client
stac_url = "https://wwestac.cloud.dkrz.de/stac-fastapi-es/"
catalog = pystac_client.Client.open(stac_url)
collections = catalog.get_collections()
for collection in collections:
    print(f"Title: {collection.title}, with collection ID: {collection.id}")
Title: ERA5, with collection ID: ERA5
Title: IFS, with collection ID: IFS
Title: MERRA-2, with collection ID: JSC-M2I3NVASM
Title: IMERG v1, with collection ID: JSC-anemoi-4d2a51d7_6763_4b15_93a8_d8090405fec9
Title: CERRA v2, with collection ID: JSC-anemoi-afeaa2db_5a20_421d_a5e6_270c7fa1bdd7
Title: ERA5 (FDB), with collection ID: JSC-class~ea_domain~g_expver~0001_stream~oper_type~an
Title: SEVIRI, with collection ID: SEVIRI
Title: , with collection ID: ngc3028
Title: , with collection ID: ngc4008
Title: , with collection ID: ngcX001
Title: WarmWorldEasierTestRun, with collection ID: stac_test

We can now select one of these collections for a further inspection, like JSC-class~ea_domain~g_expver~0001_stream~oper_type~an Let us have a look at the available fields in the entry.

collection_id = "JSC-class~ea_domain~g_expver~0001_stream~oper_type~an"
collection = catalog.get_collection(collection_id)
print(f"Inspecting keys in collection: {collection_id}")
for key in collection.to_dict().keys():
    print("-- ", key)
Inspecting keys in collection: JSC-class~ea_domain~g_expver~0001_stream~oper_type~an
--  type
--  id
--  stac_version
--  description
--  links
--  stac_extensions
--  cube:dimensions
--  cube:variables
--  title
--  extent
--  license
--  keywords
--  providers
--  summaries

We see a number of keys, that we have already seen in the STAC browser, like title, providers, summaries, keywords, extend etc. We can also see keys with the prefix cube: those are dictionaries for the data cube extension, which gives a quite nice overview on the data cube. Let us for example figure out, which variables are inside this datacube:

print("Variable in datacube:")
for var_name in collection.extra_fields["cube:variables"].keys():
    print("-- ", var_name)
Variable in datacube:
--  geopotential (ml)
--  temperature (ml)
--  u component of wind (ml)
--  v component of wind (ml)
--  specific humidity (ml)
--  vertical velocity (ml)
--  vorticity (relative) (ml)
--  logarithm of surface pressure (ml)
--  divergence (ml)
--  ozone mass mixing ratio (ml)
--  specific cloud liquid water content (ml)
--  specific cloud ice water content (ml)
--  fraction of cloud cover (ml)
--  sea ice area fraction
--  snow density
--  sea surface temperature
--  ice temperature layer 1
--  volumetric soil water layer 1
--  volumetric soil water layer 2
--  volumetric soil water layer 3
--  volumetric soil water layer 4
--  convective available potential energy
--  leaf area index, low vegetation
--  leaf area index, high vegetation
--  geopotential
--  surface pressure
--  soil temperature level 1
--  snow depth
--  mean sea level pressure
--  boundary layer height
--  total cloud cover
--  10 metre u wind component
--  10 metre v wind component
--  2 metre temperature
--  2 metre dewpoint temperature
--  soil temperature level 2
--  land-sea mask
--  soil temperature level 3
--  low cloud cover
--  medium cloud cover
--  high cloud cover
--  skin reservoir content
--  instantaneous moisture flux
--  skin temperature
--  soil temperature level 4
--  temperature of snow layer

Here, we see something interesting: 2 metre temperature. We now want to plot this. The next step is to find the corresponding item.

itmes = collection.get_items()
for item in itmes:
    print( f"Item Title : {item.properties['title']}, with item ID: {item.id}")
Item Title : ERA5 (FDB) on ml - model levels, with item ID: JSC-class~ea_domain~g_expver~0001_stream~oper_type~an_levtype~ml

We know that 2 metre temperature is a surface type variable, which is encoded as levtype=sfc in the ECMWF metadata standard. The catalog is structure, that surface variables are put together with each 3D field dataset. So we need to have a look at the results at model levels.

In a case, where a dataset has only surface variables, those would be presented as an individual item.

We will expect the assets. Here, we will focus on a few metadata entries. You can also plot the complete entry with

import json
for name, asset in item.assets.items():
    print(json.dumps(asset.to_dict(), indent=2))

We do not show this output here as it is quite long due to the inclusion of the data cube extension.

item = collection.get_item("JSC-class~ea_domain~g_expver~0001_stream~oper_type~an_levtype~ml")
for asset_name, asset in item.assets.items():
    print(f"{asset.title}")
    print(f"-- type: {asset.media_type}")
    print(f"-- roles: {asset.roles}")
    print(f"-- description: {asset.description}")
    print(f"-- Volume: {asset.extra_fields['Volume']}")
    print(f"-- Number of fields: {asset.extra_fields['number of fields']}")
    print()
zarr-Stream Request
-- type: application/json
-- roles: ['API', 'data', 'ZARR', 'ready to use']
-- description: Opening of complete dataset as zarr. Included values are given in the metadata of this asset
-- Volume: 81.69 GB
-- Number of fields: 70932

incomplete zarr-Stream Request
-- type: application/json
-- roles: ['API', 'data', 'zarr', 'requires specification']
-- description: Opening of the dataset as zarr. You need to add the missing metadata to the request. We use a MARS based format. The following keys are required: datetime, date, levelist, param, step, time, param_sfc. Possible values are given in the metadata of this asset.
-- Volume: 81.69 GB
-- Number of fields: 70932

grib-Download
-- type: application/json
-- roles: ['API', 'data', 'GRIB', 'ready to use']
-- description: Link to request download of full dataset. The preparation of the dataset can require some time. The download might require a large amount of disc space. Included values are given in the metadata of this asset.
-- Volume: 79.59 GB
-- Number of fields: 69414

incomplete grib-Download
-- type: application/json
-- roles: ['API', 'data', 'GRIB', 'requires specification']
-- description: Link to request download of full dataset. The preparation of the dataset can require some time. The download might require a large amount of disc space. You need to add the missing metadata to the request. We use a MARS based format. The following keys are required: datetime, date, levelist, param, step, time. Possible values are given in the metadata of this asset.
-- Volume: 79.59 GB
-- Number of fields: 69414

surface grib-Download
-- type: application/json
-- roles: ['API', 'data', 'GRIB', 'ready to use']
-- description: Link to request download of full dataset. The preparation of the dataset can require some time. The download might require a large amount of disc space. Included values are given in the metadata of this asset.
-- Volume: 2.1 GB
-- Number of fields: 1518

incomplete surface-grib-Download
-- type: application/json
-- roles: ['API', 'data', 'GRIB', 'requires specification']
-- description: Link to request download of full dataset. The preparation of the dataset can require some time. The download might require a large amount of disc space. You need to add the missing metadata to the request. We use a MARS based format. The following keys are required: datetime, date, param, step, time. Possible values are given in the metadata of this asset.
-- Volume: 2.1 GB
-- Number of fields: 1518

We see four different entries. The descriptions reveal, that all of them are requests to an API. Two of them are marked with “required specification”. Those are base requests, that need some details in case a user does only want to access a part of the data cube. The other two requests grant access or download the full data cube. Using those assets is a topic for another guide.

Searching a Catalog with pystac_client

As a last point in this section, we want to search for specific items with the pystac_client This should only serve as a small illustration. More sophisticated guides on filtering can be found in the pystac_client tutorials.

stac_api_url = "https://esm-data.fz-juelich.de/catalog/api/v1"
client = pystac_client.Client.open(stac_api_url)
search = client.search(
    datetime=["2022-03-15"],
#    query=["levtype=ml - model levels"]
)
print("We found the following items:")
for item in search.item_collection().items:
    coll = item.get_parent()
    print("")
    print(f"  item Title: {item.properties['title']}")
    print(f"  -- Item ID: {item.id}")
    print( "  -- Collection Info:")
    print(f"  ---- Collection Title: {coll.title}")
    print(f"  ---- Collection ID: {item.collection_id}")
We found the following items:

  item Title: IFS run started at 20220315-00:00 on pl - pressure levels
  -- Item ID: JSC-class~od_date~20220315_domain~g_expver~0001_stream~oper_time~0000_type~fc_levtype~pl
  -- Collection Info:
  ---- Collection Title: IFS
  ---- Collection ID: IFS

  item Title: IFS run started at 20220315-00:00 on ml - model levels
  -- Item ID: JSC-class~od_date~20220315_domain~g_expver~0001_stream~oper_time~0000_type~fc_levtype~ml
  -- Collection Info:
  ---- Collection Title: IFS
  ---- Collection ID: IFS

  item Title: ERA5 (FDB) on ml - model levels
  -- Item ID: JSC-class~ea_domain~g_expver~0001_stream~oper_type~an_levtype~ml
  -- Collection Info:
  ---- Collection Title: ERA5 (FDB)
  ---- Collection ID: JSC-class~ea_domain~g_expver~0001_stream~oper_type~an

The result of the search is are all items, that match the search. For the output we first get access to the parent collection of each item coll = item.get_parent(). This allows us to print the tree and access metadata from both levels.