Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
adf4cc6
Added collection search extension
tjellicoe-tpzuk Apr 22, 2024
e01a758
Merge pull request #1 from UKEODHP/feature/EODHP-219-add-collection-s…
tjellicoe-tpzuk Apr 23, 2024
16e2443
Corrected collection search response class
tjellicoe-tpzuk Apr 26, 2024
948ce1e
Updating collection search response model
tjellicoe-tpzuk Apr 26, 2024
6a12507
Updating response model for collection search
tjellicoe-tpzuk Apr 26, 2024
cb30fbb
Updating response model for collection search
tjellicoe-tpzuk Apr 26, 2024
7c4c11f
Merge pull request #2 from UKEODHP/bugfix/json-response-type-error-co…
tjellicoe-tpzuk May 1, 2024
5072729
Add catalogs to stac fastapi and updated to maintain root link for co…
tjellicoe-tpzuk May 9, 2024
4c0694f
Added catalogs to stac fastapi
tjellicoe-tpzuk May 9, 2024
8b63cd1
Moving collection-search to a core extension
tjellicoe-tpzuk May 9, 2024
8efbe92
Adding discovery-level search for catalogue search
tjellicoe-tpzuk May 9, 2024
a27a7a4
Adding discovery-search extension
tjellicoe-tpzuk May 9, 2024
569eb0c
Adding free-text search to collection-search
tjellicoe-tpzuk May 9, 2024
441a252
Adding free-text search to collection-search
tjellicoe-tpzuk May 9, 2024
a84d63f
Updates to support catalog search )discovery-level) via free-text
tjellicoe-tpzuk May 9, 2024
5dab508
Added converter for datetime and updated cataloguri definition
tjellicoe-tpzuk May 15, 2024
a22d035
Added support for catalogues and reformatting code
tjellicoe-tpzuk May 15, 2024
31fbeeb
Code reformatting
tjellicoe-tpzuk May 15, 2024
d2b2863
Corrected catalogs endpoint spelling
tjellicoe-tpzuk May 15, 2024
61118e4
Corrected class name
tjellicoe-tpzuk May 15, 2024
d8700c8
Corrected link handling
tjellicoe-tpzuk May 15, 2024
43005fd
Removed old link comment
tjellicoe-tpzuk May 15, 2024
c59c3e3
Merge pull request #3 from UKEODHP/feature/EODHP-27-resource-catalogu…
tjellicoe-tpzuk May 15, 2024
bcd5c72
Corrected endpoint path for create collection
tjellicoe-tpzuk May 15, 2024
33d3431
Merge pull request #4 from UKEODHP/bugfix/EODHP-27-correct-create-col…
tjellicoe-tpzuk May 15, 2024
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
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 @@ -23,3 +23,4 @@ class AddOns(enum.Enum):
"""Enumeration of available third party add ons."""

bulk_transaction = "bulk-transaction"
collection_search = "collection-search"
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .query import QueryExtension
from .sort import SortExtension
from .transaction import TransactionExtension
from .collectionSearch import CollectionSearchExtension

__all__ = (
"ContextExtension",
Expand All @@ -16,4 +17,5 @@
"SortExtension",
"TokenPaginationExtension",
"TransactionExtension",
"CollectionSearchExtension",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Filter extension module."""


from .collectionSearch import CollectionSearchExtension

__all__ = ["CollectionSearchExtension"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# encoding: utf-8
"""Collection Search Extension."""
from enum import Enum
from typing import List, Type, Union

import attr
from fastapi import APIRouter, FastAPI
from starlette.responses import JSONResponse, Response
from stac_pydantic.api.collections import Collections

from stac_fastapi.api.models import JSONSchemaResponse
from stac_fastapi.api.routes import create_async_endpoint
from stac_fastapi.types.core import AsyncCollectionSearchClient, CollectionSearchClient
from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import BaseCollectionSearchGetRequest, BaseCollectionSearchPostRequest

from .request import CollectionSearchExtensionGetRequest, CollectionSearchExtensionPostRequest

class CollectionSearchConformanceClasses(str, Enum):
"""Conformance classes for the Collection Search extension.

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

CORE = "https://api.stacspec.org/v1.0.0-rc.1/core"
COLLECTION_SEARCH = "https://api.stacspec.org/v1.0.0-rc.1/collection-search"
SIMPLE_QUERY = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query"

@attr.s
class CollectionSearchExtension(ApiExtension):
"""CollectionSearch Extension.

The collection search extension adds two endpoints which allow searching of
collections via GET and POST:
GET /collection-search
POST /collection-search
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add a note that the extension seems to mention GET /collections and POST /collections but due to possible conflict with the transaction extension we should to use /collection-search

also wonder if we should use /collections-search or /search/collections 🤷‍♂️


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

Attributes:
search_get_request_model: Get request model for collection search
search_post_request_model: Post request model for collection search
client: Collection Search endpoint logic
conformance_classes: Conformance classes provided by the extension
"""

GET = CollectionSearchExtensionGetRequest
POST = CollectionSearchExtensionPostRequest

collection_search_get_request_model: Type[BaseCollectionSearchGetRequest] = attr.ib(
default=BaseCollectionSearchGetRequest
)
collection_search_post_request_model: Type[BaseCollectionSearchPostRequest] = attr.ib(
default=BaseCollectionSearchPostRequest
)

client: Union[AsyncCollectionSearchClient, CollectionSearchClient] = attr.ib(
factory=CollectionSearchClient
)

conformance_classes: List[str] = attr.ib(
default=[
CollectionSearchConformanceClasses.CORE,
CollectionSearchConformanceClasses.COLLECTION_SEARCH,
CollectionSearchConformanceClasses.SIMPLE_QUERY,
]
)
router: APIRouter = attr.ib(factory=APIRouter)
response_class: Type[Response] = attr.ib(default=JSONResponse)

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="Collection Search",
path="/collection-search",
response_model=None,
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.get_collection_search, self.collection_search_get_request_model
),
)

self.router.add_api_route(
name="Collection Search",
path="/collection-search",
response_model=None,
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["POST"],
endpoint=create_async_endpoint(
self.client.post_collection_search, self.collection_search_post_request_model
),
)

app.include_router(self.router, tags=["Collection Search Extension"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Collection Search extension request models."""

from enum import Enum
from typing import Any, Dict, Optional, List

import attr
from pydantic import BaseModel, Field
from stac_pydantic.shared import BBox

from stac_fastapi.types.search import APIRequest, Limit, str2bbox

from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval

@attr.s
class CollectionSearchExtensionGetRequest(APIRequest):
"""Collection Search extension GET request model."""

bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval)
limit: Optional[int] = attr.ib(default=10)


class CollectionSearchExtensionPostRequest(BaseModel):
"""Collection Search extension POST request model."""

bbox: Optional[BBox]
datetime: Optional[DateTimeType]
limit: Optional[Limit] = Field(default=10)
77 changes: 76 additions & 1 deletion stac_fastapi/types/stac_fastapi/types/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.requests import get_base_url
from stac_fastapi.types.rfc3339 import DateTimeType
from stac_fastapi.types.search import BaseSearchPostRequest
from stac_fastapi.types.search import BaseSearchPostRequest, BaseCollectionSearchPostRequest
from stac_fastapi.types.stac import Conformance

NumType = Union[float, int]
Expand Down Expand Up @@ -801,3 +801,78 @@ def get_queryables(
"description": "Queryable names for the example STAC API Item Search filter.",
"properties": {},
}

@attr.s
class AsyncCollectionSearchClient(abc.ABC):
"""Defines a pattern for implementing the STAC Collection Search extension."""

@abc.abstractmethod
async def post_collection_search(
self, search_request: BaseCollectionSearchPostRequest, **kwargs
) -> stac_types.ItemCollection:
"""Cross catalog search (POST) of collections.

Called with `POST /collection-search`.

Args:
search_request: search request parameters.

Returns:
A tuple of (collections, next pagination token if any).
"""
...

@abc.abstractmethod
async def get_collection_search(
self,
bbox: Optional[BBox] = None,
datetime: Optional[DateTimeType] = None,
limit: Optional[int] = 10,
**kwargs,
) -> stac_types.Collections:
"""Cross catalog search (GET) of collections.

Called with `GET /collection-search`.

Returns:
A tuple of (collections, next pagination token if any).
"""
...


@attr.s
class CollectionSearchClient(abc.ABC):
"""Defines a pattern for implementing the STAC Collection Search extension."""

@abc.abstractmethod
def post_collection_search(
self, search_request: BaseCollectionSearchPostRequest, **kwargs
) -> stac_types.Collections:
"""Cross catalog search (POST) of collections.

Called with `POST /collection-search`.

Args:
search_request: search request parameters.

Returns:
A tuple of (collections, next pagination token if any).
"""
...

@abc.abstractmethod
def get_collection_search(
self,
bbox: Optional[BBox] = None,
datetime: Optional[DateTimeType] = None,
limit: Optional[int] = 10,
**kwargs,
) -> stac_types.Collections:
"""Cross catalog search (GET) of collections.

Called with `GET /collection-search`.

Returns:
A tuple of (collections, next pagination token if any).
"""
...
92 changes: 92 additions & 0 deletions stac_fastapi/types/stac_fastapi/types/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,99 @@ def validate_bbox(cls, v: Union[str, BBox]) -> BBox:
# Validate against WGS84
if xmin < -180 or ymin < -90 or xmax > 180 or ymax > 90:
raise ValueError("Bounding box must be within (-180, -90, 180, 90)")
return v

@validator("datetime", pre=True)
def validate_datetime(cls, v: Union[str, DateTimeType]) -> DateTimeType:
"""Parse datetime."""
if type(v) == str:
v = str_to_interval(v)
return v

@property
def spatial_filter(self) -> Optional[_GeometryBase]:
"""Return a geojson-pydantic object representing the spatial filter for the search
request.

Check for both because the ``bbox`` and ``intersects`` parameters are
mutually exclusive.
"""
if self.bbox:
return Polygon(
coordinates=[
[
[self.bbox[0], self.bbox[3]],
[self.bbox[2], self.bbox[3]],
[self.bbox[2], self.bbox[1]],
[self.bbox[0], self.bbox[1]],
[self.bbox[0], self.bbox[3]],
]
]
)
if self.intersects:
return self.intersects
return


@attr.s
class BaseCollectionSearchGetRequest(APIRequest):
"""Base arguments for Collection Search GET Request."""

bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval)
limit: Optional[int] = attr.ib(default=10)


class BaseCollectionSearchPostRequest(BaseModel):
"""Search model.

Replace base model in STAC-pydantic as it includes additional fields, not in the core
model.
"""

bbox: Optional[BBox]
datetime: Optional[DateTimeType]
limit: Optional[Limit] = Field(default=10)

@property
def start_date(self) -> Optional[datetime]:
"""Extract the start date from the datetime string."""
return self.datetime[0] if self.datetime else None

@property
def end_date(self) -> Optional[datetime]:
"""Extract the end date from the datetime string."""
return self.datetime[1] if self.datetime else None

@validator("bbox", pre=True)
def validate_bbox(cls, v: Union[str, BBox]) -> BBox:
"""Check order of supplied bbox coordinates."""
if v:
if type(v) == str:
v = str2bbox(v)
# Validate order
if len(v) == 4:
xmin, ymin, xmax, ymax = v
else:
xmin, ymin, min_elev, xmax, ymax, max_elev = v
if max_elev < min_elev:
raise ValueError(
"Maximum elevation must greater than minimum elevation"
)

if xmax < xmin:
raise ValueError(
"Maximum longitude must be greater than minimum longitude"
)

if ymax < ymin:
raise ValueError(
"Maximum longitude must be greater than minimum longitude"
)

# Validate against WGS84
if xmin < -180 or ymin < -90 or xmax > 180 or ymax > 90:
raise ValueError("Bounding box must be within (-180, -90, 180, 90)")
return v

@validator("datetime", pre=True)
Expand Down