diff --git a/CHANGES.md b/CHANGES.md index c2b4ee4..ada2a25 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ * add `PgstacSettings` such that the user can provide their own default settings for PgSTAC search * add check for pgstac `read-only` mode and raise `ReadOnlyPgSTACError` error when trying to write to the pgstac instance * add `/pgstac` endpoint in the application (when `TITILER_PGSTAC_API_DEBUG=TRUE`) +* add `ids`, `bbox` and `datetime` options to the `/collections/{collection_id}` endpoints ## 1.3.1 (2024-08-01) diff --git a/docker-compose.yml b/docker-compose.yml index 6a43a8a..505b5cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,6 +114,27 @@ services: depends_on: - database + stac-fastapi: + image: ghcr.io/stac-utils/stac-fastapi-pgstac:3.0.0 + ports: + - "${MY_DOCKER_IP:-127.0.0.1}:8082:8082" + environment: + # Postgres connection + - POSTGRES_USER=username + - POSTGRES_PASS=password + - POSTGRES_DBNAME=postgis + - POSTGRES_HOST_READER=database + - POSTGRES_HOST_WRITER=database + - POSTGRES_PORT=5432 + - DB_MIN_CONN_SIZE=1 + - DB_MAX_CONN_SIZE=1 + depends_on: + - database + command: + bash -c "uvicorn stac_fastapi.pgstac.app:app --host 0.0.0.0 --port 8082" + volumes: + - ./dockerfiles/scripts:/tmp/scripts + database: container_name: stac-db image: ghcr.io/stac-utils/pgstac:v${PGSTAC_VERSION-0.9.1} diff --git a/docs/src/endpoints/collections_endpoints.md b/docs/src/endpoints/collections_endpoints.md index 55d2355..41d0f06 100644 --- a/docs/src/endpoints/collections_endpoints.md +++ b/docs/src/endpoints/collections_endpoints.md @@ -53,6 +53,9 @@ - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. !!! important **assets** OR **expression** is required @@ -99,6 +102,9 @@ Example: - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. !!! important **assets** OR **expression** is required @@ -123,6 +129,10 @@ Example: - **tile_scale**: Tile size scale, default is set to 1 (256x256). OPTIONAL - **minzoom**: Overwrite default minzoom. OPTIONAL - **maxzoom**: Overwrite default maxzoom. OPTIONAL + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. + !!! important @@ -152,6 +162,10 @@ Example: - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. + Example: @@ -170,6 +184,10 @@ Example: - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. + Example: @@ -212,6 +230,9 @@ Example: - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. !!! important if **height** and **width** are provided **max_size** will be ignored. @@ -258,6 +279,9 @@ Example: - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. !!! important if **height** and **width** are provided **max_size** will be ignored. @@ -304,6 +328,9 @@ Example: - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. !!! important if **height** and **width** are provided **max_size** will be ignored. @@ -339,6 +366,9 @@ Example: - **time_limit** (int): Return after N seconds to avoid long requests, Default is 5sec in PgSTAC. - **exitwhenfull** (bool): Return as soon as the geometry is fully covered, Default is `True` in PgSTAC. - **skipcovered** (bool): Skip any items that would show up completely under the previous items, Default is `True` in PgSTAC. + - **ids** (str): Array of Item ids to show. + - **bbox** (str): Filters items intersecting this bounding box. + - **datetime** (str):Filters items that have a temporal property that intersects this value. Either a date-time or an interval, open or closed. !!! important **assets** OR **expression** is required diff --git a/tests/test_collections.py b/tests/test_collections.py index eeaf998..7da0db9 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -547,3 +547,54 @@ def test_collections_render(app, tmp_path): response = app.get("/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23/info") assert response.status_code == 200 assert len(response.json()["links"]) == 10 # self, tilejson (4), map (4), wmts (1) + + +def test_collections_additional_parameters(app): + """Check that additional parameter work.""" + # bbox + response = app.get( + "/collections/noaa-emergency-response/info", + params={"bbox": "-87.0251,36.1749,-86.9999,36.2001"}, + ) + assert response.status_code == 200 + resp = response.json() + assert resp["search"]["search"]["bbox"] == [-87.0251, 36.1749, -86.9999, 36.2001] + assert resp["search"]["metadata"]["bbox"] == [-87.0251, 36.1749, -86.9999, 36.2001] + + # ids + response = app.get( + "/collections/noaa-emergency-response/info", + params={"ids": "20200307aC0853130w361030"}, + ) + assert response.status_code == 200 + resp = response.json() + assert resp["search"]["search"]["ids"] == ["20200307aC0853130w361030"] + + response = app.get( + "/collections/noaa-emergency-response/-85.5,36.1624/assets", + params={"ids": "20200307aC0853130w361030"}, + ) + assert response.status_code == 200 + resp = response.json() + assert len(resp) == 1 + assert resp[0]["id"] == "20200307aC0853000w361030" + + # datetime + response = app.get( + "/collections/noaa-emergency-response/info", + params={"datetime": "2020-03-07T00:00:00Z"}, + ) + assert response.status_code == 200 + resp = response.json() + assert resp["search"]["search"]["datetime"] == "2020-03-07T00:00:00Z" + assert "datetime <= '2020-03-07 00:00:00+00'" in resp["search"]["_where"] + assert "end_datetime >= '2020-03-07 00:00:00+00''" in resp["search"]["_where"] + + response = app.get( + "/collections/noaa-emergency-response/info", + params={"datetime": "../2020-03-07T00:00:00Z"}, + ) + assert response.status_code == 200 + resp = response.json() + assert resp["search"]["search"]["datetime"] == "../2020-03-07T00:00:00Z" + assert "end_datetime >= '-infinity'" in resp["search"]["_where"] diff --git a/titiler/pgstac/dependencies.py b/titiler/pgstac/dependencies.py index 08eb9ab..25d3db5 100644 --- a/titiler/pgstac/dependencies.py +++ b/titiler/pgstac/dependencies.py @@ -2,7 +2,7 @@ import warnings from dataclasses import dataclass, field -from typing import Optional, Tuple +from typing import List, Optional, Tuple import morecantile import pystac @@ -10,6 +10,7 @@ from cachetools.keys import hashkey from cogeo_mosaic.errors import MosaicNotFoundError from fastapi import HTTPException, Path, Query +from geojson_pydantic.types import BBox from psycopg import errors as pgErrors from psycopg.rows import class_row, dict_row from psycopg_pool import ConnectionPool @@ -38,7 +39,12 @@ def SearchIdParams( @cached( # type: ignore TTLCache(maxsize=cache_config.maxsize, ttl=cache_config.ttl), - key=lambda pool, collection_id: hashkey(collection_id), + key=lambda pool, collection_id, ids, bbox, datetime: hashkey( + collection_id, + str(ids), + str(bbox), + str(datetime), + ), ) @retry( tries=retry_config.retry, @@ -48,9 +54,20 @@ def SearchIdParams( pgErrors.InterfaceError, ), ) -def get_collection_id(pool: ConnectionPool, collection_id: str) -> str: # noqa: C901 +def get_collection_id( + pool: ConnectionPool, + collection_id: str, + ids: Optional[List[str]] = None, + bbox: Optional[BBox] = None, + datetime: Optional[str] = None, +) -> str: # noqa: C901 """Get Search Id for a Collection.""" - search = model.PgSTACSearch(collections=[collection_id]) + search = model.PgSTACSearch( + collections=[collection_id], + ids=ids, + bbox=bbox, + datetime=datetime, + ) with pool.connection() as conn: with conn.cursor(row_factory=dict_row) as cursor: @@ -62,10 +79,12 @@ def get_collection_id(pool: ConnectionPool, collection_id: str) -> str: # noqa: if not collection: raise MosaicNotFoundError(f"CollectionId `{collection_id}` not found") - bbox = collection["extent"]["spatial"].get("bbox", [[-180, -90, 180, 90]]) + collection_bbox = collection["extent"]["spatial"].get( + "bbox", [[-180, -90, 180, 90]] + ) metadata = model.Metadata( name=f"Mosaic for '{collection_id}' Collection", - bounds=bbox[0], + bounds=bbox or collection_bbox[0], ) # item-assets https://github.com/stac-extensions/item-assets @@ -134,9 +153,48 @@ def CollectionIdParams( str, Path(description="STAC Collection Identifier"), ], + ids: Annotated[ + Optional[str], + Query( + description="Array of Item ids", + json_schema_extra={ + "example": "item1,item2", + }, + ), + ] = None, + bbox: Annotated[ + Optional[str], + Query( + description="Filters items intersecting this bounding box", + json_schema_extra={ + "example": "-175.05,-85.05,175.05,85.05", + }, + ), + ] = None, + datetime: Annotated[ + Optional[str], + Query( + description="""Filters items that have a temporal property that intersects this value.\n +Either a date-time or an interval, open or closed. Date and time expressions adhere to RFC 3339. Open intervals are expressed using double-dots.""", + openapi_examples={ + "datetime": {"value": "2018-02-12T23:20:50Z"}, + "closed-interval": { + "value": "2018-02-12T00:00:00Z/2018-03-18T12:31:12Z" + }, + "open-interval-from": {"value": "2018-02-12T00:00:00Z/.."}, + "open-interval-to": {"value": "../2018-03-18T12:31:12Z"}, + }, + ), + ] = None, ) -> str: - """collection_id Path Parameter""" - return get_collection_id(request.app.state.dbpool, collection_id=collection_id) + """Collection endpoints Parameters""" + return get_collection_id( + request.app.state.dbpool, + collection_id=collection_id, + ids=ids.split(",") if ids else None, + bbox=list(map(float, bbox.split(","))) if bbox else None, + datetime=datetime, + ) def SearchParams(