stac-fastapi.api 2.5.5.post1__tar.gz → 3.0.0a0__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-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/PKG-INFO +1 -4
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/setup.py +0 -3
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/app.py +108 -27
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/config.py +1 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/errors.py +2 -1
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/middleware.py +1 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/models.py +13 -34
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/openapi.py +2 -3
- stac_fastapi_api-3.0.0a0/stac_fastapi/api/version.py +2 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/PKG-INFO +1 -4
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/SOURCES.txt +3 -1
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/requires.txt +0 -3
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/tests/test_api.py +48 -16
- stac_fastapi_api-3.0.0a0/tests/test_app.py +188 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/tests/test_app_prefix.py +1 -1
- stac_fastapi_api-3.0.0a0/tests/test_models.py +101 -0
- stac_fastapi_api-2.5.5.post1/stac_fastapi/api/version.py +0 -2
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/README.md +0 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/setup.cfg +0 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/__init__.py +0 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi/api/routes.py +0 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/dependency_links.txt +0 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/not-zip-safe +0 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/top_level.txt +0 -0
- {stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/tests/test_middleware.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi.api
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0a0
|
|
4
4
|
Summary: An implementation of STAC API based on the FastAPI framework.
|
|
5
5
|
Home-page: https://github.com/stac-utils/stac-fastapi
|
|
6
6
|
Author: Arturo Engineering
|
|
@@ -18,9 +18,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
19
19
|
Requires-Python: >=3.8
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: attrs
|
|
22
|
-
Requires-Dist: pydantic[dotenv]<2
|
|
23
|
-
Requires-Dist: stac_pydantic==2.0.*
|
|
24
21
|
Requires-Dist: brotli_asgi
|
|
25
22
|
Requires-Dist: stac-fastapi.types
|
|
26
23
|
Provides-Extra: dev
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Fastapi app creation."""
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
4
5
|
|
|
5
6
|
import attr
|
|
@@ -7,10 +8,11 @@ from brotli_asgi import BrotliMiddleware
|
|
|
7
8
|
from fastapi import APIRouter, FastAPI
|
|
8
9
|
from fastapi.openapi.utils import get_openapi
|
|
9
10
|
from fastapi.params import Depends
|
|
10
|
-
from stac_pydantic import
|
|
11
|
-
from stac_pydantic.api import ConformanceClasses, LandingPage
|
|
11
|
+
from stac_pydantic import api
|
|
12
12
|
from stac_pydantic.api.collections import Collections
|
|
13
|
-
from stac_pydantic.version import
|
|
13
|
+
from stac_pydantic.api.version import STAC_API_VERSION
|
|
14
|
+
from stac_pydantic.shared import MimeTypes
|
|
15
|
+
from starlette.middleware import Middleware
|
|
14
16
|
from starlette.responses import JSONResponse, Response
|
|
15
17
|
|
|
16
18
|
from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
|
|
@@ -94,7 +96,7 @@ class StacApi:
|
|
|
94
96
|
lambda self: self.settings.stac_fastapi_version, takes_self=True
|
|
95
97
|
)
|
|
96
98
|
)
|
|
97
|
-
stac_version: str = attr.ib(default=
|
|
99
|
+
stac_version: str = attr.ib(default=STAC_API_VERSION)
|
|
98
100
|
description: str = attr.ib(
|
|
99
101
|
default=attr.Factory(
|
|
100
102
|
lambda self: self.settings.stac_fastapi_description, takes_self=True
|
|
@@ -108,9 +110,13 @@ class StacApi:
|
|
|
108
110
|
)
|
|
109
111
|
pagination_extension = attr.ib(default=TokenPaginationExtension)
|
|
110
112
|
response_class: Type[Response] = attr.ib(default=JSONResponse)
|
|
111
|
-
middlewares: List = attr.ib(
|
|
113
|
+
middlewares: List[Middleware] = attr.ib(
|
|
112
114
|
default=attr.Factory(
|
|
113
|
-
lambda: [
|
|
115
|
+
lambda: [
|
|
116
|
+
Middleware(BrotliMiddleware),
|
|
117
|
+
Middleware(CORSMiddleware),
|
|
118
|
+
Middleware(ProxyHeaderMiddleware),
|
|
119
|
+
]
|
|
114
120
|
)
|
|
115
121
|
)
|
|
116
122
|
route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attr.ib(default=[])
|
|
@@ -138,9 +144,17 @@ class StacApi:
|
|
|
138
144
|
self.router.add_api_route(
|
|
139
145
|
name="Landing Page",
|
|
140
146
|
path="/",
|
|
141
|
-
response_model=
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
response_model=(
|
|
148
|
+
api.LandingPage if self.settings.enable_response_models else None
|
|
149
|
+
),
|
|
150
|
+
responses={
|
|
151
|
+
200: {
|
|
152
|
+
"content": {
|
|
153
|
+
MimeTypes.json.value: {},
|
|
154
|
+
},
|
|
155
|
+
"model": api.LandingPage,
|
|
156
|
+
},
|
|
157
|
+
},
|
|
144
158
|
response_class=self.response_class,
|
|
145
159
|
response_model_exclude_unset=False,
|
|
146
160
|
response_model_exclude_none=True,
|
|
@@ -157,9 +171,17 @@ class StacApi:
|
|
|
157
171
|
self.router.add_api_route(
|
|
158
172
|
name="Conformance Classes",
|
|
159
173
|
path="/conformance",
|
|
160
|
-
response_model=
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
response_model=(
|
|
175
|
+
api.ConformanceClasses if self.settings.enable_response_models else None
|
|
176
|
+
),
|
|
177
|
+
responses={
|
|
178
|
+
200: {
|
|
179
|
+
"content": {
|
|
180
|
+
MimeTypes.json.value: {},
|
|
181
|
+
},
|
|
182
|
+
"model": api.ConformanceClasses,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
163
185
|
response_class=self.response_class,
|
|
164
186
|
response_model_exclude_unset=True,
|
|
165
187
|
response_model_exclude_none=True,
|
|
@@ -176,7 +198,15 @@ class StacApi:
|
|
|
176
198
|
self.router.add_api_route(
|
|
177
199
|
name="Get Item",
|
|
178
200
|
path="/collections/{collection_id}/items/{item_id}",
|
|
179
|
-
response_model=Item if self.settings.enable_response_models else None,
|
|
201
|
+
response_model=api.Item if self.settings.enable_response_models else None,
|
|
202
|
+
responses={
|
|
203
|
+
200: {
|
|
204
|
+
"content": {
|
|
205
|
+
MimeTypes.geojson.value: {},
|
|
206
|
+
},
|
|
207
|
+
"model": api.Item,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
180
210
|
response_class=GeoJSONResponse,
|
|
181
211
|
response_model_exclude_unset=True,
|
|
182
212
|
response_model_exclude_none=True,
|
|
@@ -194,9 +224,19 @@ class StacApi:
|
|
|
194
224
|
self.router.add_api_route(
|
|
195
225
|
name="Search",
|
|
196
226
|
path="/search",
|
|
197
|
-
response_model=(
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
response_model=(
|
|
228
|
+
(api.ItemCollection if not fields_ext else None)
|
|
229
|
+
if self.settings.enable_response_models
|
|
230
|
+
else None
|
|
231
|
+
),
|
|
232
|
+
responses={
|
|
233
|
+
200: {
|
|
234
|
+
"content": {
|
|
235
|
+
MimeTypes.geojson.value: {},
|
|
236
|
+
},
|
|
237
|
+
"model": api.ItemCollection,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
200
240
|
response_class=GeoJSONResponse,
|
|
201
241
|
response_model_exclude_unset=True,
|
|
202
242
|
response_model_exclude_none=True,
|
|
@@ -216,9 +256,19 @@ class StacApi:
|
|
|
216
256
|
self.router.add_api_route(
|
|
217
257
|
name="Search",
|
|
218
258
|
path="/search",
|
|
219
|
-
response_model=(
|
|
220
|
-
|
|
221
|
-
|
|
259
|
+
response_model=(
|
|
260
|
+
(api.ItemCollection if not fields_ext else None)
|
|
261
|
+
if self.settings.enable_response_models
|
|
262
|
+
else None
|
|
263
|
+
),
|
|
264
|
+
responses={
|
|
265
|
+
200: {
|
|
266
|
+
"content": {
|
|
267
|
+
MimeTypes.geojson.value: {},
|
|
268
|
+
},
|
|
269
|
+
"model": api.ItemCollection,
|
|
270
|
+
},
|
|
271
|
+
},
|
|
222
272
|
response_class=GeoJSONResponse,
|
|
223
273
|
response_model_exclude_unset=True,
|
|
224
274
|
response_model_exclude_none=True,
|
|
@@ -237,9 +287,17 @@ class StacApi:
|
|
|
237
287
|
self.router.add_api_route(
|
|
238
288
|
name="Get Collections",
|
|
239
289
|
path="/collections",
|
|
240
|
-
response_model=
|
|
241
|
-
|
|
242
|
-
|
|
290
|
+
response_model=(
|
|
291
|
+
Collections if self.settings.enable_response_models else None
|
|
292
|
+
),
|
|
293
|
+
responses={
|
|
294
|
+
200: {
|
|
295
|
+
"content": {
|
|
296
|
+
MimeTypes.json.value: {},
|
|
297
|
+
},
|
|
298
|
+
"model": Collections,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
243
301
|
response_class=self.response_class,
|
|
244
302
|
response_model_exclude_unset=True,
|
|
245
303
|
response_model_exclude_none=True,
|
|
@@ -256,7 +314,17 @@ class StacApi:
|
|
|
256
314
|
self.router.add_api_route(
|
|
257
315
|
name="Get Collection",
|
|
258
316
|
path="/collections/{collection_id}",
|
|
259
|
-
response_model=Collection
|
|
317
|
+
response_model=api.Collection
|
|
318
|
+
if self.settings.enable_response_models
|
|
319
|
+
else None,
|
|
320
|
+
responses={
|
|
321
|
+
200: {
|
|
322
|
+
"content": {
|
|
323
|
+
MimeTypes.json.value: {},
|
|
324
|
+
},
|
|
325
|
+
"model": api.Collection,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
260
328
|
response_class=self.response_class,
|
|
261
329
|
response_model_exclude_unset=True,
|
|
262
330
|
response_model_exclude_none=True,
|
|
@@ -283,9 +351,17 @@ class StacApi:
|
|
|
283
351
|
self.router.add_api_route(
|
|
284
352
|
name="Get ItemCollection",
|
|
285
353
|
path="/collections/{collection_id}/items",
|
|
286
|
-
response_model=
|
|
287
|
-
|
|
288
|
-
|
|
354
|
+
response_model=(
|
|
355
|
+
api.ItemCollection if self.settings.enable_response_models else None
|
|
356
|
+
),
|
|
357
|
+
responses={
|
|
358
|
+
200: {
|
|
359
|
+
"content": {
|
|
360
|
+
MimeTypes.geojson.value: {},
|
|
361
|
+
},
|
|
362
|
+
"model": api.ItemCollection,
|
|
363
|
+
},
|
|
364
|
+
},
|
|
289
365
|
response_class=GeoJSONResponse,
|
|
290
366
|
response_model_exclude_unset=True,
|
|
291
367
|
response_model_exclude_none=True,
|
|
@@ -363,6 +439,11 @@ class StacApi:
|
|
|
363
439
|
"""
|
|
364
440
|
return add_route_dependencies(self.app.router.routes, scopes, dependencies)
|
|
365
441
|
|
|
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
|
+
|
|
366
447
|
def __attrs_post_init__(self):
|
|
367
448
|
"""Post-init hook.
|
|
368
449
|
|
|
@@ -407,7 +488,7 @@ class StacApi:
|
|
|
407
488
|
|
|
408
489
|
# add middlewares
|
|
409
490
|
for middleware in self.middlewares:
|
|
410
|
-
self.
|
|
491
|
+
self.add_middleware(middleware)
|
|
411
492
|
|
|
412
493
|
# customize route dependencies
|
|
413
494
|
for scopes, dependencies in self.route_dependencies:
|
|
@@ -4,7 +4,7 @@ import logging
|
|
|
4
4
|
from typing import Callable, Dict, Type, TypedDict
|
|
5
5
|
|
|
6
6
|
from fastapi import FastAPI
|
|
7
|
-
from fastapi.exceptions import RequestValidationError
|
|
7
|
+
from fastapi.exceptions import RequestValidationError, ResponseValidationError
|
|
8
8
|
from starlette import status
|
|
9
9
|
from starlette.requests import Request
|
|
10
10
|
from starlette.responses import JSONResponse
|
|
@@ -27,6 +27,7 @@ DEFAULT_STATUS_CODES = {
|
|
|
27
27
|
DatabaseError: status.HTTP_424_FAILED_DEPENDENCY,
|
|
28
28
|
Exception: status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
29
29
|
InvalidQueryParameter: status.HTTP_400_BAD_REQUEST,
|
|
30
|
+
ResponseValidationError: status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""Api request/response models."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
-
from typing import Optional, Type, Union
|
|
4
|
+
from typing import List, Optional, Type, Union
|
|
5
5
|
|
|
6
6
|
import attr
|
|
7
|
-
from fastapi import
|
|
7
|
+
from fastapi import Path
|
|
8
8
|
from pydantic import BaseModel, create_model
|
|
9
|
-
from pydantic.fields import UndefinedType
|
|
10
9
|
from stac_pydantic.shared import BBox
|
|
11
10
|
|
|
12
11
|
from stac_fastapi.types.extension import ApiExtension
|
|
@@ -23,8 +22,8 @@ from stac_fastapi.types.search import (
|
|
|
23
22
|
def create_request_model(
|
|
24
23
|
model_name="SearchGetRequest",
|
|
25
24
|
base_model: Union[Type[BaseModel], APIRequest] = BaseSearchGetRequest,
|
|
26
|
-
extensions: Optional[ApiExtension] = None,
|
|
27
|
-
mixins: Optional[Union[BaseModel, APIRequest]] = None,
|
|
25
|
+
extensions: Optional[List[ApiExtension]] = None,
|
|
26
|
+
mixins: Optional[Union[List[BaseModel], List[APIRequest]]] = None,
|
|
28
27
|
request_type: Optional[str] = "GET",
|
|
29
28
|
) -> Union[Type[BaseModel], APIRequest]:
|
|
30
29
|
"""Create a pydantic model for validating request bodies."""
|
|
@@ -47,40 +46,19 @@ def create_request_model(
|
|
|
47
46
|
# Handle POST requests
|
|
48
47
|
elif all([issubclass(m, BaseModel) for m in models]):
|
|
49
48
|
for model in models:
|
|
50
|
-
for k,
|
|
51
|
-
|
|
52
|
-
body = Body(
|
|
53
|
-
None
|
|
54
|
-
if isinstance(field_info.default, UndefinedType)
|
|
55
|
-
else field_info.default,
|
|
56
|
-
default_factory=field_info.default_factory,
|
|
57
|
-
alias=field_info.alias,
|
|
58
|
-
alias_priority=field_info.alias_priority,
|
|
59
|
-
title=field_info.title,
|
|
60
|
-
description=field_info.description,
|
|
61
|
-
const=field_info.const,
|
|
62
|
-
gt=field_info.gt,
|
|
63
|
-
ge=field_info.ge,
|
|
64
|
-
lt=field_info.lt,
|
|
65
|
-
le=field_info.le,
|
|
66
|
-
multiple_of=field_info.multiple_of,
|
|
67
|
-
min_items=field_info.min_items,
|
|
68
|
-
max_items=field_info.max_items,
|
|
69
|
-
min_length=field_info.min_length,
|
|
70
|
-
max_length=field_info.max_length,
|
|
71
|
-
regex=field_info.regex,
|
|
72
|
-
extra=field_info.extra,
|
|
73
|
-
)
|
|
74
|
-
fields[k] = (v.outer_type_, body)
|
|
49
|
+
for k, field_info in model.model_fields.items():
|
|
50
|
+
fields[k] = (field_info.annotation, field_info)
|
|
75
51
|
return create_model(model_name, **fields, __base__=base_model)
|
|
76
52
|
|
|
77
53
|
raise TypeError("Mixed Request Model types. Check extension request types.")
|
|
78
54
|
|
|
79
55
|
|
|
80
56
|
def create_get_request_model(
|
|
81
|
-
extensions
|
|
82
|
-
|
|
57
|
+
extensions: Optional[List[ApiExtension]],
|
|
58
|
+
base_model: BaseSearchGetRequest = BaseSearchGetRequest,
|
|
59
|
+
) -> APIRequest:
|
|
83
60
|
"""Wrap create_request_model to create the GET request model."""
|
|
61
|
+
|
|
84
62
|
return create_request_model(
|
|
85
63
|
"SearchGetRequest",
|
|
86
64
|
base_model=base_model,
|
|
@@ -90,8 +68,9 @@ def create_get_request_model(
|
|
|
90
68
|
|
|
91
69
|
|
|
92
70
|
def create_post_request_model(
|
|
93
|
-
extensions
|
|
94
|
-
|
|
71
|
+
extensions: Optional[List[ApiExtension]],
|
|
72
|
+
base_model: BaseSearchPostRequest = BaseSearchPostRequest,
|
|
73
|
+
) -> Type[BaseModel]:
|
|
95
74
|
"""Wrap create_request_model to create the POST request model."""
|
|
96
75
|
return create_request_model(
|
|
97
76
|
"SearchPostRequest",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""openapi."""
|
|
2
|
+
|
|
2
3
|
import warnings
|
|
3
4
|
|
|
4
5
|
from fastapi import FastAPI
|
|
@@ -43,9 +44,7 @@ def update_openapi(app: FastAPI) -> FastAPI:
|
|
|
43
44
|
# Get the response from the old endpoint function
|
|
44
45
|
response: JSONResponse = await old_endpoint(req)
|
|
45
46
|
# Update the content type header in place
|
|
46
|
-
response.headers[
|
|
47
|
-
"content-type"
|
|
48
|
-
] = "application/vnd.oai.openapi+json;version=3.0"
|
|
47
|
+
response.headers["content-type"] = "application/vnd.oai.openapi+json;version=3.0"
|
|
49
48
|
# Return the updated response
|
|
50
49
|
return response
|
|
51
50
|
|
{stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi.api
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0a0
|
|
4
4
|
Summary: An implementation of STAC API based on the FastAPI framework.
|
|
5
5
|
Home-page: https://github.com/stac-utils/stac-fastapi
|
|
6
6
|
Author: Arturo Engineering
|
|
@@ -18,9 +18,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: License :: OSI Approved :: MIT License
|
|
19
19
|
Requires-Python: >=3.8
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: attrs
|
|
22
|
-
Requires-Dist: pydantic[dotenv]<2
|
|
23
|
-
Requires-Dist: stac_pydantic==2.0.*
|
|
24
21
|
Requires-Dist: brotli_asgi
|
|
25
22
|
Requires-Dist: stac-fastapi.types
|
|
26
23
|
Provides-Extra: dev
|
|
@@ -41,11 +41,11 @@ class TestRouteDependencies:
|
|
|
41
41
|
method=route["method"].lower(),
|
|
42
42
|
url=path,
|
|
43
43
|
auth=("bob", "dobbs"),
|
|
44
|
-
content=
|
|
44
|
+
content=route["payload"],
|
|
45
45
|
headers={"content-type": "application/json"},
|
|
46
46
|
)
|
|
47
47
|
assert (
|
|
48
|
-
response.status_code
|
|
48
|
+
200 <= response.status_code < 300
|
|
49
49
|
), "Authenticated requests should be accepted"
|
|
50
50
|
assert response.json() == "dummy response"
|
|
51
51
|
|
|
@@ -58,27 +58,59 @@ class TestRouteDependencies:
|
|
|
58
58
|
== "application/vnd.oai.openapi+json;version=3.0"
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
def test_build_api_with_route_dependencies(self):
|
|
61
|
+
def test_build_api_with_route_dependencies(self, collection, item):
|
|
62
62
|
routes = [
|
|
63
|
-
{"path": "/collections", "method": "POST"},
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
{"path": "/collections", "method": "POST", "payload": collection},
|
|
64
|
+
{
|
|
65
|
+
"path": "/collections/{collectionId}",
|
|
66
|
+
"method": "PUT",
|
|
67
|
+
"payload": collection,
|
|
68
|
+
},
|
|
69
|
+
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
|
|
70
|
+
{
|
|
71
|
+
"path": "/collections/{collectionId}/items",
|
|
72
|
+
"method": "POST",
|
|
73
|
+
"payload": item,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
77
|
+
"method": "PUT",
|
|
78
|
+
"payload": item,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
82
|
+
"method": "DELETE",
|
|
83
|
+
"payload": "",
|
|
84
|
+
},
|
|
69
85
|
]
|
|
70
86
|
dependencies = [Depends(must_be_bob)]
|
|
71
87
|
api = self._build_api(route_dependencies=[(routes, dependencies)])
|
|
72
88
|
self._assert_dependency_applied(api, routes)
|
|
73
89
|
|
|
74
|
-
def test_add_route_dependencies_after_building_api(self):
|
|
90
|
+
def test_add_route_dependencies_after_building_api(self, collection, item):
|
|
75
91
|
routes = [
|
|
76
|
-
{"path": "/collections", "method": "POST"},
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
{"path": "/collections", "method": "POST", "payload": collection},
|
|
93
|
+
{
|
|
94
|
+
"path": "/collections/{collectionId}",
|
|
95
|
+
"method": "PUT",
|
|
96
|
+
"payload": collection,
|
|
97
|
+
},
|
|
98
|
+
{"path": "/collections/{collectionId}", "method": "DELETE", "payload": ""},
|
|
99
|
+
{
|
|
100
|
+
"path": "/collections/{collectionId}/items",
|
|
101
|
+
"method": "POST",
|
|
102
|
+
"payload": item,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
106
|
+
"method": "PUT",
|
|
107
|
+
"payload": item,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"path": "/collections/{collectionId}/items/{itemId}",
|
|
111
|
+
"method": "DELETE",
|
|
112
|
+
"payload": "",
|
|
113
|
+
},
|
|
82
114
|
]
|
|
83
115
|
api = self._build_api()
|
|
84
116
|
api.add_route_dependencies(scopes=routes, dependencies=[Depends(must_be_bob)])
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
def test_filter_extension(TestCoreClient, item_dict):
|
|
114
|
+
"""Test if Filter Parameters are passed correctly."""
|
|
115
|
+
|
|
116
|
+
class FilterClient(TestCoreClient):
|
|
117
|
+
def post_search(
|
|
118
|
+
self, search_request: BaseSearchPostRequest, **kwargs
|
|
119
|
+
) -> stac.ItemCollection:
|
|
120
|
+
search_request.collections = ["test"]
|
|
121
|
+
search_request.filter = {}
|
|
122
|
+
search_request.filter_crs = "EPSG:4326"
|
|
123
|
+
search_request.filter_lang = "cql2-text"
|
|
124
|
+
|
|
125
|
+
return stac.ItemCollection(
|
|
126
|
+
type="FeatureCollection", features=[stac.Item(**item_dict)]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def get_search(
|
|
130
|
+
self,
|
|
131
|
+
collections: Optional[List[str]] = None,
|
|
132
|
+
ids: Optional[List[str]] = None,
|
|
133
|
+
bbox: Optional[List[NumType]] = None,
|
|
134
|
+
intersects: Optional[str] = None,
|
|
135
|
+
datetime: Optional[Union[str, datetime]] = None,
|
|
136
|
+
limit: Optional[int] = 10,
|
|
137
|
+
filter: Optional[str] = None,
|
|
138
|
+
filter_crs: Optional[str] = None,
|
|
139
|
+
filter_lang: Optional[str] = None,
|
|
140
|
+
**kwargs,
|
|
141
|
+
) -> stac.ItemCollection:
|
|
142
|
+
# Check if all filter parameters are passed correctly
|
|
143
|
+
|
|
144
|
+
assert filter == "TEST"
|
|
145
|
+
|
|
146
|
+
# FIXME: https://github.com/stac-utils/stac-fastapi/issues/638
|
|
147
|
+
# hyphen alias for filter_crs and filter_lang are currently not working
|
|
148
|
+
# Query parameters `filter-crs` and `filter-lang`
|
|
149
|
+
# should be recognized by the API
|
|
150
|
+
# They are present in the `request.query_params` but not in the `kwargs`
|
|
151
|
+
|
|
152
|
+
# assert filter_crs == "EPSG:4326"
|
|
153
|
+
# assert filter_lang == "cql2-text"
|
|
154
|
+
|
|
155
|
+
return stac.ItemCollection(
|
|
156
|
+
type="FeatureCollection", features=[stac.Item(**item_dict)]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
post_request_model = create_post_request_model([FilterExtension()])
|
|
160
|
+
|
|
161
|
+
test_app = app.StacApi(
|
|
162
|
+
settings=ApiSettings(),
|
|
163
|
+
client=FilterClient(post_request_model=post_request_model),
|
|
164
|
+
search_get_request_model=create_get_request_model([FilterExtension()]),
|
|
165
|
+
search_post_request_model=post_request_model,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
with TestClient(test_app.app) as client:
|
|
169
|
+
get_search = client.get(
|
|
170
|
+
"/search",
|
|
171
|
+
params={
|
|
172
|
+
"filter": "TEST",
|
|
173
|
+
"filter-crs": "EPSG:4326",
|
|
174
|
+
"filter-lang": "cql2-text",
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
post_search = client.post(
|
|
178
|
+
"/search",
|
|
179
|
+
json={
|
|
180
|
+
"collections": ["test"],
|
|
181
|
+
"filter": {},
|
|
182
|
+
"filter-crs": "EPSG:4326",
|
|
183
|
+
"filter-lang": "cql2-text",
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
assert get_search.status_code == 200, get_search.text
|
|
188
|
+
assert post_search.status_code == 200, post_search.text
|
|
@@ -51,7 +51,7 @@ def test_api_prefix(TestCoreClient, prefix):
|
|
|
51
51
|
collections = client.get(f"{prefix}/collections")
|
|
52
52
|
assert collections.status_code == 200, collections.json()
|
|
53
53
|
collection_id = collections.json()["collections"][0]["id"]
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
collection = client.get(f"{prefix}/collections/{collection_id}")
|
|
56
56
|
assert collection.status_code == 200, collection.json()
|
|
57
57
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
|
|
7
|
+
from stac_fastapi.extensions.core.filter.filter import FilterExtension
|
|
8
|
+
from stac_fastapi.extensions.core.sort.sort import SortExtension
|
|
9
|
+
from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_create_get_request_model():
|
|
13
|
+
extensions = [FilterExtension()]
|
|
14
|
+
request_model = create_get_request_model(extensions, BaseSearchGetRequest)
|
|
15
|
+
|
|
16
|
+
model = request_model(
|
|
17
|
+
collections="test1,test2",
|
|
18
|
+
ids="test1,test2",
|
|
19
|
+
bbox="0,0,1,1",
|
|
20
|
+
intersects=json.dumps(
|
|
21
|
+
{
|
|
22
|
+
"type": "Polygon",
|
|
23
|
+
"coordinates": [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
|
|
24
|
+
}
|
|
25
|
+
),
|
|
26
|
+
datetime="2020-01-01T00:00:00Z",
|
|
27
|
+
limit=10,
|
|
28
|
+
filter="test==test",
|
|
29
|
+
# FIXME: https://github.com/stac-utils/stac-fastapi/issues/638
|
|
30
|
+
# hyphen aliases are not properly working
|
|
31
|
+
# **{"filter-crs": "epsg:4326", "filter-lang": "cql2-text"},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert model.collections == ["test1", "test2"]
|
|
35
|
+
# assert model.filter_crs == "epsg:4326"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize(
|
|
39
|
+
"filter,passes",
|
|
40
|
+
[(None, True), ({"test": "test"}, True), ("test==test", False), ([], False)],
|
|
41
|
+
)
|
|
42
|
+
def test_create_post_request_model(filter, passes):
|
|
43
|
+
extensions = [FilterExtension()]
|
|
44
|
+
request_model = create_post_request_model(extensions, BaseSearchPostRequest)
|
|
45
|
+
|
|
46
|
+
if not passes:
|
|
47
|
+
with pytest.raises(ValidationError):
|
|
48
|
+
model = request_model(filter=filter)
|
|
49
|
+
else:
|
|
50
|
+
model = request_model(
|
|
51
|
+
collections=["test1", "test2"],
|
|
52
|
+
ids=["test1", "test2"],
|
|
53
|
+
bbox=[0, 0, 1, 1],
|
|
54
|
+
datetime="2020-01-01T00:00:00Z",
|
|
55
|
+
limit=10,
|
|
56
|
+
filter=filter,
|
|
57
|
+
**{"filter-crs": "epsg:4326", "filter-lang": "cql2-text"},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assert model.collections == ["test1", "test2"]
|
|
61
|
+
assert model.filter_crs == "epsg:4326"
|
|
62
|
+
assert model.filter == filter
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize(
|
|
66
|
+
"sortby,passes",
|
|
67
|
+
[
|
|
68
|
+
(None, True),
|
|
69
|
+
(
|
|
70
|
+
[
|
|
71
|
+
{"field": "test", "direction": "asc"},
|
|
72
|
+
{"field": "test2", "direction": "desc"},
|
|
73
|
+
],
|
|
74
|
+
True,
|
|
75
|
+
),
|
|
76
|
+
({"field": "test", "direction": "desc"}, False),
|
|
77
|
+
("test", False),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
def test_create_post_request_model_nested_fields(sortby, passes):
|
|
81
|
+
extensions = [SortExtension()]
|
|
82
|
+
request_model = create_post_request_model(extensions, BaseSearchPostRequest)
|
|
83
|
+
|
|
84
|
+
if not passes:
|
|
85
|
+
with pytest.raises(ValidationError):
|
|
86
|
+
model = request_model(sortby=sortby)
|
|
87
|
+
else:
|
|
88
|
+
model = request_model(
|
|
89
|
+
collections=["test1", "test2"],
|
|
90
|
+
ids=["test1", "test2"],
|
|
91
|
+
bbox=[0, 0, 1, 1],
|
|
92
|
+
datetime="2020-01-01T00:00:00Z",
|
|
93
|
+
limit=10,
|
|
94
|
+
sortby=sortby,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert model.collections == ["test1", "test2"]
|
|
98
|
+
if model.sortby is None:
|
|
99
|
+
assert sortby is None
|
|
100
|
+
else:
|
|
101
|
+
assert model.model_dump(mode="json")["sortby"] == sortby
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{stac_fastapi_api-2.5.5.post1 → stac_fastapi_api-3.0.0a0}/stac_fastapi.api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|