Skip to content

Commit 7a09dd4

Browse files
authored
Merge branch 'main' into CAT-1622
2 parents 81ee57d + 4139cb4 commit 7a09dd4

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
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111
- Added `STAC_FASTAPI_ES_MAPPINGS_FILE` environment variable to support file-based custom mappings configuration.
1212
- Added configuration-based support for extending Elasticsearch/OpenSearch index mappings via environment variables, allowing users to customize field mappings without code change through `STAC_FASTAPI_ES_CUSTOM_MAPPINGS` environment variable. Also added `STAC_FASTAPI_ES_DYNAMIC_MAPPING` variable to control dynamic mapping behavior. [#546](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/546)
1313

14+
- 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)
15+
1416
### Changed
1517

1618
### 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)
@@ -169,6 +170,8 @@ SFEOS provides enhanced collection search capabilities through two primary route
169170
- **GET/POST `/collections`**: The standard STAC endpoint with extended query parameters
170171
- **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)
171172

173+
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.
174+
172175
These endpoints support advanced collection discovery features including:
173176

174177
- **Sorting**: Sort collections by sortable fields using the `sortby` parameter
@@ -228,6 +231,96 @@ These extensions make it easier to build user interfaces that display and naviga
228231
> **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.
229232
230233

234+
## Catalogs Route
235+
236+
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.
237+
238+
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.
239+
240+
### Features
241+
242+
- **Hierarchical Navigation**: Browse catalogs and sub-catalogs in a parent-child relationship structure
243+
- **Collection Discovery**: Access collections within specific catalog contexts
244+
- **STAC API Compliance**: Follows STAC specification for catalog objects and linking
245+
- **Flexible Querying**: Support for standard STAC API query parameters when browsing collections within catalogs
246+
247+
### Endpoints
248+
249+
- **GET `/catalogs`**: Retrieve the root catalog and its child catalogs
250+
- **POST `/catalogs`**: Create a new catalog (requires appropriate permissions)
251+
- **GET `/catalogs/{catalog_id}`**: Retrieve a specific catalog and its children
252+
- **DELETE `/catalogs/{catalog_id}`**: Delete a catalog (optionally cascade delete all collections)
253+
- **GET `/catalogs/{catalog_id}/collections`**: Retrieve collections within a specific catalog
254+
- **POST `/catalogs/{catalog_id}/collections`**: Create a new collection within a specific catalog
255+
- **GET `/catalogs/{catalog_id}/collections/{collection_id}`**: Retrieve a specific collection within a catalog
256+
- **GET `/catalogs/{catalog_id}/collections/{collection_id}/items`**: Retrieve items within a collection in a catalog context
257+
- **GET `/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}`**: Retrieve a specific item within a catalog context
258+
259+
### Usage Examples
260+
261+
```bash
262+
# Get root catalog
263+
curl "http://localhost:8081/catalogs"
264+
265+
# Get specific catalog
266+
curl "http://localhost:8081/catalogs/earth-observation"
267+
268+
# Get collections in a catalog
269+
curl "http://localhost:8081/catalogs/earth-observation/collections"
270+
271+
# Create a new collection within a catalog
272+
curl -X POST "http://localhost:8081/catalogs/earth-observation/collections" \
273+
-H "Content-Type: application/json" \
274+
-d '{
275+
"id": "landsat-9",
276+
"type": "Collection",
277+
"stac_version": "1.0.0",
278+
"description": "Landsat 9 satellite imagery collection",
279+
"title": "Landsat 9",
280+
"license": "MIT",
281+
"extent": {
282+
"spatial": {"bbox": [[-180, -90, 180, 90]]},
283+
"temporal": {"interval": [["2021-09-27T00:00:00Z", null]]}
284+
}
285+
}'
286+
287+
# Get specific collection within a catalog
288+
curl "http://localhost:8081/catalogs/earth-observation/collections/sentinel-2"
289+
290+
# Get items in a collection within a catalog
291+
curl "http://localhost:8081/catalogs/earth-observation/collections/sentinel-2/items"
292+
293+
# Get specific item within a catalog
294+
curl "http://localhost:8081/catalogs/earth-observation/collections/sentinel-2/items/S2A_20231015_123456"
295+
296+
# Delete a catalog (collections remain intact)
297+
curl -X DELETE "http://localhost:8081/catalogs/earth-observation"
298+
299+
# Delete a catalog and all its collections (cascade delete)
300+
curl -X DELETE "http://localhost:8081/catalogs/earth-observation?cascade=true"
301+
```
302+
303+
### Delete Catalog Parameters
304+
305+
The DELETE endpoint supports the following query parameter:
306+
307+
- **`cascade`** (boolean, default: `false`):
308+
- If `false`: Only deletes the catalog. Collections linked to the catalog remain in the database but lose their catalog link.
309+
- If `true`: Deletes the catalog AND all collections linked to it. Use with caution as this is a destructive operation.
310+
311+
### Response Structure
312+
313+
Catalog responses include:
314+
- **Catalog metadata**: ID, title, description, and other catalog properties
315+
- **Child catalogs**: Links to sub-catalogs for hierarchical navigation
316+
- **Collections**: Links to collections contained within the catalog
317+
- **STAC links**: Properly formatted STAC API links for navigation
318+
319+
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.
320+
321+
> **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**.
322+
323+
231324
## Package Structure
232325

233326
This project is organized into several packages, each with a specific purpose:
@@ -361,6 +454,7 @@ You can customize additional settings in your `.env` file:
361454
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering) on the core `/collections` endpoint. | `true` | Optional |
362455
| `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 |
363456
| `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 |
457+
| `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 |
364458
| `STAC_GLOBAL_COLLECTION_MAX_LIMIT` | Configures the maximum number of STAC collections that can be returned in a single search request. | N/A | Optional |
365459
| `STAC_DEFAULT_COLLECTION_LIMIT` | Configures the default number of STAC collections returned when no limit parameter is specified in the request. | `300` | Optional |
366460
| `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)