Skip to content

Commit

Permalink
Feature/add collection search extension V2 (#736)
Browse files Browse the repository at this point in the history
* sketch

* sketch

* fix

* set limit to 10

* Update CHANGES.md
  • Loading branch information
vincentsarago authored Jul 22, 2024
1 parent fe4d0df commit 4df4947
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 2 deletions.
4 changes: 2 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* add more openapi metadata in input models [#734](https://github.com/stac-utils/stac-fastapi/pull/734)

### Added

* Add Free-text Extension to third party extensions ([#655](https://github.com/stac-utils/stac-fastapi/pull/655))
* Add Free-text Extension ([#655](https://github.com/stac-utils/stac-fastapi/pull/655))
* Add Collection-Search Extension ([#736](https://github.com/stac-utils/stac-fastapi/pull/736))

## [3.0.0b2] - 2024-07-09

Expand Down
1 change: 1 addition & 0 deletions stac_fastapi/api/stac_fastapi/api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ApiExtensions(enum.Enum):
sort = "sort"
transaction = "transaction"
aggregation = "aggregation"
collection_search = "collection-search"
free_text = "free-text"


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""stac_api.extensions.core module."""

from .aggregation import AggregationExtension
from .collection_search import CollectionSearchExtension
from .context import ContextExtension
from .fields import FieldsExtension
from .filter import FilterExtension
Expand All @@ -22,4 +23,5 @@
"SortExtension",
"TokenPaginationExtension",
"TransactionExtension",
"CollectionSearchExtension",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Collection-Search extension module."""

from .collection_search import CollectionSearchExtension, ConformanceClasses

__all__ = ["CollectionSearchExtension", "ConformanceClasses"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Collection-Search extension."""

from enum import Enum
from typing import List, Optional

import attr
from fastapi import FastAPI

from stac_fastapi.types.extension import ApiExtension

from .request import CollectionSearchExtensionGetRequest


class ConformanceClasses(str, Enum):
"""Conformance classes for the Collection-Search extension.
See
https://github.com/stac-api-extensions/collection-search
"""

COLLECTIONSEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search"
BASIS = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"
FREETEXT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text"
FILTER = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter"
QUERY = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query"
SORT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort"
FIELDS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields"


@attr.s
class CollectionSearchExtension(ApiExtension):
"""Collection-Search Extension.
The Collection-Search extension adds functionality to the `GET - /collections`
endpoint which allows the caller to include or exclude specific from the API
response.
Registering this extension with the application has the added effect of
removing the `ItemCollection` response model from the `/search` endpoint, as
the Fields extension allows the API to return potentially invalid responses
by excluding fields which are required by the STAC spec, such as geometry.
https://github.com/stac-api-extensions/collection-search
Attributes:
conformance_classes (list): Defines the list of conformance classes for
the extension
"""

GET = CollectionSearchExtensionGetRequest
POST = None

conformance_classes: List[str] = attr.ib(
default=[ConformanceClasses.COLLECTIONSEARCH, ConformanceClasses.BASIS]
)
schema_href: Optional[str] = attr.ib(default=None)

def register(self, app: FastAPI) -> None:
"""Register the extension with a FastAPI application.
Args:
app (fastapi.FastAPI): target FastAPI application.
Returns:
None
"""
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Request models for the Collection-Search extension."""

from typing import Optional

import attr
from fastapi import Query
from stac_pydantic.shared import BBox
from typing_extensions import Annotated

from stac_fastapi.types.rfc3339 import DateTimeType
from stac_fastapi.types.search import APIRequest, _bbox_converter, _datetime_converter


@attr.s
class CollectionSearchExtensionGetRequest(APIRequest):
"""Basics additional Collection-Search parameters for the GET request."""

bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter)
datetime: Optional[DateTimeType] = attr.ib(
default=None, converter=_datetime_converter
)
limit: Annotated[
Optional[int],
Query(
description="Limits the number of results that are included in each page of the response." # noqa: E501
),
] = attr.ib(default=10)
181 changes: 181 additions & 0 deletions stac_fastapi/extensions/tests/test_collection_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import json
from urllib.parse import quote_plus

from starlette.testclient import TestClient

from stac_fastapi.api.app import StacApi
from stac_fastapi.api.models import create_request_model
from stac_fastapi.extensions.core import CollectionSearchExtension
from stac_fastapi.extensions.core.collection_search import ConformanceClasses
from stac_fastapi.extensions.core.collection_search.request import (
CollectionSearchExtensionGetRequest,
)
from stac_fastapi.extensions.core.fields.request import FieldsExtensionGetRequest
from stac_fastapi.extensions.core.filter.request import FilterExtensionGetRequest
from stac_fastapi.extensions.core.free_text.request import FreeTextExtensionGetRequest
from stac_fastapi.extensions.core.query.request import QueryExtensionGetRequest
from stac_fastapi.extensions.core.sort.request import SortExtensionGetRequest
from stac_fastapi.types.config import ApiSettings
from stac_fastapi.types.core import BaseCoreClient


class DummyCoreClient(BaseCoreClient):
def all_collections(self, *args, **kwargs):
_ = kwargs.pop("request", None)
return kwargs

def get_collection(self, *args, **kwargs):
raise NotImplementedError

def get_item(self, *args, **kwargs):
raise NotImplementedError

def get_search(self, *args, **kwargs):
raise NotImplementedError

def post_search(self, *args, **kwargs):
return args[0].model_dump()

def item_collection(self, *args, **kwargs):
raise NotImplementedError


def test_collection_search_extension_default():
"""Test /collections endpoint with collection-search ext."""
api = StacApi(
settings=ApiSettings(),
client=DummyCoreClient(),
extensions=[CollectionSearchExtension()],
collections_get_request_model=CollectionSearchExtensionGetRequest,
)
with TestClient(api.app) as client:
response = client.get("/conformance")
assert response.is_success, response.json()
response_dict = response.json()
assert (
"https://api.stacspec.org/v1.0.0-rc.1/collection-search"
in response_dict["conformsTo"]
)
assert (
"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"
in response_dict["conformsTo"]
)

response = client.get("/collections")
assert response.is_success, response.json()
response_dict = response.json()
assert "bbox" in response_dict
assert "datetime" in response_dict
assert "limit" in response_dict

response = client.get(
"/collections",
params={
"datetime": "2020-06-13T13:00:00Z/2020-06-13T14:00:00Z",
"bbox": "-175.05,-85.05,175.05,85.05",
"limit": 100,
},
)
assert response.is_success, response.json()
response_dict = response.json()
assert [-175.05, -85.05, 175.05, 85.05] == response_dict["bbox"]
assert [
"2020-06-13T13:00:00+00:00",
"2020-06-13T14:00:00+00:00",
] == response_dict["datetime"]
assert 100 == response_dict["limit"]


def test_collection_search_extension_models():
"""Test /collections endpoint with collection-search ext with additional models."""
collections_get_request_model = create_request_model(
model_name="SearchGetRequest",
base_model=CollectionSearchExtensionGetRequest,
mixins=[
FreeTextExtensionGetRequest,
FilterExtensionGetRequest,
QueryExtensionGetRequest,
SortExtensionGetRequest,
FieldsExtensionGetRequest,
],
request_type="GET",
)

api = StacApi(
settings=ApiSettings(),
client=DummyCoreClient(),
extensions=[
CollectionSearchExtension(
conformance_classes=[
ConformanceClasses.COLLECTIONSEARCH,
ConformanceClasses.BASIS,
ConformanceClasses.FREETEXT,
ConformanceClasses.FILTER,
ConformanceClasses.QUERY,
ConformanceClasses.SORT,
ConformanceClasses.FIELDS,
]
)
],
collections_get_request_model=collections_get_request_model,
)
with TestClient(api.app) as client:
response = client.get("/conformance")
assert response.is_success, response.json()
response_dict = response.json()
conforms = response_dict["conformsTo"]
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search" in conforms
assert (
"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"
in conforms
)
assert (
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" in conforms
)
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter" in conforms
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query" in conforms
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort" in conforms
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields" in conforms

response = client.get("/collections")
assert response.is_success, response.json()
response_dict = response.json()
assert "bbox" in response_dict
assert "datetime" in response_dict
assert "limit" in response_dict
assert "q" in response_dict
assert "filter" in response_dict
assert "query" in response_dict
assert "sortby" in response_dict
assert "fields" in response_dict

response = client.get(
"/collections",
params={
"datetime": "2020-06-13T13:00:00Z/2020-06-13T14:00:00Z",
"bbox": "-175.05,-85.05,175.05,85.05",
"limit": 100,
"q": "EO,Earth Observation",
"filter": "id='item_id' AND collection='collection_id'",
"query": quote_plus(
json.dumps({"eo:cloud_cover": {"gte": 95}}),
),
"sortby": "-gsd,-datetime",
"fields": "properties.datetime",
},
)
assert response.is_success, response.json()
response_dict = response.json()
assert [-175.05, -85.05, 175.05, 85.05] == response_dict["bbox"]
assert [
"2020-06-13T13:00:00+00:00",
"2020-06-13T14:00:00+00:00",
] == response_dict["datetime"]
assert 100 == response_dict["limit"]
assert ["EO", "Earth Observation"] == response_dict["q"]
assert "id='item_id' AND collection='collection_id'" == response_dict["filter"]
assert "filter_crs" in response_dict
assert "cql2-text" in response_dict["filter_lang"]
assert "query" in response_dict
assert ["-gsd", "-datetime"] == response_dict["sortby"]
assert ["properties.datetime"] == response_dict["fields"]

0 comments on commit 4df4947

Please sign in to comment.