diff --git a/stac_fastapi/elasticsearch/setup.py b/stac_fastapi/elasticsearch/setup.py index 51c845c9..e78bad36 100644 --- a/stac_fastapi/elasticsearch/setup.py +++ b/stac_fastapi/elasticsearch/setup.py @@ -26,6 +26,7 @@ "pytest-asyncio", "pre-commit", "requests", + "ciso8601", ], "docs": ["mkdocs", "mkdocs-material", "pdocs"], "server": ["uvicorn[standard]>=0.12.0,<0.14.0"], diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/datetime_utils.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/datetime_utils.py new file mode 100644 index 00000000..2b7a3017 --- /dev/null +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/datetime_utils.py @@ -0,0 +1,14 @@ +"""A few datetime methods.""" +from datetime import datetime, timezone + +from pystac.utils import datetime_to_str + + +def now_in_utc() -> datetime: + """Return a datetime value of now with the UTC timezone applied.""" + return datetime.now(timezone.utc) + + +def now_to_rfc3339_str() -> str: + """Return an RFC 3339 string representing now.""" + return datetime_to_str(now_in_utc()) diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/serializers.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/serializers.py index 0595da2c..a8c19413 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/serializers.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/serializers.py @@ -1,11 +1,10 @@ """Serializers.""" import abc -from datetime import datetime from typing import TypedDict import attr -from stac_pydantic.shared import DATETIME_RFC339 +from stac_fastapi.elasticsearch.datetime_utils import now_to_rfc3339_str from stac_fastapi.types import stac as stac_types from stac_fastapi.types.links import CollectionLinks, ItemLinks, resolve_links @@ -42,10 +41,10 @@ def stac_to_db(cls, stac_data: TypedDict, base_url: str) -> stac_types.Item: v = float(v) wave.update({k: v}) - now = datetime.utcnow().strftime(DATETIME_RFC339) + now = now_to_rfc3339_str() if "created" not in stac_data["properties"]: - stac_data["properties"]["created"] = str(now) - stac_data["properties"]["updated"] = str(now) + stac_data["properties"]["created"] = now + stac_data["properties"]["updated"] = now return stac_data @classmethod diff --git a/stac_fastapi/elasticsearch/tests/resources/test_item.py b/stac_fastapi/elasticsearch/tests/resources/test_item.py index f8b2967f..3bd07b56 100644 --- a/stac_fastapi/elasticsearch/tests/resources/test_item.py +++ b/stac_fastapi/elasticsearch/tests/resources/test_item.py @@ -7,15 +7,21 @@ from random import randint from urllib.parse import parse_qs, urlparse, urlsplit +import ciso8601 import pystac import pytest from geojson_pydantic.geometries import Polygon -from stac_pydantic.shared import DATETIME_RFC339 +from pystac.utils import datetime_to_str from stac_fastapi.elasticsearch.core import CoreCrudClient +from stac_fastapi.elasticsearch.datetime_utils import now_to_rfc3339_str from stac_fastapi.types.core import LandingPageMixin +def rfc3339_str_to_datetime(s: str) -> datetime: + return ciso8601.parse_rfc3339(s) + + @pytest.mark.skip(reason="unknown") def test_create_and_delete_item(app_client, load_test_data): """Test creation and deletion of a single item (transactions extension)""" @@ -275,7 +281,7 @@ def test_pagination(app_client, load_test_data): def test_item_timestamps(app_client, load_test_data): """Test created and updated timestamps (common metadata)""" test_item = load_test_data("test_item.json") - start_time = datetime.utcnow().strftime(DATETIME_RFC339) + start_time = now_to_rfc3339_str() time.sleep(1) # Confirm `created` timestamp resp = app_client.post( @@ -285,9 +291,7 @@ def test_item_timestamps(app_client, load_test_data): created_dt = item["properties"]["created"] time.sleep(1) assert resp.status_code == 200 - assert ( - str(start_time) < created_dt < str(datetime.utcnow().strftime(DATETIME_RFC339)) - ) + assert start_time < created_dt < now_to_rfc3339_str() time.sleep(1) # Confirm `updated` timestamp @@ -364,13 +368,13 @@ def test_item_search_temporal_query_post(app_client, load_test_data): ) assert resp.status_code == 200 - item_date = datetime.strptime(test_item["properties"]["datetime"], DATETIME_RFC339) + item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"]) item_date = item_date + timedelta(seconds=1) params = { "collections": [test_item["collection"]], "intersects": test_item["geometry"], - "datetime": f"../{item_date.strftime(DATETIME_RFC339)}", + "datetime": f"../{datetime_to_str(item_date)}", } resp = app_client.post("/search", json=params) resp_json = resp.json() @@ -391,14 +395,14 @@ def test_item_search_temporal_window_post(app_client, load_test_data): ) assert resp.status_code == 200 - item_date = datetime.strptime(test_item["properties"]["datetime"], DATETIME_RFC339) + item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"]) item_date_before = item_date - timedelta(seconds=1) item_date_after = item_date + timedelta(seconds=1) params = { "collections": [test_item["collection"]], "intersects": test_item["geometry"], - "datetime": f"{item_date_before.strftime(DATETIME_RFC339)}/{item_date_after.strftime(DATETIME_RFC339)}", + "datetime": f"{datetime_to_str(item_date_before)}/{datetime_to_str(item_date_after)}", } resp = app_client.post("/search", json=params) resp_json = resp.json() @@ -438,7 +442,7 @@ def test_item_search_temporal_open_window(app_client, load_test_data): def test_item_search_sort_post(app_client, load_test_data): """Test POST search with sorting (sort extension)""" first_item = load_test_data("test_item.json") - item_date = datetime.strptime(first_item["properties"]["datetime"], DATETIME_RFC339) + item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"]) resp = app_client.post( f"/collections/{first_item['collection']}/items", json=first_item ) @@ -447,7 +451,7 @@ def test_item_search_sort_post(app_client, load_test_data): second_item = load_test_data("test_item.json") second_item["id"] = "another-item" another_item_date = item_date - timedelta(days=1) - second_item["properties"]["datetime"] = another_item_date.strftime(DATETIME_RFC339) + second_item["properties"]["datetime"] = datetime_to_str(another_item_date) resp = app_client.post( f"/collections/{second_item['collection']}/items", json=second_item ) @@ -542,14 +546,14 @@ def test_item_search_temporal_window_get(app_client, load_test_data): ) assert resp.status_code == 200 - item_date = datetime.strptime(test_item["properties"]["datetime"], DATETIME_RFC339) + item_date = rfc3339_str_to_datetime(test_item["properties"]["datetime"]) item_date_before = item_date - timedelta(seconds=1) item_date_after = item_date + timedelta(seconds=1) params = { "collections": test_item["collection"], "bbox": ",".join([str(coord) for coord in test_item["bbox"]]), - "datetime": f"{item_date_before.strftime(DATETIME_RFC339)}/{item_date_after.strftime(DATETIME_RFC339)}", + "datetime": f"{datetime_to_str(item_date_before)}/{datetime_to_str(item_date_after)}", } resp = app_client.get("/search", params=params) resp_json = resp.json() @@ -565,7 +569,7 @@ def test_item_search_temporal_window_get(app_client, load_test_data): def test_item_search_sort_get(app_client, load_test_data): """Test GET search with sorting (sort extension)""" first_item = load_test_data("test_item.json") - item_date = datetime.strptime(first_item["properties"]["datetime"], DATETIME_RFC339) + item_date = rfc3339_str_to_datetime(first_item["properties"]["datetime"]) resp = app_client.post( f"/collections/{first_item['collection']}/items", json=first_item ) @@ -574,7 +578,7 @@ def test_item_search_sort_get(app_client, load_test_data): second_item = load_test_data("test_item.json") second_item["id"] = "another-item" another_item_date = item_date - timedelta(days=1) - second_item["properties"]["datetime"] = another_item_date.strftime(DATETIME_RFC339) + second_item["properties"]["datetime"] = datetime_to_str(another_item_date) resp = app_client.post( f"/collections/{second_item['collection']}/items", json=second_item )