Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add collection search extension #696

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

* switch from `fastapi` to `fastapi-slim` to avoid installing unwanted dependencies. ([#687](https://github.com/stac-utils/stac-fastapi/pull/687))
* replace Enum with `Literal` for `FilterLang`. ([#686](https://github.com/stac-utils/stac-fastapi/pull/686))
* Added `collection-search` extension

### Removed

Expand Down
45 changes: 29 additions & 16 deletions stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Fastapi app creation."""


from typing import Any, Dict, List, Optional, Tuple, Type, Union

import attr
from brotli_asgi import BrotliMiddleware
from fastapi import APIRouter, FastAPI
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from pydantic import BaseModel
from stac_pydantic import api
from stac_pydantic.api.collections import Collections
from stac_pydantic.api.version import STAC_API_VERSION
Expand All @@ -29,11 +29,20 @@
from stac_fastapi.api.routes import Scope, add_route_dependencies, create_async_endpoint

# TODO: make this module not depend on `stac_fastapi.extensions`
from stac_fastapi.extensions.core import FieldsExtension, TokenPaginationExtension
from stac_fastapi.extensions.core import (
CollectionSearchExtension,
FieldsExtension,
TokenPaginationExtension,
)
from stac_fastapi.types.config import ApiSettings, Settings
from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest
from stac_fastapi.types.search import (
BaseCollectionSearchGetRequest,
BaseCollectionSearchPostRequest,
BaseSearchGetRequest,
BaseSearchPostRequest,
)


@attr.s
Expand Down Expand Up @@ -121,6 +130,13 @@ class StacApi:
)
route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attr.ib(default=[])

collections_get_request_model: Type[BaseCollectionSearchGetRequest] = attr.ib(
default=EmptyRequest
)
collections_post_request_model: Type[BaseCollectionSearchPostRequest] = attr.ib(
default=BaseModel
)

def get_extension(self, extension: Type[ApiExtension]) -> Optional[ApiExtension]:
"""Get an extension.

Expand Down Expand Up @@ -284,25 +300,22 @@ def register_get_collections(self):
Returns:
None
"""
collection_search_ext = self.get_extension(CollectionSearchExtension)
self.router.add_api_route(
name="Get Collections",
path="/collections",
response_model=(
Collections if self.settings.enable_response_models else None
(Collections if not collection_search_ext else None)
if self.settings.enable_response_models
else None
),
responses={
200: {
"content": {
MimeTypes.json.value: {},
},
"model": Collections,
},
},
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(self.client.all_collections, EmptyRequest),
endpoint=create_async_endpoint(
self.client.all_collections, self.collections_get_request_model
),
)

def register_get_collection(self):
Expand All @@ -314,9 +327,9 @@ def register_get_collection(self):
self.router.add_api_route(
name="Get Collection",
path="/collections/{collection_id}",
response_model=api.Collection
if self.settings.enable_response_models
else None,
response_model=(
api.Collection if self.settings.enable_response_models else None
),
responses={
200: {
"content": {
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 @@ -18,6 +18,7 @@ class ApiExtensions(enum.Enum):
query = "query"
sort = "sort"
transaction = "transaction"
collection_search = "collection-search"


class AddOns(enum.Enum):
Expand Down
25 changes: 25 additions & 0 deletions stac_fastapi/api/stac_fastapi/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@ def create_post_request_model(
)


def create_get_collections_request_model(
extensions, base_model: BaseSearchGetRequest = BaseSearchGetRequest
):
"""Wrap create_request_model to create the GET request model."""
return create_request_model(
"CollectionsGetRequest",
base_model=base_model,
extensions=extensions,
request_type="GET",
)


def create_post_collections_request_model(
extensions, base_model: BaseSearchPostRequest = BaseSearchPostRequest
):
"""Wrap create_request_model to create the POST request model."""

return create_request_model(
"CollectionsPostRequest",
base_model=base_model,
extensions=extensions,
request_type="POST",
)


@attr.s # type:ignore
class CollectionUri(APIRequest):
"""Get or delete collection."""
Expand Down
8 changes: 8 additions & 0 deletions stac_fastapi/api/stac_fastapi/api/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ def custom_openapi():
"application/json"
]["schema"] = {"$ref": "#/components/schemas/ItemCollection"}

if settings.api_extension_is_enabled(ApiExtensions.collection_search):
openapi_schema["paths"]["/collections"]["get"]["responses"]["200"]["content"][
"application/json"
]["schema"] = {"$ref": "#/components/schemas/Collections"}
openapi_schema["paths"]["/collections"]["post"]["responses"]["200"][
"content"
]["application/json"]["schema"] = {"$ref": "#/components/schemas/Collections"}

app.openapi_schema = openapi_schema
return app.openapi_schema

Expand Down
1 change: 1 addition & 0 deletions stac_fastapi/api/stac_fastapi/api/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
"""Library version."""

__version__ = "3.0.0a0"
76 changes: 71 additions & 5 deletions stac_fastapi/api/tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
from datetime import datetime
from datetime import datetime as datetimetype
from typing import List, Optional, Union

import pytest
from fastapi.testclient import TestClient
from pydantic import ValidationError
from pydantic import BaseModel, ValidationError
from stac_pydantic import api

from stac_fastapi.api import app
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
from stac_fastapi.api.models import (
EmptyRequest,
create_get_collections_request_model,
create_get_request_model,
create_post_collections_request_model,
create_post_request_model,
)
from stac_fastapi.extensions.core.collection_search.collection_search import (
CollectionSearchExtension,
)
from stac_fastapi.extensions.core.filter.filter import FilterExtension
from stac_fastapi.types import stac
from stac_fastapi.types.config import ApiSettings
from stac_fastapi.types.core import NumType
from stac_fastapi.types.search import BaseSearchPostRequest
from stac_fastapi.types.rfc3339 import str_to_interval
from stac_fastapi.types.search import (
BaseSearchPostRequest,
)


def test_client_response_type(TestCoreClient):
Expand Down Expand Up @@ -132,7 +144,7 @@ def get_search(
ids: Optional[List[str]] = None,
bbox: Optional[List[NumType]] = None,
intersects: Optional[str] = None,
datetime: Optional[Union[str, datetime]] = None,
datetime: Optional[Union[str, datetimetype]] = None,
limit: Optional[int] = 10,
filter: Optional[str] = None,
filter_crs: Optional[str] = None,
Expand Down Expand Up @@ -186,3 +198,57 @@ def get_search(

assert get_search.status_code == 200, get_search.text
assert post_search.status_code == 200, post_search.text


def test_collection_search_extension(TestCoreClient, collection_dict):
"""Test if Collection Search Parameters are passed correctly."""

class CollectionSearchClient(TestCoreClient):
def all_collections(
self,
bbox: Optional[List[NumType]] = None,
datetime: Optional[Union[str, datetimetype]] = None,
limit: Optional[int] = 10,
**kwargs,
) -> stac.Collections:
# Check if all collection search parameters are passed correctly

assert bbox == (-180, -90, 180, 90)
assert datetime == str_to_interval("2024-01-01T00:00:00Z")
assert limit == 10

return stac.Collections(
collections=[stac.Collection(**collection_dict)],
links=[
{"href": "test", "rel": "root"},
{"href": "test", "rel": "self"},
{"href": "test", "rel": "parent"},
],
)

collections_post_request_model = create_post_collections_request_model(
[CollectionSearchExtension()], BaseModel
)
collections_get_request_model = create_get_collections_request_model(
[CollectionSearchExtension()], EmptyRequest
)

test_app = app.StacApi(
settings=ApiSettings(),
client=CollectionSearchClient(),
collections_get_request_model=collections_get_request_model,
collections_post_request_model=collections_post_request_model,
)

with TestClient(test_app.app) as client:
get_collections = client.get(
"/collections",
params={
"bbox": "-180,-90,180,90",
"datetime": "2024-01-01T00:00:00Z",
"limit": 10,
},
)

assert get_collections.status_code == 200, get_collections.text
api.collections.Collections(**get_collections.json())
1 change: 0 additions & 1 deletion stac_fastapi/extensions/setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""stac_fastapi: extensions module."""


from setuptools import find_namespace_packages, setup

with open("README.md") as f:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""stac_api.extensions.core module."""

from .collection_search import CollectionSearchExtension
from .context import ContextExtension
from .fields import FieldsExtension
from .filter import FilterExtension
Expand All @@ -17,4 +18,5 @@
"SortExtension",
"TokenPaginationExtension",
"TransactionExtension",
"CollectionSearchExtension",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Filter extension module."""

from .collection_search import CollectionSearchExtension

__all__ = ["CollectionSearchExtension"]
Loading
Loading