stac-fastapi.api 3.0.0a3__tar.gz → 3.0.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/PKG-INFO +1 -1
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/app.py +27 -37
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/models.py +36 -63
- stac_fastapi_api-3.0.0b1/stac_fastapi/api/version.py +2 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/PKG-INFO +1 -1
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/tests/test_api.py +9 -0
- stac_fastapi_api-3.0.0b1/tests/test_app.py +388 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/tests/test_middleware.py +30 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/tests/test_models.py +26 -4
- stac_fastapi_api-3.0.0a3/stac_fastapi/api/version.py +0 -2
- stac_fastapi_api-3.0.0a3/tests/test_app.py +0 -192
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/README.md +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/setup.cfg +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/setup.py +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/__init__.py +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/config.py +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/errors.py +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/middleware.py +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/openapi.py +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi/api/routes.py +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/SOURCES.txt +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/requires.txt +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/top_level.txt +0 -0
- {stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/tests/test_app_prefix.py +0 -0
|
@@ -18,18 +18,15 @@ from starlette.responses import JSONResponse, Response
|
|
|
18
18
|
from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
|
|
19
19
|
from stac_fastapi.api.middleware import CORSMiddleware, ProxyHeaderMiddleware
|
|
20
20
|
from stac_fastapi.api.models import (
|
|
21
|
+
APIRequest,
|
|
21
22
|
CollectionUri,
|
|
22
23
|
EmptyRequest,
|
|
23
24
|
GeoJSONResponse,
|
|
24
25
|
ItemCollectionUri,
|
|
25
26
|
ItemUri,
|
|
26
|
-
create_request_model,
|
|
27
27
|
)
|
|
28
28
|
from stac_fastapi.api.openapi import update_openapi
|
|
29
29
|
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, TokenPaginationExtension
|
|
33
30
|
from stac_fastapi.types.config import ApiSettings, Settings
|
|
34
31
|
from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
|
|
35
32
|
from stac_fastapi.types.extension import ApiExtension
|
|
@@ -108,7 +105,10 @@ class StacApi:
|
|
|
108
105
|
search_post_request_model: Type[BaseSearchPostRequest] = attr.ib(
|
|
109
106
|
default=BaseSearchPostRequest
|
|
110
107
|
)
|
|
111
|
-
|
|
108
|
+
collections_get_request_model: Type[APIRequest] = attr.ib(default=EmptyRequest)
|
|
109
|
+
collection_get_request_model: Type[APIRequest] = attr.ib(default=CollectionUri)
|
|
110
|
+
items_get_request_model: Type[APIRequest] = attr.ib(default=ItemCollectionUri)
|
|
111
|
+
item_get_request_model: Type[APIRequest] = attr.ib(default=ItemUri)
|
|
112
112
|
response_class: Type[Response] = attr.ib(default=JSONResponse)
|
|
113
113
|
middlewares: List[Middleware] = attr.ib(
|
|
114
114
|
default=attr.Factory(
|
|
@@ -211,7 +211,9 @@ class StacApi:
|
|
|
211
211
|
response_model_exclude_unset=True,
|
|
212
212
|
response_model_exclude_none=True,
|
|
213
213
|
methods=["GET"],
|
|
214
|
-
endpoint=create_async_endpoint(
|
|
214
|
+
endpoint=create_async_endpoint(
|
|
215
|
+
self.client.get_item, self.item_get_request_model
|
|
216
|
+
),
|
|
215
217
|
)
|
|
216
218
|
|
|
217
219
|
def register_post_search(self):
|
|
@@ -220,15 +222,12 @@ class StacApi:
|
|
|
220
222
|
Returns:
|
|
221
223
|
None
|
|
222
224
|
"""
|
|
223
|
-
fields_ext = self.get_extension(FieldsExtension)
|
|
224
225
|
self.router.add_api_route(
|
|
225
226
|
name="Search",
|
|
226
227
|
path="/search",
|
|
227
|
-
response_model=
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
else None
|
|
231
|
-
),
|
|
228
|
+
response_model=api.ItemCollection
|
|
229
|
+
if self.settings.enable_response_models
|
|
230
|
+
else None,
|
|
232
231
|
responses={
|
|
233
232
|
200: {
|
|
234
233
|
"content": {
|
|
@@ -252,15 +251,12 @@ class StacApi:
|
|
|
252
251
|
Returns:
|
|
253
252
|
None
|
|
254
253
|
"""
|
|
255
|
-
fields_ext = self.get_extension(FieldsExtension)
|
|
256
254
|
self.router.add_api_route(
|
|
257
255
|
name="Search",
|
|
258
256
|
path="/search",
|
|
259
|
-
response_model=
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
else None
|
|
263
|
-
),
|
|
257
|
+
response_model=api.ItemCollection
|
|
258
|
+
if self.settings.enable_response_models
|
|
259
|
+
else None,
|
|
264
260
|
responses={
|
|
265
261
|
200: {
|
|
266
262
|
"content": {
|
|
@@ -302,7 +298,9 @@ class StacApi:
|
|
|
302
298
|
response_model_exclude_unset=True,
|
|
303
299
|
response_model_exclude_none=True,
|
|
304
300
|
methods=["GET"],
|
|
305
|
-
endpoint=create_async_endpoint(
|
|
301
|
+
endpoint=create_async_endpoint(
|
|
302
|
+
self.client.all_collections, self.collections_get_request_model
|
|
303
|
+
),
|
|
306
304
|
)
|
|
307
305
|
|
|
308
306
|
def register_get_collection(self):
|
|
@@ -329,7 +327,9 @@ class StacApi:
|
|
|
329
327
|
response_model_exclude_unset=True,
|
|
330
328
|
response_model_exclude_none=True,
|
|
331
329
|
methods=["GET"],
|
|
332
|
-
endpoint=create_async_endpoint(
|
|
330
|
+
endpoint=create_async_endpoint(
|
|
331
|
+
self.client.get_collection, self.collection_get_request_model
|
|
332
|
+
),
|
|
333
333
|
)
|
|
334
334
|
|
|
335
335
|
def register_get_item_collection(self):
|
|
@@ -338,16 +338,6 @@ class StacApi:
|
|
|
338
338
|
Returns:
|
|
339
339
|
None
|
|
340
340
|
"""
|
|
341
|
-
pagination_extension = self.get_extension(self.pagination_extension)
|
|
342
|
-
if pagination_extension is not None:
|
|
343
|
-
mixins = [pagination_extension.GET]
|
|
344
|
-
else:
|
|
345
|
-
mixins = None
|
|
346
|
-
request_model = create_request_model(
|
|
347
|
-
"ItemCollectionURI",
|
|
348
|
-
base_model=ItemCollectionUri,
|
|
349
|
-
mixins=mixins,
|
|
350
|
-
)
|
|
351
341
|
self.router.add_api_route(
|
|
352
342
|
name="Get ItemCollection",
|
|
353
343
|
path="/collections/{collection_id}/items",
|
|
@@ -366,7 +356,9 @@ class StacApi:
|
|
|
366
356
|
response_model_exclude_unset=True,
|
|
367
357
|
response_model_exclude_none=True,
|
|
368
358
|
methods=["GET"],
|
|
369
|
-
endpoint=create_async_endpoint(
|
|
359
|
+
endpoint=create_async_endpoint(
|
|
360
|
+
self.client.item_collection, self.items_get_request_model
|
|
361
|
+
),
|
|
370
362
|
)
|
|
371
363
|
|
|
372
364
|
def register_core(self):
|
|
@@ -439,11 +431,6 @@ class StacApi:
|
|
|
439
431
|
"""
|
|
440
432
|
return add_route_dependencies(self.app.router.routes, scopes, dependencies)
|
|
441
433
|
|
|
442
|
-
def add_middleware(self, middleware: Middleware):
|
|
443
|
-
"""Add a middleware class to the application."""
|
|
444
|
-
self.app.user_middleware.insert(0, middleware)
|
|
445
|
-
self.app.middleware_stack = self.app.build_middleware_stack()
|
|
446
|
-
|
|
447
434
|
def __attrs_post_init__(self):
|
|
448
435
|
"""Post-init hook.
|
|
449
436
|
|
|
@@ -483,8 +470,11 @@ class StacApi:
|
|
|
483
470
|
self.app.openapi = self.customize_openapi
|
|
484
471
|
|
|
485
472
|
# add middlewares
|
|
473
|
+
if self.middlewares and self.app.middleware_stack is not None:
|
|
474
|
+
raise RuntimeError("Cannot add middleware after an application has started")
|
|
475
|
+
|
|
486
476
|
for middleware in self.middlewares:
|
|
487
|
-
self.
|
|
477
|
+
self.app.user_middleware.insert(0, middleware)
|
|
488
478
|
|
|
489
479
|
# customize route dependencies
|
|
490
480
|
for scopes, dependencies in self.route_dependencies:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Api request/response models."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from dataclasses import dataclass, make_dataclass
|
|
4
4
|
from typing import List, Optional, Type, Union
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
from fastapi import Path
|
|
6
|
+
from fastapi import Path, Query
|
|
8
7
|
from pydantic import BaseModel, create_model
|
|
9
8
|
from stac_pydantic.shared import BBox
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
10
|
|
|
11
11
|
from stac_fastapi.types.extension import ApiExtension
|
|
12
12
|
from stac_fastapi.types.rfc3339 import DateTimeType
|
|
@@ -18,6 +18,12 @@ from stac_fastapi.types.search import (
|
|
|
18
18
|
str_to_interval,
|
|
19
19
|
)
|
|
20
20
|
|
|
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
|
+
|
|
21
27
|
|
|
22
28
|
def create_request_model(
|
|
23
29
|
model_name="SearchGetRequest",
|
|
@@ -37,11 +43,11 @@ def create_request_model(
|
|
|
37
43
|
|
|
38
44
|
mixins = mixins or []
|
|
39
45
|
|
|
40
|
-
models =
|
|
46
|
+
models = extension_models + mixins + [base_model]
|
|
41
47
|
|
|
42
48
|
# Handle GET requests
|
|
43
49
|
if all([issubclass(m, APIRequest) for m in models]):
|
|
44
|
-
return
|
|
50
|
+
return make_dataclass(model_name, [], bases=tuple(models))
|
|
45
51
|
|
|
46
52
|
# Handle POST requests
|
|
47
53
|
elif all([issubclass(m, BaseModel) for m in models]):
|
|
@@ -80,85 +86,52 @@ def create_post_request_model(
|
|
|
80
86
|
)
|
|
81
87
|
|
|
82
88
|
|
|
83
|
-
@
|
|
89
|
+
@dataclass
|
|
84
90
|
class CollectionUri(APIRequest):
|
|
85
91
|
"""Get or delete collection."""
|
|
86
92
|
|
|
87
|
-
collection_id: str
|
|
93
|
+
collection_id: Annotated[str, Path(description="Collection ID")]
|
|
88
94
|
|
|
89
95
|
|
|
90
|
-
@
|
|
91
|
-
class ItemUri(
|
|
96
|
+
@dataclass
|
|
97
|
+
class ItemUri(APIRequest):
|
|
92
98
|
"""Get or delete item."""
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
collection_id: Annotated[str, Path(description="Collection ID")]
|
|
101
|
+
item_id: Annotated[str, Path(description="Item ID")]
|
|
95
102
|
|
|
96
103
|
|
|
97
|
-
@
|
|
104
|
+
@dataclass
|
|
98
105
|
class EmptyRequest(APIRequest):
|
|
99
106
|
"""Empty request."""
|
|
100
107
|
|
|
101
108
|
...
|
|
102
109
|
|
|
103
110
|
|
|
104
|
-
@
|
|
105
|
-
class ItemCollectionUri(
|
|
111
|
+
@dataclass
|
|
112
|
+
class ItemCollectionUri(APIRequest):
|
|
106
113
|
"""Get item collection."""
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class POSTTokenPagination(BaseModel):
|
|
114
|
-
"""Token pagination model for POST requests."""
|
|
115
|
-
|
|
116
|
-
token: Optional[str] = None
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
@attr.s
|
|
120
|
-
class GETTokenPagination(APIRequest):
|
|
121
|
-
"""Token pagination for GET requests."""
|
|
122
|
-
|
|
123
|
-
token: Optional[str] = attr.ib(default=None)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class POSTPagination(BaseModel):
|
|
127
|
-
"""Page based pagination for POST requests."""
|
|
115
|
+
collection_id: Annotated[str, Path(description="Collection ID")]
|
|
116
|
+
limit: Annotated[int, Query()] = 10
|
|
117
|
+
bbox: Annotated[Optional[BBox], Query()] = None
|
|
118
|
+
datetime: Annotated[Optional[DateTimeType], Query()] = None
|
|
128
119
|
|
|
129
|
-
|
|
120
|
+
def __post_init__(self):
|
|
121
|
+
"""convert attributes."""
|
|
122
|
+
if self.bbox:
|
|
123
|
+
self.bbox = str2bbox(self.bbox) # type: ignore
|
|
124
|
+
if self.datetime:
|
|
125
|
+
self.datetime = str_to_interval(self.datetime) # type: ignore
|
|
130
126
|
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"""Page based pagination for GET requests."""
|
|
135
|
-
|
|
136
|
-
page: Optional[str] = attr.ib(default=None)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# Test for ORJSON and use it rather than stdlib JSON where supported
|
|
140
|
-
if importlib.util.find_spec("orjson") is not None:
|
|
141
|
-
from fastapi.responses import ORJSONResponse
|
|
142
|
-
|
|
143
|
-
class GeoJSONResponse(ORJSONResponse):
|
|
144
|
-
"""JSON with custom, vendor content-type."""
|
|
145
|
-
|
|
146
|
-
media_type = "application/geo+json"
|
|
147
|
-
|
|
148
|
-
class JSONSchemaResponse(ORJSONResponse):
|
|
149
|
-
"""JSON with custom, vendor content-type."""
|
|
150
|
-
|
|
151
|
-
media_type = "application/schema+json"
|
|
152
|
-
|
|
153
|
-
else:
|
|
154
|
-
from starlette.responses import JSONResponse
|
|
128
|
+
class GeoJSONResponse(JSONResponse):
|
|
129
|
+
"""JSON with custom, vendor content-type."""
|
|
155
130
|
|
|
156
|
-
|
|
157
|
-
"""JSON with custom, vendor content-type."""
|
|
131
|
+
media_type = "application/geo+json"
|
|
158
132
|
|
|
159
|
-
media_type = "application/geo+json"
|
|
160
133
|
|
|
161
|
-
|
|
162
|
-
|
|
134
|
+
class JSONSchemaResponse(JSONResponse):
|
|
135
|
+
"""JSON with custom, vendor content-type."""
|
|
163
136
|
|
|
164
|
-
|
|
137
|
+
media_type = "application/schema+json"
|
|
@@ -2,6 +2,7 @@ from fastapi import Depends, HTTPException, security, status
|
|
|
2
2
|
from starlette.testclient import TestClient
|
|
3
3
|
|
|
4
4
|
from stac_fastapi.api.app import StacApi
|
|
5
|
+
from stac_fastapi.api.models import ItemCollectionUri, create_request_model
|
|
5
6
|
from stac_fastapi.extensions.core import (
|
|
6
7
|
TokenPaginationExtension,
|
|
7
8
|
TransactionExtension,
|
|
@@ -13,6 +14,13 @@ class TestRouteDependencies:
|
|
|
13
14
|
@staticmethod
|
|
14
15
|
def _build_api(**overrides):
|
|
15
16
|
settings = config.ApiSettings()
|
|
17
|
+
|
|
18
|
+
items_get_request_model = create_request_model(
|
|
19
|
+
"ItemCollectionURI",
|
|
20
|
+
base_model=ItemCollectionUri,
|
|
21
|
+
mixins=[TokenPaginationExtension().GET],
|
|
22
|
+
)
|
|
23
|
+
|
|
16
24
|
return StacApi(
|
|
17
25
|
**{
|
|
18
26
|
"settings": settings,
|
|
@@ -23,6 +31,7 @@ class TestRouteDependencies:
|
|
|
23
31
|
),
|
|
24
32
|
TokenPaginationExtension(),
|
|
25
33
|
],
|
|
34
|
+
"items_get_request_model": items_get_request_model,
|
|
26
35
|
**overrides,
|
|
27
36
|
}
|
|
28
37
|
)
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fastapi import Path, Query
|
|
7
|
+
from fastapi.testclient import TestClient
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
from stac_pydantic import api
|
|
10
|
+
|
|
11
|
+
from stac_fastapi.api import app
|
|
12
|
+
from stac_fastapi.api.models import (
|
|
13
|
+
APIRequest,
|
|
14
|
+
JSONResponse,
|
|
15
|
+
create_get_request_model,
|
|
16
|
+
create_post_request_model,
|
|
17
|
+
)
|
|
18
|
+
from stac_fastapi.extensions.core import FieldsExtension, FilterExtension
|
|
19
|
+
from stac_fastapi.types import stac
|
|
20
|
+
from stac_fastapi.types.config import ApiSettings
|
|
21
|
+
from stac_fastapi.types.core import BaseCoreClient, NumType
|
|
22
|
+
from stac_fastapi.types.search import BaseSearchPostRequest
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_client_response_type(TestCoreClient):
|
|
26
|
+
"""Test all GET endpoints. Verify that responses are valid STAC items."""
|
|
27
|
+
|
|
28
|
+
test_app = app.StacApi(
|
|
29
|
+
settings=ApiSettings(),
|
|
30
|
+
client=TestCoreClient(),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
with TestClient(test_app.app) as client:
|
|
34
|
+
landing = client.get("/")
|
|
35
|
+
collection = client.get("/collections/test")
|
|
36
|
+
collections = client.get("/collections")
|
|
37
|
+
item = client.get("/collections/test/items/test")
|
|
38
|
+
item_collection = client.get(
|
|
39
|
+
"/collections/test/items",
|
|
40
|
+
params={"limit": 10},
|
|
41
|
+
)
|
|
42
|
+
get_search = client.get(
|
|
43
|
+
"/search",
|
|
44
|
+
params={
|
|
45
|
+
"collections": ["test"],
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
post_search = client.post(
|
|
49
|
+
"/search",
|
|
50
|
+
json={
|
|
51
|
+
"collections": ["test"],
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert landing.status_code == 200, landing.text
|
|
56
|
+
api.LandingPage(**landing.json())
|
|
57
|
+
|
|
58
|
+
assert collection.status_code == 200, collection.text
|
|
59
|
+
api.Collection(**collection.json())
|
|
60
|
+
|
|
61
|
+
assert collections.status_code == 200, collections.text
|
|
62
|
+
api.collections.Collections(**collections.json())
|
|
63
|
+
|
|
64
|
+
assert item.status_code == 200, item.text
|
|
65
|
+
api.Item(**item.json())
|
|
66
|
+
|
|
67
|
+
assert item_collection.status_code == 200, item_collection.text
|
|
68
|
+
api.ItemCollection(**item_collection.json())
|
|
69
|
+
|
|
70
|
+
assert get_search.status_code == 200, get_search.text
|
|
71
|
+
api.ItemCollection(**get_search.json())
|
|
72
|
+
|
|
73
|
+
assert post_search.status_code == 200, post_search.text
|
|
74
|
+
api.ItemCollection(**post_search.json())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.parametrize("validate", [True, False])
|
|
78
|
+
def test_client_invalid_response_type(validate, TestCoreClient, item_dict):
|
|
79
|
+
"""Check if the build in response validation switch works."""
|
|
80
|
+
|
|
81
|
+
class InValidResponseClient(TestCoreClient):
|
|
82
|
+
def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
|
|
83
|
+
item_dict.pop("bbox")
|
|
84
|
+
item_dict.pop("geometry")
|
|
85
|
+
return stac.Item(**item_dict)
|
|
86
|
+
|
|
87
|
+
test_app = app.StacApi(
|
|
88
|
+
settings=ApiSettings(enable_response_models=validate),
|
|
89
|
+
client=InValidResponseClient(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
with TestClient(test_app.app) as client:
|
|
93
|
+
item = client.get("/collections/test/items/test")
|
|
94
|
+
|
|
95
|
+
# Even if API validation passes, we should receive an invalid item
|
|
96
|
+
if item.status_code == 200:
|
|
97
|
+
with pytest.raises(ValidationError):
|
|
98
|
+
api.Item(**item.json())
|
|
99
|
+
|
|
100
|
+
# If internal validation is on, we should expect an internal error
|
|
101
|
+
if validate:
|
|
102
|
+
assert item.status_code == 500, item.text
|
|
103
|
+
else:
|
|
104
|
+
assert item.status_code == 200, item.text
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_client_openapi(TestCoreClient):
|
|
108
|
+
"""Test if response models are all documented with OpenAPI."""
|
|
109
|
+
|
|
110
|
+
test_app = app.StacApi(
|
|
111
|
+
settings=ApiSettings(),
|
|
112
|
+
client=TestCoreClient(),
|
|
113
|
+
)
|
|
114
|
+
test_app.app.openapi()
|
|
115
|
+
components = ["LandingPage", "Collection", "Collections", "Item", "ItemCollection"]
|
|
116
|
+
for component in components:
|
|
117
|
+
assert component in test_app.app.openapi_schema["components"]["schemas"]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize("validate", [True, False])
|
|
121
|
+
def test_filter_extension(validate, TestCoreClient, item_dict):
|
|
122
|
+
"""Test if Filter Parameters are passed correctly."""
|
|
123
|
+
|
|
124
|
+
class FilterClient(TestCoreClient):
|
|
125
|
+
def post_search(
|
|
126
|
+
self, search_request: BaseSearchPostRequest, **kwargs
|
|
127
|
+
) -> stac.ItemCollection:
|
|
128
|
+
search_request.collections = ["test"]
|
|
129
|
+
search_request.filter = {}
|
|
130
|
+
search_request.filter_crs = "EPSG:4326"
|
|
131
|
+
search_request.filter_lang = "cql2-text"
|
|
132
|
+
|
|
133
|
+
return stac.ItemCollection(
|
|
134
|
+
type="FeatureCollection", features=[stac.Item(**item_dict)]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def get_search(
|
|
138
|
+
self,
|
|
139
|
+
collections: Optional[List[str]] = None,
|
|
140
|
+
ids: Optional[List[str]] = None,
|
|
141
|
+
bbox: Optional[List[NumType]] = None,
|
|
142
|
+
intersects: Optional[str] = None,
|
|
143
|
+
datetime: Optional[Union[str, datetime]] = None,
|
|
144
|
+
limit: Optional[int] = 10,
|
|
145
|
+
filter: Optional[str] = None,
|
|
146
|
+
filter_crs: Optional[str] = None,
|
|
147
|
+
filter_lang: Optional[str] = None,
|
|
148
|
+
**kwargs,
|
|
149
|
+
) -> stac.ItemCollection:
|
|
150
|
+
# Check if all filter parameters are passed correctly
|
|
151
|
+
|
|
152
|
+
assert filter == "TEST"
|
|
153
|
+
|
|
154
|
+
# FIXME: https://github.com/stac-utils/stac-fastapi/issues/638
|
|
155
|
+
# hyphen alias for filter_crs and filter_lang are currently not working
|
|
156
|
+
# Query parameters `filter-crs` and `filter-lang`
|
|
157
|
+
# should be recognized by the API
|
|
158
|
+
# They are present in the `request.query_params` but not in the `kwargs`
|
|
159
|
+
|
|
160
|
+
# assert filter_crs == "EPSG:4326"
|
|
161
|
+
# assert filter_lang == "cql2-text"
|
|
162
|
+
|
|
163
|
+
return stac.ItemCollection(
|
|
164
|
+
type="FeatureCollection", features=[stac.Item(**item_dict)]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
post_request_model = create_post_request_model([FilterExtension()])
|
|
168
|
+
|
|
169
|
+
test_app = app.StacApi(
|
|
170
|
+
settings=ApiSettings(enable_response_models=validate),
|
|
171
|
+
client=FilterClient(post_request_model=post_request_model),
|
|
172
|
+
search_get_request_model=create_get_request_model([FilterExtension()]),
|
|
173
|
+
search_post_request_model=post_request_model,
|
|
174
|
+
extensions=[FilterExtension()],
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
with TestClient(test_app.app) as client:
|
|
178
|
+
landing = client.get("/")
|
|
179
|
+
get_search = client.get(
|
|
180
|
+
"/search",
|
|
181
|
+
params={
|
|
182
|
+
"filter": "TEST",
|
|
183
|
+
"filter-crs": "EPSG:4326",
|
|
184
|
+
"filter-lang": "cql2-text",
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
post_search = client.post(
|
|
188
|
+
"/search",
|
|
189
|
+
json={
|
|
190
|
+
"collections": ["test"],
|
|
191
|
+
"filter": {},
|
|
192
|
+
"filter-crs": "EPSG:4326",
|
|
193
|
+
"filter-lang": "cql2-text",
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
assert landing.status_code == 200, landing.text
|
|
198
|
+
assert get_search.status_code == 200, get_search.text
|
|
199
|
+
assert post_search.status_code == 200, post_search.text
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize("validate", [True, False])
|
|
203
|
+
def test_fields_extension(validate, TestCoreClient, item_dict):
|
|
204
|
+
"""Test if fields Parameters are passed correctly."""
|
|
205
|
+
|
|
206
|
+
class BadCoreClient(BaseCoreClient):
|
|
207
|
+
def post_search(
|
|
208
|
+
self, search_request: BaseSearchPostRequest, **kwargs
|
|
209
|
+
) -> stac.ItemCollection:
|
|
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
|
|
218
|
+
|
|
219
|
+
def get_search(
|
|
220
|
+
self,
|
|
221
|
+
collections: Optional[List[str]] = None,
|
|
222
|
+
ids: Optional[List[str]] = None,
|
|
223
|
+
bbox: Optional[List[NumType]] = None,
|
|
224
|
+
intersects: Optional[str] = None,
|
|
225
|
+
datetime: Optional[Union[str, datetime]] = None,
|
|
226
|
+
limit: Optional[int] = 10,
|
|
227
|
+
**kwargs,
|
|
228
|
+
) -> stac.ItemCollection:
|
|
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
|
|
237
|
+
|
|
238
|
+
def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
|
|
239
|
+
raise NotImplementedError
|
|
240
|
+
|
|
241
|
+
def all_collections(self, **kwargs) -> stac.Collections:
|
|
242
|
+
raise NotImplementedError
|
|
243
|
+
|
|
244
|
+
def get_collection(self, collection_id: str, **kwargs) -> stac.Collection:
|
|
245
|
+
raise NotImplementedError
|
|
246
|
+
|
|
247
|
+
def item_collection(
|
|
248
|
+
self,
|
|
249
|
+
collection_id: str,
|
|
250
|
+
bbox: Optional[List[Union[float, int]]] = None,
|
|
251
|
+
datetime: Optional[Union[str, datetime]] = None,
|
|
252
|
+
limit: int = 10,
|
|
253
|
+
token: str = None,
|
|
254
|
+
**kwargs,
|
|
255
|
+
) -> stac.ItemCollection:
|
|
256
|
+
raise NotImplementedError
|
|
257
|
+
|
|
258
|
+
# With FieldsExtension
|
|
259
|
+
test_app = app.StacApi(
|
|
260
|
+
settings=ApiSettings(enable_response_models=validate),
|
|
261
|
+
client=BadCoreClient(),
|
|
262
|
+
search_get_request_model=create_get_request_model([FieldsExtension()]),
|
|
263
|
+
search_post_request_model=create_post_request_model([FieldsExtension()]),
|
|
264
|
+
extensions=[FieldsExtension()],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
with TestClient(test_app.app) as client:
|
|
268
|
+
get_search = client.get(
|
|
269
|
+
"/search",
|
|
270
|
+
params={"fields": "properties.datetime"},
|
|
271
|
+
)
|
|
272
|
+
post_search = client.post(
|
|
273
|
+
"/search",
|
|
274
|
+
json={
|
|
275
|
+
"collections": ["test"],
|
|
276
|
+
"fields": {
|
|
277
|
+
"include": ["properties.datetime"],
|
|
278
|
+
"exclude": [],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
|
|
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)
|
|
286
|
+
assert get_search.status_code == 200, get_search.text
|
|
287
|
+
assert post_search.status_code == 200, post_search.text
|
|
288
|
+
|
|
289
|
+
# Without FieldsExtension
|
|
290
|
+
test_app = app.StacApi(
|
|
291
|
+
settings=ApiSettings(enable_response_models=validate),
|
|
292
|
+
client=BadCoreClient(),
|
|
293
|
+
search_get_request_model=create_get_request_model([]),
|
|
294
|
+
search_post_request_model=create_post_request_model([]),
|
|
295
|
+
extensions=[],
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
with TestClient(test_app.app) as client:
|
|
299
|
+
get_search = client.get(
|
|
300
|
+
"/search",
|
|
301
|
+
params={"fields": "properties.datetime"},
|
|
302
|
+
)
|
|
303
|
+
post_search = client.post(
|
|
304
|
+
"/search",
|
|
305
|
+
json={
|
|
306
|
+
"collections": ["test"],
|
|
307
|
+
"fields": {
|
|
308
|
+
"include": ["properties.datetime"],
|
|
309
|
+
"exclude": [],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
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
|
|
317
|
+
assert get_search.status_code == 500, (
|
|
318
|
+
get_search.json()["code"] == "ResponseValidationError"
|
|
319
|
+
)
|
|
320
|
+
assert post_search.status_code == 500, (
|
|
321
|
+
post_search.json()["code"] == "ResponseValidationError"
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
assert get_search.status_code == 200, get_search.text
|
|
325
|
+
assert post_search.status_code == 200, post_search.text
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_request_model(AsyncTestCoreClient):
|
|
329
|
+
"""Test if request models are passed correctly."""
|
|
330
|
+
|
|
331
|
+
@dataclass
|
|
332
|
+
class CollectionsRequest(APIRequest):
|
|
333
|
+
user: str = Query(...)
|
|
334
|
+
|
|
335
|
+
@dataclass
|
|
336
|
+
class CollectionRequest(APIRequest):
|
|
337
|
+
collection_id: str = Path(description="Collection ID")
|
|
338
|
+
user: str = Query(...)
|
|
339
|
+
|
|
340
|
+
@dataclass
|
|
341
|
+
class ItemsRequest(APIRequest):
|
|
342
|
+
collection_id: str = Path(description="Collection ID")
|
|
343
|
+
user: str = Query(...)
|
|
344
|
+
|
|
345
|
+
@dataclass
|
|
346
|
+
class ItemRequest(APIRequest):
|
|
347
|
+
collection_id: str = Path(description="Collection ID")
|
|
348
|
+
item_id: str = Path(description="Item ID")
|
|
349
|
+
user: str = Query(...)
|
|
350
|
+
|
|
351
|
+
test_app = app.StacApi(
|
|
352
|
+
settings=ApiSettings(),
|
|
353
|
+
client=AsyncTestCoreClient(),
|
|
354
|
+
collections_get_request_model=CollectionsRequest,
|
|
355
|
+
collection_get_request_model=CollectionRequest,
|
|
356
|
+
items_get_request_model=ItemsRequest,
|
|
357
|
+
item_get_request_model=ItemRequest,
|
|
358
|
+
extensions=[],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
with TestClient(test_app.app) as client:
|
|
362
|
+
resp = client.get("/collections")
|
|
363
|
+
assert resp.status_code == 400
|
|
364
|
+
|
|
365
|
+
resp = client.get("/collections", params={"user": "Luke"})
|
|
366
|
+
assert resp.status_code == 200
|
|
367
|
+
|
|
368
|
+
resp = client.get("/collections/test_collection")
|
|
369
|
+
assert resp.status_code == 400
|
|
370
|
+
|
|
371
|
+
resp = client.get("/collections/test_collection", params={"user": "Leia"})
|
|
372
|
+
assert resp.status_code == 200
|
|
373
|
+
|
|
374
|
+
resp = client.get("/collections/test_collection/items")
|
|
375
|
+
assert resp.status_code == 400
|
|
376
|
+
|
|
377
|
+
resp = client.get(
|
|
378
|
+
"/collections/test_collection/items", params={"user": "Obi-Wan"}
|
|
379
|
+
)
|
|
380
|
+
assert resp.status_code == 200
|
|
381
|
+
|
|
382
|
+
resp = client.get("/collections/test_collection/items/test_item")
|
|
383
|
+
assert resp.status_code == 400
|
|
384
|
+
|
|
385
|
+
resp = client.get(
|
|
386
|
+
"/collections/test_collection/items/test_item", params={"user": "Chewbacca"}
|
|
387
|
+
)
|
|
388
|
+
assert resp.status_code == 200
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from unittest import mock
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
+
from fastapi import Request
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
4
6
|
from starlette.applications import Starlette
|
|
5
7
|
from starlette.testclient import TestClient
|
|
6
8
|
|
|
@@ -166,3 +168,31 @@ def test_cors_middleware(test_client):
|
|
|
166
168
|
resp = test_client.get("/_mgmt/ping", headers={"Origin": "http://netloc"})
|
|
167
169
|
assert resp.status_code == 200
|
|
168
170
|
assert resp.headers["access-control-allow-origin"] == "*"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_middleware_stack():
|
|
174
|
+
stac_api = StacApi(
|
|
175
|
+
settings=ApiSettings(), client=mock.create_autospec(BaseCoreClient)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
179
|
+
return JSONResponse(
|
|
180
|
+
status_code=400,
|
|
181
|
+
content={"customerrordetail": "yoo", "body": "yo"},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
class CustomException(Exception):
|
|
185
|
+
"Custom Exception"
|
|
186
|
+
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
stac_api.app.add_exception_handler(CustomException, exception_handler)
|
|
190
|
+
|
|
191
|
+
@stac_api.app.get("/error")
|
|
192
|
+
def error_endpoint():
|
|
193
|
+
raise CustomException("got you!")
|
|
194
|
+
|
|
195
|
+
with TestClient(stac_api.app) as client:
|
|
196
|
+
resp = client.get("/error")
|
|
197
|
+
assert resp.status_code == 400
|
|
198
|
+
assert resp.json()["customerrordetail"] == "yoo"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
|
+
from fastapi import Depends, FastAPI
|
|
5
|
+
from fastapi.testclient import TestClient
|
|
4
6
|
from pydantic import ValidationError
|
|
5
7
|
|
|
6
8
|
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
|
|
@@ -26,13 +28,33 @@ def test_create_get_request_model():
|
|
|
26
28
|
datetime="2020-01-01T00:00:00Z",
|
|
27
29
|
limit=10,
|
|
28
30
|
filter="test==test",
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# **{"filter-crs": "epsg:4326", "filter-lang": "cql2-text"},
|
|
31
|
+
filter_crs="epsg:4326",
|
|
32
|
+
filter_lang="cql2-text",
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
assert model.collections == ["test1", "test2"]
|
|
35
|
-
|
|
36
|
+
assert model.filter_crs == "epsg:4326"
|
|
37
|
+
|
|
38
|
+
app = FastAPI()
|
|
39
|
+
|
|
40
|
+
@app.get("/test")
|
|
41
|
+
def route(model=Depends(request_model)):
|
|
42
|
+
return model
|
|
43
|
+
|
|
44
|
+
with TestClient(app) as client:
|
|
45
|
+
resp = client.get(
|
|
46
|
+
"/test",
|
|
47
|
+
params={
|
|
48
|
+
"collections": "test1,test2",
|
|
49
|
+
"filter-crs": "epsg:4326",
|
|
50
|
+
"filter-lang": "cql2-text",
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
assert resp.status_code == 200
|
|
54
|
+
response_dict = resp.json()
|
|
55
|
+
assert response_dict["collections"] == ["test1", "test2"]
|
|
56
|
+
assert response_dict["filter_crs"] == "epsg:4326"
|
|
57
|
+
assert response_dict["filter_lang"] == "cql2-text"
|
|
36
58
|
|
|
37
59
|
|
|
38
60
|
@pytest.mark.parametrize(
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
from typing import List, Optional, Union
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from fastapi.testclient import TestClient
|
|
6
|
-
from pydantic import ValidationError
|
|
7
|
-
from stac_pydantic import api
|
|
8
|
-
|
|
9
|
-
from stac_fastapi.api import app
|
|
10
|
-
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
|
|
11
|
-
from stac_fastapi.extensions.core.filter.filter import FilterExtension
|
|
12
|
-
from stac_fastapi.types import stac
|
|
13
|
-
from stac_fastapi.types.config import ApiSettings
|
|
14
|
-
from stac_fastapi.types.core import NumType
|
|
15
|
-
from stac_fastapi.types.search import BaseSearchPostRequest
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_client_response_type(TestCoreClient):
|
|
19
|
-
"""Test all GET endpoints. Verify that responses are valid STAC items."""
|
|
20
|
-
|
|
21
|
-
test_app = app.StacApi(
|
|
22
|
-
settings=ApiSettings(),
|
|
23
|
-
client=TestCoreClient(),
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
with TestClient(test_app.app) as client:
|
|
27
|
-
landing = client.get("/")
|
|
28
|
-
collection = client.get("/collections/test")
|
|
29
|
-
collections = client.get("/collections")
|
|
30
|
-
item = client.get("/collections/test/items/test")
|
|
31
|
-
item_collection = client.get(
|
|
32
|
-
"/collections/test/items",
|
|
33
|
-
params={"limit": 10},
|
|
34
|
-
)
|
|
35
|
-
get_search = client.get(
|
|
36
|
-
"/search",
|
|
37
|
-
params={
|
|
38
|
-
"collections": ["test"],
|
|
39
|
-
},
|
|
40
|
-
)
|
|
41
|
-
post_search = client.post(
|
|
42
|
-
"/search",
|
|
43
|
-
json={
|
|
44
|
-
"collections": ["test"],
|
|
45
|
-
},
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
assert landing.status_code == 200, landing.text
|
|
49
|
-
api.LandingPage(**landing.json())
|
|
50
|
-
|
|
51
|
-
assert collection.status_code == 200, collection.text
|
|
52
|
-
api.Collection(**collection.json())
|
|
53
|
-
|
|
54
|
-
assert collections.status_code == 200, collections.text
|
|
55
|
-
api.collections.Collections(**collections.json())
|
|
56
|
-
|
|
57
|
-
assert item.status_code == 200, item.text
|
|
58
|
-
api.Item(**item.json())
|
|
59
|
-
|
|
60
|
-
assert item_collection.status_code == 200, item_collection.text
|
|
61
|
-
api.ItemCollection(**item_collection.json())
|
|
62
|
-
|
|
63
|
-
assert get_search.status_code == 200, get_search.text
|
|
64
|
-
api.ItemCollection(**get_search.json())
|
|
65
|
-
|
|
66
|
-
assert post_search.status_code == 200, post_search.text
|
|
67
|
-
api.ItemCollection(**post_search.json())
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@pytest.mark.parametrize("validate", [True, False])
|
|
71
|
-
def test_client_invalid_response_type(validate, TestCoreClient, item_dict):
|
|
72
|
-
"""Check if the build in response validation switch works."""
|
|
73
|
-
|
|
74
|
-
class InValidResponseClient(TestCoreClient):
|
|
75
|
-
def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac.Item:
|
|
76
|
-
item_dict.pop("bbox")
|
|
77
|
-
item_dict.pop("geometry")
|
|
78
|
-
return stac.Item(**item_dict)
|
|
79
|
-
|
|
80
|
-
test_app = app.StacApi(
|
|
81
|
-
settings=ApiSettings(enable_response_models=validate),
|
|
82
|
-
client=InValidResponseClient(),
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
with TestClient(test_app.app) as client:
|
|
86
|
-
item = client.get("/collections/test/items/test")
|
|
87
|
-
|
|
88
|
-
# Even if API validation passes, we should receive an invalid item
|
|
89
|
-
if item.status_code == 200:
|
|
90
|
-
with pytest.raises(ValidationError):
|
|
91
|
-
api.Item(**item.json())
|
|
92
|
-
|
|
93
|
-
# If internal validation is on, we should expect an internal error
|
|
94
|
-
if validate:
|
|
95
|
-
assert item.status_code == 500, item.text
|
|
96
|
-
else:
|
|
97
|
-
assert item.status_code == 200, item.text
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_client_openapi(TestCoreClient):
|
|
101
|
-
"""Test if response models are all documented with OpenAPI."""
|
|
102
|
-
|
|
103
|
-
test_app = app.StacApi(
|
|
104
|
-
settings=ApiSettings(),
|
|
105
|
-
client=TestCoreClient(),
|
|
106
|
-
)
|
|
107
|
-
test_app.app.openapi()
|
|
108
|
-
components = ["LandingPage", "Collection", "Collections", "Item", "ItemCollection"]
|
|
109
|
-
for component in components:
|
|
110
|
-
assert component in test_app.app.openapi_schema["components"]["schemas"]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@pytest.mark.parametrize("validate", [True, False])
|
|
114
|
-
def test_filter_extension(validate, TestCoreClient, item_dict):
|
|
115
|
-
"""Test if Filter Parameters are passed correctly."""
|
|
116
|
-
|
|
117
|
-
class FilterClient(TestCoreClient):
|
|
118
|
-
def post_search(
|
|
119
|
-
self, search_request: BaseSearchPostRequest, **kwargs
|
|
120
|
-
) -> stac.ItemCollection:
|
|
121
|
-
search_request.collections = ["test"]
|
|
122
|
-
search_request.filter = {}
|
|
123
|
-
search_request.filter_crs = "EPSG:4326"
|
|
124
|
-
search_request.filter_lang = "cql2-text"
|
|
125
|
-
|
|
126
|
-
return stac.ItemCollection(
|
|
127
|
-
type="FeatureCollection", features=[stac.Item(**item_dict)]
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
def get_search(
|
|
131
|
-
self,
|
|
132
|
-
collections: Optional[List[str]] = None,
|
|
133
|
-
ids: Optional[List[str]] = None,
|
|
134
|
-
bbox: Optional[List[NumType]] = None,
|
|
135
|
-
intersects: Optional[str] = None,
|
|
136
|
-
datetime: Optional[Union[str, datetime]] = None,
|
|
137
|
-
limit: Optional[int] = 10,
|
|
138
|
-
filter: Optional[str] = None,
|
|
139
|
-
filter_crs: Optional[str] = None,
|
|
140
|
-
filter_lang: Optional[str] = None,
|
|
141
|
-
**kwargs,
|
|
142
|
-
) -> stac.ItemCollection:
|
|
143
|
-
# Check if all filter parameters are passed correctly
|
|
144
|
-
|
|
145
|
-
assert filter == "TEST"
|
|
146
|
-
|
|
147
|
-
# FIXME: https://github.com/stac-utils/stac-fastapi/issues/638
|
|
148
|
-
# hyphen alias for filter_crs and filter_lang are currently not working
|
|
149
|
-
# Query parameters `filter-crs` and `filter-lang`
|
|
150
|
-
# should be recognized by the API
|
|
151
|
-
# They are present in the `request.query_params` but not in the `kwargs`
|
|
152
|
-
|
|
153
|
-
# assert filter_crs == "EPSG:4326"
|
|
154
|
-
# assert filter_lang == "cql2-text"
|
|
155
|
-
|
|
156
|
-
return stac.ItemCollection(
|
|
157
|
-
type="FeatureCollection", features=[stac.Item(**item_dict)]
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
post_request_model = create_post_request_model([FilterExtension()])
|
|
161
|
-
|
|
162
|
-
test_app = app.StacApi(
|
|
163
|
-
settings=ApiSettings(enable_response_models=validate),
|
|
164
|
-
client=FilterClient(post_request_model=post_request_model),
|
|
165
|
-
search_get_request_model=create_get_request_model([FilterExtension()]),
|
|
166
|
-
search_post_request_model=post_request_model,
|
|
167
|
-
extensions=[FilterExtension()],
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
with TestClient(test_app.app) as client:
|
|
171
|
-
landing = client.get("/")
|
|
172
|
-
get_search = client.get(
|
|
173
|
-
"/search",
|
|
174
|
-
params={
|
|
175
|
-
"filter": "TEST",
|
|
176
|
-
"filter-crs": "EPSG:4326",
|
|
177
|
-
"filter-lang": "cql2-text",
|
|
178
|
-
},
|
|
179
|
-
)
|
|
180
|
-
post_search = client.post(
|
|
181
|
-
"/search",
|
|
182
|
-
json={
|
|
183
|
-
"collections": ["test"],
|
|
184
|
-
"filter": {},
|
|
185
|
-
"filter-crs": "EPSG:4326",
|
|
186
|
-
"filter-lang": "cql2-text",
|
|
187
|
-
},
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
assert landing.status_code == 200, landing.text
|
|
191
|
-
assert get_search.status_code == 200, get_search.text
|
|
192
|
-
assert post_search.status_code == 200, post_search.text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/requires.txt
RENAMED
|
File without changes
|
{stac_fastapi_api-3.0.0a3 → stac_fastapi_api-3.0.0b1}/stac_fastapi.api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|