Registering analysis results#

After computing a comparison or validation result for a matchup, you can attach the output file to the matchup’s STAC item as a versioned asset. This keeps the catalogue as the single source of truth: anyone querying the central catalogue can see what analysis has been run on each matchup and retrieve the result directly from the item metadata.

Two asset keys are written each time:

  • {prefix}:YYYY-MM-DD — a dated snapshot that is never overwritten. Each run adds a new dated key alongside any previous ones.

  • {prefix}:latest — always updated to point at the most recently registered file. Useful when you only want the current result without having to know its date.

The default prefix is comparison; you can supply any prefix to distinguish different kinds of analysis (e.g. validation, uncertainty).

—

From the command line#

eomatch-add-asset \
    --catalogue /data/my_catalogue \
    --collection-id LANDSAT_C2L1-Landsat-9-vs-S2_MSI_L1C-S2A \
    --item-id     LC09_L1GT_089087_20220607_20220616_02_T2--S3A_EFR_20220607 \
    --file        /data/results/comparison_20260512.nc

This writes two assets to the item JSON on disk:

  • comparison:2026-05-12 (today’s date by default)

  • comparison:latest

Override the date, prefix, or MIME type if needed:

eomatch-add-asset \
    --catalogue /data/my_catalogue \
    --collection-id LANDSAT_C2L1-Landsat-9-vs-S2_MSI_L1C-S2A \
    --item-id     LC09_L1GT_... \
    --file        /data/results/validation_run2.nc \
    --key-prefix  validation \
    --date        2026-05-01 \
    --media-type  application/x-netcdf \
    --title       "Validation run 2"

Add --push to also upsert the updated item directly into the running pgSTAC database (requires the ingest extra and a reachable database):

eomatch-add-asset \
    --catalogue /data/my_catalogue \
    --collection-id LANDSAT_C2L1-Landsat-9-vs-S2_MSI_L1C-S2A \
    --item-id     LC09_L1GT_... \
    --file        /data/results/comparison_20260512.nc \
    --push \
    --db-host     your-server

Without --push, run eomatch-ingest afterwards to synchronise the whole catalogue (or just this item’s collection) with pgSTAC.

—

From Python#

import datetime as dt
from eomatch.add_asset import register_analysis

register_analysis(
    catalogue_path="/data/my_catalogue",
    collection_id="LANDSAT_C2L1-Landsat-9-vs-S2_MSI_L1C-S2A",
    item_id="LC09_L1GT_089087_20220607_20220616_02_T2--S3A_EFR_20220607",
    file_path="/data/results/comparison_20260512.nc",
)

Retrieve the item afterwards to confirm both keys were registered:

from eomatch.mu_stac import MatchupCatalogue

cat = MatchupCatalogue.open("/data/my_catalogue")
col = cat.catalog.get_child("LANDSAT_C2L1-Landsat-9-vs-S2_MSI_L1C-S2A")
item = next(col.get_items("LC09_L1GT_..."))

print(item.assets.keys())
# dict_keys(['comparison:2026-05-12', 'comparison:latest'])

print(item.assets["comparison:latest"].href)
# /data/results/comparison_20260512.nc

Multiple runs accumulate dated snapshots while latest rolls forward:

register_analysis(..., file_path="comparison_v1.nc", date=dt.date(2026, 5, 1))
register_analysis(..., file_path="comparison_v2.nc", date=dt.date(2026, 5, 12))

# item.assets now contains:
# comparison:2026-05-01  →  comparison_v1.nc
# comparison:2026-05-12  →  comparison_v2.nc
# comparison:latest      →  comparison_v2.nc

Push to pgSTAC in the same call by passing push=True:

register_analysis(
    catalogue_path="/data/my_catalogue",
    collection_id="LANDSAT_C2L1-Landsat-9-vs-S2_MSI_L1C-S2A",
    item_id="LC09_L1GT_...",
    file_path="/data/results/comparison_20260512.nc",
    push=True,
    db_host="your-server",
    db_name="eomatch",
)

Connection parameters fall back to the ingest section of your eomatch config if not supplied explicitly:

# ~/.config/eomatch/user_config.yaml
ingest:
  db_host: your-server
  db_name: eomatch
  db_user: postgres

Pass the password via the PGPASSWORD environment variable rather than storing it in the config file:

export PGPASSWORD=your-postgres-password

—

Media type detection#

The MIME type is inferred from the file extension when media_type is not given:

Extension

MIME type

.nc

application/x-netcdf

.zarr

application/vnd.zarr

.tif

image/tiff

.tiff

image/tiff

.json

application/json

.csv

text/csv

.png

image/png

.jpg

image/jpeg

other

application/octet-stream

—

How assets appear in the central catalogue#

Assets with file:// hrefs (NFS paths) are visible on the internal API (/api/) but are stripped from responses served by the external API (/external/). This means:

  • Internal users (VPN access) see all assets, including analysis NetCDFs on NFS.

  • External collaborators using the STAC Browser or eomatch-query see the matchup metadata but not the file paths.

To expose an analysis result to external users, serve it over HTTP and register it with an http:// href rather than a local path.

—

Generating assets in bulk#

generate_missing_assets() wraps the common “generate one asset per item that doesn’t already have it” loop: it skips items that already have asset_key (unless overwrite=True), calls your generator function to produce the file, and registers it via whichever add_*_asset method you pass in — so it works for both matchups and events, and is not tied to the dated-snapshot convention above:

import matplotlib.pyplot as plt
from eomatch.add_asset import generate_missing_assets

def make_thumbnail(matchup, path):
    fig = build_preview(matchup, output_path=path)
    plt.close(fig)

events = cat.get_events(missing_assets=["thumbnail"])
matchups = [mu for event in events for mu in (event.matchup_set or [])]

n = generate_missing_assets(
    items=matchups,
    asset_key="thumbnail",
    output_path=lambda mu: f"/data/thumbnails/{mu.stac_id}.png",
    generate=make_thumbnail,
    add_asset=cat.add_matchup_asset,
    media_type="image/png",
    roles=["thumbnail"],
)
print(f"Generated {n} thumbnail(s)")

A generation failure for one item (any exception raised by generate) is logged and that item is skipped, rather than aborting the whole run.

—

Removing assets in bulk#

remove_assets() is the counterpart to generate_missing_assets(): it removes asset_key from every item you pass in that has it, via whichever remove_*_asset method you give it. Select the set of matchups or events first (by collection pair, time range, has_assets filter, etc.), then pass them in:

from eomatch.add_asset import remove_assets

events = cat.get_events(has_assets=["thumbnail"])
matchups = [mu for event in events for mu in (event.matchup_set or [])]

n = remove_assets(
    items=matchups,
    asset_key="thumbnail",
    remove_asset=cat.remove_matchup_asset,
    delete_file=True,
)
print(f"Removed {n} thumbnail(s)")

Items without the asset are skipped. With delete_file=True (the default) the local file or directory the asset points to is also deleted; remote URLs are never deleted. A failure for one item is logged and that item is skipped, unless raise_error=True.

—

Full reference#

eomatch.add_asset.register_analysis(catalogue_path: str, collection_id: str, item_id: str, file_path: str, key_prefix: str = 'comparison', date: date | None = None, media_type: str | None = None, title: str | None = None, push: bool = False, config=None, db_host: str | None = None, db_port: int | None = None, db_name: str | None = None, db_user: str | None = None, db_password: str | None = None, assets_base_url: str | None = None) → None[source]

Register an analysis output file as versioned STAC assets on a matchup item.

Adds two assets to the item in the local catalogue:

  • {key_prefix}:{YYYY-MM-DD} — dated snapshot, never overwritten by future runs.

  • {key_prefix}:latest — always updated to point at the most recent file.

The local catalogue JSON is written immediately. Pass push=True to also upsert the updated item into the running pgSTAC database.

Example:

from eomatch.add_asset import register_analysis

register_analysis(
    catalogue_path="/data/catalogue",
    collection_id="LANDSAT_C2L1-Landsat-8-vs-S2_MSI_L1C-S2A",
    item_id="L8-S2A-20220613T001234",
    file_path="/data/results/comparison_20260512.nc",
    push=True,
    db_host="localhost",
    db_name="eomatch",
)
Parameters:
  • catalogue_path – path to the root catalogue directory or catalog.json.

  • collection_id – STAC collection ID containing the target item.

  • item_id – STAC item ID to attach the asset to.

  • file_path – path to the analysis output file.

  • key_prefix – prefix for the asset keys; defaults to "comparison".

  • date – date for the dated snapshot key; defaults to today.

  • media_type – MIME type for the asset; auto-detected from extension if None.

  • title – human-readable title for the asset; defaults to the filename.

  • push – if True, upsert the updated item into pgSTAC after saving locally.

  • config – path to a eomatch YAML config file, or a dict of overrides.

  • db_host – pgSTAC database host (used only when push is True).

  • db_port – pgSTAC database port.

  • db_name – pgSTAC database name.

  • db_user – pgSTAC database user.

  • db_password – pgSTAC database password.

  • assets_base_url – HTTP base URL at which catalogue assets are served; used to rewrite file:// hrefs when pushing to pgSTAC.

eomatch.add_asset.generate_missing_assets(items: Iterable[Any], asset_key: str, output_path: Callable[[Any], str], generate: Callable[[Any, str], Any], add_asset: Callable[[...], bool], asset_href: Callable[[Any, str], str] | None = None, media_type: str | None = None, title: str | None = None, roles: List[str] | None = None, overwrite: bool = False, return_values: bool = False, raise_error: bool = False) → Any[source]

Generate and register asset_key for every item in items that doesn’t already have it.

For each MatchupEvent or Matchup in items missing asset_key on its .assets (or every item, if overwrite is True):

  1. output_path(item) gives the local file path to generate the asset at.

  2. generate(item, path) is called to produce that file; its return value is ignored, so callers whose generator function returns something (e.g. a matplotlib figure to close) can just do that inline in the callable passed here.

  3. asset_href(item, path), if given, computes the href to register from path — e.g. relative to the item’s own directory, for a portable link. Defaults to path itself when not given.

  4. add_asset(item, asset_key, href, media_type=media_type, title=title, roles=roles) registers it — pass a bound method such as add_matchup_asset() or add_event_asset().

A failure raised by any of the above for one item is logged and that item is skipped, rather than aborting the whole run.

Example — generating a matchup thumbnail with a user-provided plotting function:

from eomatch.add_asset import generate_missing_assets

def make_thumbnail(matchup, path):
    fig = build_preview(matchup, output_path=path)
    plt.close(fig)

n = generate_missing_assets(
    items=[mu for event in cat.get_events() for mu in (event.matchup_set or [])],
    asset_key="thumbnail",
    output_path=lambda mu: f"/data/thumbnails/{mu.stac_id}.png",
    generate=make_thumbnail,
    add_asset=cat.add_matchup_asset,
    media_type="image/png",
    roles=["thumbnail"],
)
Parameters:
  • items – MatchupEvent or Matchup instances to process.

  • asset_key – asset key to check for and register, e.g. "thumbnail".

  • output_path – given an item, returns the local file path to generate the asset at.

  • generate – given (item, output_path), writes the asset file. Return value ignored.

  • add_asset – bound method used to register the generated file as an asset, e.g. add_matchup_asset() or add_event_asset().

  • asset_href – given (item, output_path), returns the href to register — use this when the file must be written to one location but linked to from the STAC item with a different (typically relative) path. Defaults to registering output_path unchanged.

  • media_type – MIME type passed through to add_asset; auto-detected if None.

  • title – title passed through to add_asset; defaults to the filename.

  • roles – STAC asset roles passed through to add_asset, e.g. ["thumbnail"].

  • overwrite – if True, regenerate and re-register even when asset_key is already present.

  • return_values – if True, return the values returned by generate for each successfully generated item rather than the count.

  • raise_error – if True, raise errors instead of catching them in a try statement

Returns:

number of assets successfully generated and registered, or a list of generate return values when return_values is True.

eomatch.add_asset.remove_assets(items: Iterable[Any], asset_key: str, remove_asset: Callable[[...], bool], delete_file: bool = True, raise_error: bool = False) → int[source]

Remove asset_key from every item in items that has it.

The counterpart to generate_missing_assets(): items is whatever subset of MatchupEvent or Matchup objects you have selected (by collection pair, time range, property filter, etc.), and the asset is removed from each one that carries it.

A failure raised for one item is logged and that item is skipped, rather than aborting the whole run.

Example — removing a stale thumbnail from a filtered set of matchups:

from eomatch.add_asset import remove_assets

n = remove_assets(
    items=[mu for event in cat.get_events() for mu in (event.matchup_set or [])],
    asset_key="thumbnail",
    remove_asset=cat.remove_matchup_asset,
)
Parameters:
  • items – MatchupEvent or Matchup instances to process.

  • asset_key – asset key to remove, e.g. "thumbnail".

  • remove_asset – bound method used to remove the asset, e.g. remove_matchup_asset() or remove_event_asset().

  • delete_file – if True (default), also delete the local file or directory the asset points to. Remote URLs are never deleted.

  • raise_error – if True, raise errors instead of logging them and continuing.

Returns:

number of items the asset was successfully removed from.