diff --git a/CHANGES.md b/CHANGES.md index e21ed5df8..413b255c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ ### Added +* Add support in pgstac backend for /queryables and /collections/{collection_id}/queryables endpoints with functions exposed in pgstac 0.6.8 +* Update pgstac requirement to 0.6.8 + ### Changed ### Removed diff --git a/docker-compose.yml b/docker-compose.yml index 6c79e5eca..8b262cc9e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,7 +63,7 @@ services: database: container_name: stac-db - image: ghcr.io/stac-utils/pgstac:v0.6.6 + image: ghcr.io/stac-utils/pgstac:v0.6.8 environment: - POSTGRES_USER=username - POSTGRES_PASSWORD=password diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 46c4568b3..33065fcd6 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -18,14 +18,14 @@ class PostItem(CollectionUri): """Create Item.""" - item: stac_types.Item = attr.ib(default=Body()) + item: stac_types.Item = attr.ib(default=Body(None)) @attr.s class PutItem(ItemUri): """Update Item.""" - item: stac_types.Item = attr.ib(default=Body()) + item: stac_types.Item = attr.ib(default=Body(None)) @attr.s diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py index a715433cc..250623c0c 100644 --- a/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py @@ -6,6 +6,7 @@ from stac_fastapi.extensions.core import ( ContextExtension, FieldsExtension, + FilterExtension, SortExtension, TokenPaginationExtension, TransactionExtension, @@ -15,6 +16,7 @@ from stac_fastapi.pgstac.core import CoreCrudClient from stac_fastapi.pgstac.db import close_db_connection, connect_to_db from stac_fastapi.pgstac.extensions import QueryExtension +from stac_fastapi.pgstac.extensions.filter import FiltersClient from stac_fastapi.pgstac.transactions import BulkTransactionsClient, TransactionsClient from stac_fastapi.pgstac.types.search import PgstacSearch @@ -30,6 +32,7 @@ FieldsExtension(), TokenPaginationExtension(), ContextExtension(), + FilterExtension(client=FiltersClient()), BulkTransactionExtension(client=BulkTransactionsClient()), ] diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py index 410bc63f1..005441794 100644 --- a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py +++ b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py @@ -1,5 +1,6 @@ """pgstac extension customisations.""" +from .filter import FiltersClient from .query import QueryExtension -__all__ = ["QueryExtension"] +__all__ = ["QueryExtension", "FiltersClient"] diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py new file mode 100644 index 000000000..80c448c49 --- /dev/null +++ b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py @@ -0,0 +1,37 @@ +"""Get Queryables.""" +from typing import Any, Optional + +from buildpg import render +from fastapi import Request +from fastapi.responses import JSONResponse + +from stac_fastapi.types.core import AsyncBaseFiltersClient + + +class FiltersClient(AsyncBaseFiltersClient): + """Defines a pattern for implementing the STAC filter extension.""" + + async def get_queryables( + self, request: Request, collection_id: Optional[str] = None, **kwargs: Any + ) -> JSONResponse: + """Get the queryables available for the given collection_id. + + If collection_id is None, returns the intersection of all + queryables over all collections. + This base implementation returns a blank queryable schema. This is not allowed + under OGC CQL but it is allowed by the STAC API Filter Extension + https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables + """ + pool = request.app.state.readpool + + async with pool.acquire() as conn: + q, p = render( + """ + SELECT * FROM get_queryables(:collection::text); + """, + collection=collection_id, + ) + queryables = await conn.fetchval(q, *p) + queryables["$id"] = str(request.url) + headers = {"Content-Type": "application/schema+json"} + return JSONResponse(queryables, headers=headers) diff --git a/stac_fastapi/pgstac/tests/api/test_api.py b/stac_fastapi/pgstac/tests/api/test_api.py index 4ace9496d..115cb5c63 100644 --- a/stac_fastapi/pgstac/tests/api/test_api.py +++ b/stac_fastapi/pgstac/tests/api/test_api.py @@ -391,3 +391,27 @@ async def test_search_duplicate_forward_headers( for feature in features: for link in feature["links"]: assert link["href"].startswith("https://test:1234/") + + +@pytest.mark.asyncio +async def test_base_queryables(load_test_data, app_client, load_test_collection): + resp = await app_client.get("/queryables") + assert resp.headers["Content-Type"] == "application/schema+json" + q = resp.json() + assert q["$id"].endswith("/queryables") + assert q["type"] == "object" + assert "properties" in q + assert "id" in q["properties"] + assert "eo:cloud_cover" in q["properties"] + + +@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.headers["Content-Type"] == "application/schema+json" + q = resp.json() + assert q["$id"].endswith("/collections/test-collection/queryables") + assert q["type"] == "object" + assert "properties" in q + assert "id" in q["properties"] + assert "eo:cloud_cover" in q["properties"] diff --git a/stac_fastapi/pgstac/tests/conftest.py b/stac_fastapi/pgstac/tests/conftest.py index ed3f970eb..75601daf4 100644 --- a/stac_fastapi/pgstac/tests/conftest.py +++ b/stac_fastapi/pgstac/tests/conftest.py @@ -17,6 +17,7 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import create_get_request_model, create_post_request_model from stac_fastapi.extensions.core import ( + ContextExtension, FieldsExtension, FilterExtension, SortExtension, @@ -28,6 +29,7 @@ from stac_fastapi.pgstac.core import CoreCrudClient from stac_fastapi.pgstac.db import close_db_connection, connect_to_db from stac_fastapi.pgstac.extensions import QueryExtension +from stac_fastapi.pgstac.extensions.filter import FiltersClient from stac_fastapi.pgstac.transactions import BulkTransactionsClient, TransactionsClient from stac_fastapi.pgstac.types.search import PgstacSearch @@ -133,12 +135,14 @@ def api_client(request, pg): extensions = [ TransactionExtension(client=TransactionsClient(), settings=settings), QueryExtension(), - FilterExtension(), SortExtension(), FieldsExtension(), TokenPaginationExtension(), + ContextExtension(), + FilterExtension(client=FiltersClient()), BulkTransactionExtension(client=BulkTransactionsClient()), ] + post_request_model = create_post_request_model(extensions, base_model=PgstacSearch) api = StacApi( settings=api_settings,