Skip to content

STAC

STAC catalog management, item builders, validators, and backends.

Backend Protocol

fair.stac.backend

StacBackend

Bases: Protocol

Structural interface for STAC catalog operations.

StacCatalogManager (local JSON) and PgStacBackend (pgstac) both conform without explicit inheritance.

Catalog Manager

fair.stac.catalog_manager

StacCatalogManager(catalog_path)

CRUD on a pystac Catalog persisted as local JSON.

Source code in fair/stac/catalog_manager.py
def __init__(self, catalog_path: str):
    self.catalog = pystac.Catalog.from_file(catalog_path)

PgSTAC Backend

fair.stac.pgstac_backend

PgStacBackend(dsn, stac_api_url)

STAC backend using pypgstac Loader and pystac-client.

Writes use pypgstac's Loader (bulk upsert via COPY + pgstac SQL). Reads use pystac-client against the STAC API. Delete uses pgstac's delete_item() SQL function directly since Loader has no delete API.

Source code in fair/stac/pgstac_backend.py
def __init__(self, dsn: str, stac_api_url: str) -> None:
    self._dsn = dsn
    self._stac_api_url = stac_api_url
    self._http = httpx.Client(timeout=30)
    self._bootstrap_collections()

Builders

fair.stac.builders

Collections

fair.stac.collections

create_base_models_collection()

base-models: model blueprints contributed via PR.

Source code in fair/stac/collections.py
def create_base_models_collection() -> pystac.Collection:
    """base-models: model blueprints contributed via PR."""
    return pystac.Collection(
        id=BASE_MODELS_COLLECTION,
        description="Model blueprints contributed via PR. Each item is a complete model card.",
        extent=pystac.Extent(
            spatial=pystac.SpatialExtent(bboxes=[[-180, -90, 180, 90]]),
            temporal=pystac.TemporalExtent(intervals=[[datetime(2026, 1, 1, tzinfo=UTC), None]]),
        ),
        license="various",
        stac_extensions=BASE_MODEL_EXTENSIONS,
    )

create_local_models_collection()

local-models: finetuned models, only promoted versions.

Source code in fair/stac/collections.py
def create_local_models_collection() -> pystac.Collection:
    """local-models: finetuned models, only promoted versions."""
    return pystac.Collection(
        id=LOCAL_MODELS_COLLECTION,
        description="Finetuned models produced by ZenML pipelines. Only promoted versions appear here.",
        extent=pystac.Extent(
            spatial=pystac.SpatialExtent(bboxes=[[-180, -90, 180, 90]]),
            temporal=pystac.TemporalExtent(intervals=[[datetime(2026, 1, 1, tzinfo=UTC), None]]),
        ),
        license="various",
        stac_extensions=LOCAL_MODEL_EXTENSIONS,
    )

create_datasets_collection()

datasets: training data registered via fAIr UI/backend.

Source code in fair/stac/collections.py
def create_datasets_collection() -> pystac.Collection:
    """datasets: training data registered via fAIr UI/backend."""
    return pystac.Collection(
        id=DATASETS_COLLECTION,
        description="Training data registered via fAIr UI/backend.",
        extent=pystac.Extent(
            spatial=pystac.SpatialExtent(bboxes=[[-180, -90, 180, 90]]),
            temporal=pystac.TemporalExtent(intervals=[[datetime(2026, 1, 1, tzinfo=UTC), None]]),
        ),
        license="various",
        stac_extensions=DATASET_EXTENSIONS,
    )

initialize_catalog(catalog_path)

Create catalog.json + 3 empty collections. Saves to disk.

returns existing catalog if already present.

Source code in fair/stac/collections.py
def initialize_catalog(catalog_path: str) -> pystac.Catalog:
    """Create catalog.json + 3 empty collections. Saves to disk.

    returns existing catalog if already present.
    """
    if os.path.exists(catalog_path):
        return pystac.Catalog.from_file(catalog_path)

    catalog = pystac.Catalog(
        id="fair-models",
        description="fAIr model registry and dataset catalog",
    )

    catalog.add_child(create_base_models_collection())
    catalog.add_child(create_local_models_collection())
    catalog.add_child(create_datasets_collection())

    catalog_dir = os.path.dirname(catalog_path) or "."
    catalog.normalize_hrefs(catalog_dir)
    catalog.save(catalog_type=CatalogType.SELF_CONTAINED)

    return catalog

Validators

fair.stac.validators

Versioning

fair.stac.versioning

Rewrite version-link relative hrefs (../../coll/id/id.json) to absolute API URLs.

pystac's local-catalog write produces relative hrefs; remote backends need the same links pointing at the API URL the catalog will live at. Use raw link.target so we don't trigger pystac's root resolution (which makes HTTP calls).

Source code in fair/stac/versioning.py
def normalize_version_link_hrefs(
    item: pystac.Item,
    item_href_factory,
    default_collection_id: str,
) -> None:
    """Rewrite version-link relative hrefs (`../../coll/id/id.json`) to absolute API URLs.

    pystac's local-catalog write produces relative hrefs; remote backends need the same
    links pointing at the API URL the catalog will live at. Use raw `link.target` so we
    don't trigger pystac's root resolution (which makes HTTP calls).
    """
    for link in item.links:
        if link.rel not in _VERSION_RELS:
            continue
        target = link.target if isinstance(link.target, str) else ""
        if target.startswith(("http://", "https://")):
            continue
        parts = target.replace("\\", "/").split("/")
        json_parts = [p for p in parts if p.endswith(".json")]
        if not json_parts:
            continue
        target_item_id = json_parts[-1].removesuffix(".json")
        target_coll = parts[-3] if len(parts) >= 3 else default_collection_id
        link.target = item_href_factory(target_coll, target_item_id)

archive_previous_version(backend, collection_id, old_item, new_item_href)

Copy the old item to a version-suffixed ID and deprecate it.

Preserves the old version as a discoverable, deprecated STAC item per the STAC Version Extension v1.2.0 spec.

Source code in fair/stac/versioning.py
def archive_previous_version(
    backend: StacBackend,
    collection_id: str,
    old_item: pystac.Item,
    new_item_href: str,
) -> pystac.Item:
    """Copy the old item to a version-suffixed ID and deprecate it.

    Preserves the old version as a discoverable, deprecated STAC item
    per the STAC Version Extension v1.2.0 spec.
    """
    old_version = old_item.properties.get("version", "1")
    archived_id = f"{old_item.id}-v{old_version}"

    archived = old_item.clone()
    archived.id = archived_id
    archived.properties["deprecated"] = True
    archived.links = [lnk for lnk in archived.links if lnk.rel not in {"latest-version", "self"}]
    archived.add_link(pystac.Link(rel="successor-version", target=new_item_href))

    archived_href = backend.item_href(collection_id, archived_id)
    archived.add_link(pystac.Link(rel="self", target=archived_href, media_type="application/geo+json"))

    published = backend.publish_item(collection_id, archived)
    log.info("Archived %s/%s as deprecated", collection_id, archived_id)
    return published

Constants

fair.stac.constants

BASE_MODELS_COLLECTION = 'base-models' module-attribute

BASE_MODEL_EXTENSIONS = [MLM_SCHEMA, VERSION_SCHEMA, CLASSIFICATION_SCHEMA, FILE_SCHEMA, RASTER_SCHEMA, FAIR_BASE_MODEL_SCHEMA] module-attribute

CLASSIFICATION_SCHEMA = 'https://stac-extensions.github.io/classification/v2.0.0/schema.json' module-attribute

CONTAINER_REGISTRIES = ('ghcr.io', 'docker.io', 'quay.io') module-attribute

DATASETS_COLLECTION = 'datasets' module-attribute

DATASET_EXTENSIONS = [LABEL_SCHEMA, FILE_SCHEMA, VERSION_SCHEMA, FAIR_DATASET_SCHEMA] module-attribute

FAIR_BASE_MODEL_SCHEMA = 'https://hotosm.github.io/fAIr-models/schemas/v1.0.0/base-model/schema.json' module-attribute

FAIR_DATASET_SCHEMA = 'https://hotosm.github.io/fAIr-models/schemas/v1.0.0/dataset/schema.json' module-attribute

FAIR_LOCAL_MODEL_SCHEMA = 'https://hotosm.github.io/fAIr-models/schemas/v1.0.0/local-model/schema.json' module-attribute

FILE_SCHEMA = 'https://stac-extensions.github.io/file/v2.1.0/schema.json' module-attribute

LABEL_SCHEMA = 'https://stac-extensions.github.io/label/v1.0.1/schema.json' module-attribute

LOCAL_MODELS_COLLECTION = 'local-models' module-attribute

LOCAL_MODEL_EXTENSIONS = [MLM_SCHEMA, VERSION_SCHEMA, CLASSIFICATION_SCHEMA, FILE_SCHEMA, RASTER_SCHEMA, FAIR_LOCAL_MODEL_SCHEMA] module-attribute

MLM_SCHEMA = 'https://stac-extensions.github.io/mlm/v1.5.1/schema.json' module-attribute

MODEL_EXTENSIONS = BASE_MODEL_EXTENSIONS module-attribute

OCI_IMAGE_INDEX_TYPE = 'application/vnd.oci.image.index.v1+json' module-attribute

RASTER_SCHEMA = 'https://stac-extensions.github.io/raster/v1.1.0/schema.json' module-attribute

VERSION_SCHEMA = 'https://stac-extensions.github.io/version/v1.2.0/schema.json' module-attribute