Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collection search draft #298

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 73 additions & 14 deletions stac_fastapi/core/stac_fastapi/core/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Core client."""

import json
import logging
from datetime import datetime as datetime_type
from datetime import timezone
Expand All @@ -21,16 +21,21 @@

from stac_fastapi.core.base_database_logic import BaseDatabaseLogic
from stac_fastapi.core.base_settings import ApiBaseSettings
from stac_fastapi.core.models import CollectionSearchPostRequest
from stac_fastapi.core.models.links import PagingLinks
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
from stac_fastapi.core.session import Session
from stac_fastapi.core.utilities import filter_fields
from stac_fastapi.extensions.core.collection_search.collection_search import (
AsyncBaseCollectionSearchClient,
)
from stac_fastapi.extensions.core.filter.client import AsyncBaseFiltersClient
from stac_fastapi.extensions.third_party.bulk_transactions import (
BaseBulkTransactionsClient,
BulkTransactionMethod,
Items,
)
from stac_fastapi.types import stac
from stac_fastapi.types import stac as stac_types
from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES
from stac_fastapi.types.core import AsyncBaseCoreClient, AsyncBaseTransactionsClient
Expand Down Expand Up @@ -553,7 +558,8 @@ async def post_search(
"""
base_url = str(request.base_url)

search = self.database.make_search()
is_collection_search = isinstance(search_request, CollectionSearchPostRequest)
search = self.database.make_search(is_collection_search=is_collection_search)

if search_request.ids:
search = self.database.apply_ids_filter(
Expand Down Expand Up @@ -594,17 +600,20 @@ async def post_search(
)

# only cql2_json is supported here
if hasattr(search_request, "filter"):
cql2_filter = getattr(search_request, "filter", None)
if filter := search_request.filter:
cql2_filter = filter
if search_request.filter_lang == "cql2-text":
cql2_filter = json.loads(to_cql2(parse_cql2_text(cql2_filter)))
# only cql2_json is supported here
try:
search = self.database.apply_cql2_filter(search, cql2_filter)
except Exception as e:
raise HTTPException(
status_code=400, detail=f"Error with cql2_json filter: {e}"
)

if hasattr(search_request, "q"):
free_text_queries = getattr(search_request, "q", None)
if q := search_request.q:
free_text_queries = q
try:
search = self.database.apply_free_text_filter(search, free_text_queries)
except Exception as e:
Expand Down Expand Up @@ -636,23 +645,44 @@ async def post_search(
include: Set[str] = fields.include if fields and fields.include else set()
exclude: Set[str] = fields.exclude if fields and fields.exclude else set()

if is_collection_search:

def serializer(self, item):
return self.collection_serializer.db_to_stac(
collection=item, request=request, extensions=self.extensions
)

else:

def serializer(self, item):
return self.item_serializer.db_to_stac(item, base_url=base_url)

items = [
filter_fields(
self.item_serializer.db_to_stac(item, base_url=base_url),
serializer(self, item),
include,
exclude,
)
for item in items
]
links = await PagingLinks(request=request, next=next_token).get_links()

return stac_types.ItemCollection(
type="FeatureCollection",
features=items,
links=links,
numReturned=len(items),
numMatched=maybe_count,
)
if is_collection_search:
return stac_types.ItemCollection(
type="FeatureCollection",
collections=items,
links=links,
numReturned=len(items),
numMatched=maybe_count,
)
else:
return stac_types.ItemCollection(
type="FeatureCollection",
features=items,
links=links,
numReturned=len(items),
numMatched=maybe_count,
)


@attr.s
Expand Down Expand Up @@ -984,3 +1014,32 @@ async def get_queryables(
},
"additionalProperties": True,
}


@attr.s
class AsyncCollectionSearchClient(AsyncBaseCollectionSearchClient, CoreClient):
"""AsyncCollectionSearchClient class."""

def __init__(self, database_logic, **kwargs):
"""Run the Constructor."""
super(AsyncCollectionSearchClient, self).__init__(**kwargs)
self.database = database_logic

async def post_all_collections(
self, search_request: CollectionSearchPostRequest, **kwargs
) -> stac.ItemCollection:
"""
Perform a POST search on the catalog.

Args:
search_request (BaseSearchPostRequest): Request object that includes the parameters for the search.
kwargs: Keyword arguments passed to the function.

Returns:
ItemCollection: A collection of items matching the search criteria.

Raises:
HTTPException: If there is an error with the cql2_json filter.
"""
request = kwargs.get("request")
return await self.post_search(search_request=search_request, request=request)
8 changes: 7 additions & 1 deletion stac_fastapi/core/stac_fastapi/core/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""elasticsearch extensions modifications."""

from .collection_post_search import CollectionSearchPostExtension
from .query import Operator, QueryableTypes, QueryExtension

__all__ = ["Operator", "QueryableTypes", "QueryExtension"]
__all__ = [
"Operator",
"QueryableTypes",
"QueryExtension",
"CollectionSearchPostExtension",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Request model for the Aggregation extension."""

from typing import List, Optional, Union

import attr
from fastapi import APIRouter, FastAPI
from stac_pydantic.api.collections import Collections
from stac_pydantic.shared import MimeTypes

from stac_fastapi.api.models import GeoJSONResponse
from stac_fastapi.api.routes import create_async_endpoint
from stac_fastapi.extensions.core.collection_search import (
CollectionSearchExtension,
ConformanceClasses,
)
from stac_fastapi.extensions.core.collection_search.client import (
AsyncBaseCollectionSearchClient,
BaseCollectionSearchClient,
)
from stac_fastapi.extensions.core.collection_search.request import (
BaseCollectionSearchGetRequest,
BaseCollectionSearchPostRequest,
)
from stac_fastapi.types.config import ApiSettings


@attr.s
class CollectionSearchPostExtension(CollectionSearchExtension):
"""Collection-Search Extension.

Extents the collection-search extension with an additional
POST - /collections endpoint

NOTE: the POST - /collections endpoint can be conflicting with the
POST /collections endpoint registered for the Transaction extension.

https://github.com/stac-api-extensions/collection-search

Attributes:
conformance_classes (list): Defines the list of conformance classes for
the extension
"""

client: Union[
AsyncBaseCollectionSearchClient, BaseCollectionSearchClient
] = attr.ib()
settings: ApiSettings = attr.ib()
conformance_classes: List[str] = attr.ib(
default=[ConformanceClasses.COLLECTIONSEARCH, ConformanceClasses.BASIS]
)
schema_href: Optional[str] = attr.ib(default=None)
router: APIRouter = attr.ib(factory=APIRouter)

GET: BaseCollectionSearchGetRequest = attr.ib(
default=BaseCollectionSearchGetRequest
)
POST: BaseCollectionSearchPostRequest = attr.ib(
default=BaseCollectionSearchPostRequest
)

def register(self, app: FastAPI) -> None:
"""Register the extension with a FastAPI application.

Args:
app: target FastAPI application.

Returns:
None
"""
self.router.prefix = app.state.router_prefix

self.router.add_api_route(
name="Collections searcb",
path="/collections-search",
methods=["POST"],
response_model=(
Collections if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
},
"model": Collections,
},
},
response_class=GeoJSONResponse,
endpoint=create_async_endpoint(self.client.post_all_collections, self.POST),
)
app.include_router(self.router)
4 changes: 4 additions & 0 deletions stac_fastapi/core/stac_fastapi/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
"""stac_fastapi.elasticsearch.models module."""
from .search import CollectionSearchPostRequest

"""elasticsearch extensions modifications."""
__all__ = ["CollectionSearchPostRequest"]
36 changes: 35 additions & 1 deletion stac_fastapi/core/stac_fastapi/core/models/search.py
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
"""Unused search model."""
"""Search model."""

from typing import List, Optional

from stac_fastapi.extensions.core.collection_search.collection_search import (
BaseCollectionSearchPostRequest,
)
from stac_fastapi.types.search import BaseSearchPostRequest


# CollectionSearchPostRequest model.
class CollectionSearchPostRequest(
BaseCollectionSearchPostRequest, BaseSearchPostRequest
):
"""The CollectionSearchPostRequest class."""

query: Optional[str] = None
token: Optional[str] = None
fields: Optional[List[str]] = None
sortby: Optional[str] = None
intersects: Optional[str] = None
filter: Optional[str] = None
filter_lang: Optional[str] = None
q: Optional[str] = None

def __init__(self, **kwargs):
"""Run the Constructor."""
super().__init__(**kwargs)
self.query = kwargs.get("query", None)
self.token = kwargs.get("token", None)
self.sortby = kwargs.get("sortby", None)
self.fields = kwargs.get("fields", None)
self.filter = kwargs.get("filter", None)
self.filter_lang = kwargs.get("filter-lang", None)
self.q = kwargs.get("q", None)
10 changes: 8 additions & 2 deletions stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
from stac_fastapi.api.app import StacApi
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
from stac_fastapi.core.core import (
AsyncCollectionSearchClient,
BulkTransactionsClient,
CoreClient,
EsAsyncBaseFiltersClient,
TransactionsClient,
)
from stac_fastapi.core.extensions import QueryExtension
from stac_fastapi.core.extensions import CollectionSearchPostExtension, QueryExtension
from stac_fastapi.core.extensions.aggregation import (
EsAggregationExtensionGetRequest,
EsAggregationExtensionPostRequest,
EsAsyncAggregationClient,
)
from stac_fastapi.core.extensions.fields import FieldsExtension
from stac_fastapi.core.models import CollectionSearchPostRequest
from stac_fastapi.core.route_dependencies import get_route_dependencies
from stac_fastapi.core.session import Session
from stac_fastapi.elasticsearch.config import ElasticsearchSettings
Expand All @@ -25,7 +27,7 @@
create_collection_index,
create_index_templates,
)
from stac_fastapi.extensions.core import (
from stac_fastapi.extensions.core import ( # CollectionSearchExtension,; CollectionSearchPostExtension
AggregationExtension,
FilterExtension,
FreeTextExtension,
Expand Down Expand Up @@ -53,7 +55,11 @@
aggregation_extension.POST = EsAggregationExtensionPostRequest
aggregation_extension.GET = EsAggregationExtensionGetRequest

collection_client = AsyncCollectionSearchClient(database=database_logic)
search_extensions = [
CollectionSearchPostExtension(
POST=CollectionSearchPostRequest, client=collection_client, settings=settings
),
TransactionExtension(
client=TransactionsClient(
database=database_logic, session=session, settings=settings
Expand Down
Loading
Loading