> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tilebox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

## Prerequisites

* You have a [Tilebox API key](/authentication).
* You have installed the [Python SDK](/sdks/python/install).

```bash theme={"system"}
uv add tilebox geopandas shapely
```

## Define the catalog schema

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 Python theme={"system"}
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.

## Create the 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 Python theme={"system"}
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.

<Note>
  The Python SDK currently sets the dataset display name and schema. Field-level `description` and `example_value` entries populate the generated schema documentation. Use the Tilebox Console when you want to add rich Markdown documentation to the dataset page.
</Note>

## 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:

| Field              | Type     | Queryable                | Purpose                                    |
| ------------------ | -------- | ------------------------ | ------------------------------------------ |
| `time`             | Required | Dedicated time filter    | Timestamp associated with the datapoint.   |
| `id`               | Required | Dedicated ID filter      | Tilebox-generated UUID for the datapoint.  |
| `ingestion_time`   | Required | No                       | Time when Tilebox ingested the datapoint.  |
| `geometry`         | Required | Dedicated spatial filter | Geometry used for spatial queries.         |
| `product_id`       | Custom   | No                       | Stable product or scene identifier.        |
| `assets`           | Custom   | No                       | Files associated with the imagery product. |
| `cloud_cover`      | Custom   | Yes                      | Cloud cover percentage for filtering.      |
| `processing_level` | Custom   | Yes                      | Provider processing level or product type. |

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

## Add richer dataset 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.

<Frame>
  <img src="https://mintcdn.com/tilebox/TYquvc9froFIydg1/assets/console/datasets-documentation-light.png?fit=max&auto=format&n=TYquvc9froFIydg1&q=85&s=503b75faaeaf17928f99fd2733072530" alt="Tilebox Console dataset documentation editor" className="dark:hidden" width="1536" height="970" data-path="assets/console/datasets-documentation-light.png" />

  <img src="https://mintcdn.com/tilebox/TYquvc9froFIydg1/assets/console/datasets-documentation-dark.png?fit=max&auto=format&n=TYquvc9froFIydg1&q=85&s=6cf8569b38ddd2bf4ee85b66ae6d417c" alt="Tilebox Console dataset documentation editor" className="hidden dark:block" width="1536" height="970" data-path="assets/console/datasets-documentation-dark.png" />
</Frame>

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

```md Markdown theme={"system"}
# 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.
```

## Create a collection

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 Python theme={"system"}
collection = dataset.get_or_create_collection("landsat_level_2")
print(collection)
```

## Prepare datapoints

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

```python Python theme={"system"}
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"]
]
```

## Add asset references

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

```python Python theme={"system"}
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 catalog

Ingest the prepared records into a collection.

```python Python theme={"system"}
collection.ingest(records)
```

## 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 Python theme={"system"}
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"),
)
```

## Next steps

<Columns cols={2}>
  <Card title="Spatio-temporal datasets" icon="globe" href="/datasets/types/spatiotemporal" horizontal>
    Learn the required fields and query behavior.
  </Card>

  <Card title="Filter by custom fields" icon="filter-list" href="/datasets/query/filter-by-fields" horizontal>
    Combine queryable field expressions with temporal and spatial filters.
  </Card>

  <Card title="Ingest from common file formats" icon="file-binary" href="/guides/datasets/ingest-format" horizontal>
    Load CSV, Parquet, GeoParquet, and NetCDF data before ingestion.
  </Card>

  <Card title="Ingest into a spatio-temporal catalog" icon="up-from-bracket" href="/guides/datasets/ingest-into-spatiotemporal-catalog" horizontal>
    Prepare GeoParquet metadata and ingest it into this catalog.
  </Card>
</Columns>
