diff --git a/CHANGES.md b/CHANGES.md index 575965d1..1a5076a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,22 +4,28 @@ ### Changed -- handle `next` and `dev` tokens now returned as links from pgstac>=0.9.0 (author @zstatmanweil, ) +- 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, ) + ### 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 diff --git a/docker-compose.nginx.yml b/docker-compose.nginx.yml index da4df3aa..637a8c64 100644 --- a/docker-compose.nginx.yml +++ b/docker-compose.nginx.yml @@ -1,4 +1,3 @@ -version: '3' services: nginx: image: nginx diff --git a/nginx.conf b/nginx.conf index 0d23c0f8..7431549d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -17,4 +17,4 @@ http { proxy_redirect off; } } -} \ No newline at end of file +} diff --git a/setup.py b/setup.py index 8dae9911..702c420c 100644 --- a/setup.py +++ b/setup.py @@ -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", diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index b5d57e66..fcc12889 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -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 @@ -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(), } @@ -123,6 +137,7 @@ extensions=itm_col_extensions, request_type="GET", ) + application_extensions.extend(itm_col_extensions) # /collections model collections_get_request_model = EmptyRequest @@ -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), diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index 28b4c54f..a1bb6294 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -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, @@ -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( { @@ -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. @@ -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) @@ -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). diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 6ce3d603..743a88e4 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -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 @@ -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" @@ -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") @@ -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") @@ -733,7 +738,7 @@ async def get_collection( collection_search_extension = CollectionSearchExtension.from_extensions( extensions=[ - FieldsExtension(), + FieldsExtension(conformance_classes=[FieldsConformanceClasses.COLLECTIONS]), ] ) diff --git a/tests/conftest.py b/tests/conftest.py index c1b07ebd..ce456534 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 @@ -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( @@ -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", @@ -179,8 +193,6 @@ 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, @@ -188,7 +200,7 @@ def api_client(request, database): 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), ) diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index a0771164..61413690 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -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()