diff --git a/CHANGELOG.md b/CHANGELOG.md index 979094a4..dc2e463f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added + +- Added support for Collections search + ## [v6.0.0] - 2025-06-22 ### Added diff --git a/dockerfiles/Dockerfile.dev.os b/dockerfiles/Dockerfile.dev.os index a544e94a..a7fc113d 100644 --- a/dockerfiles/Dockerfile.dev.os +++ b/dockerfiles/Dockerfile.dev.os @@ -4,11 +4,10 @@ FROM python:3.10-slim # update apt pkgs, and install build-essential for ciso8601 RUN apt-get update && \ apt-get -y upgrade && \ - apt-get -y install build-essential && \ + apt-get -y install build-essential git && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN apt-get -y install git # update certs used by Requests ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 7e145072..e9ff23e3 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -31,13 +31,19 @@ ) from stac_fastapi.extensions.core import ( AggregationExtension, + CollectionSearchExtension, + CollectionSearchFilterExtension, FilterExtension, FreeTextExtension, SortExtension, TokenPaginationExtension, TransactionExtension, ) +from stac_fastapi.extensions.core.fields import FieldsConformanceClasses from stac_fastapi.extensions.core.filter import FilterConformanceClasses +from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses +from stac_fastapi.extensions.core.query import QueryConformanceClasses +from stac_fastapi.extensions.core.sort import SortConformanceClasses from stac_fastapi.extensions.third_party import BulkTransactionExtension from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient from stac_fastapi.sfeos_helpers.filter import EsAsyncBaseFiltersClient @@ -59,7 +65,6 @@ filter_extension.conformance_classes.append( FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS ) - aggregation_extension = AggregationExtension( client=EsAsyncBaseAggregationClient( database=database_logic, session=session, settings=settings @@ -68,6 +73,7 @@ aggregation_extension.POST = EsAggregationExtensionPostRequest aggregation_extension.GET = EsAggregationExtensionGetRequest +# Base search extensions (without CollectionSearchExtension to avoid duplicates) search_extensions = [ FieldsExtension(), QueryExtension(), @@ -98,8 +104,29 @@ ), ) +# Initialize extensions with just the search and aggregation extensions +# Initialize with base extensions extensions = [aggregation_extension] + search_extensions +# Create collection search extensions +collection_search_extensions = [ + QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]), + SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]), + FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]), + CollectionSearchFilterExtension( + conformance_classes=[FilterConformanceClasses.COLLECTIONS] + ), + FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]), +] + +# Initialize collection search with its extensions +collection_search_ext = CollectionSearchExtension.from_extensions( + collection_search_extensions +) +collections_get_request_model = collection_search_ext.GET + +extensions.append(collection_search_ext) + database_logic.extensions = [type(ext).__name__ for ext in extensions] post_request_model = create_post_request_model(search_extensions) diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py index c047014a..dd73384b 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py @@ -25,13 +25,19 @@ from stac_fastapi.core.utilities import get_bool_env from stac_fastapi.extensions.core import ( AggregationExtension, + CollectionSearchExtension, + CollectionSearchFilterExtension, FilterExtension, FreeTextExtension, SortExtension, TokenPaginationExtension, TransactionExtension, ) +from stac_fastapi.extensions.core.fields import FieldsConformanceClasses from stac_fastapi.extensions.core.filter import FilterConformanceClasses +from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses +from stac_fastapi.extensions.core.query import QueryConformanceClasses +from stac_fastapi.extensions.core.sort import SortConformanceClasses from stac_fastapi.extensions.third_party import BulkTransactionExtension from stac_fastapi.opensearch.config import OpensearchSettings from stac_fastapi.opensearch.database_logic import ( @@ -59,7 +65,6 @@ filter_extension.conformance_classes.append( FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS ) - aggregation_extension = AggregationExtension( client=EsAsyncBaseAggregationClient( database=database_logic, session=session, settings=settings @@ -68,6 +73,7 @@ aggregation_extension.POST = EsAggregationExtensionPostRequest aggregation_extension.GET = EsAggregationExtensionGetRequest +# Base search extensions (without CollectionSearchExtension to avoid duplicates) search_extensions = [ FieldsExtension(), QueryExtension(), @@ -77,7 +83,6 @@ FreeTextExtension(), ] - if TRANSACTIONS_EXTENSIONS: search_extensions.insert( 0, @@ -99,8 +104,29 @@ ), ) +# Initialize extensions with just the search and aggregation extensions +# Initialize with base extensions extensions = [aggregation_extension] + search_extensions +# Create collection search extensions +collection_search_extensions = [ + QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]), + SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]), + FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]), + CollectionSearchFilterExtension( + conformance_classes=[FilterConformanceClasses.COLLECTIONS] + ), + FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.COLLECTIONS]), +] + +# Initialize collection search with its extensions +collection_search_ext = CollectionSearchExtension.from_extensions( + collection_search_extensions +) +collections_get_request_model = collection_search_ext.GET + +extensions.append(collection_search_ext) + database_logic.extensions = [type(ext).__name__ for ext in extensions] post_request_model = create_post_request_model(search_extensions) diff --git a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/mappings.py b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/mappings.py index 476d656a..6428f9db 100644 --- a/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/mappings.py +++ b/stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/mappings.py @@ -158,8 +158,17 @@ class Geometry(Protocol): # noqa "dynamic_templates": ES_MAPPINGS_DYNAMIC_TEMPLATES, "properties": { "id": {"type": "keyword"}, - "extent.spatial.bbox": {"type": "long"}, - "extent.temporal.interval": {"type": "date"}, + "bbox_shape": {"type": "geo_shape"}, # Only this is used for spatial queries + "extent": { + "properties": {"temporal": {"properties": {"interval": {"type": "date"}}}} + }, + "properties": { + "properties": { + "datetime": {"type": "date"}, + "start_datetime": {"type": "date"}, + "end_datetime": {"type": "date"}, + } + }, "providers": {"type": "object", "enabled": False}, "links": {"type": "object", "enabled": False}, "item_assets": {"type": "object", "enabled": False}, diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index d8c5fc88..c5cdbd2e 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -18,14 +18,9 @@ CoreClient, TransactionsClient, ) -from stac_fastapi.core.extensions import QueryExtension -from stac_fastapi.core.extensions.aggregation import ( - EsAggregationExtensionGetRequest, - EsAggregationExtensionPostRequest, -) from stac_fastapi.core.rate_limit import setup_rate_limit from stac_fastapi.core.utilities import get_bool_env -from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient +from stac_fastapi.extensions.third_party import BulkTransactionExtension if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": from stac_fastapi.opensearch.app import app_config @@ -50,15 +45,7 @@ create_index_templates, ) -from stac_fastapi.extensions.core import ( - AggregationExtension, - FieldsExtension, - FilterExtension, - FreeTextExtension, - SortExtension, - TokenPaginationExtension, - TransactionExtension, -) +from stac_fastapi.extensions.core import TransactionExtension from stac_fastapi.types.config import Settings DATA_DIR = os.path.join(os.path.dirname(__file__), "data") @@ -340,7 +327,7 @@ async def route_dependencies_client(route_dependencies_app): def build_test_app(): """Build a test app with configurable transaction extensions.""" - # Create a copy of the base config + # Create a copy of the base config which already has all extensions configured test_config = app_config.copy() # Get transaction extensions setting @@ -348,48 +335,33 @@ def build_test_app(): "ENABLE_TRANSACTIONS_EXTENSIONS", default=True ) - # Configure extensions - settings = AsyncSettings() - aggregation_extension = AggregationExtension( - client=EsAsyncBaseAggregationClient( - database=database, session=None, settings=settings - ) - ) - aggregation_extension.POST = EsAggregationExtensionPostRequest - aggregation_extension.GET = EsAggregationExtensionGetRequest - - search_extensions = [ - SortExtension(), - FieldsExtension(), - QueryExtension(), - TokenPaginationExtension(), - FilterExtension(), - FreeTextExtension(), - ] + # First remove any existing transaction extensions + if "extensions" in test_config: + test_config["extensions"] = [ + ext + for ext in test_config["extensions"] + if not isinstance(ext, (TransactionExtension, BulkTransactionExtension)) + ] # Add transaction extension if enabled if TRANSACTIONS_EXTENSIONS: - search_extensions.append( - TransactionExtension( - client=TransactionsClient( - database=database, session=None, settings=settings + settings = AsyncSettings() + test_config["extensions"].extend( + [ + TransactionExtension( + client=TransactionsClient( + database=database, session=None, settings=settings + ), + settings=settings, + ), + BulkTransactionExtension( + client=BulkTransactionsClient( + database=database, session=None, settings=settings + ) ), - settings=settings, - ) + ] ) - # Update extensions in config - extensions = [aggregation_extension] + search_extensions - test_config["extensions"] = extensions - - # Update client with new extensions - test_config["client"] = CoreClient( - database=database, - session=None, - extensions=extensions, - post_request_model=test_config["search_post_request_model"], - ) - # Create and return the app api = StacApi(**test_config) return api.app