Skip to content

Build a spatio-temporal catalog

Create a custom spatio-temporal dataset catalog with the Python SDK, ingest geospatial metadata, and query it by time, location, and custom fields.

Use a spatio-temporal dataset when each datapoint has both a time and a geometry. This is useful for internal imagery catalogs, derived products, ground truth data, regions of interest, and processing outputs that need geospatial lookup.

This guide creates an imagery catalog from code. You will define the dataset schema with the Python SDK, reference the image files as assets, ingest geospatial metadata, and query the catalog by time, location, and custom fields.

Terminal window
uv add tilebox geopandas shapely

Start by choosing the spatio-temporal dataset kind and the custom fields for your catalog. Tilebox adds the required time, id, ingestion_time, and geometry fields automatically.

The example catalog tracks imagery products with a provider product ID, file assets, cloud cover, and processing level. Field descriptions and example values become part of the generated schema documentation.

Python
from tilebox.datasets import Client
from tilebox.datasets.data.datasets import DatasetKind
from tilebox.datasets.schema import Assets
client = Client()
fields = [
{
"name": "product_id",
"type": str,
"description": "Stable product or scene identifier from the source catalog.",
"example_value": "LC08_L2SP_033033_20240808_20240814_02_T1",
},
{
"name": "assets",
"type": Assets,
"description": "Files associated with the imagery product.",
},
{
"name": "cloud_cover",
"type": float,
"description": "Cloud cover percentage for the product footprint.",
"example_value": "3.2",
"queryable": True,
},
{
"name": "processing_level",
"type": str,
"description": "Processing level or product type assigned by the source provider.",
"example_value": "L2_SR",
"queryable": True,
},
]

Use field names that are stable and descriptive. Changing or removing fields after ingesting datapoints requires emptying the affected collections first, because existing datapoints must continue to match the dataset schema. The same rule applies to queryable fields: choose them before ingestion because you cannot make an existing field queryable or add a new queryable field to a non-empty dataset.

Call create_or_update_dataset with the dataset kind, code name, field list, and display name. The code name becomes the stable identifier used in SDK calls.

Python
dataset = client.create_or_update_dataset(
kind=DatasetKind.SPATIOTEMPORAL,
code_name="internal_imagery_catalog",
fields=fields,
name="Internal imagery catalog",
)
print(dataset)

If a dataset with the same code name already exists, create_or_update_dataset updates it instead of creating a duplicate. This makes the snippet safe to keep in a setup script.

Inspect the generated schema documentation

Section titled “Inspect the generated schema documentation”

Tilebox uses the dataset kind and field annotations to document the schema. Required fields are added by the dataset kind, and your custom fields appear with their descriptions and examples.

For this catalog, the complete schema includes:

FieldTypeQueryablePurpose
timeRequiredDedicated time filterTimestamp associated with the datapoint.
idRequiredDedicated ID filterTilebox-generated UUID for the datapoint.
ingestion_timeRequiredNoTime when Tilebox ingested the datapoint.
geometryRequiredDedicated spatial filterGeometry used for spatial queries.
product_idCustomNoStable product or scene identifier.
assetsCustomNoFiles associated with the imagery product.
cloud_coverCustomYesCloud cover percentage for filtering.
processing_levelCustomYesProvider processing level or product type.

The descriptions and example values you provided in the SDK call appear in the dataset schema documentation.

Use field descriptions for schema-level documentation. Use the Console documentation editor when you want longer Markdown documentation for the dataset, such as provenance notes, quality caveats, ingestion rules, or examples for downstream users.

Tilebox Console dataset documentation editor

Open the dataset in the Console, click the edit pencil on the documentation section, and add Markdown content. A short documentation block often includes:

Markdown
# Internal imagery catalog
This dataset indexes analysis-ready imagery products used by the operations team.
## Source
Products are copied from the provider archive after validation.
## Usage notes
Use `cloud_cover < 10` for workflows that require mostly cloud-free scenes.

After creating the dataset, create a collection to hold datapoints. Collections let you organize datapoints within the same schema, for example by provider, product family, or processing pipeline.

Python
collection = dataset.get_or_create_collection("landsat_level_2")
print(collection)

Load your source metadata into a GeoDataFrame. The geometry column should contain the footprint for each datapoint.

Python
import geopandas as gpd
products = gpd.read_parquet("products.geoparquet")
products = products.rename(
columns={
"timestamp": "time",
"scene": "product_id",
"path": "source_href",
}
)
products = products[
["time", "geometry", "product_id", "source_href", "cloud_cover", "processing_level"]
]

Convert each source file into an asset collection, then add its dataset fields to the record:

Python
from tilebox.datasets.assets import Asset, AssetCollection, AssetLocation, MediaType
records = []
for record in products.to_dict(orient="records"):
source_href = record.pop("source_href")
assets = AssetCollection.from_assets(
[
Asset(
key="image",
primary=AssetLocation(source_href),
media_type=MediaType.CLOUD_OPTIMIZED_GEOTIFF,
roles=frozenset({"data"}),
)
]
)
records.append({**record, **assets.to_fields()})

AssetCollection.from_assets validates and normalizes the metadata into the structure consumed by the storage client. It does not upload the referenced file or test its availability.

Ingest the prepared records into a collection.

Python
collection.ingest(records)

Query by time, location, and custom fields

Section titled “Query by time, location, and custom fields”

After ingestion, combine the queryable custom fields with temporal and spatial filters. Tilebox applies all three filters on the server before returning matching datapoints.

Python
from shapely import box
from tilebox.datasets import field
area = box(11.0, 46.0, 12.0, 47.0)
matches = collection.query(
temporal_extent=("2026-01-01", "2026-02-01"),
spatial_extent=area,
filter=(field("cloud_cover") < 10) & (field("processing_level") == "L2_SR"),
)