Skip to content

Commit

Permalink
collection search draft
Browse files Browse the repository at this point in the history
  • Loading branch information
ccancellieri committed Sep 6, 2024
1 parent c25229e commit 3c790b4
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 34 deletions.
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

0 comments on commit 3c790b4

Please sign in to comment.