Skip to content

Commit

Permalink
split free-text ext
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentsarago committed Jul 18, 2024
1 parent 6bc4ce3 commit 5be3671
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .context import ContextExtension
from .fields import FieldsExtension
from .filter import FilterExtension
from .free_text import FreeTextExtension
from .free_text import FreeTextAdvancedExtension, FreeTextExtension
from .pagination import PaginationExtension, TokenPaginationExtension
from .query import QueryExtension
from .sort import SortExtension
Expand All @@ -16,6 +16,7 @@
"FieldsExtension",
"FilterExtension",
"FreeTextExtension",
"FreeTextAdvancedExtension",
"PaginationExtension",
"QueryExtension",
"SortExtension",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
"""Query extension module."""

from .free_text import FreeTextConformanceClasses, FreeTextExtension
from .free_text import (
FreeTextAdvancedExtension,
FreeTextConformanceClasses,
FreeTextExtension,
)

__all__ = ["FreeTextExtension", "FreeTextConformanceClasses"]
__all__ = [
"FreeTextExtension",
"FreeTextAdvancedExtension",
"FreeTextConformanceClasses",
]
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@

from stac_fastapi.types.extension import ApiExtension

from .request import FreeTextExtensionGetRequest, FreeTextExtensionPostRequest
from .request import (
FreeTextAdvancedExtensionGetRequest,
FreeTextAdvancedExtensionPostRequest,
FreeTextExtensionGetRequest,
FreeTextExtensionPostRequest,
)


class FreeTextConformanceClasses(str, Enum):
Expand All @@ -19,9 +24,9 @@ class FreeTextConformanceClasses(str, Enum):
"""

# https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#basic
SEARCH_BASIC = "https://api.stacspec.org/v1.0.0-rc.1/item-search#free-text"
COLLECTIONS_BASIC = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text"
ITEMS_BASIC = "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features#free-text"
SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/item-search#free-text"
COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#free-text"
ITEMS = "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features#free-text"

# https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#advanced
SEARCH_ADVANCED = (
Expand All @@ -42,14 +47,55 @@ class FreeTextExtension(ApiExtension):
The Free-text extension adds an additional `q` parameter to `/search` requests which
allows the caller to perform free-text queries against STAC metadata.
https://github.com/stac-api-extensions/freetext-search/README.md
https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#basic
"""

GET = FreeTextExtensionGetRequest
POST = FreeTextExtensionPostRequest

conformance_classes: List[str] = attr.ib()
conformance_classes: List[str] = attr.ib(
default=[
FreeTextConformanceClasses.SEARCH,
FreeTextConformanceClasses.COLLECTIONS,
FreeTextConformanceClasses.ITEMS,
]
)
schema_href: Optional[str] = attr.ib(default=None)

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


@attr.s
class FreeTextAdvancedExtension(ApiExtension):
"""Free-text Extension.
The Free-text extension adds an additional `q` parameter to `/search` requests which
allows the caller to perform free-text queries against STAC metadata.
https://github.com/stac-api-extensions/freetext-search?tab=readme-ov-file#advanced
"""

GET = FreeTextAdvancedExtensionGetRequest
POST = FreeTextAdvancedExtensionPostRequest

conformance_classes: List[str] = attr.ib(
default=[
FreeTextConformanceClasses.SEARCH_ADVANCED,
FreeTextConformanceClasses.COLLECTIONS_ADVANCED,
FreeTextConformanceClasses.ITEMS_ADVANCED,
]
)
schema_href: Optional[str] = attr.ib(default=None)

def register(self, app: FastAPI) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
"""Request model for the Free-text extension."""

from typing import Optional
from typing import List, Optional

import attr
from fastapi import Query
from pydantic import BaseModel, Field
from typing_extensions import Annotated

from stac_fastapi.types.search import APIRequest
from stac_fastapi.types.search import APIRequest, str2list


def _ft_converter(
val: Annotated[
Optional[str],
Query(
description="Parameter to perform free-text queries against STAC metadata",
json_schema_extra={
"example": "ocean,coast",
},
),
] = None,
) -> Optional[List[str]]:
return str2list(val)


@attr.s
class FreeTextExtensionGetRequest(APIRequest):
"""Free-text Extension GET request model."""

q: Optional[List[str]] = attr.ib(default=None, converter=_ft_converter)


class FreeTextExtensionPostRequest(BaseModel):
"""Free-text Extension POST request model."""

q: Optional[List[str]] = Field(
None,
description="Parameter to perform free-text queries against STAC metadata",
)


@attr.s
class FreeTextAdvancedExtensionGetRequest(APIRequest):
"""Free-text Extension GET request model."""

q: Annotated[
Optional[str],
Query(
description="Parameter to perform free-text queries against STAC metadata",
json_schema_extra={
"example": "item1,item2",
"example": "ocean,coast",
},
),
] = attr.ib(default=None)


class FreeTextExtensionPostRequest(BaseModel):
class FreeTextAdvancedExtensionPostRequest(BaseModel):
"""Free-text Extension POST request model."""

q: Optional[str] = Field(
Expand Down
128 changes: 99 additions & 29 deletions stac_fastapi/extensions/tests/test_free_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
create_post_request_model,
create_request_model,
)
from stac_fastapi.extensions.core import FreeTextExtension
from stac_fastapi.extensions.core import FreeTextAdvancedExtension, FreeTextExtension
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
from stac_fastapi.types.config import ApiSettings
from stac_fastapi.types.core import BaseCoreClient
Expand Down Expand Up @@ -41,9 +41,7 @@ def test_search_free_text_search():
"""Test search endpoints with free-text ext."""
settings = ApiSettings()
extensions = [
FreeTextExtension(
conformance_classes=[FreeTextConformanceClasses.SEARCH_BASIC.value]
)
FreeTextExtension(conformance_classes=[FreeTextConformanceClasses.SEARCH])
]

api = StacApi(
Expand All @@ -57,9 +55,8 @@ def test_search_free_text_search():
response = client.get("/conformance")
assert response.is_success, response.json()
response_dict = response.json()
assert (
FreeTextConformanceClasses.SEARCH_BASIC.value in response_dict["conformsTo"]
)
conforms = response_dict["conformsTo"]
assert FreeTextConformanceClasses.SEARCH in conforms

# /search - GET, no free-text
response = client.get(
Expand All @@ -78,7 +75,7 @@ def test_search_free_text_search():
},
)
assert response.is_success, response.text
assert response.json() == "ocean,coast"
assert response.json() == ["ocean", "coast"]

# /search - POST, no free-text
response = client.post(
Expand All @@ -95,20 +92,98 @@ def test_search_free_text_search():
"/search",
json={
"collections": ["test"],
"q": ["ocean", "coast"],
},
)

assert response.is_success, response.text
assert response.json() == ["ocean", "coast"]


def test_search_free_text_complete():
"""Test search,collections,items endpoints with free-text ext."""
settings = ApiSettings()

free_text = FreeTextExtension(
conformance_classes=[
FreeTextConformanceClasses.SEARCH,
FreeTextConformanceClasses.ITEMS,
FreeTextConformanceClasses.COLLECTIONS,
]
)

search_get_model = create_get_request_model([free_text])
search_post_model = create_post_request_model([free_text])
items_get_model = create_request_model(
"ItemCollectionURI",
base_model=ItemCollectionUri,
mixins=[free_text.GET],
)

api = StacApi(
settings=settings,
client=DummyCoreClient(),
extensions=[free_text],
search_get_request_model=search_get_model,
search_post_request_model=search_post_model,
collections_get_request_model=free_text.GET,
items_get_request_model=items_get_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 FreeTextConformanceClasses.SEARCH in conforms
assert FreeTextConformanceClasses.ITEMS in conforms
assert FreeTextConformanceClasses.COLLECTIONS in conforms

# /search - GET, no free-text
response = client.get(
"/search",
params={"collections": ["test"]},
)
assert response.is_success
assert not response.text

# /search - GET, free-text option
response = client.get(
"/search",
params={
"collections": ["test"],
"q": "ocean,coast",
},
)
assert response.is_success, response.text
assert response.json() == ["ocean", "coast"]

# /collections - GET, free-text option
response = client.get(
"/collections",
params={
"q": "ocean,coast",
},
)
assert response.is_success, response.text
assert response.json() == ["ocean", "coast"]

# /items - GET, free-text option
response = client.get(
"/collections/test/items",
params={
"q": "ocean,coast",
},
)
assert response.is_success, response.text
assert response.json() == "ocean,coast"
assert response.json() == ["ocean", "coast"]


def test_search_free_text_search_advances():
def test_search_free_text_search_advanced():
"""Test search endpoints with free-text ext."""
settings = ApiSettings()
extensions = [
FreeTextExtension(
conformance_classes=[FreeTextConformanceClasses.SEARCH_ADVANCED.value]
FreeTextAdvancedExtension(
conformance_classes=[FreeTextConformanceClasses.SEARCH_ADVANCED]
)
]

Expand All @@ -123,10 +198,9 @@ def test_search_free_text_search_advances():
response = client.get("/conformance")
assert response.is_success, response.json()
response_dict = response.json()
assert (
FreeTextConformanceClasses.SEARCH_ADVANCED.value
in response_dict["conformsTo"]
)

conforms = response_dict["conformsTo"]
assert FreeTextConformanceClasses.SEARCH_ADVANCED in conforms

# /search - GET, no free-text
response = client.get(
Expand Down Expand Up @@ -170,15 +244,15 @@ def test_search_free_text_search_advances():
assert response.json() == "+ocean,-coast"


def test_search_free_text_complete():
def test_search_free_text_advanced_complete():
"""Test search,collections,items endpoints with free-text ext."""
settings = ApiSettings()

free_text = FreeTextExtension(
free_text = FreeTextAdvancedExtension(
conformance_classes=[
FreeTextConformanceClasses.SEARCH_BASIC.value,
FreeTextConformanceClasses.ITEMS_BASIC.value,
FreeTextConformanceClasses.COLLECTIONS_BASIC.value,
FreeTextConformanceClasses.SEARCH_ADVANCED,
FreeTextConformanceClasses.ITEMS_ADVANCED,
FreeTextConformanceClasses.COLLECTIONS_ADVANCED,
]
)

Expand All @@ -203,14 +277,10 @@ def test_search_free_text_complete():
response = client.get("/conformance")
assert response.is_success, response.json()
response_dict = response.json()
assert (
FreeTextConformanceClasses.SEARCH_BASIC.value in response_dict["conformsTo"]
)
assert FreeTextConformanceClasses.ITEMS_BASIC.value in response_dict["conformsTo"]
assert (
FreeTextConformanceClasses.COLLECTIONS_BASIC.value
in response_dict["conformsTo"]
)
conforms = response_dict["conformsTo"]
assert FreeTextConformanceClasses.SEARCH_ADVANCED in conforms
assert FreeTextConformanceClasses.ITEMS_ADVANCED in conforms
assert FreeTextConformanceClasses.COLLECTIONS_ADVANCED in conforms

# /search - GET, no free-text
response = client.get(
Expand Down

0 comments on commit 5be3671

Please sign in to comment.