Skip to content

Commit 4139cb4

Browse files
authored
feat: Add /catalogs route for Federated STAC API Support (#547)
**Related Issue(s):** - #520 - #308 - radiantearth/stac-api-spec#239 - radiantearth/stac-api-spec#329 - stac-api-extensions/stac-api-extensions.github.io#7 - https://github.com/Healy-Hyperspatial/stac-api-extensions-catalogs #### Description This PR introduces the **Catalogs Extension**, enabling a federated "Hub and Spoke" architecture within `stac-fastapi`. Currently, the API assumes a single Root Catalog containing a flat list of Collections. This works for simple deployments but becomes unwieldy for large-scale implementations aggregating multiple providers, missions, or projects. This change adds a `/catalogs` endpoint that acts as a **Registry**, allowing the API to serve multiple distinct sub-catalogs from a single infrastructure. #### Key Features * **New Endpoints:** Implements the full suite of hierarchical endpoints: * `GET /catalogs` (List all sub-catalogs) * `POST /catalogs` (Create new sub-catalog) * `DELETE /catalogs/{catalog_id}` (Delete a catalog (supports ?cascade=true to delete child collections)) * `GET /catalogs/{catalog_id}` (Sub-catalog Landing Page) * `GET /catalogs/{catalog_id}/collections` (Scoped collections) * `POST /catalogs/{catalog_id}/collections` (Create a new collection directly linked to a specific catalog) * `GET /catalogs/{catalog_id}/collections/{collection_id}` (Get one collection) * `GET /catalogs/{catalog_id}/collections/{collection_id}/items` (Scoped item search) * `GET /catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}` (Get one item) * **Serialization:** Updates Pydantic models and serializers to support `type: "Catalog"` objects within the API tree (previously restricted to Collections). * **Configuration:** Controlled via `ENABLE_CATALOGS_ROUTE` environment variable (default: `false`). #### Storage Strategy (Non-Breaking) To ensure **zero breaking changes** and avoid complex database migrations, this implementation stores `Catalog` objects within the existing `collections` index. * **Differentiation:** Objects are distinguished using the `type` field (`type: "Catalog"` vs. `type: "Collection"`). * **Backward Compatibility:** Existing queries for Collections remain unaffected as they continue to function on the same index structure. * **No Overhead:** No new Elasticsearch/OpenSearch indices or infrastructure changes are required to enable this feature. #### Architectural Alignment This implementation follows the proposed **[STAC API Catalogs Endpoint Extension](https://github.com/Healy-Hyperspatial/stac-api-extensions-catalogs-endpoint)** (Community Extension). It addresses the "Data Silo" problem by allowing organizations to host distinct catalogs on a single API instance, rather than deploying separate containers for every project or provider. #### Changes * `stac_fastapi/core/extensions/catalogs.py`: Added the main extension logic and router. * `stac_fastapi/core/models/`: Added `Catalog` Pydantic models. * `stac_fastapi/elasticsearch/database_logic.py`: Added CRUD logic filtering by `type: "Catalog"`. * `tests/`: Added comprehensive test suite (`test_catalogs.py`) covering CRUD operations and hierarchical navigation. **PR Checklist:** - [x] Code is formatted and linted (run `pre-commit run --all-files`) - [x] Tests pass (run `make test`) - [x] Documentation has been updated to reflect changes, if applicable - [x] Changes are added to the changelog
1 parent f99c6e6 commit 4139cb4

File tree

17 files changed

+2099
-5
lines changed

17 files changed

+2099
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99

1010
### Added
1111

12+
- Added catalogs route support to enable federated hierarchical catalog browsing and navigation in the STAC API. [#547](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/547)
13+
1214
### Changed
1315

1416
### Fixed

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
9292
- [Technologies](#technologies)
9393
- [Table of Contents](#table-of-contents)
9494
- [Collection Search Extensions](#collection-search-extensions)
95+
- [Catalogs Route](#catalogs-route)
9596
- [Documentation & Resources](#documentation--resources)
9697
- [SFEOS STAC Viewer](#sfeos-stac-viewer)
9798
- [Package Structure](#package-structure)
@@ -168,6 +169,8 @@ SFEOS provides enhanced collection search capabilities through two primary route
168169
- **GET/POST `/collections`**: The standard STAC endpoint with extended query parameters
169170
- **GET/POST `/collections-search`**: A custom endpoint that supports the same parameters, created to avoid conflicts with the STAC Transactions extension if enabled (which uses POST `/collections` for collection creation)
170171

172+
The `/collections-search` endpoint follows the [STAC API Collection Search Endpoint](https://github.com/Healy-Hyperspatial/stac-api-extensions-collection-search-endpoint) specification, which provides a dedicated, conflict-free mechanism for advanced collection searching.
173+
171174
These endpoints support advanced collection discovery features including:
172175

173176
- **Sorting**: Sort collections by sortable fields using the `sortby` parameter
@@ -227,6 +230,96 @@ These extensions make it easier to build user interfaces that display and naviga
227230
> **Important**: Adding keyword fields to make text fields sortable can significantly increase the index size, especially for large text fields. Consider the storage implications when deciding which fields to make sortable.
228231
229232

233+
## Catalogs Route
234+
235+
SFEOS supports federated hierarchical catalog browsing through the `/catalogs` endpoint, enabling users to navigate through STAC catalog structures in a tree-like fashion. This extension allows for organized discovery and browsing of collections and sub-catalogs.
236+
237+
This implementation follows the [STAC API Catalogs Extension](https://github.com/Healy-Hyperspatial/stac-api-extensions-catalogs) specification, which enables a Federated STAC API architecture with a "Hub and Spoke" structure.
238+
239+
### Features
240+
241+
- **Hierarchical Navigation**: Browse catalogs and sub-catalogs in a parent-child relationship structure
242+
- **Collection Discovery**: Access collections within specific catalog contexts
243+
- **STAC API Compliance**: Follows STAC specification for catalog objects and linking
244+
- **Flexible Querying**: Support for standard STAC API query parameters when browsing collections within catalogs
245+
246+
### Endpoints
247+
248+
- **GET `/catalogs`**: Retrieve the root catalog and its child catalogs
249+
- **POST `/catalogs`**: Create a new catalog (requires appropriate permissions)
250+
- **GET `/catalogs/{catalog_id}`**: Retrieve a specific catalog and its children
251+
- **DELETE `/catalogs/{catalog_id}`**: Delete a catalog (optionally cascade delete all collections)
252+
- **GET `/catalogs/{catalog_id}/collections`**: Retrieve collections within a specific catalog
253+
- **POST `/catalogs/{catalog_id}/collections`**: Create a new collection within a specific catalog
254+
- **GET `/catalogs/{catalog_id}/collections/{collection_id}`**: Retrieve a specific collection within a catalog
255+
- **GET `/catalogs/{catalog_id}/collections/{collection_id}/items`**: Retrieve items within a collection in a catalog context
256+
- **GET `/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}`**: Retrieve a specific item within a catalog context
257+
258+
### Usage Examples
259+
260+
```bash
261+
# Get root catalog
262+
curl "http://localhost:8081/catalogs"
263+
264+
# Get specific catalog
265+
curl "http://localhost:8081/catalogs/earth-observation"
266+
267+
# Get collections in a catalog
268+
curl "http://localhost:8081/catalogs/earth-observation/collections"
269+
270+
# Create a new collection within a catalog
271+
curl -X POST "http://localhost:8081/catalogs/earth-observation/collections" \
272+
-H "Content-Type: application/json" \
273+
-d '{
274+
"id": "landsat-9",
275+
"type": "Collection",
276+
"stac_version": "1.0.0",
277+
"description": "Landsat 9 satellite imagery collection",
278+
"title": "Landsat 9",
279+
"license": "MIT",
280+
"extent": {
281+
"spatial": {"bbox": [[-180, -90, 180, 90]]},
282+
"temporal": {"interval": [["2021-09-27T00:00:00Z", null]]}
283+
}
284+
}'
285+
286+
# Get specific collection within a catalog
287+
curl "http://localhost:8081/catalogs/earth-observation/collections/sentinel-2"
288+
289+
# Get items in a collection within a catalog
290+
curl "http://localhost:8081/catalogs/earth-observation/collections/sentinel-2/items"
291+
292+
# Get specific item within a catalog
293+
curl "http://localhost:8081/catalogs/earth-observation/collections/sentinel-2/items/S2A_20231015_123456"
294+
295+
# Delete a catalog (collections remain intact)
296+
curl -X DELETE "http://localhost:8081/catalogs/earth-observation"
297+
298+
# Delete a catalog and all its collections (cascade delete)
299+
curl -X DELETE "http://localhost:8081/catalogs/earth-observation?cascade=true"
300+
```
301+
302+
### Delete Catalog Parameters
303+
304+
The DELETE endpoint supports the following query parameter:
305+
306+
- **`cascade`** (boolean, default: `false`):
307+
- If `false`: Only deletes the catalog. Collections linked to the catalog remain in the database but lose their catalog link.
308+
- If `true`: Deletes the catalog AND all collections linked to it. Use with caution as this is a destructive operation.
309+
310+
### Response Structure
311+
312+
Catalog responses include:
313+
- **Catalog metadata**: ID, title, description, and other catalog properties
314+
- **Child catalogs**: Links to sub-catalogs for hierarchical navigation
315+
- **Collections**: Links to collections contained within the catalog
316+
- **STAC links**: Properly formatted STAC API links for navigation
317+
318+
This feature enables building user interfaces that provide organized, hierarchical browsing of STAC collections, making it easier for users to discover and navigate through large collections organized by theme, provider, or any other categorization scheme.
319+
320+
> **Configuration**: The catalogs route can be enabled or disabled by setting the `ENABLE_CATALOGS_ROUTE` environment variable to `true` or `false`. By default, this endpoint is **disabled**.
321+
322+
230323
## Package Structure
231324

232325
This project is organized into several packages, each with a specific purpose:
@@ -360,6 +453,7 @@ You can customize additional settings in your `.env` file:
360453
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering) on the core `/collections` endpoint. | `true` | Optional |
361454
| `ENABLE_COLLECTIONS_SEARCH_ROUTE` | Enable the custom `/collections-search` endpoint (both GET and POST methods). When disabled, the custom endpoint will not be available, but collection search extensions will still be available on the core `/collections` endpoint if `ENABLE_COLLECTIONS_SEARCH` is true. | `false` | Optional |
362455
| `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. This is useful for deployments where mutating the catalog via the API should be prevented. If set to `true`, the POST `/collections` route for search will be unavailable in the API. | `true` | Optional |
456+
| `ENABLE_CATALOGS_ROUTE` | Enable the `/catalogs` endpoint for federated hierarchical catalog browsing and navigation. When enabled, provides access to federated STAC API architecture with hub-and-spoke pattern. | `false` | Optional |
363457
| `STAC_GLOBAL_COLLECTION_MAX_LIMIT` | Configures the maximum number of STAC collections that can be returned in a single search request. | N/A | Optional |
364458
| `STAC_DEFAULT_COLLECTION_LIMIT` | Configures the default number of STAC collections returned when no limit parameter is specified in the request. | `300` | Optional |
365459
| `STAC_GLOBAL_ITEM_MAX_LIMIT` | Configures the maximum number of STAC items that can be returned in a single search request. | N/A | Optional |

compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323
- BACKEND=elasticsearch
2424
- DATABASE_REFRESH=true
2525
- ENABLE_COLLECTIONS_SEARCH_ROUTE=true
26+
- ENABLE_CATALOGS_ROUTE=true
2627
- REDIS_ENABLE=true
2728
- REDIS_HOST=redis
2829
- REDIS_PORT=6379
@@ -62,6 +63,7 @@ services:
6263
- BACKEND=opensearch
6364
- STAC_FASTAPI_RATE_LIMIT=200/minute
6465
- ENABLE_COLLECTIONS_SEARCH_ROUTE=true
66+
- ENABLE_CATALOGS_ROUTE=true
6567
- REDIS_ENABLE=true
6668
- REDIS_HOST=redis
6769
- REDIS_PORT=6379

stac_fastapi/core/stac_fastapi/core/base_database_logic.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,39 @@ async def delete_collection(
138138
) -> None:
139139
"""Delete a collection from the database."""
140140
pass
141+
142+
@abc.abstractmethod
143+
async def get_all_catalogs(
144+
self,
145+
token: Optional[str],
146+
limit: int,
147+
request: Any = None,
148+
sort: Optional[List[Dict[str, Any]]] = None,
149+
) -> Tuple[List[Dict[str, Any]], Optional[str], Optional[int]]:
150+
"""Retrieve a list of catalogs from the database, supporting pagination.
151+
152+
Args:
153+
token (Optional[str]): The pagination token.
154+
limit (int): The number of results to return.
155+
request (Any, optional): The FastAPI request object. Defaults to None.
156+
sort (Optional[List[Dict[str, Any]]], optional): Optional sort parameter. Defaults to None.
157+
158+
Returns:
159+
A tuple of (catalogs, next pagination token if any, optional count).
160+
"""
161+
pass
162+
163+
@abc.abstractmethod
164+
async def create_catalog(self, catalog: Dict, refresh: bool = False) -> None:
165+
"""Create a catalog in the database."""
166+
pass
167+
168+
@abc.abstractmethod
169+
async def find_catalog(self, catalog_id: str) -> Dict:
170+
"""Find a catalog in the database."""
171+
pass
172+
173+
@abc.abstractmethod
174+
async def delete_catalog(self, catalog_id: str, refresh: bool = False) -> None:
175+
"""Delete a catalog from the database."""
176+
pass

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
from stac_fastapi.core.base_settings import ApiBaseSettings
2525
from stac_fastapi.core.datetime_utils import format_datetime_range
2626
from stac_fastapi.core.models.links import PagingLinks
27-
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
27+
from stac_fastapi.core.serializers import (
28+
CatalogSerializer,
29+
CollectionSerializer,
30+
ItemSerializer,
31+
)
2832
from stac_fastapi.core.session import Session
2933
from stac_fastapi.core.utilities import filter_fields, get_bool_env
3034
from stac_fastapi.extensions.core.transaction import AsyncBaseTransactionsClient
@@ -81,12 +85,24 @@ class CoreClient(AsyncBaseCoreClient):
8185
collection_serializer: Type[CollectionSerializer] = attr.ib(
8286
default=CollectionSerializer
8387
)
88+
catalog_serializer: Type[CatalogSerializer] = attr.ib(default=CatalogSerializer)
8489
post_request_model = attr.ib(default=BaseSearchPostRequest)
8590
stac_version: str = attr.ib(default=STAC_VERSION)
8691
landing_page_id: str = attr.ib(default="stac-fastapi")
8792
title: str = attr.ib(default="stac-fastapi")
8893
description: str = attr.ib(default="stac-fastapi")
8994

95+
def extension_is_enabled(self, extension_name: str) -> bool:
96+
"""Check if an extension is enabled by checking self.extensions.
97+
98+
Args:
99+
extension_name: Name of the extension class to check for.
100+
101+
Returns:
102+
True if the extension is in self.extensions, False otherwise.
103+
"""
104+
return any(ext.__class__.__name__ == extension_name for ext in self.extensions)
105+
90106
def _landing_page(
91107
self,
92108
base_url: str,
@@ -150,6 +166,7 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
150166
API landing page, serving as an entry point to the API.
151167
"""
152168
request: Request = kwargs["request"]
169+
153170
base_url = get_base_url(request)
154171
landing_page = self._landing_page(
155172
base_url=base_url,
@@ -207,6 +224,16 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
207224
]
208225
)
209226

227+
if self.extension_is_enabled("CatalogsExtension"):
228+
landing_page["links"].append(
229+
{
230+
"rel": "catalogs",
231+
"type": "application/json",
232+
"title": "Catalogs",
233+
"href": urljoin(base_url, "catalogs"),
234+
}
235+
)
236+
210237
# Add OpenAPI URL
211238
landing_page["links"].append(
212239
{

stac_fastapi/core/stac_fastapi/core/extensions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""elasticsearch extensions modifications."""
22

3+
from .catalogs import CatalogsExtension
34
from .collections_search import CollectionsSearchEndpointExtension
45
from .query import Operator, QueryableTypes, QueryExtension
56

@@ -8,4 +9,5 @@
89
"QueryableTypes",
910
"QueryExtension",
1011
"CollectionsSearchEndpointExtension",
12+
"CatalogsExtension",
1113
]

0 commit comments

Comments
 (0)