Skip to content

Commit 524f9f9

Browse files
authored
Merge branch 'main' into CAT-1525
2 parents 1b586e4 + a7cdd15 commit 524f9f9

File tree

27 files changed

+2851
-25
lines changed

27 files changed

+2851
-25
lines changed

CHANGELOG.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1717

1818
### Fixed
1919

20+
- Fix unawaited coroutine in `stac_fastapi.core.core`. [#551](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/551)
21+
- Parse `ES_TIMEOUT` environment variable as an integer. [#556](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/556)
22+
- Implemented "Smart Unlink" logic in delete_catalog: when cascade=False (default), collections are unlinked from the catalog and become root-level orphans if they have no other parents, rather than being deleted. When cascade=True, collections are deleted entirely. This prevents accidental data loss and supports poly-hierarchy scenarios where collections belong to multiple catalogs. [#557](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/557)
23+
- Fixed delete_catalog to use reverse lookup query on parent_ids field instead of fragile link parsing. This ensures all collections are found and updated correctly, preventing ghost relationships where collections remain tagged with deleted catalogs, especially in large catalogs or pagination scenarios. [#557](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/557)
24+
25+
### Removed
26+
27+
### Updated
28+
29+
## [v6.7.6] - 2025-12-04
30+
31+
### Fixed
32+
2033
- Fix incorrect min/max date formatting in `apply_datetime_filter` for `POST` requests. [#539](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/539)
2134
- Fixed datetime filtering for .0Z milliseconds to preserve precision in apply_filter_datetime, ensuring only items exactly within the specified range are returned. [#535](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/535)
2235
- Normalize datetime in POST /search requests to match GET /search behavior. [#543](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/543)
23-
24-
### Removed
36+
- Fix optional Redis support in core.py. [#549](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/549)
2537

2638
## [v6.7.5] - 2025-11-25
2739

@@ -663,7 +675,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
663675
- Use genexp in execute_search and get_all_collections to return results.
664676
- Added db_to_stac serializer to item_collection method in core.py.
665677

666-
[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.5...main
678+
[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.6...main
679+
[v6.7.6]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.5...v6.7.6
667680
[v6.7.5]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.4...v6.7.5
668681
[v6.7.4]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.3...v6.7.4
669682
[v6.7.3]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.2...v6.7.3

README.md

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[![GitHub forks](https://img.shields.io/github/forks/stac-utils/stac-fastapi-elasticsearch-opensearch.svg?color=blue)](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/network/members)
1414
[![PyPI version](https://img.shields.io/pypi/v/stac-fastapi-elasticsearch.svg?color=blue)](https://pypi.org/project/stac-fastapi-elasticsearch/)
1515
[![STAC](https://img.shields.io/badge/STAC-1.1.0-blue.svg)](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
16-
[![stac-fastapi](https://img.shields.io/badge/stac--fastapi-6.0.0-blue.svg)](https://github.com/stac-utils/stac-fastapi)
16+
[![stac-fastapi](https://img.shields.io/badge/stac--fastapi-6.1.1-blue.svg)](https://github.com/stac-utils/stac-fastapi)
1717

1818
## Sponsors & Supporters
1919

@@ -28,6 +28,7 @@ The following organizations have contributed time and/or funding to support the
2828

2929
## Latest News
3030

31+
- **12/09/2025:** Feature Merge: **Federated Catalogs**. The [`Catalogs Endpoint`](https://github.com/Healy-Hyperspatial/stac-api-extensions-catalogs-endpoint) extension is now in main! This enables a registry of catalogs and supports **poly-hierarchy** (collections belonging to multiple catalogs simultaneously). Enable it via `ENABLE_CATALOGS_EXTENSION`. _Coming next: Support for nested sub-catalogs._
3132
- **11/07/2025:** 🌍 The SFEOS STAC Viewer is now available at: https://healy-hyperspatial.github.io/sfeos-web. Use this site to examine your data and test your STAC API!
3233
- **10/24/2025:** Added `previous_token` pagination using Redis for efficient navigation. This feature allows users to navigate backwards through large result sets by storing pagination state in Redis. To use this feature, ensure Redis is configured (see [Redis for navigation](#redis-for-navigation)) and set `REDIS_ENABLE=true` in your environment.
3334
- **10/23/2025:** The `EXCLUDED_FROM_QUERYABLES` environment variable was added to exclude fields from the `queryables` endpoint. See [docs](#excluding-fields-from-queryables).
@@ -92,6 +93,7 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
9293
- [Technologies](#technologies)
9394
- [Table of Contents](#table-of-contents)
9495
- [Collection Search Extensions](#collection-search-extensions)
96+
- [Catalogs Route](#catalogs-route)
9597
- [Documentation & Resources](#documentation--resources)
9698
- [SFEOS STAC Viewer](#sfeos-stac-viewer)
9799
- [Package Structure](#package-structure)
@@ -168,6 +170,8 @@ SFEOS provides enhanced collection search capabilities through two primary route
168170
- **GET/POST `/collections`**: The standard STAC endpoint with extended query parameters
169171
- **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)
170172

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+
171175
These endpoints support advanced collection discovery features including:
172176

173177
- **Sorting**: Sort collections by sortable fields using the `sortby` parameter
@@ -227,6 +231,103 @@ These extensions make it easier to build user interfaces that display and naviga
227231
> **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.
228232
229233

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

232333
This project is organized into several packages, each with a specific purpose:
@@ -360,6 +461,7 @@ You can customize additional settings in your `.env` file:
360461
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering) on the core `/collections` endpoint. | `true` | Optional |
361462
| `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 |
362463
| `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 |
464+
| `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 |
363465
| `STAC_GLOBAL_COLLECTION_MAX_LIMIT` | Configures the maximum number of STAC collections that can be returned in a single search request. | N/A | Optional |
364466
| `STAC_DEFAULT_COLLECTION_LIMIT` | Configures the default number of STAC collections returned when no limit parameter is specified in the request. | `300` | Optional |
365467
| `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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ services:
2020
- ES_PORT=9200
2121
- ES_USE_SSL=false
2222
- ES_VERIFY_CERTS=false
23+
- ES_TIMEOUT=30
2324
- BACKEND=elasticsearch
2425
- DATABASE_REFRESH=true
2526
- ENABLE_COLLECTIONS_SEARCH_ROUTE=true
27+
- ENABLE_CATALOGS_ROUTE=true
2628
- REDIS_ENABLE=true
2729
- REDIS_HOST=redis
2830
- REDIS_PORT=6379
@@ -59,9 +61,11 @@ services:
5961
- ES_PORT=9202
6062
- ES_USE_SSL=false
6163
- ES_VERIFY_CERTS=false
64+
- ES_TIMEOUT=30
6265
- BACKEND=opensearch
6366
- STAC_FASTAPI_RATE_LIMIT=200/minute
6467
- ENABLE_COLLECTIONS_SEARCH_ROUTE=true
68+
- ENABLE_CATALOGS_ROUTE=true
6569
- REDIS_ENABLE=true
6670
- REDIS_HOST=redis
6771
- REDIS_PORT=6379

stac_fastapi/core/stac_fastapi/core/base_database_logic.py

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

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
)
3131
from stac_fastapi.core.redis_utils import redis_pagination_links
3232
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
33+
from stac_fastapi.core.serializers import (
34+
CatalogSerializer,
35+
CollectionSerializer,
36+
ItemSerializer,
37+
)
3338
from stac_fastapi.core.session import Session
3439
from stac_fastapi.core.utilities import filter_fields, get_bool_env
3540
from stac_fastapi.extensions.core.transaction import AsyncBaseTransactionsClient
@@ -86,6 +91,7 @@ class CoreClient(AsyncBaseCoreClient):
8691
collection_serializer: Type[CollectionSerializer] = attr.ib(
8792
default=CollectionSerializer
8893
)
94+
catalog_serializer: Type[CatalogSerializer] = attr.ib(default=CatalogSerializer)
8995
post_request_model = attr.ib(default=BaseSearchPostRequest)
9096
stac_version: str = attr.ib(default=STAC_VERSION)
9197
landing_page_id: str = attr.ib(default="stac-fastapi")
@@ -95,6 +101,16 @@ class CoreClient(AsyncBaseCoreClient):
95101
def __attrs_post_init__(self):
96102
"""Initialize the queryables cache."""
97103
self.queryables_cache = QueryablesCache(self.database)
104+
def extension_is_enabled(self, extension_name: str) -> bool:
105+
"""Check if an extension is enabled by checking self.extensions.
106+
107+
Args:
108+
extension_name: Name of the extension class to check for.
109+
110+
Returns:
111+
True if the extension is in self.extensions, False otherwise.
112+
"""
113+
return any(ext.__class__.__name__ == extension_name for ext in self.extensions)
98114

99115
def _landing_page(
100116
self,
@@ -159,6 +175,7 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
159175
API landing page, serving as an entry point to the API.
160176
"""
161177
request: Request = kwargs["request"]
178+
162179
base_url = get_base_url(request)
163180
landing_page = self._landing_page(
164181
base_url=base_url,
@@ -216,6 +233,16 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
216233
]
217234
)
218235

236+
if self.extension_is_enabled("CatalogsExtension"):
237+
landing_page["links"].append(
238+
{
239+
"rel": "catalogs",
240+
"type": "application/json",
241+
"title": "Catalogs",
242+
"href": urljoin(base_url, "catalogs"),
243+
}
244+
)
245+
219246
# Add OpenAPI URL
220247
landing_page["links"].append(
221248
{
@@ -434,6 +461,8 @@ async def all_collections(
434461
]
435462

436463
if redis_enable:
464+
from stac_fastapi.core.redis_utils import redis_pagination_links
465+
437466
await redis_pagination_links(
438467
current_url=str(request.url),
439468
token=token,
@@ -766,7 +795,7 @@ async def post_search(
766795

767796
body_limit = None
768797
try:
769-
if request.method == "POST" and request.body():
798+
if request.method == "POST" and await request.body():
770799
body_data = await request.json()
771800
body_limit = body_data.get("limit")
772801
except Exception:
@@ -918,6 +947,8 @@ async def post_search(
918947
links.extend(collection_links)
919948

920949
if redis_enable:
950+
from stac_fastapi.core.redis_utils import redis_pagination_links
951+
921952
await redis_pagination_links(
922953
current_url=str(request.url),
923954
token=token_param,

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)