Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update stac-fastapi.core to v3.0.0a0 #23

Merged
merged 7 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/

## [Unreleased]

### Changed

- Updated sfeos core to v3.0.0a0, fixed datetime functionality. [#23](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/pull/23)


## [v3.2.1]

Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
desc = f.read()

install_requires = [
"stac-fastapi.core==2.4.1",
"stac-fastapi.core==3.0.0a0",
"motor==3.3.2",
"pymongo==4.6.2",
"uvicorn",
"starlette",
"typing_extensions==4.4.0",
"typing_extensions==4.8.0",
]

extra_reqs = {
Expand Down
8 changes: 1 addition & 7 deletions stac_fastapi/mongo/database_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from stac_fastapi.mongo.config import AsyncMongoDBSettings as AsyncSearchSettings
from stac_fastapi.mongo.config import MongoDBSettings as SyncSearchSettings
from stac_fastapi.mongo.utilities import (
convert_obj_datetimes,
decode_token,
encode_token,
parse_datestring,
Expand Down Expand Up @@ -383,7 +382,6 @@ def translate_cql2_to_mongo(cql2_filter: Dict[str, Any]) -> Dict[str, Any]:
Returns:
A MongoDB query as a dictionary.
"""
print("CQL2 filter:", cql2_filter)
op_mapping = {
">": "$gt",
">=": "$gte",
Expand Down Expand Up @@ -505,7 +503,6 @@ def apply_cql2_filter(
mongo_query = DatabaseLogic.translate_cql2_to_mongo(_filter)
search_adapter.add_filter(mongo_query)

print("search adapter: ", search_adapter)
return search_adapter

@staticmethod
Expand Down Expand Up @@ -569,8 +566,6 @@ async def execute_search(

query = {"$and": search.filters} if search and search.filters else {}

print("Query: ", query)

if collection_ids:
query["collection"] = {"$in": collection_ids}

Expand Down Expand Up @@ -654,7 +649,6 @@ async def create_item(self, item: Item, refresh: bool = False):

new_item = item.copy()
new_item["_id"] = item.get("_id", ObjectId())
convert_obj_datetimes(new_item)

existing_item = await items_collection.find_one({"_id": new_item["_id"]})
if existing_item:
Expand Down Expand Up @@ -737,7 +731,7 @@ def sync_prep_create_item(

# Transform item using item_serializer for MongoDB compatibility
mongo_item = self.item_serializer.stac_to_db(item, base_url)
print("mongo item id: ", mongo_item["id"])

if not exist_ok:
existing_item = items_collection.find_one({"id": mongo_item["id"]})
if existing_item:
Expand Down
50 changes: 12 additions & 38 deletions stac_fastapi/mongo/utilities.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""utilities for stac-fastapi.mongo."""

from base64 import urlsafe_b64decode, urlsafe_b64encode
from datetime import timezone

from bson import ObjectId
from dateutil import parser # type: ignore
Expand Down Expand Up @@ -31,48 +32,21 @@ def encode_token(token_value: str) -> str:
return encoded_token


def parse_datestring(str):
"""Parse date string using dateutil.parser.parse() and returns a string formatted \
as ISO 8601 with milliseconds and 'Z' timezone indicator.
def parse_datestring(dt_str: str) -> str:
"""
Normalize various ISO 8601 datetime formats to a consistent format.

Args:
str (str): The date string to parse.
dt_str (str): The datetime string in ISO 8601 format.

Returns:
str: The parsed and formatted date string in the format 'YYYY-MM-DDTHH:MM:SS.ssssssZ'.
str: The normalized datetime string in the format "YYYY-MM-DDTHH:MM:SSZ".
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is %Y-%m-%dT%H:%M:%SZ the default accepted format? I think microseconds are important to include too. Guessing it's enforced by stac-fastapi.core or stac-fastapi.api ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @pedro-cf I meant to look into microseconds and then I forgot ...

"""
parsed_value = parser.parse(str)
return parsed_value.strftime("%Y-%m-%dT%H:%M:%S.%fZ")


def convert_obj_datetimes(obj):
"""Recursively explores dictionaries and lists, attempting to parse strings as datestrings \
into a specific format.
# Parse the datetime string to datetime object
dt = parser.isoparse(dt_str)

Args:
obj (dict or list): The dictionary or list containing date strings to convert.
# Convert the datetime to UTC and remove microseconds
dt = dt.astimezone(timezone.utc).replace(microsecond=0)

Returns:
dict or list: The converted dictionary or list with date strings in the desired format.
"""
if isinstance(obj, dict):
for key, value in obj.items():
if isinstance(value, dict) or isinstance(value, list):
obj[key] = convert_obj_datetimes(value)
elif isinstance(value, str):
try:
obj[key] = parse_datestring(value)
except ValueError:
pass # If parsing fails, retain the original value
elif value is None:
obj[key] = None # Handle null values
elif isinstance(obj, list):
for i, value in enumerate(obj):
if isinstance(value, str): # Only attempt to parse strings
try:
obj[i] = parse_datestring(value)
except ValueError:
pass # If parsing fails, retain the original value
elif isinstance(value, list):
obj[i] = convert_obj_datetimes(value) # Recursively handle nested lists
return obj
# Format the datetime to the specified format
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
52 changes: 21 additions & 31 deletions stac_fastapi/tests/api/test_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import copy
import uuid
from datetime import datetime, timedelta
from datetime import timedelta

import pytest

Expand Down Expand Up @@ -62,11 +61,11 @@ async def test_router(app):


@pytest.mark.asyncio
async def test_app_transaction_extension(app_client, ctx):
item = copy.deepcopy(ctx.item)
async def test_app_transaction_extension(app_client, ctx, load_test_data):
item = load_test_data("test_item.json")
item["id"] = str(uuid.uuid4())
resp = await app_client.post(f"/collections/{item['collection']}/items", json=item)
assert resp.status_code == 200
assert resp.status_code == 201

await app_client.delete(f"/collections/{item['collection']}/items/{item['id']}")

Expand Down Expand Up @@ -181,8 +180,10 @@ async def test_app_fields_extension_no_null_fields(app_client, ctx, txn_client):


@pytest.mark.asyncio
async def test_app_fields_extension_return_all_properties(app_client, ctx, txn_client):
item = ctx.item
async def test_app_fields_extension_return_all_properties(
app_client, ctx, txn_client, load_test_data
):
item = load_test_data("test_item.json")
resp = await app_client.get(
"/search", params={"collections": ["test-collection"], "fields": "properties"}
)
Expand Down Expand Up @@ -237,16 +238,14 @@ async def test_app_query_extension_limit_10000(app_client):
@pytest.mark.asyncio
async def test_app_sort_extension_get_asc(app_client, txn_client, ctx):
first_item = ctx.item
item_date = datetime.strptime(
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
)

second_item = dict(first_item)
second_item["id"] = "another-item"
another_item_date = item_date - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.strftime(
"%Y-%m-%dT%H:%M:%S.%fZ"
another_item_date = first_item["properties"]["datetime"] - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.isoformat().replace(
"+00:00", "Z"
)

await create_item(txn_client, second_item)

resp = await app_client.get("/search?sortby=+properties.datetime")
Expand All @@ -259,15 +258,12 @@ async def test_app_sort_extension_get_asc(app_client, txn_client, ctx):
@pytest.mark.asyncio
async def test_app_sort_extension_get_desc(app_client, txn_client, ctx):
first_item = ctx.item
item_date = datetime.strptime(
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
)

second_item = dict(first_item)
second_item["id"] = "another-item"
another_item_date = item_date - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.strftime(
"%Y-%m-%dT%H:%M:%S.%fZ"
another_item_date = first_item["properties"]["datetime"] - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.isoformat().replace(
"+00:00", "Z"
)
await create_item(txn_client, second_item)

Expand All @@ -281,15 +277,12 @@ async def test_app_sort_extension_get_desc(app_client, txn_client, ctx):
@pytest.mark.asyncio
async def test_app_sort_extension_post_asc(app_client, txn_client, ctx):
first_item = ctx.item
item_date = datetime.strptime(
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
)

second_item = dict(first_item)
second_item["id"] = "another-item"
another_item_date = item_date - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.strftime(
"%Y-%m-%dT%H:%M:%S.%fZ"
another_item_date = first_item["properties"]["datetime"] - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.isoformat().replace(
"+00:00", "Z"
)
await create_item(txn_client, second_item)

Expand All @@ -307,15 +300,12 @@ async def test_app_sort_extension_post_asc(app_client, txn_client, ctx):
@pytest.mark.asyncio
async def test_app_sort_extension_post_desc(app_client, txn_client, ctx):
first_item = ctx.item
item_date = datetime.strptime(
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
)

second_item = dict(first_item)
second_item["id"] = "another-item"
another_item_date = item_date - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.strftime(
"%Y-%m-%dT%H:%M:%S.%fZ"
another_item_date = first_item["properties"]["datetime"] - timedelta(days=1)
second_item["properties"]["datetime"] = another_item_date.isoformat().replace(
"+00:00", "Z"
)
await create_item(txn_client, second_item)

Expand Down
Loading