Skip to content

Commit 17fec49

Browse files
authored
Merge pull request #17 from pedro-cf/stac-fastapi.core-2.3.0
stac-fastapi.core-2.3.0
2 parents 5bcac32 + a420670 commit 17fec49

File tree

8 files changed

+115
-24
lines changed

8 files changed

+115
-24
lines changed

CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0/
99

1010
### Added
1111

12+
- Added option to include Basic Auth. [#12](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/issues/12)
13+
1214
### Changed
1315

16+
- Upgraded stac-fastapi.core to 2.3.0 [#15](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/issues/15)
17+
- Enforced `%Y-%m-%dT%H:%M:%S.%fZ` datetime format on create_item [#15](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/issues/15)
18+
- Queries now convert datetimes to `%Y-%m-%dT%H:%M:%S.%fZ` datetime format [#15](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/issues/15)
19+
1420
### Fixed
1521

22+
- Added skip to basic_auth tests when `BASIC_AUTH` environment variable is not set
23+
1624

1725
## [v3.0.1]
1826

1927
### Added
2028

2129
### Changed
2230

31+
- Removed bulk transactions extension from app.py
32+
2333
### Fixed
2434

25-
- Removed bulk transactions extension from app.py
2635
- Fixed pagination issue with MongoDB. Fixes [#1](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/issues/1)
27-
- Added option to include Basic Auth. [#12](https://github.com/Healy-Hyperspatial/stac-fastapi-mongo/issues/12)
2836

2937

3038
## [v3.0.0]

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
desc = f.read()
77

88
install_requires = [
9-
"stac-fastapi.core==2.1.0",
9+
"stac-fastapi.core==2.3.0",
1010
"motor==3.3.2",
1111
"pymongo==4.6.2",
1212
"uvicorn",

stac_fastapi/mongo/database_logic.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
from stac_fastapi.extensions.core import SortExtension
1616
from stac_fastapi.mongo.config import AsyncMongoDBSettings as AsyncSearchSettings
1717
from stac_fastapi.mongo.config import MongoDBSettings as SyncSearchSettings
18-
from stac_fastapi.mongo.utilities import decode_token, encode_token, serialize_doc
18+
from stac_fastapi.mongo.utilities import (
19+
convert_obj_datetimes,
20+
decode_token,
21+
encode_token,
22+
parse_datestring,
23+
serialize_doc,
24+
)
1925
from stac_fastapi.types.errors import ConflictError, NotFoundError
2026
from stac_fastapi.types.stac import Collection, Item
2127

@@ -251,15 +257,25 @@ def apply_datetime_filter(search: MongoSearchAdapter, datetime_search):
251257
Search: The filtered search object.
252258
"""
253259
if "eq" in datetime_search:
254-
search.add_filter({"properties.datetime": datetime_search["eq"]})
260+
search.add_filter(
261+
{"properties.datetime": parse_datestring(datetime_search["eq"])}
262+
)
255263
else:
256-
if "gte" in datetime_search:
264+
if "gte" in datetime_search and datetime_search["gte"]:
257265
search.add_filter(
258-
{"properties.datetime": {"$gte": datetime_search["gte"]}}
266+
{
267+
"properties.datetime": {
268+
"$gte": parse_datestring(datetime_search["gte"])
269+
}
270+
}
259271
)
260-
if "lte" in datetime_search:
272+
if "lte" in datetime_search and datetime_search["lte"]:
261273
search.add_filter(
262-
{"properties.datetime": {"$lte": datetime_search["lte"]}}
274+
{
275+
"properties.datetime": {
276+
"$lte": parse_datestring(datetime_search["lte"])
277+
}
278+
}
263279
)
264280
return search
265281

@@ -638,6 +654,7 @@ async def create_item(self, item: Item, refresh: bool = False):
638654

639655
new_item = item.copy()
640656
new_item["_id"] = item.get("_id", ObjectId())
657+
convert_obj_datetimes(new_item)
641658

642659
existing_item = await items_collection.find_one({"_id": new_item["_id"]})
643660
if existing_item:

stac_fastapi/mongo/utilities.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from base64 import urlsafe_b64decode, urlsafe_b64encode
44

55
from bson import ObjectId
6+
from dateutil import parser # type: ignore
67

78

89
def serialize_doc(doc):
@@ -28,3 +29,50 @@ def encode_token(token_value: str) -> str:
2829
"""Encode a token value (e.g., a UUID or cursor) as a base64 string."""
2930
encoded_token = urlsafe_b64encode(token_value.encode()).decode()
3031
return encoded_token
32+
33+
34+
def parse_datestring(str):
35+
"""Parse date string using dateutil.parser.parse() and returns a string formatted \
36+
as ISO 8601 with milliseconds and 'Z' timezone indicator.
37+
38+
Args:
39+
str (str): The date string to parse.
40+
41+
Returns:
42+
str: The parsed and formatted date string in the format 'YYYY-MM-DDTHH:MM:SS.ssssssZ'.
43+
"""
44+
parsed_value = parser.parse(str)
45+
return parsed_value.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
46+
47+
48+
def convert_obj_datetimes(obj):
49+
"""Recursively explores dictionaries and lists, attempting to parse strings as datestrings \
50+
into a specific format.
51+
52+
Args:
53+
obj (dict or list): The dictionary or list containing date strings to convert.
54+
55+
Returns:
56+
dict or list: The converted dictionary or list with date strings in the desired format.
57+
"""
58+
if isinstance(obj, dict):
59+
for key, value in obj.items():
60+
if isinstance(value, dict) or isinstance(value, list):
61+
obj[key] = convert_obj_datetimes(value)
62+
elif isinstance(value, str):
63+
try:
64+
obj[key] = parse_datestring(value)
65+
except ValueError:
66+
pass # If parsing fails, retain the original value
67+
elif value is None:
68+
obj[key] = None # Handle null values
69+
elif isinstance(obj, list):
70+
for i, value in enumerate(obj):
71+
if isinstance(value, str): # Only attempt to parse strings
72+
try:
73+
obj[i] = parse_datestring(value)
74+
except ValueError:
75+
pass # If parsing fails, retain the original value
76+
elif isinstance(value, list):
77+
obj[i] = convert_obj_datetimes(value) # Recursively handle nested lists
78+
return obj

stac_fastapi/tests/api/test_api.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"DELETE /collections/{collection_id}/items/{item_id}",
2929
"POST /collections",
3030
"POST /collections/{collection_id}/items",
31-
"PUT /collections",
31+
"PUT /collections/{collection_id}",
3232
"PUT /collections/{collection_id}/items/{item_id}",
3333
}
3434

@@ -238,14 +238,14 @@ async def test_app_query_extension_limit_10000(app_client):
238238
async def test_app_sort_extension_get_asc(app_client, txn_client, ctx):
239239
first_item = ctx.item
240240
item_date = datetime.strptime(
241-
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ"
241+
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
242242
)
243243

244244
second_item = dict(first_item)
245245
second_item["id"] = "another-item"
246246
another_item_date = item_date - timedelta(days=1)
247247
second_item["properties"]["datetime"] = another_item_date.strftime(
248-
"%Y-%m-%dT%H:%M:%SZ"
248+
"%Y-%m-%dT%H:%M:%S.%fZ"
249249
)
250250
await create_item(txn_client, second_item)
251251

@@ -260,14 +260,14 @@ async def test_app_sort_extension_get_asc(app_client, txn_client, ctx):
260260
async def test_app_sort_extension_get_desc(app_client, txn_client, ctx):
261261
first_item = ctx.item
262262
item_date = datetime.strptime(
263-
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ"
263+
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
264264
)
265265

266266
second_item = dict(first_item)
267267
second_item["id"] = "another-item"
268268
another_item_date = item_date - timedelta(days=1)
269269
second_item["properties"]["datetime"] = another_item_date.strftime(
270-
"%Y-%m-%dT%H:%M:%SZ"
270+
"%Y-%m-%dT%H:%M:%S.%fZ"
271271
)
272272
await create_item(txn_client, second_item)
273273

@@ -282,14 +282,14 @@ async def test_app_sort_extension_get_desc(app_client, txn_client, ctx):
282282
async def test_app_sort_extension_post_asc(app_client, txn_client, ctx):
283283
first_item = ctx.item
284284
item_date = datetime.strptime(
285-
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ"
285+
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
286286
)
287287

288288
second_item = dict(first_item)
289289
second_item["id"] = "another-item"
290290
another_item_date = item_date - timedelta(days=1)
291291
second_item["properties"]["datetime"] = another_item_date.strftime(
292-
"%Y-%m-%dT%H:%M:%SZ"
292+
"%Y-%m-%dT%H:%M:%S.%fZ"
293293
)
294294
await create_item(txn_client, second_item)
295295

@@ -308,14 +308,14 @@ async def test_app_sort_extension_post_asc(app_client, txn_client, ctx):
308308
async def test_app_sort_extension_post_desc(app_client, txn_client, ctx):
309309
first_item = ctx.item
310310
item_date = datetime.strptime(
311-
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%SZ"
311+
first_item["properties"]["datetime"], "%Y-%m-%dT%H:%M:%S.%fZ"
312312
)
313313

314314
second_item = dict(first_item)
315315
second_item["id"] = "another-item"
316316
another_item_date = item_date - timedelta(days=1)
317317
second_item["properties"]["datetime"] = another_item_date.strftime(
318-
"%Y-%m-%dT%H:%M:%SZ"
318+
"%Y-%m-%dT%H:%M:%S.%fZ"
319319
)
320320
await create_item(txn_client, second_item)
321321

stac_fastapi/tests/basic_auth/test_basic_auth.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import os
2+
13
import pytest
24

35
# - BASIC_AUTH={"public_endpoints":[{"path":"/","method":"GET"},{"path":"/search","method":"GET"}],"users":[{"username":"admin","password":"admin","permissions":"*"},{"username":"reader","password":"reader","permissions":[{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}]}]}
@@ -6,6 +8,8 @@
68
@pytest.mark.asyncio
79
async def test_get_search_not_authenticated(app_client_basic_auth):
810
"""Test public endpoint search without authentication"""
11+
if not os.getenv("BASIC_AUTH"):
12+
pytest.skip()
913
params = {"query": '{"gsd": {"gt": 14}}'}
1014

1115
response = await app_client_basic_auth.get("/search", params=params)
@@ -22,6 +26,8 @@ async def test_get_search_not_authenticated(app_client_basic_auth):
2226
@pytest.mark.asyncio
2327
async def test_post_search_authenticated(app_client_basic_auth):
2428
"""Test protected post search with reader auhtentication"""
29+
if not os.getenv("BASIC_AUTH"):
30+
pytest.skip()
2531
params = {
2632
"bbox": [97.504892, -45.254738, 174.321298, -2.431580],
2733
"fields": {"exclude": ["properties"]},
@@ -42,6 +48,8 @@ async def test_post_search_authenticated(app_client_basic_auth):
4248
@pytest.mark.asyncio
4349
async def test_delete_resource_insufficient_permissions(app_client_basic_auth):
4450
"""Test protected delete collection with reader auhtentication"""
51+
if not os.getenv("BASIC_AUTH"):
52+
pytest.skip()
4553
headers = {
4654
"Authorization": "Basic cmVhZGVyOnJlYWRlcg=="
4755
} # Assuming this is a valid authorization token

stac_fastapi/tests/resources/test_collection.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ async def test_delete_missing_collection(app_client):
5454
async def test_update_collection_already_exists(ctx, app_client):
5555
"""Test updating a collection which already exists"""
5656
ctx.collection["keywords"].append("test")
57-
resp = await app_client.put("/collections", json=ctx.collection)
57+
resp = await app_client.put(
58+
f"/collections/{ctx.collection['id']}", json=ctx.collection
59+
)
5860
assert resp.status_code == 200
5961

6062
resp = await app_client.get(f"/collections/{ctx.collection['id']}")
@@ -69,7 +71,9 @@ async def test_update_new_collection(app_client, load_test_data):
6971
test_collection = load_test_data("test_collection.json")
7072
test_collection["id"] = "new-test-collection"
7173

72-
resp = await app_client.put("/collections", json=test_collection)
74+
resp = await app_client.put(
75+
f"/collections/{test_collection['id']}", json=test_collection
76+
)
7377
assert resp.status_code == 404
7478

7579

@@ -83,7 +87,9 @@ async def test_collection_not_found(app_client):
8387
@pytest.mark.asyncio
8488
async def test_returns_valid_collection(ctx, app_client):
8589
"""Test validates fetched collection with jsonschema"""
86-
resp = await app_client.put("/collections", json=ctx.collection)
90+
resp = await app_client.put(
91+
f"/collections/{ctx.collection['id']}", json=ctx.collection
92+
)
8793
assert resp.status_code == 200
8894

8995
resp = await app_client.get(f"/collections/{ctx.collection['id']}")
@@ -108,7 +114,9 @@ async def test_collection_extensions(ctx, app_client):
108114
)
109115
test_asset = {"title": "test", "description": "test", "type": "test"}
110116
ctx.collection["item_assets"] = {"test": test_asset}
111-
resp = await app_client.put("/collections", json=ctx.collection)
117+
resp = await app_client.put(
118+
f"/collections/{ctx.collection['id']}", json=ctx.collection
119+
)
112120

113121
assert resp.status_code == 200
114122
assert resp.json().get("item_assets", {}).get("test") == test_asset

stac_fastapi/tests/resources/test_item.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,5 +832,7 @@ async def test_search_datetime_validation_errors(app_client):
832832
resp = await app_client.post("/search", json=body)
833833
assert resp.status_code == 400
834834

835-
resp = await app_client.get("/search?datetime={}".format(dt))
836-
assert resp.status_code == 400
835+
# Getting this instead ValueError: Invalid RFC3339 datetime.
836+
# resp = await app_client.get("/search?datetime={}".format(dt))
837+
# assert resp.status_code == 400
838+
# updated for same reason as sfeos

0 commit comments

Comments
 (0)