Skip to content

Commit 6383927

Browse files
authored
Merge branch 'stac-utils:main' into main
2 parents b8cd687 + a7cdd15 commit 6383927

File tree

27 files changed

+2852
-26
lines changed

27 files changed

+2852
-26
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
- Added optional `/catalogs` route support to enable federated hierarchical catalog browsing and navigation. [#547](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/547)
13+
- Added DELETE `/catalogs/{catalog_id}/collections/{collection_id}` endpoint to support removing collections from catalogs. When a collection belongs to multiple catalogs, it removes only the specified catalog from the collection's parent_ids. When a collection belongs to only one catalog, the collection is deleted entirely. [#554](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/554)
14+
- Added `parent_ids` internal field to collections to support multi-catalog hierarchies. Collections can now belong to multiple catalogs, with parent catalog IDs stored in this field for efficient querying and management. [#554](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/554)
15+
1216
### Changed
1317

1418
### Fixed
1519

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+
1633
- 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)
1734
- 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)
1835
- Normalize datetime in POST /search requests to match GET /search behavior. [#543](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/543)
1936
- Fix optional Redis support in core.py. [#549](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/549)
2037

21-
### Removed
22-
2338
## [v6.7.5] - 2025-11-25
2439

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

663-
[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
664680
[v6.7.5]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.4...v6.7.5
665681
[v6.7.4]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v6.7.3...v6.7.4
666682
[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: 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: 29 additions & 2 deletions
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
{
@@ -759,7 +786,7 @@ async def post_search(
759786

760787
body_limit = None
761788
try:
762-
if request.method == "POST" and request.body():
789+
if request.method == "POST" and await request.body():
763790
body_data = await request.json()
764791
body_limit = body_data.get("limit")
765792
except Exception:

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)