Skip to content

Commit

Permalink
add query/sort/fields extension support for item_collection (#192)
Browse files Browse the repository at this point in the history
* add query/sort/fields extension support for item_collection and refactor extensions mapping

* update for stac-fastapi 5.0

* more tests

* do not migrate items

* update changelog
  • Loading branch information
vincentsarago authored Jan 30, 2025
1 parent df4c12a commit d2749b2
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 47 deletions.
12 changes: 9 additions & 3 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@

### Changed

- handle `next` and `dev` tokens now returned as links from pgstac>=0.9.0 (author @zstatmanweil, <https://github.com/stac-utils/stac-fastapi-pgstac/pull/140>)
- remove `python 3.8` support
- update `stac-fastapi-*` requirement to `~=5.0`
- keep `/search` and `/collections` extensions separate ([#158](https://github.com/stac-utils/stac-fastapi-pgstac/pull/158))
- update `pypgstac` requirement to `>=0.8,<0.10`
- set `pypgstac==0.9.*` for test requirements
- update `stac-fastapi-*` requirement to `~=4.0`
- remove `python 3.8` support
- renamed `post_request_model` attribute to `pgstac_search_model` in `CoreCrudClient` class
- changed `datetime` input type to `string` in GET endpoint methods
- renamed `filter` to `filter_expr` input attributes in GET endpoint methods
- delete `utils.format_datetime_range` function

### Fixed

- handle `next` and `dev` tokens now returned as links from pgstac>=0.9.0 (author @zstatmanweil, <https://github.com/stac-utils/stac-fastapi-pgstac/pull/140>)

### Added

- add [collection search extension](https://github.com/stac-api-extensions/collection-search) support ([#139](https://github.com/stac-utils/stac-fastapi-pgstac/pull/139))
- add [free-text extension](https://github.com/stac-api-extensions/freetext-search) to collection search extensions ([#162](https://github.com/stac-utils/stac-fastapi-pgstac/pull/162))
- add [filter extension](https://github.com/stac-api-extensions/filter) support to Item Collection endpoint
- add [sort extension](https://github.com/stac-api-extensions/sort) support to Item Collection endpoint ([#192](https://github.com/stac-utils/stac-fastapi-pgstac/pull/192))
- add [query extension](https://github.com/stac-api-extensions/query) support to Item Collection endpoint ([#162](https://github.com/stac-utils/stac-fastapi-pgstac/pull/192))
- add [fields extension](https://github.com/stac-api-extensions/fields) support to Item Collection endpoint ([#162](https://github.com/stac-utils/stac-fastapi-pgstac/pull/192))

### Fixed

Expand Down
1 change: 0 additions & 1 deletion docker-compose.nginx.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3'
services:
nginx:
image: nginx
Expand Down
2 changes: 1 addition & 1 deletion nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ http {
proxy_redirect off;
}
}
}
}
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"orjson",
"pydantic",
"stac_pydantic==3.1.*",
"stac-fastapi.api~=4.0",
"stac-fastapi.extensions~=4.0",
"stac-fastapi.types~=4.0",
"stac-fastapi.api~=5.0",
"stac-fastapi.extensions~=5.0",
"stac-fastapi.types~=5.0",
"asyncpg",
"buildpg",
"brotli_asgi",
Expand Down
55 changes: 35 additions & 20 deletions stac_fastapi/pgstac/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,22 @@
create_post_request_model,
create_request_model,
)
from stac_fastapi.api.openapi import update_openapi
from stac_fastapi.extensions.core import (
CollectionSearchExtension,
CollectionSearchFilterExtension,
FieldsExtension,
FilterExtension,
FreeTextExtension,
ItemCollectionFilterExtension,
OffsetPaginationExtension,
SearchFilterExtension,
SortExtension,
TokenPaginationExtension,
TransactionExtension,
)
from stac_fastapi.extensions.core.collection_search import CollectionSearchExtension
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
from stac_fastapi.extensions.core.query import QueryConformanceClasses
from stac_fastapi.extensions.core.sort import SortConformanceClasses
from stac_fastapi.extensions.third_party import BulkTransactionExtension
from starlette.middleware import Middleware

Expand Down Expand Up @@ -59,23 +64,32 @@
"query": QueryExtension(),
"sort": SortExtension(),
"fields": FieldsExtension(),
"filter": FilterExtension(client=FiltersClient()),
"filter": SearchFilterExtension(client=FiltersClient()),
"pagination": TokenPaginationExtension(),
}

# collection_search extensions
cs_extensions_map = {
"query": QueryExtension(),
"sort": SortExtension(),
"fields": FieldsExtension(),
"filter": FilterExtension(client=FiltersClient()),
"free_text": FreeTextExtension(),
"query": QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
"sort": SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
"fields": FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
"filter": CollectionSearchFilterExtension(client=FiltersClient()),
"free_text": FreeTextExtension(
conformance_classes=[FreeTextConformanceClasses.COLLECTIONS],
),
"pagination": OffsetPaginationExtension(),
}

# item_collection extensions
itm_col_extensions_map = {
"filter": FilterExtension(client=FiltersClient()),
"query": QueryExtension(
conformance_classes=[QueryConformanceClasses.ITEMS],
),
"sort": SortExtension(
conformance_classes=[SortConformanceClasses.ITEMS],
),
"fields": FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
"filter": ItemCollectionFilterExtension(client=FiltersClient()),
"pagination": TokenPaginationExtension(),
}

Expand Down Expand Up @@ -123,6 +137,7 @@
extensions=itm_col_extensions,
request_type="GET",
)
application_extensions.extend(itm_col_extensions)

# /collections model
collections_get_request_model = EmptyRequest
Expand All @@ -145,17 +160,17 @@ async def lifespan(app: FastAPI):
await close_db_connection(app)


fastapp = FastAPI(
openapi_url=settings.openapi_url,
docs_url=settings.docs_url,
redoc_url=None,
root_path=settings.root_path,
lifespan=lifespan,
)


api = StacApi(
app=update_openapi(fastapp),
app=FastAPI(
openapi_url=settings.openapi_url,
docs_url=settings.docs_url,
redoc_url=None,
root_path=settings.root_path,
title=settings.stac_fastapi_title,
version=settings.stac_fastapi_version,
description=settings.stac_fastapi_description,
lifespan=lifespan,
),
settings=settings,
extensions=application_extensions,
client=CoreCrudClient(pgstac_search_model=post_request_model),
Expand Down
18 changes: 14 additions & 4 deletions stac_fastapi/pgstac/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ async def all_collections( # noqa: C901
collection_id=coll["id"], request=request
).get_links(extra_links=coll.get("links"))

if self.extension_is_enabled("FilterExtension"):
if self.extension_is_enabled(
"FilterExtension"
) or self.extension_is_enabled("ItemCollectionFilterExtension"):
coll["links"].append(
{
"rel": Relations.queryables.value,
Expand Down Expand Up @@ -178,7 +180,9 @@ async def get_collection(
collection_id=collection_id, request=request
).get_links(extra_links=collection.get("links"))

if self.extension_is_enabled("FilterExtension"):
if self.extension_is_enabled("FilterExtension") or self.extension_is_enabled(
"ItemCollectionFilterExtension"
):
base_url = get_base_url(request)
collection["links"].append(
{
Expand Down Expand Up @@ -343,9 +347,12 @@ async def item_collection(
datetime: Optional[str] = None,
limit: Optional[int] = None,
# Extensions
token: Optional[str] = None,
query: Optional[str] = None,
fields: Optional[List[str]] = None,
sortby: Optional[str] = None,
filter_expr: Optional[str] = None,
filter_lang: Optional[str] = None,
token: Optional[str] = None,
**kwargs,
) -> ItemCollection:
"""Get all items from a specific collection.
Expand All @@ -369,12 +376,15 @@ async def item_collection(
"datetime": datetime,
"limit": limit,
"token": token,
"query": orjson.loads(unquote_plus(query)) if query else query,
}

clean = self._clean_search_args(
base_args=base_args,
filter_query=filter_expr,
filter_lang=filter_lang,
fields=fields,
sortby=sortby,
)

search_request = self.pgstac_search_model(**clean)
Expand Down Expand Up @@ -450,11 +460,11 @@ async def get_search(
limit: Optional[int] = None,
# Extensions
query: Optional[str] = None,
token: Optional[str] = None,
fields: Optional[List[str]] = None,
sortby: Optional[str] = None,
filter_expr: Optional[str] = None,
filter_lang: Optional[str] = None,
token: Optional[str] = None,
**kwargs,
) -> ItemCollection:
"""Cross catalog search (GET).
Expand Down
7 changes: 6 additions & 1 deletion tests/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
FieldsExtension,
TransactionExtension,
)
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
from stac_fastapi.types import stac as stac_types

from stac_fastapi.pgstac.core import CoreCrudClient, Settings
Expand Down Expand Up @@ -86,10 +87,12 @@ async def test_landing_links(app_client):

async def test_get_queryables_content_type(app_client, load_test_collection):
resp = await app_client.get("queryables")
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/schema+json"

coll = load_test_collection
resp = await app_client.get(f"collections/{coll['id']}/queryables")
assert resp.status_code == 200
assert resp.headers["content-type"] == "application/schema+json"


Expand Down Expand Up @@ -487,6 +490,7 @@ async def test_search_duplicate_forward_headers(
@pytest.mark.asyncio
async def test_base_queryables(load_test_data, app_client, load_test_collection):
resp = await app_client.get("/queryables")
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "application/schema+json"
q = resp.json()
assert q["$id"].endswith("/queryables")
Expand All @@ -498,6 +502,7 @@ async def test_base_queryables(load_test_data, app_client, load_test_collection)
@pytest.mark.asyncio
async def test_collection_queryables(load_test_data, app_client, load_test_collection):
resp = await app_client.get("/collections/test-collection/queryables")
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "application/schema+json"
q = resp.json()
assert q["$id"].endswith("/collections/test-collection/queryables")
Expand Down Expand Up @@ -733,7 +738,7 @@ async def get_collection(

collection_search_extension = CollectionSearchExtension.from_extensions(
extensions=[
FieldsExtension(),
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
]
)

Expand Down
38 changes: 25 additions & 13 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@
)
from stac_fastapi.extensions.core import (
CollectionSearchExtension,
CollectionSearchFilterExtension,
FieldsExtension,
FilterExtension,
FreeTextExtension,
ItemCollectionFilterExtension,
OffsetPaginationExtension,
SearchFilterExtension,
SortExtension,
TokenPaginationExtension,
TransactionExtension,
)
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
from stac_fastapi.extensions.core.free_text import FreeTextConformanceClasses
from stac_fastapi.extensions.core.query import QueryConformanceClasses
from stac_fastapi.extensions.core.sort import SortConformanceClasses
from stac_fastapi.extensions.third_party import BulkTransactionExtension
from stac_pydantic import Collection, Item

Expand Down Expand Up @@ -143,17 +149,19 @@ def api_client(request, database):
QueryExtension(),
SortExtension(),
FieldsExtension(),
FilterExtension(client=FiltersClient()),
SearchFilterExtension(client=FiltersClient()),
TokenPaginationExtension(),
]
application_extensions.extend(search_extensions)

collection_extensions = [
QueryExtension(),
SortExtension(),
FieldsExtension(),
FilterExtension(client=FiltersClient()),
FreeTextExtension(),
QueryExtension(conformance_classes=[QueryConformanceClasses.COLLECTIONS]),
SortExtension(conformance_classes=[SortConformanceClasses.COLLECTIONS]),
FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]),
CollectionSearchFilterExtension(client=FiltersClient()),
FreeTextExtension(
conformance_classes=[FreeTextConformanceClasses.COLLECTIONS],
),
OffsetPaginationExtension(),
]
collection_search_extension = CollectionSearchExtension.from_extensions(
Expand All @@ -162,11 +170,17 @@ def api_client(request, database):
application_extensions.append(collection_search_extension)

item_collection_extensions = [
FilterExtension(client=FiltersClient()),
QueryExtension(
conformance_classes=[QueryConformanceClasses.ITEMS],
),
SortExtension(
conformance_classes=[SortConformanceClasses.ITEMS],
),
FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
ItemCollectionFilterExtension(client=FiltersClient()),
TokenPaginationExtension(),
]
# NOTE: we don't need to add the extensions to application_extensions
# because they are already in it
application_extensions.extend(item_collection_extensions)

items_get_request_model = create_request_model(
model_name="ItemCollectionUri",
Expand All @@ -179,16 +193,14 @@ def api_client(request, database):
search_extensions, base_model=PgstacSearch
)

collections_get_request_model = collection_search_extension.GET

api = StacApi(
settings=api_settings,
extensions=application_extensions,
client=CoreCrudClient(pgstac_search_model=search_post_request_model),
items_get_request_model=items_get_request_model,
search_get_request_model=search_get_request_model,
search_post_request_model=search_post_request_model,
collections_get_request_model=collections_get_request_model,
collections_get_request_model=collection_search_extension.GET,
response_class=ORJSONResponse,
router=APIRouter(prefix=prefix),
)
Expand Down
4 changes: 3 additions & 1 deletion tests/resources/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ async def test_fetches_valid_item(
mock_root = pystac.Catalog(
id="test", description="test desc", href="https://example.com"
)
item = pystac.Item.from_dict(item_dict, preserve_dict=False, root=mock_root)
item = pystac.Item.from_dict(
item_dict, preserve_dict=False, root=mock_root, migrate=False
)
item.validate()


Expand Down

0 comments on commit d2749b2

Please sign in to comment.