eodag 2.12.1__py3-none-any.whl → 3.0.0b1__py3-none-any.whl
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.
- eodag/api/core.py +434 -319
- eodag/api/product/__init__.py +5 -1
- eodag/api/product/_assets.py +7 -2
- eodag/api/product/_product.py +46 -68
- eodag/api/product/metadata_mapping.py +181 -66
- eodag/api/search_result.py +21 -1
- eodag/cli.py +20 -6
- eodag/config.py +95 -6
- eodag/plugins/apis/base.py +8 -165
- eodag/plugins/apis/ecmwf.py +36 -24
- eodag/plugins/apis/usgs.py +40 -24
- eodag/plugins/authentication/aws_auth.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +13 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +256 -46
- eodag/plugins/authentication/qsauth.py +3 -0
- eodag/plugins/authentication/sas_auth.py +8 -1
- eodag/plugins/authentication/token.py +92 -46
- eodag/plugins/authentication/token_exchange.py +120 -0
- eodag/plugins/download/aws.py +86 -91
- eodag/plugins/download/base.py +72 -40
- eodag/plugins/download/http.py +607 -264
- eodag/plugins/download/s3rest.py +28 -15
- eodag/plugins/manager.py +73 -57
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +225 -18
- eodag/plugins/search/build_search_result.py +389 -32
- eodag/plugins/search/cop_marine.py +378 -0
- eodag/plugins/search/creodias_s3.py +15 -14
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +44 -20
- eodag/plugins/search/qssearch.py +508 -203
- eodag/plugins/search/static_stac_search.py +99 -36
- eodag/resources/constraints/climate-dt.json +13 -0
- eodag/resources/constraints/extremes-dt.json +8 -0
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1897 -34
- eodag/resources/providers.yml +3539 -3277
- eodag/resources/stac.yml +48 -54
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +51 -3
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +27 -0
- eodag/rest/core.py +757 -0
- eodag/rest/server.py +397 -258
- eodag/rest/stac.py +438 -307
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +232 -43
- eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
- eodag/rest/types/stac_search.py +277 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +65 -0
- eodag/types/__init__.py +99 -9
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +31 -0
- eodag/types/search_args.py +58 -7
- eodag/types/whoosh.py +81 -0
- eodag/utils/__init__.py +72 -9
- eodag/utils/constraints.py +37 -37
- eodag/utils/exceptions.py +23 -17
- eodag/utils/requests.py +138 -0
- eodag/utils/rest.py +104 -0
- eodag/utils/stac_reader.py +100 -16
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/METADATA +64 -44
- eodag-3.0.0b1.dist-info/RECORD +109 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/entry_points.txt +6 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/top_level.txt +0 -0
eodag/rest/server.py
CHANGED
|
@@ -19,9 +19,11 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
+
import re
|
|
22
23
|
import traceback
|
|
23
24
|
from contextlib import asynccontextmanager
|
|
24
25
|
from importlib.metadata import version
|
|
26
|
+
from json import JSONDecodeError
|
|
25
27
|
from typing import (
|
|
26
28
|
TYPE_CHECKING,
|
|
27
29
|
Any,
|
|
@@ -29,37 +31,39 @@ from typing import (
|
|
|
29
31
|
Awaitable,
|
|
30
32
|
Callable,
|
|
31
33
|
Dict,
|
|
32
|
-
List,
|
|
33
34
|
Optional,
|
|
34
|
-
Union,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
from fastapi import APIRouter as FastAPIRouter
|
|
38
38
|
from fastapi import FastAPI, HTTPException, Request
|
|
39
|
-
from fastapi.encoders import jsonable_encoder
|
|
40
39
|
from fastapi.middleware.cors import CORSMiddleware
|
|
41
40
|
from fastapi.openapi.utils import get_openapi
|
|
42
|
-
from fastapi.responses import ORJSONResponse
|
|
43
|
-
from pydantic import
|
|
41
|
+
from fastapi.responses import ORJSONResponse
|
|
42
|
+
from pydantic import ValidationError as pydanticValidationError
|
|
43
|
+
from pygeofilter.backends.cql2_json import to_cql2
|
|
44
|
+
from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
|
|
44
45
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
45
46
|
|
|
46
47
|
from eodag.config import load_stac_api_config
|
|
47
|
-
from eodag.rest.
|
|
48
|
-
from eodag.rest.
|
|
49
|
-
|
|
48
|
+
from eodag.rest.cache import init_cache
|
|
49
|
+
from eodag.rest.core import (
|
|
50
|
+
all_collections,
|
|
51
|
+
download_stac_item,
|
|
50
52
|
eodag_api_init,
|
|
51
|
-
|
|
53
|
+
get_collection,
|
|
52
54
|
get_detailled_collections_list,
|
|
55
|
+
get_queryables,
|
|
53
56
|
get_stac_api_version,
|
|
54
57
|
get_stac_catalogs,
|
|
55
|
-
get_stac_collection_by_id,
|
|
56
|
-
get_stac_collections,
|
|
57
58
|
get_stac_conformance,
|
|
58
59
|
get_stac_extension_oseo,
|
|
59
|
-
get_stac_item_by_id,
|
|
60
60
|
search_stac_items,
|
|
61
61
|
)
|
|
62
|
-
from eodag.
|
|
62
|
+
from eodag.rest.types.eodag_search import EODAGSearch
|
|
63
|
+
from eodag.rest.types.queryables import QueryablesGetParams
|
|
64
|
+
from eodag.rest.types.stac_search import SearchPostRequest, sortby2list
|
|
65
|
+
from eodag.rest.utils import format_pydantic_error, str2json, str2list
|
|
66
|
+
from eodag.utils import parse_header, update_nested_dict
|
|
63
67
|
from eodag.utils.exceptions import (
|
|
64
68
|
AuthenticationError,
|
|
65
69
|
DownloadError,
|
|
@@ -77,8 +81,15 @@ if TYPE_CHECKING:
|
|
|
77
81
|
from fastapi.types import DecoratedCallable
|
|
78
82
|
from requests import Response
|
|
79
83
|
|
|
84
|
+
from starlette.responses import Response as StarletteResponse
|
|
80
85
|
|
|
81
86
|
logger = logging.getLogger("eodag.rest.server")
|
|
87
|
+
ERRORS_WITH_500_STATUS_CODE = {
|
|
88
|
+
"MisconfiguredError",
|
|
89
|
+
"AuthenticationError",
|
|
90
|
+
"DownloadError",
|
|
91
|
+
"RequestError",
|
|
92
|
+
}
|
|
82
93
|
|
|
83
94
|
|
|
84
95
|
class APIRouter(FastAPIRouter):
|
|
@@ -118,6 +129,7 @@ router = APIRouter()
|
|
|
118
129
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
119
130
|
"""API init and tear-down"""
|
|
120
131
|
eodag_api_init()
|
|
132
|
+
init_cache(app)
|
|
121
133
|
yield
|
|
122
134
|
|
|
123
135
|
|
|
@@ -127,14 +139,16 @@ app = FastAPI(lifespan=lifespan, title="EODAG", docs_url="/api.html")
|
|
|
127
139
|
stac_api_config = load_stac_api_config()
|
|
128
140
|
|
|
129
141
|
|
|
130
|
-
@router.
|
|
131
|
-
|
|
142
|
+
@router.api_route(
|
|
143
|
+
methods=["GET", "HEAD"], path="/api", tags=["Capabilities"], include_in_schema=False
|
|
144
|
+
)
|
|
145
|
+
async def eodag_openapi(request: Request) -> Dict[str, Any]:
|
|
132
146
|
"""Customized openapi"""
|
|
133
147
|
logger.debug("URL: /api")
|
|
134
148
|
if app.openapi_schema:
|
|
135
149
|
return app.openapi_schema
|
|
136
150
|
|
|
137
|
-
root_catalog = get_stac_catalogs(url=""
|
|
151
|
+
root_catalog = await get_stac_catalogs(request=request, url="")
|
|
138
152
|
stac_api_version = get_stac_api_version()
|
|
139
153
|
|
|
140
154
|
openapi_schema = get_openapi(
|
|
@@ -151,11 +165,11 @@ def eodag_openapi() -> Dict[str, Any]:
|
|
|
151
165
|
openapi_schema["components"] = stac_api_config["components"]
|
|
152
166
|
openapi_schema["tags"] = stac_api_config["tags"]
|
|
153
167
|
|
|
154
|
-
detailled_collections_list = get_detailled_collections_list(
|
|
168
|
+
detailled_collections_list = get_detailled_collections_list()
|
|
155
169
|
|
|
156
170
|
openapi_schema["info"]["description"] = (
|
|
157
171
|
root_catalog["description"]
|
|
158
|
-
+ " (stac-api-spec {})"
|
|
172
|
+
+ f" (stac-api-spec {stac_api_version})"
|
|
159
173
|
+ "<details><summary>Available collections / product types</summary>"
|
|
160
174
|
+ "".join(
|
|
161
175
|
[
|
|
@@ -195,8 +209,10 @@ async def forward_middleware(
|
|
|
195
209
|
|
|
196
210
|
if "forwarded" in request.headers:
|
|
197
211
|
header_forwarded = parse_header(request.headers["forwarded"])
|
|
198
|
-
forwarded_host = header_forwarded.get_param("host", None) or forwarded_host
|
|
199
|
-
forwarded_proto =
|
|
212
|
+
forwarded_host = str(header_forwarded.get_param("host", None)) or forwarded_host
|
|
213
|
+
forwarded_proto = (
|
|
214
|
+
str(header_forwarded.get_param("proto", None)) or forwarded_proto
|
|
215
|
+
)
|
|
200
216
|
|
|
201
217
|
request.state.url_root = f"{forwarded_proto or request.url.scheme}://{forwarded_host or request.url.netloc}"
|
|
202
218
|
request.state.url = f"{request.state.url_root}{request.url.path}"
|
|
@@ -221,13 +237,30 @@ async def default_exception_handler(
|
|
|
221
237
|
)
|
|
222
238
|
|
|
223
239
|
|
|
240
|
+
@app.exception_handler(ValidationError)
|
|
241
|
+
async def handle_invalid_usage_with_validation_error(
|
|
242
|
+
request: Request, error: ValidationError
|
|
243
|
+
) -> ORJSONResponse:
|
|
244
|
+
"""Invalid usage [400] ValidationError handle"""
|
|
245
|
+
if error.parameters:
|
|
246
|
+
for error_param in error.parameters:
|
|
247
|
+
stac_param = EODAGSearch.to_stac(error_param)
|
|
248
|
+
error.message = error.message.replace(error_param, stac_param)
|
|
249
|
+
logger.debug(traceback.format_exc())
|
|
250
|
+
return await default_exception_handler(
|
|
251
|
+
request,
|
|
252
|
+
HTTPException(
|
|
253
|
+
status_code=400,
|
|
254
|
+
detail=f"{type(error).__name__}: {str(error.message)}",
|
|
255
|
+
),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
224
259
|
@app.exception_handler(NoMatchingProductType)
|
|
225
260
|
@app.exception_handler(UnsupportedProductType)
|
|
226
261
|
@app.exception_handler(UnsupportedProvider)
|
|
227
|
-
@app.exception_handler(ValidationError)
|
|
228
262
|
async def handle_invalid_usage(request: Request, error: Exception) -> ORJSONResponse:
|
|
229
263
|
"""Invalid usage [400] errors handle"""
|
|
230
|
-
logger.warning(traceback.format_exc())
|
|
231
264
|
return await default_exception_handler(
|
|
232
265
|
request,
|
|
233
266
|
HTTPException(
|
|
@@ -254,8 +287,8 @@ async def handle_resource_not_found(
|
|
|
254
287
|
@app.exception_handler(MisconfiguredError)
|
|
255
288
|
@app.exception_handler(AuthenticationError)
|
|
256
289
|
async def handle_auth_error(request: Request, error: Exception) -> ORJSONResponse:
|
|
257
|
-
"""
|
|
258
|
-
logger.error(
|
|
290
|
+
"""These errors should be sent as internal server error to the client"""
|
|
291
|
+
logger.error("%s: %s", type(error).__name__, str(error))
|
|
259
292
|
return await default_exception_handler(
|
|
260
293
|
request,
|
|
261
294
|
HTTPException(
|
|
@@ -266,9 +299,36 @@ async def handle_auth_error(request: Request, error: Exception) -> ORJSONRespons
|
|
|
266
299
|
|
|
267
300
|
|
|
268
301
|
@app.exception_handler(DownloadError)
|
|
302
|
+
async def handle_download_error(request: Request, error: Exception) -> ORJSONResponse:
|
|
303
|
+
"""DownloadError should be sent as internal server error with details to the client"""
|
|
304
|
+
logger.error(f"{type(error).__name__}: {str(error)}")
|
|
305
|
+
return await default_exception_handler(
|
|
306
|
+
request,
|
|
307
|
+
HTTPException(
|
|
308
|
+
status_code=500,
|
|
309
|
+
detail=f"{type(error).__name__}: {str(error)}",
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
269
314
|
@app.exception_handler(RequestError)
|
|
270
|
-
async def
|
|
271
|
-
"""
|
|
315
|
+
async def handle_request_error(request: Request, error: RequestError) -> ORJSONResponse:
|
|
316
|
+
"""RequestError should be sent as internal server error with details to the client"""
|
|
317
|
+
if getattr(error, "history", None):
|
|
318
|
+
error_history_tmp = list(error.history)
|
|
319
|
+
for i, search_error in enumerate(error_history_tmp):
|
|
320
|
+
if search_error[1].__class__.__name__ in ERRORS_WITH_500_STATUS_CODE:
|
|
321
|
+
search_error[1].args = ("an internal error occured",)
|
|
322
|
+
error_history_tmp[i] = search_error
|
|
323
|
+
continue
|
|
324
|
+
if getattr(error, "parameters", None):
|
|
325
|
+
for error_param in error.parameters:
|
|
326
|
+
stac_param = EODAGSearch.to_stac(error_param)
|
|
327
|
+
search_error[1].args = (
|
|
328
|
+
search_error[1].args[0].replace(error_param, stac_param),
|
|
329
|
+
)
|
|
330
|
+
error_history_tmp[i] = search_error
|
|
331
|
+
error.history = set(error_history_tmp)
|
|
272
332
|
logger.error(f"{type(error).__name__}: {str(error)}")
|
|
273
333
|
return await default_exception_handler(
|
|
274
334
|
request,
|
|
@@ -292,164 +352,166 @@ async def handle_timeout(request: Request, error: Exception) -> ORJSONResponse:
|
|
|
292
352
|
)
|
|
293
353
|
|
|
294
354
|
|
|
295
|
-
@router.
|
|
296
|
-
def catalogs_root(request: Request) ->
|
|
355
|
+
@router.api_route(methods=["GET", "HEAD"], path="/", tags=["Capabilities"])
|
|
356
|
+
async def catalogs_root(request: Request) -> ORJSONResponse:
|
|
297
357
|
"""STAC catalogs root"""
|
|
298
|
-
logger.debug(
|
|
358
|
+
logger.debug("URL: %s", request.url)
|
|
299
359
|
|
|
300
|
-
response = get_stac_catalogs(
|
|
360
|
+
response = await get_stac_catalogs(
|
|
361
|
+
request=request,
|
|
301
362
|
url=request.state.url,
|
|
302
|
-
root=request.state.url_root,
|
|
303
|
-
catalogs=[],
|
|
304
363
|
provider=request.query_params.get("provider", None),
|
|
305
364
|
)
|
|
306
365
|
|
|
307
|
-
return
|
|
366
|
+
return ORJSONResponse(response)
|
|
308
367
|
|
|
309
368
|
|
|
310
|
-
@router.
|
|
311
|
-
def conformance() ->
|
|
369
|
+
@router.api_route(methods=["GET", "HEAD"], path="/conformance", tags=["Capabilities"])
|
|
370
|
+
def conformance() -> ORJSONResponse:
|
|
312
371
|
"""STAC conformance"""
|
|
313
372
|
logger.debug("URL: /conformance")
|
|
314
373
|
response = get_stac_conformance()
|
|
315
374
|
|
|
316
|
-
return
|
|
375
|
+
return ORJSONResponse(response)
|
|
317
376
|
|
|
318
377
|
|
|
319
|
-
@router.
|
|
320
|
-
|
|
378
|
+
@router.api_route(
|
|
379
|
+
methods=["GET", "HEAD"],
|
|
380
|
+
path="/extensions/oseo/json-schema/schema.json",
|
|
381
|
+
include_in_schema=False,
|
|
382
|
+
)
|
|
383
|
+
def stac_extension_oseo(request: Request) -> ORJSONResponse:
|
|
321
384
|
"""STAC OGC / OpenSearch extension for EO"""
|
|
322
|
-
logger.debug(
|
|
385
|
+
logger.debug("URL: %s", request.url)
|
|
323
386
|
response = get_stac_extension_oseo(url=request.state.url)
|
|
324
387
|
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
class SearchBody(BaseModel):
|
|
329
|
-
"""
|
|
330
|
-
class which describes the body of a search request
|
|
331
|
-
"""
|
|
388
|
+
return ORJSONResponse(response)
|
|
332
389
|
|
|
333
|
-
provider: Optional[str] = None
|
|
334
|
-
collections: Union[List[str], str]
|
|
335
|
-
datetime: Optional[str] = None
|
|
336
|
-
bbox: Optional[List[Union[int, float]]] = None
|
|
337
|
-
intersects: Optional[Dict[str, Any]] = None
|
|
338
|
-
limit: Optional[int] = DEFAULT_ITEMS_PER_PAGE
|
|
339
|
-
page: Optional[int] = 1
|
|
340
|
-
query: Optional[Dict[str, Any]] = None
|
|
341
|
-
ids: Optional[List[str]] = None
|
|
342
390
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"/collections/{collection_id}/items/{item_id}/download",
|
|
391
|
+
@router.api_route(
|
|
392
|
+
methods=["GET", "HEAD"],
|
|
393
|
+
path="/collections/{collection_id}/items/{item_id}/download",
|
|
346
394
|
tags=["Data"],
|
|
347
395
|
include_in_schema=False,
|
|
348
396
|
)
|
|
349
397
|
def stac_collections_item_download(
|
|
350
398
|
collection_id: str, item_id: str, request: Request
|
|
351
|
-
) ->
|
|
399
|
+
) -> StarletteResponse:
|
|
352
400
|
"""STAC collection item download"""
|
|
353
|
-
logger.debug(
|
|
401
|
+
logger.debug("URL: %s", request.url)
|
|
354
402
|
|
|
355
403
|
arguments = dict(request.query_params)
|
|
356
404
|
provider = arguments.pop("provider", None)
|
|
357
405
|
|
|
358
|
-
return
|
|
359
|
-
|
|
406
|
+
return download_stac_item(
|
|
407
|
+
request=request,
|
|
408
|
+
catalogs=[collection_id],
|
|
409
|
+
item_id=item_id,
|
|
410
|
+
provider=provider,
|
|
411
|
+
**arguments,
|
|
360
412
|
)
|
|
361
413
|
|
|
362
414
|
|
|
363
|
-
@router.
|
|
364
|
-
"
|
|
415
|
+
@router.api_route(
|
|
416
|
+
methods=["GET", "HEAD"],
|
|
417
|
+
path="/collections/{collection_id}/items/{item_id}/download/{asset}",
|
|
365
418
|
tags=["Data"],
|
|
366
419
|
include_in_schema=False,
|
|
367
420
|
)
|
|
368
421
|
def stac_collections_item_download_asset(
|
|
369
|
-
collection_id, item_id,
|
|
422
|
+
collection_id: str, item_id: str, asset: str, request: Request
|
|
370
423
|
):
|
|
371
424
|
"""STAC collection item asset download"""
|
|
372
|
-
logger.debug(
|
|
425
|
+
logger.debug("URL: %s", request.url)
|
|
373
426
|
|
|
374
427
|
arguments = dict(request.query_params)
|
|
375
428
|
provider = arguments.pop("provider", None)
|
|
376
429
|
|
|
377
|
-
return
|
|
430
|
+
return download_stac_item(
|
|
431
|
+
request=request,
|
|
378
432
|
catalogs=[collection_id],
|
|
379
433
|
item_id=item_id,
|
|
380
434
|
provider=provider,
|
|
381
|
-
asset=
|
|
382
|
-
**arguments,
|
|
435
|
+
asset=asset,
|
|
383
436
|
)
|
|
384
437
|
|
|
385
438
|
|
|
386
|
-
@router.
|
|
387
|
-
"
|
|
439
|
+
@router.api_route(
|
|
440
|
+
methods=["GET", "HEAD"],
|
|
441
|
+
path="/collections/{collection_id}/items/{item_id}",
|
|
388
442
|
tags=["Data"],
|
|
389
443
|
include_in_schema=False,
|
|
390
444
|
)
|
|
391
|
-
def stac_collections_item(
|
|
445
|
+
def stac_collections_item(
|
|
446
|
+
collection_id: str, item_id: str, request: Request, provider: Optional[str] = None
|
|
447
|
+
) -> ORJSONResponse:
|
|
392
448
|
"""STAC collection item by id"""
|
|
393
|
-
logger.debug(
|
|
394
|
-
url = request.state.url
|
|
395
|
-
url_root = request.state.url_root
|
|
449
|
+
logger.debug("URL: %s", request.url)
|
|
396
450
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
response = get_stac_item_by_id(
|
|
401
|
-
url=url,
|
|
402
|
-
item_id=item_id,
|
|
403
|
-
root=url_root,
|
|
404
|
-
catalogs=[collection_id],
|
|
405
|
-
provider=provider,
|
|
406
|
-
**arguments,
|
|
451
|
+
search_request = SearchPostRequest(
|
|
452
|
+
provider=provider, ids=[item_id], collections=[collection_id], limit=1
|
|
407
453
|
)
|
|
408
454
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
455
|
+
item_collection = search_stac_items(request, search_request)
|
|
456
|
+
|
|
457
|
+
if not item_collection["features"]:
|
|
412
458
|
raise HTTPException(
|
|
413
459
|
status_code=404,
|
|
414
|
-
detail="
|
|
415
|
-
item_id, collection_id
|
|
416
|
-
),
|
|
460
|
+
detail=f"Item {item_id} in Collection {collection_id} does not exist.",
|
|
417
461
|
)
|
|
418
462
|
|
|
463
|
+
return ORJSONResponse(item_collection["features"][0])
|
|
419
464
|
|
|
420
|
-
|
|
421
|
-
|
|
465
|
+
|
|
466
|
+
@router.api_route(
|
|
467
|
+
methods=["GET", "HEAD"],
|
|
468
|
+
path="/collections/{collection_id}/items",
|
|
422
469
|
tags=["Data"],
|
|
423
470
|
include_in_schema=False,
|
|
424
471
|
)
|
|
425
|
-
def stac_collections_items(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
472
|
+
def stac_collections_items(
|
|
473
|
+
collection_id: str,
|
|
474
|
+
request: Request,
|
|
475
|
+
provider: Optional[str] = None,
|
|
476
|
+
bbox: Optional[str] = None,
|
|
477
|
+
datetime: Optional[str] = None,
|
|
478
|
+
limit: Optional[int] = None,
|
|
479
|
+
query: Optional[str] = None,
|
|
480
|
+
page: Optional[int] = None,
|
|
481
|
+
sortby: Optional[str] = None,
|
|
482
|
+
filter: Optional[str] = None,
|
|
483
|
+
filter_lang: Optional[str] = "cql2-text",
|
|
484
|
+
crunch: Optional[str] = None,
|
|
485
|
+
) -> ORJSONResponse:
|
|
486
|
+
"""Fetch collection's features"""
|
|
433
487
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
arguments=arguments,
|
|
437
|
-
root=url_root,
|
|
488
|
+
return get_search(
|
|
489
|
+
request=request,
|
|
438
490
|
provider=provider,
|
|
439
|
-
|
|
491
|
+
collections=collection_id,
|
|
492
|
+
bbox=bbox,
|
|
493
|
+
datetime=datetime,
|
|
494
|
+
limit=limit,
|
|
495
|
+
query=query,
|
|
496
|
+
page=page,
|
|
497
|
+
sortby=sortby,
|
|
498
|
+
filter=filter,
|
|
499
|
+
filter_lang=filter_lang,
|
|
500
|
+
crunch=crunch,
|
|
440
501
|
)
|
|
441
|
-
return jsonable_encoder(response)
|
|
442
502
|
|
|
443
503
|
|
|
444
|
-
@router.
|
|
445
|
-
"
|
|
504
|
+
@router.api_route(
|
|
505
|
+
methods=["GET", "HEAD"],
|
|
506
|
+
path="/collections/{collection_id}/queryables",
|
|
446
507
|
tags=["Capabilities"],
|
|
447
508
|
include_in_schema=False,
|
|
448
509
|
response_model_exclude_none=True,
|
|
449
510
|
)
|
|
450
|
-
def list_collection_queryables(
|
|
451
|
-
request: Request,
|
|
452
|
-
|
|
511
|
+
async def list_collection_queryables(
|
|
512
|
+
request: Request,
|
|
513
|
+
collection_id: str,
|
|
514
|
+
) -> ORJSONResponse:
|
|
453
515
|
"""Returns the list of queryable properties for a specific collection.
|
|
454
516
|
|
|
455
517
|
This endpoint provides a list of properties that can be used as filters when querying
|
|
@@ -460,118 +522,117 @@ def list_collection_queryables(
|
|
|
460
522
|
:type request: fastapi.Request
|
|
461
523
|
:param collection_id: The identifier of the collection for which to retrieve queryable properties.
|
|
462
524
|
:type collection_id: str
|
|
463
|
-
:param provider: (optional) The provider for which to retrieve additional properties.
|
|
464
|
-
:type provider: str
|
|
465
525
|
:returns: A json object containing the list of available queryable properties for the specified collection.
|
|
466
526
|
:rtype: Any
|
|
467
527
|
"""
|
|
468
528
|
logger.debug(f"URL: {request.url}")
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
additional_params.pop("provider", None)
|
|
472
|
-
|
|
473
|
-
queryables = StacQueryables(q_id=request.state.url, additional_properties=False)
|
|
529
|
+
additional_params = dict(request.query_params)
|
|
530
|
+
provider = additional_params.pop("provider", None)
|
|
474
531
|
|
|
475
|
-
|
|
476
|
-
|
|
532
|
+
queryables = await get_queryables(
|
|
533
|
+
request,
|
|
534
|
+
QueryablesGetParams(collection=collection_id, **additional_params),
|
|
535
|
+
provider=provider,
|
|
477
536
|
)
|
|
478
|
-
for key, collection_queryable in collection_queryables.items():
|
|
479
|
-
queryables[key] = collection_queryable
|
|
480
|
-
queryables.properties.pop("collections")
|
|
481
537
|
|
|
482
|
-
return
|
|
538
|
+
return ORJSONResponse(queryables)
|
|
483
539
|
|
|
484
540
|
|
|
485
|
-
@router.
|
|
486
|
-
"
|
|
541
|
+
@router.api_route(
|
|
542
|
+
methods=["GET", "HEAD"],
|
|
543
|
+
path="/collections/{collection_id}",
|
|
487
544
|
tags=["Capabilities"],
|
|
488
545
|
include_in_schema=False,
|
|
489
546
|
)
|
|
490
|
-
def collection_by_id(
|
|
547
|
+
async def collection_by_id(
|
|
548
|
+
collection_id: str, request: Request, provider: Optional[str] = None
|
|
549
|
+
) -> ORJSONResponse:
|
|
491
550
|
"""STAC collection by id"""
|
|
492
|
-
logger.debug(
|
|
493
|
-
url = request.state.url_root + "/collections"
|
|
494
|
-
url_root = request.state.url_root
|
|
551
|
+
logger.debug("URL: %s", request.url)
|
|
495
552
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
response = get_stac_collection_by_id(
|
|
500
|
-
url=url,
|
|
501
|
-
root=url_root,
|
|
553
|
+
response = await get_collection(
|
|
554
|
+
request=request,
|
|
502
555
|
collection_id=collection_id,
|
|
503
556
|
provider=provider,
|
|
504
557
|
)
|
|
505
558
|
|
|
506
|
-
return
|
|
559
|
+
return ORJSONResponse(response)
|
|
507
560
|
|
|
508
561
|
|
|
509
|
-
@router.
|
|
510
|
-
"
|
|
562
|
+
@router.api_route(
|
|
563
|
+
methods=["GET", "HEAD"],
|
|
564
|
+
path="/collections",
|
|
511
565
|
tags=["Capabilities"],
|
|
512
566
|
include_in_schema=False,
|
|
513
567
|
)
|
|
514
|
-
def collections(
|
|
568
|
+
async def collections(
|
|
569
|
+
request: Request,
|
|
570
|
+
provider: Optional[str] = None,
|
|
571
|
+
q: Optional[str] = None,
|
|
572
|
+
platform: Optional[str] = None,
|
|
573
|
+
instrument: Optional[str] = None,
|
|
574
|
+
constellation: Optional[str] = None,
|
|
575
|
+
datetime: Optional[str] = None,
|
|
576
|
+
) -> ORJSONResponse:
|
|
515
577
|
"""STAC collections
|
|
516
578
|
|
|
517
|
-
Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
|
|
579
|
+
Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
|
|
580
|
+
processingLevel
|
|
518
581
|
"""
|
|
519
|
-
logger.debug(
|
|
520
|
-
url = request.state.url
|
|
521
|
-
url_root = request.state.url_root
|
|
582
|
+
logger.debug("URL: %s", request.url)
|
|
522
583
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
response = get_stac_collections(
|
|
527
|
-
url=url,
|
|
528
|
-
root=url_root,
|
|
529
|
-
arguments=arguments,
|
|
530
|
-
provider=provider,
|
|
584
|
+
collections = await all_collections(
|
|
585
|
+
request, provider, q, platform, instrument, constellation, datetime
|
|
531
586
|
)
|
|
587
|
+
return ORJSONResponse(collections)
|
|
532
588
|
|
|
533
|
-
return jsonable_encoder(response)
|
|
534
589
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
"/catalogs/{catalogs:path}/items/{item_id}/download",
|
|
590
|
+
@router.api_route(
|
|
591
|
+
methods=["GET", "HEAD"],
|
|
592
|
+
path="/catalogs/{catalogs:path}/items/{item_id}/download",
|
|
538
593
|
tags=["Data"],
|
|
539
594
|
include_in_schema=False,
|
|
540
595
|
)
|
|
541
596
|
def stac_catalogs_item_download(
|
|
542
597
|
catalogs: str, item_id: str, request: Request
|
|
543
|
-
) ->
|
|
598
|
+
) -> StarletteResponse:
|
|
544
599
|
"""STAC Catalog item download"""
|
|
545
|
-
logger.debug(
|
|
600
|
+
logger.debug("URL: %s", request.url)
|
|
546
601
|
|
|
547
602
|
arguments = dict(request.query_params)
|
|
548
603
|
provider = arguments.pop("provider", None)
|
|
549
604
|
|
|
550
605
|
list_catalog = catalogs.strip("/").split("/")
|
|
551
606
|
|
|
552
|
-
return
|
|
553
|
-
|
|
607
|
+
return download_stac_item(
|
|
608
|
+
request=request,
|
|
609
|
+
catalogs=list_catalog,
|
|
610
|
+
item_id=item_id,
|
|
611
|
+
provider=provider,
|
|
612
|
+
**arguments,
|
|
554
613
|
)
|
|
555
614
|
|
|
556
615
|
|
|
557
|
-
@router.
|
|
558
|
-
"
|
|
616
|
+
@router.api_route(
|
|
617
|
+
methods=["GET", "HEAD"],
|
|
618
|
+
path="/catalogs/{catalogs:path}/items/{item_id}/download/{asset_filter}",
|
|
559
619
|
tags=["Data"],
|
|
560
620
|
include_in_schema=False,
|
|
561
621
|
)
|
|
562
622
|
def stac_catalogs_item_download_asset(
|
|
563
|
-
catalogs, item_id, asset_filter, request: Request
|
|
623
|
+
catalogs: str, item_id: str, asset_filter: str, request: Request
|
|
564
624
|
):
|
|
565
625
|
"""STAC Catalog item asset download"""
|
|
566
|
-
logger.debug(
|
|
626
|
+
logger.debug("URL: %s", request.url)
|
|
567
627
|
|
|
568
628
|
arguments = dict(request.query_params)
|
|
569
629
|
provider = arguments.pop("provider", None)
|
|
570
630
|
|
|
571
|
-
|
|
631
|
+
list_catalog = catalogs.strip("/").split("/")
|
|
572
632
|
|
|
573
|
-
return
|
|
574
|
-
|
|
633
|
+
return download_stac_item(
|
|
634
|
+
request,
|
|
635
|
+
catalogs=list_catalog,
|
|
575
636
|
item_id=item_id,
|
|
576
637
|
provider=provider,
|
|
577
638
|
asset=asset_filter,
|
|
@@ -579,99 +640,116 @@ def stac_catalogs_item_download_asset(
|
|
|
579
640
|
)
|
|
580
641
|
|
|
581
642
|
|
|
582
|
-
@router.
|
|
583
|
-
"
|
|
643
|
+
@router.api_route(
|
|
644
|
+
methods=["GET", "HEAD"],
|
|
645
|
+
path="/catalogs/{catalogs:path}/items/{item_id}",
|
|
584
646
|
tags=["Data"],
|
|
585
647
|
include_in_schema=False,
|
|
586
648
|
)
|
|
587
|
-
def stac_catalogs_item(
|
|
649
|
+
def stac_catalogs_item(
|
|
650
|
+
catalogs: str, item_id: str, request: Request, provider: Optional[str] = None
|
|
651
|
+
):
|
|
588
652
|
"""Fetch catalog's single features."""
|
|
589
|
-
logger.debug(
|
|
590
|
-
url = request.state.url
|
|
591
|
-
url_root = request.state.url_root
|
|
592
|
-
|
|
593
|
-
arguments = dict(request.query_params)
|
|
594
|
-
provider = arguments.pop("provider", None)
|
|
653
|
+
logger.debug("URL: %s", request.url)
|
|
595
654
|
|
|
596
655
|
list_catalog = catalogs.strip("/").split("/")
|
|
597
|
-
response = get_stac_item_by_id(
|
|
598
|
-
url=url,
|
|
599
|
-
item_id=item_id,
|
|
600
|
-
root=url_root,
|
|
601
|
-
catalogs=list_catalog,
|
|
602
|
-
provider=provider,
|
|
603
|
-
**arguments,
|
|
604
|
-
)
|
|
605
656
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
657
|
+
search_request = SearchPostRequest(provider=provider, ids=[item_id], limit=1)
|
|
658
|
+
|
|
659
|
+
item_collection = search_stac_items(request, search_request, catalogs=list_catalog)
|
|
660
|
+
|
|
661
|
+
if not item_collection["features"]:
|
|
609
662
|
raise HTTPException(
|
|
610
663
|
status_code=404,
|
|
611
|
-
detail="
|
|
612
|
-
item_id, catalogs
|
|
613
|
-
),
|
|
664
|
+
detail=f"Item {item_id} in Catalog {catalogs} does not exist.",
|
|
614
665
|
)
|
|
615
666
|
|
|
667
|
+
return ORJSONResponse(item_collection["features"][0])
|
|
616
668
|
|
|
617
|
-
|
|
618
|
-
|
|
669
|
+
|
|
670
|
+
@router.api_route(
|
|
671
|
+
methods=["GET", "HEAD"],
|
|
672
|
+
path="/catalogs/{catalogs:path}/items",
|
|
619
673
|
tags=["Data"],
|
|
620
674
|
include_in_schema=False,
|
|
621
675
|
)
|
|
622
|
-
def stac_catalogs_items(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
676
|
+
def stac_catalogs_items(
|
|
677
|
+
catalogs: str,
|
|
678
|
+
request: Request,
|
|
679
|
+
provider: Optional[str] = None,
|
|
680
|
+
bbox: Optional[str] = None,
|
|
681
|
+
datetime: Optional[str] = None,
|
|
682
|
+
limit: Optional[int] = None,
|
|
683
|
+
page: Optional[int] = None,
|
|
684
|
+
sortby: Optional[str] = None,
|
|
685
|
+
crunch: Optional[str] = None,
|
|
686
|
+
) -> ORJSONResponse:
|
|
687
|
+
"""Fetch catalog's features"""
|
|
688
|
+
logger.debug("URL: %s", request.state.url)
|
|
628
689
|
|
|
629
|
-
|
|
630
|
-
|
|
690
|
+
base_args = {
|
|
691
|
+
"provider": provider,
|
|
692
|
+
"datetime": datetime,
|
|
693
|
+
"bbox": str2list(bbox),
|
|
694
|
+
"limit": limit,
|
|
695
|
+
"page": page,
|
|
696
|
+
"sortby": sortby2list(sortby),
|
|
697
|
+
"crunch": crunch,
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
clean = {k: v for k, v in base_args.items() if v is not None and v != []}
|
|
631
701
|
|
|
632
702
|
list_catalog = catalogs.strip("/").split("/")
|
|
633
703
|
|
|
704
|
+
try:
|
|
705
|
+
search_request = SearchPostRequest.model_validate(clean)
|
|
706
|
+
except pydanticValidationError as e:
|
|
707
|
+
raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
|
|
708
|
+
|
|
634
709
|
response = search_stac_items(
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
root=url_root,
|
|
710
|
+
request=request,
|
|
711
|
+
search_request=search_request,
|
|
638
712
|
catalogs=list_catalog,
|
|
639
|
-
provider=provider,
|
|
640
713
|
)
|
|
641
|
-
return
|
|
714
|
+
return ORJSONResponse(response)
|
|
642
715
|
|
|
643
716
|
|
|
644
|
-
@router.
|
|
645
|
-
"
|
|
717
|
+
@router.api_route(
|
|
718
|
+
methods=["GET", "HEAD"],
|
|
719
|
+
path="/catalogs/{catalogs:path}",
|
|
646
720
|
tags=["Capabilities"],
|
|
647
721
|
include_in_schema=False,
|
|
648
722
|
)
|
|
649
|
-
def stac_catalogs(
|
|
723
|
+
async def stac_catalogs(
|
|
724
|
+
catalogs: str, request: Request, provider: Optional[str] = None
|
|
725
|
+
) -> ORJSONResponse:
|
|
650
726
|
"""Describe the given catalog and list available sub-catalogs"""
|
|
651
|
-
logger.debug(
|
|
652
|
-
url = request.state.url
|
|
653
|
-
url_root = request.state.url_root
|
|
727
|
+
logger.debug("URL: %s", request.url)
|
|
654
728
|
|
|
655
|
-
|
|
656
|
-
|
|
729
|
+
if not catalogs:
|
|
730
|
+
raise HTTPException(
|
|
731
|
+
status_code=404,
|
|
732
|
+
detail="Not found",
|
|
733
|
+
)
|
|
657
734
|
|
|
658
735
|
list_catalog = catalogs.strip("/").split("/")
|
|
659
|
-
response = get_stac_catalogs(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
catalogs=list_catalog,
|
|
736
|
+
response = await get_stac_catalogs(
|
|
737
|
+
request=request,
|
|
738
|
+
url=request.state.url,
|
|
739
|
+
catalogs=tuple(list_catalog),
|
|
663
740
|
provider=provider,
|
|
664
741
|
)
|
|
665
|
-
return
|
|
742
|
+
return ORJSONResponse(response)
|
|
666
743
|
|
|
667
744
|
|
|
668
|
-
@router.
|
|
669
|
-
"
|
|
745
|
+
@router.api_route(
|
|
746
|
+
methods=["GET", "HEAD"],
|
|
747
|
+
path="/queryables",
|
|
670
748
|
tags=["Capabilities"],
|
|
671
749
|
response_model_exclude_none=True,
|
|
672
750
|
include_in_schema=False,
|
|
673
751
|
)
|
|
674
|
-
def list_queryables(request: Request
|
|
752
|
+
async def list_queryables(request: Request) -> ORJSONResponse:
|
|
675
753
|
"""Returns the list of terms available for use when writing filter expressions.
|
|
676
754
|
|
|
677
755
|
This endpoint provides a list of terms that can be used as filters when querying
|
|
@@ -684,57 +762,118 @@ def list_queryables(request: Request, provider: Optional[str] = None) -> Any:
|
|
|
684
762
|
:rtype: Any
|
|
685
763
|
"""
|
|
686
764
|
logger.debug(f"URL: {request.url}")
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
queryables.properties.update(
|
|
693
|
-
fetch_collection_queryable_properties(None, provider, **additional_params)
|
|
694
|
-
)
|
|
765
|
+
additional_params = dict(request.query_params.items())
|
|
766
|
+
provider = additional_params.pop("provider", None)
|
|
767
|
+
queryables = await get_queryables(
|
|
768
|
+
request, QueryablesGetParams(**additional_params), provider=provider
|
|
769
|
+
)
|
|
695
770
|
|
|
696
|
-
return
|
|
771
|
+
return ORJSONResponse(queryables)
|
|
697
772
|
|
|
698
773
|
|
|
699
|
-
@router.
|
|
700
|
-
"
|
|
774
|
+
@router.api_route(
|
|
775
|
+
methods=["GET", "HEAD"],
|
|
776
|
+
path="/search",
|
|
701
777
|
tags=["STAC"],
|
|
702
778
|
include_in_schema=False,
|
|
703
779
|
)
|
|
704
|
-
|
|
705
|
-
|
|
780
|
+
def get_search(
|
|
781
|
+
request: Request,
|
|
782
|
+
provider: Optional[str] = None,
|
|
783
|
+
collections: Optional[str] = None,
|
|
784
|
+
ids: Optional[str] = None,
|
|
785
|
+
bbox: Optional[str] = None,
|
|
786
|
+
datetime: Optional[str] = None,
|
|
787
|
+
intersects: Optional[str] = None,
|
|
788
|
+
limit: Optional[int] = None,
|
|
789
|
+
query: Optional[str] = None,
|
|
790
|
+
page: Optional[int] = None,
|
|
791
|
+
sortby: Optional[str] = None,
|
|
792
|
+
filter: Optional[str] = None, # pylint: disable=redefined-builtin
|
|
793
|
+
filter_lang: Optional[str] = "cql2-text",
|
|
794
|
+
crunch: Optional[str] = None,
|
|
795
|
+
) -> ORJSONResponse:
|
|
796
|
+
"""Handler for GET /search"""
|
|
797
|
+
logger.debug("URL: %s", request.state.url)
|
|
798
|
+
|
|
799
|
+
query_params = str(request.query_params)
|
|
800
|
+
|
|
801
|
+
# Kludgy fix because using factory does not allow alias for filter-lang
|
|
802
|
+
if filter_lang is None:
|
|
803
|
+
match = re.search(r"filter-lang=([a-z0-9-]+)", query_params, re.IGNORECASE)
|
|
804
|
+
if match:
|
|
805
|
+
filter_lang = match.group(1)
|
|
806
|
+
|
|
807
|
+
base_args = {
|
|
808
|
+
"provider": provider,
|
|
809
|
+
"collections": str2list(collections),
|
|
810
|
+
"ids": str2list(ids),
|
|
811
|
+
"datetime": datetime,
|
|
812
|
+
"bbox": str2list(bbox),
|
|
813
|
+
"intersects": str2json("intersects", intersects),
|
|
814
|
+
"limit": limit,
|
|
815
|
+
"query": str2json("query", query),
|
|
816
|
+
"page": page,
|
|
817
|
+
"sortby": sortby2list(sortby),
|
|
818
|
+
"crunch": crunch,
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if filter:
|
|
822
|
+
if filter_lang == "cql2-text":
|
|
823
|
+
ast = parse_cql2_text(filter)
|
|
824
|
+
base_args["filter"] = str2json("filter", to_cql2(ast)) # type: ignore
|
|
825
|
+
base_args["filter-lang"] = "cql2-json"
|
|
826
|
+
elif filter_lang == "cql-json":
|
|
827
|
+
base_args["filter"] = str2json(filter)
|
|
828
|
+
|
|
829
|
+
clean = {k: v for k, v in base_args.items() if v is not None and v != []}
|
|
830
|
+
|
|
831
|
+
try:
|
|
832
|
+
search_request = SearchPostRequest.model_validate(clean)
|
|
833
|
+
except pydanticValidationError as e:
|
|
834
|
+
raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
|
|
835
|
+
|
|
836
|
+
response = search_stac_items(
|
|
837
|
+
request=request,
|
|
838
|
+
search_request=search_request,
|
|
839
|
+
)
|
|
840
|
+
return ORJSONResponse(content=response, media_type="application/json")
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
@router.api_route(
|
|
844
|
+
methods=["POST", "HEAD"],
|
|
845
|
+
path="/search",
|
|
706
846
|
tags=["STAC"],
|
|
707
847
|
include_in_schema=False,
|
|
708
848
|
)
|
|
709
|
-
def
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
"""STAC collections items"""
|
|
713
|
-
logger.debug(f"URL: {request.url}")
|
|
714
|
-
logger.debug(f"Body: {search_body}")
|
|
849
|
+
async def post_search(request: Request) -> ORJSONResponse:
|
|
850
|
+
"""STAC post search"""
|
|
851
|
+
logger.debug("URL: %s", request.url)
|
|
715
852
|
|
|
716
|
-
|
|
717
|
-
url_root = request.state.url_root
|
|
853
|
+
content_type = request.headers.get("Content-Type")
|
|
718
854
|
|
|
719
|
-
if
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
855
|
+
if content_type is None:
|
|
856
|
+
raise HTTPException(status_code=400, detail="No Content-Type provided")
|
|
857
|
+
if content_type != "application/json":
|
|
858
|
+
raise HTTPException(status_code=400, detail="Content-Type not supported")
|
|
723
859
|
|
|
724
|
-
|
|
725
|
-
|
|
860
|
+
try:
|
|
861
|
+
payload = await request.json()
|
|
862
|
+
except JSONDecodeError as e:
|
|
863
|
+
raise HTTPException(status_code=400, detail="Invalid JSON data") from e
|
|
864
|
+
|
|
865
|
+
try:
|
|
866
|
+
search_request = SearchPostRequest.model_validate(payload)
|
|
867
|
+
except pydanticValidationError as e:
|
|
868
|
+
raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
|
|
869
|
+
|
|
870
|
+
logger.debug("Body: %s", search_request.model_dump(exclude_none=True))
|
|
726
871
|
|
|
727
872
|
response = search_stac_items(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
root=url_root,
|
|
731
|
-
provider=provider,
|
|
732
|
-
method=request.method,
|
|
733
|
-
)
|
|
734
|
-
resp = ORJSONResponse(
|
|
735
|
-
content=response, status_code=200, media_type="application/json"
|
|
873
|
+
request=request,
|
|
874
|
+
search_request=search_request,
|
|
736
875
|
)
|
|
737
|
-
return
|
|
876
|
+
return ORJSONResponse(content=response, media_type="application/json")
|
|
738
877
|
|
|
739
878
|
|
|
740
879
|
app.include_router(router)
|