Skip to content

Commit 4df4947

Browse files
Feature/add collection search extension V2 (#736)
* sketch * sketch * fix * set limit to 10 * Update CHANGES.md
1 parent fe4d0df commit 4df4947

File tree

7 files changed

+284
-2
lines changed

7 files changed

+284
-2
lines changed

CHANGES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
* add more openapi metadata in input models [#734](https://github.com/stac-utils/stac-fastapi/pull/734)
88

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

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

stac_fastapi/api/stac_fastapi/api/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ApiExtensions(enum.Enum):
1919
sort = "sort"
2020
transaction = "transaction"
2121
aggregation = "aggregation"
22+
collection_search = "collection-search"
2223
free_text = "free-text"
2324

2425

stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""stac_api.extensions.core module."""
22

33
from .aggregation import AggregationExtension
4+
from .collection_search import CollectionSearchExtension
45
from .context import ContextExtension
56
from .fields import FieldsExtension
67
from .filter import FilterExtension
@@ -22,4 +23,5 @@
2223
"SortExtension",
2324
"TokenPaginationExtension",
2425
"TransactionExtension",
26+
"CollectionSearchExtension",
2527
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Collection-Search extension module."""
2+
3+
from .collection_search import CollectionSearchExtension, ConformanceClasses
4+
5+
__all__ = ["CollectionSearchExtension", "ConformanceClasses"]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Collection-Search extension."""
2+
3+
from enum import Enum
4+
from typing import List, Optional
5+
6+
import attr
7+
from fastapi import FastAPI
8+
9+
from stac_fastapi.types.extension import ApiExtension
10+
11+
from .request import CollectionSearchExtensionGetRequest
12+
13+
14+
class ConformanceClasses(str, Enum):
15+
"""Conformance classes for the Collection-Search extension.
16+
17+
See
18+
https://github.com/stac-api-extensions/collection-search
19+
"""
20+
21+
COLLECTIONSEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search"
22+
BASIS = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"
23+
FREETEXT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text"
24+
FILTER = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter"
25+
QUERY = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query"
26+
SORT = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort"
27+
FIELDS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields"
28+
29+
30+
@attr.s
31+
class CollectionSearchExtension(ApiExtension):
32+
"""Collection-Search Extension.
33+
34+
The Collection-Search extension adds functionality to the `GET - /collections`
35+
endpoint which allows the caller to include or exclude specific from the API
36+
response.
37+
Registering this extension with the application has the added effect of
38+
removing the `ItemCollection` response model from the `/search` endpoint, as
39+
the Fields extension allows the API to return potentially invalid responses
40+
by excluding fields which are required by the STAC spec, such as geometry.
41+
42+
https://github.com/stac-api-extensions/collection-search
43+
44+
Attributes:
45+
conformance_classes (list): Defines the list of conformance classes for
46+
the extension
47+
"""
48+
49+
GET = CollectionSearchExtensionGetRequest
50+
POST = None
51+
52+
conformance_classes: List[str] = attr.ib(
53+
default=[ConformanceClasses.COLLECTIONSEARCH, ConformanceClasses.BASIS]
54+
)
55+
schema_href: Optional[str] = attr.ib(default=None)
56+
57+
def register(self, app: FastAPI) -> None:
58+
"""Register the extension with a FastAPI application.
59+
60+
Args:
61+
app (fastapi.FastAPI): target FastAPI application.
62+
63+
Returns:
64+
None
65+
"""
66+
pass
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Request models for the Collection-Search extension."""
2+
3+
from typing import Optional
4+
5+
import attr
6+
from fastapi import Query
7+
from stac_pydantic.shared import BBox
8+
from typing_extensions import Annotated
9+
10+
from stac_fastapi.types.rfc3339 import DateTimeType
11+
from stac_fastapi.types.search import APIRequest, _bbox_converter, _datetime_converter
12+
13+
14+
@attr.s
15+
class CollectionSearchExtensionGetRequest(APIRequest):
16+
"""Basics additional Collection-Search parameters for the GET request."""
17+
18+
bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter)
19+
datetime: Optional[DateTimeType] = attr.ib(
20+
default=None, converter=_datetime_converter
21+
)
22+
limit: Annotated[
23+
Optional[int],
24+
Query(
25+
description="Limits the number of results that are included in each page of the response." # noqa: E501
26+
),
27+
] = attr.ib(default=10)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import json
2+
from urllib.parse import quote_plus
3+
4+
from starlette.testclient import TestClient
5+
6+
from stac_fastapi.api.app import StacApi
7+
from stac_fastapi.api.models import create_request_model
8+
from stac_fastapi.extensions.core import CollectionSearchExtension
9+
from stac_fastapi.extensions.core.collection_search import ConformanceClasses
10+
from stac_fastapi.extensions.core.collection_search.request import (
11+
CollectionSearchExtensionGetRequest,
12+
)
13+
from stac_fastapi.extensions.core.fields.request import FieldsExtensionGetRequest
14+
from stac_fastapi.extensions.core.filter.request import FilterExtensionGetRequest
15+
from stac_fastapi.extensions.core.free_text.request import FreeTextExtensionGetRequest
16+
from stac_fastapi.extensions.core.query.request import QueryExtensionGetRequest
17+
from stac_fastapi.extensions.core.sort.request import SortExtensionGetRequest
18+
from stac_fastapi.types.config import ApiSettings
19+
from stac_fastapi.types.core import BaseCoreClient
20+
21+
22+
class DummyCoreClient(BaseCoreClient):
23+
def all_collections(self, *args, **kwargs):
24+
_ = kwargs.pop("request", None)
25+
return kwargs
26+
27+
def get_collection(self, *args, **kwargs):
28+
raise NotImplementedError
29+
30+
def get_item(self, *args, **kwargs):
31+
raise NotImplementedError
32+
33+
def get_search(self, *args, **kwargs):
34+
raise NotImplementedError
35+
36+
def post_search(self, *args, **kwargs):
37+
return args[0].model_dump()
38+
39+
def item_collection(self, *args, **kwargs):
40+
raise NotImplementedError
41+
42+
43+
def test_collection_search_extension_default():
44+
"""Test /collections endpoint with collection-search ext."""
45+
api = StacApi(
46+
settings=ApiSettings(),
47+
client=DummyCoreClient(),
48+
extensions=[CollectionSearchExtension()],
49+
collections_get_request_model=CollectionSearchExtensionGetRequest,
50+
)
51+
with TestClient(api.app) as client:
52+
response = client.get("/conformance")
53+
assert response.is_success, response.json()
54+
response_dict = response.json()
55+
assert (
56+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search"
57+
in response_dict["conformsTo"]
58+
)
59+
assert (
60+
"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"
61+
in response_dict["conformsTo"]
62+
)
63+
64+
response = client.get("/collections")
65+
assert response.is_success, response.json()
66+
response_dict = response.json()
67+
assert "bbox" in response_dict
68+
assert "datetime" in response_dict
69+
assert "limit" in response_dict
70+
71+
response = client.get(
72+
"/collections",
73+
params={
74+
"datetime": "2020-06-13T13:00:00Z/2020-06-13T14:00:00Z",
75+
"bbox": "-175.05,-85.05,175.05,85.05",
76+
"limit": 100,
77+
},
78+
)
79+
assert response.is_success, response.json()
80+
response_dict = response.json()
81+
assert [-175.05, -85.05, 175.05, 85.05] == response_dict["bbox"]
82+
assert [
83+
"2020-06-13T13:00:00+00:00",
84+
"2020-06-13T14:00:00+00:00",
85+
] == response_dict["datetime"]
86+
assert 100 == response_dict["limit"]
87+
88+
89+
def test_collection_search_extension_models():
90+
"""Test /collections endpoint with collection-search ext with additional models."""
91+
collections_get_request_model = create_request_model(
92+
model_name="SearchGetRequest",
93+
base_model=CollectionSearchExtensionGetRequest,
94+
mixins=[
95+
FreeTextExtensionGetRequest,
96+
FilterExtensionGetRequest,
97+
QueryExtensionGetRequest,
98+
SortExtensionGetRequest,
99+
FieldsExtensionGetRequest,
100+
],
101+
request_type="GET",
102+
)
103+
104+
api = StacApi(
105+
settings=ApiSettings(),
106+
client=DummyCoreClient(),
107+
extensions=[
108+
CollectionSearchExtension(
109+
conformance_classes=[
110+
ConformanceClasses.COLLECTIONSEARCH,
111+
ConformanceClasses.BASIS,
112+
ConformanceClasses.FREETEXT,
113+
ConformanceClasses.FILTER,
114+
ConformanceClasses.QUERY,
115+
ConformanceClasses.SORT,
116+
ConformanceClasses.FIELDS,
117+
]
118+
)
119+
],
120+
collections_get_request_model=collections_get_request_model,
121+
)
122+
with TestClient(api.app) as client:
123+
response = client.get("/conformance")
124+
assert response.is_success, response.json()
125+
response_dict = response.json()
126+
conforms = response_dict["conformsTo"]
127+
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search" in conforms
128+
assert (
129+
"http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"
130+
in conforms
131+
)
132+
assert (
133+
"https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text" in conforms
134+
)
135+
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#filter" in conforms
136+
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query" in conforms
137+
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort" in conforms
138+
assert "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields" in conforms
139+
140+
response = client.get("/collections")
141+
assert response.is_success, response.json()
142+
response_dict = response.json()
143+
assert "bbox" in response_dict
144+
assert "datetime" in response_dict
145+
assert "limit" in response_dict
146+
assert "q" in response_dict
147+
assert "filter" in response_dict
148+
assert "query" in response_dict
149+
assert "sortby" in response_dict
150+
assert "fields" in response_dict
151+
152+
response = client.get(
153+
"/collections",
154+
params={
155+
"datetime": "2020-06-13T13:00:00Z/2020-06-13T14:00:00Z",
156+
"bbox": "-175.05,-85.05,175.05,85.05",
157+
"limit": 100,
158+
"q": "EO,Earth Observation",
159+
"filter": "id='item_id' AND collection='collection_id'",
160+
"query": quote_plus(
161+
json.dumps({"eo:cloud_cover": {"gte": 95}}),
162+
),
163+
"sortby": "-gsd,-datetime",
164+
"fields": "properties.datetime",
165+
},
166+
)
167+
assert response.is_success, response.json()
168+
response_dict = response.json()
169+
assert [-175.05, -85.05, 175.05, 85.05] == response_dict["bbox"]
170+
assert [
171+
"2020-06-13T13:00:00+00:00",
172+
"2020-06-13T14:00:00+00:00",
173+
] == response_dict["datetime"]
174+
assert 100 == response_dict["limit"]
175+
assert ["EO", "Earth Observation"] == response_dict["q"]
176+
assert "id='item_id' AND collection='collection_id'" == response_dict["filter"]
177+
assert "filter_crs" in response_dict
178+
assert "cql2-text" in response_dict["filter_lang"]
179+
assert "query" in response_dict
180+
assert ["-gsd", "-datetime"] == response_dict["sortby"]
181+
assert ["properties.datetime"] == response_dict["fields"]

0 commit comments

Comments
 (0)