Skip to content

Commit 9502eda

Browse files
fix(#159): missing stac behaviour
* fix(#159): correct behaviour when index's item or collection is missing from store * fix(#159): support async tests * fix(#159): test UriNotFoundException handling in core.py * fix(#159): search handler tests
1 parent 5e0d254 commit 9502eda

File tree

7 files changed

+1018
-599
lines changed

7 files changed

+1018
-599
lines changed

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ dependencies = [
2424

2525
[project.optional-dependencies]
2626
dev = [
27-
"pre-commit>=4.1.0",
28-
"pytest~=8.2.2",
29-
"requests>=2.32.3",
27+
"pre-commit~=4.1.0",
3028
]
3129
server = [
3230
"uvicorn~=0.30.1",
@@ -36,6 +34,8 @@ lambda = [
3634
]
3735
test = [
3836
"pytest~=8.2.2",
37+
"pytest-asyncio~=1.0.0",
38+
"requests~=2.32.3",
3939
]
4040
iac = [
4141
"aws-cdk-lib~=2.189.0",

src/stac_fastapi/indexed/core.py

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from json import loads
33
from logging import Logger, getLogger
44
from re import IGNORECASE, match, search
5-
from typing import Final, List, Optional, cast
5+
from typing import Any, Dict, Final, List, Optional, cast
66
from urllib.parse import unquote_plus
77

88
import attr
@@ -14,6 +14,7 @@
1414
from stac_fastapi.types.search import BaseSearchPostRequest
1515
from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection
1616
from stac_index.indexer.stac_parser import StacParser
17+
from stac_index.io.readers.exceptions import UriNotFoundException
1718
from stac_pydantic.shared import BBox
1819

1920
from stac_fastapi.indexed.constants import rel_parent, rel_root, rel_self
@@ -57,11 +58,28 @@ async def get_collection(
5758
[collection_id],
5859
)
5960
if row is not None:
60-
return fix_collection_links(
61-
Collection(**await fetch_dict(row[0])),
62-
request,
61+
try:
62+
return fix_collection_links(
63+
Collection(**await fetch_dict(row[0])),
64+
request,
65+
)
66+
except UriNotFoundException as e:
67+
_logger.warning(
68+
"Collection {collection_id} exists in the index but does not exist in the data store, index is outdated".format(
69+
collection_id=collection_id
70+
)
71+
)
72+
raise NotFoundError(
73+
"Collection {collection_id} not found in the indexed data store at {uri}. This means the index is outdated, and suggests this collection has been removed by the data store and may disappear at the next index update.".format(
74+
collection_id=collection_id,
75+
uri=e.uri,
76+
)
77+
)
78+
raise NotFoundError(
79+
"Collection {collection_id} does not exist.".format(
80+
collection_id=collection_id
6381
)
64-
raise NotFoundError(f"Collection {collection_id} does not exist.")
82+
)
6583

6684
async def item_collection(
6785
self,
@@ -106,16 +124,31 @@ async def get_item(
106124
[collection_id, item_id],
107125
)
108126
if row is not None:
109-
return fix_item_links(
110-
Item(
111-
StacParser(row[1].split(",")).parse_stac_item(
112-
await fetch_dict(row[0])
113-
)[1]
114-
),
115-
request,
116-
)
127+
try:
128+
return fix_item_links(
129+
Item(
130+
StacParser(row[1].split(",")).parse_stac_item(
131+
await fetch_dict(row[0])
132+
)[1]
133+
),
134+
request,
135+
)
136+
except UriNotFoundException as e:
137+
_logger.warning(
138+
"Item {collection_id}/{item_id} exists in the index but does not exist in the data store, index is outdated".format(
139+
collection_id=collection_id, item_id=item_id
140+
)
141+
)
142+
raise NotFoundError(
143+
"Item {item_id} not found in the indexed data store at {uri}. This means the index is outdated, and suggests this item has been removed by the data store and may disappear at the next index update.".format(
144+
item_id=item_id,
145+
uri=e.uri,
146+
)
147+
)
117148
raise NotFoundError(
118-
f"Item {item_id} in Collection {collection_id} does not exist."
149+
"Item {item_id} in Collection {collection_id} does not exist.".format(
150+
item_id=item_id, collection_id=collection_id
151+
)
119152
)
120153

121154
async def post_search(
@@ -210,9 +243,20 @@ async def _get_minimal_collections_response(self) -> Collections:
210243
)
211244

212245
async def _get_full_collections_response(self, request: Request) -> Collections:
246+
async def get_each_collection(uri: str) -> Optional[Dict[str, Any]]:
247+
try:
248+
return await fetch_dict(uri=uri)
249+
except UriNotFoundException:
250+
_logger.warning(
251+
"Collection '{uri}' exists in the index but does not exist in the data store, index is outdated".format(
252+
uri=uri
253+
)
254+
)
255+
return None
256+
213257
fetch_tasks = [
214-
fetch_dict(url)
215-
for url in [
258+
get_each_collection(uri)
259+
for uri in [
216260
row[0]
217261
for row in await fetchall(
218262
f"SELECT stac_location FROM {format_query_object_name('collections')} ORDER BY id"
@@ -224,7 +268,9 @@ async def _get_full_collections_response(self, request: Request) -> Collections:
224268
Collection(**collection_dict),
225269
request,
226270
)
227-
for collection_dict in await gather(*fetch_tasks)
271+
for collection_dict in [
272+
entry for entry in await gather(*fetch_tasks) if entry is not None
273+
]
228274
]
229275
return Collections(
230276
collections=collections,

src/stac_fastapi/indexed/search/search_handler.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from stac_fastapi.types.search import BaseSearchPostRequest
1616
from stac_fastapi.types.stac import Item, ItemCollection
1717
from stac_index.indexer.stac_parser import StacParser
18+
from stac_index.io.readers.exceptions import UriNotFoundException
1819
from stac_pydantic.api.extensions.sort import SortDirections, SortExtension
1920

2021
from stac_fastapi.indexed.constants import collection_wildcard, rel_root, rel_self
@@ -108,15 +109,36 @@ async def search(self) -> ItemCollection:
108109
)
109110
has_next_page = len(rows) > query_info.limit
110111
has_previous_page = query_info.offset is not None
112+
113+
async def get_each_item(uri: str) -> Optional[Dict[str, Any]]:
114+
try:
115+
return await fetch_dict(uri=uri)
116+
except UriNotFoundException:
117+
_logger.warning(
118+
"Item '{uri}' exists in the index but does not exist in the data store, index is outdated".format(
119+
uri=uri
120+
)
121+
)
122+
return None
123+
111124
fetch_tasks = [
112-
fetch_dict(url) for url in [row[0] for row in rows[0 : query_info.limit]]
125+
get_each_item(url) for url in [row[0] for row in rows[0 : query_info.limit]]
113126
]
127+
fetched_dicts = []
128+
missing_entry_indices = []
129+
for i, entry in enumerate(await gather(*fetch_tasks)):
130+
if entry is None:
131+
missing_entry_indices.append(i)
132+
else:
133+
fetched_dicts.append(entry)
114134
fixes_to_apply = [
115135
fix_list.split(",")
116-
for fix_list in [row[1] for row in rows[0 : query_info.limit]]
136+
for fix_list in [
137+
row[1]
138+
for i, row in enumerate(rows[0 : query_info.limit])
139+
if i not in missing_entry_indices
140+
]
117141
]
118-
fetched_dicts = await gather(*fetch_tasks)
119-
120142
items = [
121143
fix_item_links(
122144
Item(**StacParser(fixers).parse_stac_item(item_dict)[1]),

tests/common.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import sys
2+
from uuid import uuid4
3+
4+
from pytest import MonkeyPatch
5+
6+
7+
def monkeypatch_settings(monkeypatch: MonkeyPatch, **kwargs):
8+
for module_name in ("stac_fastapi.indexed.settings",):
9+
if module_name in sys.modules:
10+
del sys.modules[module_name]
11+
default_settings = {
12+
"stac_api_indexed_token_jwt_secret": uuid4().hex,
13+
}
14+
for key, value in {**default_settings, **kwargs}.items():
15+
if value is None:
16+
monkeypatch.delenv(key)
17+
else:
18+
monkeypatch.setenv(key, value)

0 commit comments

Comments
 (0)