diff --git a/CHANGELOG.md b/CHANGELOG.md index 81092f73..1f3a8ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,3 +29,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated search limit constraints to avoid 500s [#15](https://github.com/microsoft/planetary-computer-apis/pull/15) - Fixed STAC `describedby` and `preview` links [#33](https://github.com/microsoft/planetary-computer-apis/pull/33) - Default search limit restored to 250 [#36](https://github.com/microsoft/planetary-computer-apis/pull/36) +- Work around issue with LandPage stac_extensions [#37](https://github.com/microsoft/planetary-computer-apis/pull/37) \ No newline at end of file diff --git a/pcstac/pcstac/client.py b/pcstac/pcstac/client.py index 60c0e0c2..3170ee72 100644 --- a/pcstac/pcstac/client.py +++ b/pcstac/pcstac/client.py @@ -150,9 +150,10 @@ async def _search_base( } ) + # Remove once https://github.com/stac-utils/stac-fastapi/issues/334 is fixed. async def landing_page(self, **kwargs: Dict[str, Any]) -> LandingPage: landing = await super().landing_page(**kwargs) - landing["type"] = "Catalog" + del landing["stac_extensions"] return landing @classmethod diff --git a/pcstac/pcstac/search.py b/pcstac/pcstac/search.py index b110f978..6d94db5c 100644 --- a/pcstac/pcstac/search.py +++ b/pcstac/pcstac/search.py @@ -1,7 +1,18 @@ -from typing import Optional +from typing import List, Optional, Union import attr +from geojson_pydantic.geometries import ( + GeometryCollection, + LineString, + MultiLineString, + MultiPoint, + MultiPolygon, + Point, + Polygon, +) +from pydantic import validator from pydantic.types import conint +from pystac.utils import str_to_datetime from stac_fastapi.api.models import BaseSearchGetRequest, ItemCollectionUri from stac_fastapi.pgstac.types.search import PgstacSearch @@ -13,6 +24,49 @@ class PCSearch(PgstacSearch): # Ignore "Illegal type annotation: call expression not allowed" limit: Optional[conint(ge=1, le=1000)] = DEFAULT_LIMIT # type:ignore + # Can be removed when + # https://github.com/stac-utils/stac-fastapi/issues/187 is closed + intersects: Optional[ + Union[ + Point, + MultiPoint, + LineString, + MultiLineString, + Polygon, + MultiPolygon, + GeometryCollection, + ] + ] + + @validator("datetime") + def validate_datetime(cls, v: str) -> str: + """Validate datetime. + + Custom to allow for users to supply dates only. + """ + if "/" in v: + values = v.split("/") + else: + # Single date is interpreted as end date + values = ["..", v] + + dates: List[str] = [] + for value in values: + if value == "..": + dates.append(value) + continue + + str_to_datetime(value) + dates.append(value) + + if ".." not in dates: + if str_to_datetime(dates[0]) > str_to_datetime(dates[1]): + raise ValueError( + "Invalid datetime range, must match format (begin_date, end_date)" + ) + + return v + @attr.s class PCItemCollectionUri(ItemCollectionUri): diff --git a/pcstac/tests/resources/test_conformance.py b/pcstac/tests/resources/test_conformance.py index e8d632db..3f1dffac 100644 --- a/pcstac/tests/resources/test_conformance.py +++ b/pcstac/tests/resources/test_conformance.py @@ -1,6 +1,17 @@ +from typing import Any, Dict + +import pystac import pytest +def remove_root(stac_object: Dict[str, Any]) -> None: + links = [] + for link in stac_object["links"]: + if link["rel"] != "root": + links.append(link) + stac_object["links"] = links + + @pytest.mark.asyncio async def test_landing_page(app_client): """Test landing page""" @@ -13,6 +24,10 @@ async def test_landing_page(app_client): # } # result = set(resp_json) # assert result == expected + + remove_root(resp_json) + pystac.Catalog.from_dict(resp_json).validate() + assert "stac_version" in resp_json # Make sure OpenAPI docs are linked diff --git a/pcstac/tests/resources/test_item.py b/pcstac/tests/resources/test_item.py index 764fde31..525b8ea7 100644 --- a/pcstac/tests/resources/test_item.py +++ b/pcstac/tests/resources/test_item.py @@ -242,6 +242,33 @@ async def test_item_search_temporal_window_get(app_client): assert resp_json["features"][0]["id"] == first_item["id"] +@pytest.mark.asyncio +async def test_item_search_temporal_window_get_date_only(app_client): + """Test GET search with spatio-temporal query (core)""" + items_resp = await app_client.get("/collections/naip/items") + assert items_resp.status_code == 200 + + first_item = items_resp.json()["features"][0] + item_date = datetime.strptime(first_item["properties"]["datetime"], DATETIME_RFC339) + item_date_before = item_date - timedelta(days=1) + item_date_after = item_date + timedelta(days=1) + + params = { + "collections": first_item["collection"], + "bbox": ",".join([str(coord) for coord in first_item["bbox"]]), + "datetime": f"{item_date_before.strftime('%Y-%m-%d')}/" + f"{item_date_after.strftime('%Y-%m-%d')}", + } + resp = await app_client.get("/search", params=params) + assert resp.status_code == 200 + resp_json = resp.json() + import json + + print(json.dumps(resp_json, indent=2)) + + assert resp_json["features"][0]["id"] == first_item["id"] + + @pytest.mark.asyncio async def test_item_search_post_without_collection(app_client): """Test POST search without specifying a collection""" @@ -560,3 +587,26 @@ async def test_search_post_page_limits(app_client): assert resp.status_code == 200 resp_json = resp.json() assert len(resp_json["features"]) == 12 + + +@pytest.mark.asyncio +async def test_item_search_geometry_collection(app_client): + """Test POST search by item id (core)""" + aoi = { + "type": "GeometryCollection", + "geometries": [ + {"type": "Point", "coordinates": [-67.67578124999999, 4.390228926463396]}, + {"type": "Point", "coordinates": [-74.619140625, 4.302591077119676]}, + { + "type": "LineString", + "coordinates": [ + [-59.765625, 6.227933930268672], + [-63.984375, -2.108898659243126], + ], + }, + ], + } + + params = {"collections": ["naip"], "intersects": aoi} + resp = await app_client.post("/search", json=params) + assert resp.status_code == 200 diff --git a/pctiler/pctiler/colormaps/noaa_c_cap.py b/pctiler/pctiler/colormaps/noaa_c_cap.py index e9b6129d..a9d42dac 100644 --- a/pctiler/pctiler/colormaps/noaa_c_cap.py +++ b/pctiler/pctiler/colormaps/noaa_c_cap.py @@ -2,7 +2,6 @@ from rio_tiler.types import ColorMapType - noaa_c_cap_colormaps: Dict[str, ColorMapType] = { "c-cap": { 0: (0, 0, 0, 255),