Skip to content

Commit dbd0464

Browse files
remove FieldsExtension check in StacApi (#725)
1 parent 3c58f0f commit dbd0464

File tree

5 files changed

+110
-42
lines changed

5 files changed

+110
-42
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* Removed the Filter Extension dependency from `AggregationExtensionPostRequest` and `AggregationExtensionGetRequest` [#716](https://github.com/stac-utils/stac-fastapi/pull/716)
1616
* Removed `pagination_extension` attribute in `stac_fastapi.api.app.StacApi`
1717
* Removed use of `pagination_extension` in `register_get_item_collection` function (User now need to construct the request model and pass it using `items_get_request_model` attribute)
18+
* Removed use of `FieldsExtension` in `stac_fastapi.api.app.StacApi`. If users use `FieldsExtension`, they would have to handle overpassing the model validation step by returning a `JSONResponse` from the `post_search` and `get_search` client methods.
1819

1920
### Changed
2021

docs/src/migrations/v3.0.0.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,67 @@ stac=StacApi(
203203
items_get_request_model=items_get_request_model,
204204
)
205205
```
206+
207+
208+
## Fields extension and model validation
209+
210+
When using the `Fields` extension, the `/search` endpoint should be able to return `**invalid** STAC Items. This creates an issue when *model validation* is enabled at the application level.
211+
212+
Previously when adding the `FieldsExtension` to the extensions list and if setting output model validation, we were turning off the validation for both GET/POST `/search` endpoints. This was by-passing validation even when users were not using the `fields` options in requests.
213+
214+
In `stac-fastapi` v3.0, implementers will have to by-pass the *validation step* at `Client` level by returning `JSONResponse` from the `post_search` and `get_search` client methods.
215+
216+
```python
217+
# before
218+
class BadCoreClient(BaseCoreClient):
219+
def post_search(
220+
self, search_request: BaseSearchPostRequest, **kwargs
221+
) -> stac.ItemCollection:
222+
return {"not": "a proper stac item"}
223+
224+
def get_search(
225+
self,
226+
collections: Optional[List[str]] = None,
227+
ids: Optional[List[str]] = None,
228+
bbox: Optional[List[NumType]] = None,
229+
intersects: Optional[str] = None,
230+
datetime: Optional[Union[str, datetime]] = None,
231+
limit: Optional[int] = 10,
232+
**kwargs,
233+
) -> stac.ItemCollection:
234+
return {"not": "a proper stac item"}
235+
236+
# now
237+
class BadCoreClient(BaseCoreClient):
238+
def post_search(
239+
self, search_request: BaseSearchPostRequest, **kwargs
240+
) -> stac.ItemCollection:
241+
resp = {"not": "a proper stac item"}
242+
243+
# if `fields` extension is enabled, then we return a JSONResponse
244+
# to avoid Item validation
245+
if getattr(search_request, "fields", None):
246+
return JSONResponse(content=resp)
247+
248+
return resp
249+
250+
def get_search(
251+
self,
252+
collections: Optional[List[str]] = None,
253+
ids: Optional[List[str]] = None,
254+
bbox: Optional[List[NumType]] = None,
255+
intersects: Optional[str] = None,
256+
datetime: Optional[Union[str, datetime]] = None,
257+
limit: Optional[int] = 10,
258+
**kwargs,
259+
) -> stac.ItemCollection:
260+
resp = {"not": "a proper stac item"}
261+
262+
# if `fields` extension is enabled, then we return a JSONResponse
263+
# to avoid Item validation
264+
if "fields" in kwargs:
265+
return JSONResponse(content=resp)
266+
267+
return resp
268+
269+
```

stac_fastapi/api/stac_fastapi/api/app.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@
2727
)
2828
from stac_fastapi.api.openapi import update_openapi
2929
from stac_fastapi.api.routes import Scope, add_route_dependencies, create_async_endpoint
30-
31-
# TODO: make this module not depend on `stac_fastapi.extensions`
32-
from stac_fastapi.extensions.core import FieldsExtension
3330
from stac_fastapi.types.config import ApiSettings, Settings
3431
from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
3532
from stac_fastapi.types.extension import ApiExtension
@@ -225,15 +222,12 @@ def register_post_search(self):
225222
Returns:
226223
None
227224
"""
228-
fields_ext = self.get_extension(FieldsExtension)
229225
self.router.add_api_route(
230226
name="Search",
231227
path="/search",
232-
response_model=(
233-
(api.ItemCollection if not fields_ext else None)
234-
if self.settings.enable_response_models
235-
else None
236-
),
228+
response_model=api.ItemCollection
229+
if self.settings.enable_response_models
230+
else None,
237231
responses={
238232
200: {
239233
"content": {
@@ -257,15 +251,12 @@ def register_get_search(self):
257251
Returns:
258252
None
259253
"""
260-
fields_ext = self.get_extension(FieldsExtension)
261254
self.router.add_api_route(
262255
name="Search",
263256
path="/search",
264-
response_model=(
265-
(api.ItemCollection if not fields_ext else None)
266-
if self.settings.enable_response_models
267-
else None
268-
),
257+
response_model=api.ItemCollection
258+
if self.settings.enable_response_models
259+
else None,
269260
responses={
270261
200: {
271262
"content": {

stac_fastapi/api/stac_fastapi/api/models.py

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Api request/response models."""
22

3-
import importlib.util
43
from dataclasses import dataclass, make_dataclass
54
from typing import List, Optional, Type, Union
65

@@ -19,6 +18,12 @@
1918
str_to_interval,
2019
)
2120

21+
try:
22+
import orjson # noqa
23+
from fastapi.responses import ORJSONResponse as JSONResponse
24+
except ImportError: # pragma: nocover
25+
from starlette.responses import JSONResponse
26+
2227

2328
def create_request_model(
2429
model_name="SearchGetRequest",
@@ -120,29 +125,13 @@ def __post_init__(self):
120125
self.datetime = str_to_interval(self.datetime) # type: ignore
121126

122127

123-
# Test for ORJSON and use it rather than stdlib JSON where supported
124-
if importlib.util.find_spec("orjson") is not None:
125-
from fastapi.responses import ORJSONResponse
126-
127-
class GeoJSONResponse(ORJSONResponse):
128-
"""JSON with custom, vendor content-type."""
129-
130-
media_type = "application/geo+json"
131-
132-
class JSONSchemaResponse(ORJSONResponse):
133-
"""JSON with custom, vendor content-type."""
134-
135-
media_type = "application/schema+json"
136-
137-
else:
138-
from starlette.responses import JSONResponse
128+
class GeoJSONResponse(JSONResponse):
129+
"""JSON with custom, vendor content-type."""
139130

140-
class GeoJSONResponse(JSONResponse):
141-
"""JSON with custom, vendor content-type."""
131+
media_type = "application/geo+json"
142132

143-
media_type = "application/geo+json"
144133

145-
class JSONSchemaResponse(JSONResponse):
146-
"""JSON with custom, vendor content-type."""
134+
class JSONSchemaResponse(JSONResponse):
135+
"""JSON with custom, vendor content-type."""
147136

148-
media_type = "application/schema+json"
137+
media_type = "application/schema+json"

stac_fastapi/api/tests/test_app.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from stac_fastapi.api import app
1212
from stac_fastapi.api.models import (
1313
APIRequest,
14+
JSONResponse,
1415
create_get_request_model,
1516
create_post_request_model,
1617
)
@@ -206,7 +207,14 @@ class BadCoreClient(BaseCoreClient):
206207
def post_search(
207208
self, search_request: BaseSearchPostRequest, **kwargs
208209
) -> stac.ItemCollection:
209-
return {"not": "a proper stac item"}
210+
resp = {"not": "a proper stac item"}
211+
212+
# if `fields` extension is enabled, then we return a JSONResponse
213+
# to avoid Item validation
214+
if getattr(search_request, "fields", None):
215+
return JSONResponse(content=resp)
216+
217+
return resp
210218

211219
def get_search(
212220
self,
@@ -218,7 +226,14 @@ def get_search(
218226
limit: Optional[int] = 10,
219227
**kwargs,
220228
) -> stac.ItemCollection:
221-
return {"not": "a proper stac item"}
229+
resp = {"not": "a proper stac item"}
230+
231+
# if `fields` extension is enabled, then we return a JSONResponse
232+
# to avoid Item validation
233+
if "fields" in kwargs:
234+
return JSONResponse(content=resp)
235+
236+
return resp
222237

223238
def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
224239
raise NotImplementedError
@@ -240,6 +255,7 @@ def item_collection(
240255
) -> stac.ItemCollection:
241256
raise NotImplementedError
242257

258+
# With FieldsExtension
243259
test_app = app.StacApi(
244260
settings=ApiSettings(enable_response_models=validate),
245261
client=BadCoreClient(),
@@ -264,14 +280,18 @@ def item_collection(
264280
},
265281
)
266282

283+
# With or without validation, /search endpoints will always return 200
284+
# because we have the `FieldsExtension` enabled, so the endpoint
285+
# will avoid the model validation (by returning JSONResponse)
267286
assert get_search.status_code == 200, get_search.text
268287
assert post_search.status_code == 200, post_search.text
269288

289+
# Without FieldsExtension
270290
test_app = app.StacApi(
271291
settings=ApiSettings(enable_response_models=validate),
272292
client=BadCoreClient(),
273-
search_get_request_model=create_get_request_model([FieldsExtension()]),
274-
search_post_request_model=create_post_request_model([FieldsExtension()]),
293+
search_get_request_model=create_get_request_model([]),
294+
search_post_request_model=create_post_request_model([]),
275295
extensions=[],
276296
)
277297

@@ -290,7 +310,10 @@ def item_collection(
290310
},
291311
},
292312
)
313+
293314
if validate:
315+
# NOTE: the `fields` options will be ignored by fastAPI because it's
316+
# not part of the request model, so the client should not by-pass the validation
294317
assert get_search.status_code == 500, (
295318
get_search.json()["code"] == "ResponseValidationError"
296319
)

0 commit comments

Comments
 (0)