eodag 3.0.0b3__py3-none-any.whl → 3.1.0__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 +347 -247
- eodag/api/product/_assets.py +44 -15
- eodag/api/product/_product.py +58 -47
- eodag/api/product/drivers/__init__.py +81 -4
- eodag/api/product/drivers/base.py +65 -4
- eodag/api/product/drivers/generic.py +65 -0
- eodag/api/product/drivers/sentinel1.py +97 -0
- eodag/api/product/drivers/sentinel2.py +95 -0
- eodag/api/product/metadata_mapping.py +129 -93
- eodag/api/search_result.py +28 -12
- eodag/cli.py +61 -24
- eodag/config.py +457 -167
- eodag/plugins/apis/base.py +10 -4
- eodag/plugins/apis/ecmwf.py +53 -23
- eodag/plugins/apis/usgs.py +41 -17
- eodag/plugins/authentication/aws_auth.py +30 -18
- eodag/plugins/authentication/base.py +14 -3
- eodag/plugins/authentication/generic.py +14 -3
- eodag/plugins/authentication/header.py +14 -6
- eodag/plugins/authentication/keycloak.py +44 -25
- eodag/plugins/authentication/oauth.py +18 -4
- eodag/plugins/authentication/openid_connect.py +192 -171
- eodag/plugins/authentication/qsauth.py +12 -4
- eodag/plugins/authentication/sas_auth.py +22 -5
- eodag/plugins/authentication/token.py +95 -17
- eodag/plugins/authentication/token_exchange.py +19 -19
- eodag/plugins/base.py +4 -4
- eodag/plugins/crunch/base.py +8 -5
- eodag/plugins/crunch/filter_date.py +9 -6
- eodag/plugins/crunch/filter_latest_intersect.py +9 -8
- eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
- eodag/plugins/crunch/filter_overlap.py +9 -11
- eodag/plugins/crunch/filter_property.py +10 -10
- eodag/plugins/download/aws.py +181 -105
- eodag/plugins/download/base.py +49 -67
- eodag/plugins/download/creodias_s3.py +40 -2
- eodag/plugins/download/http.py +247 -223
- eodag/plugins/download/s3rest.py +29 -28
- eodag/plugins/manager.py +176 -41
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +123 -60
- eodag/plugins/search/build_search_result.py +1046 -355
- eodag/plugins/search/cop_marine.py +132 -39
- eodag/plugins/search/creodias_s3.py +19 -68
- eodag/plugins/search/csw.py +48 -8
- eodag/plugins/search/data_request_search.py +124 -23
- eodag/plugins/search/qssearch.py +531 -310
- eodag/plugins/search/stac_list_assets.py +85 -0
- eodag/plugins/search/static_stac_search.py +23 -24
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1295 -355
- eodag/resources/providers.yml +1819 -3010
- eodag/resources/stac.yml +3 -163
- eodag/resources/stac_api.yml +2 -2
- eodag/resources/user_conf_template.yml +115 -99
- eodag/rest/cache.py +2 -2
- eodag/rest/config.py +3 -4
- eodag/rest/constants.py +0 -1
- eodag/rest/core.py +157 -117
- eodag/rest/errors.py +181 -0
- eodag/rest/server.py +57 -339
- eodag/rest/stac.py +133 -581
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +41 -30
- eodag/rest/types/queryables.py +42 -32
- eodag/rest/types/stac_search.py +15 -16
- eodag/rest/utils/__init__.py +14 -21
- eodag/rest/utils/cql_evaluate.py +6 -6
- eodag/rest/utils/rfc3339.py +2 -2
- eodag/types/__init__.py +153 -32
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +4 -4
- eodag/types/queryables.py +183 -73
- eodag/types/search_args.py +6 -6
- eodag/types/whoosh.py +127 -3
- eodag/utils/__init__.py +228 -106
- eodag/utils/exceptions.py +47 -26
- eodag/utils/import_system.py +2 -2
- eodag/utils/logging.py +37 -77
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +13 -15
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +231 -0
- eodag/utils/stac_reader.py +11 -11
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
- eodag-3.1.0.dist-info/RECORD +113 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
- eodag/resources/constraints/climate-dt.json +0 -13
- eodag/resources/constraints/extremes-dt.json +0 -8
- eodag/utils/constraints.py +0 -244
- eodag-3.0.0b3.dist-info/RECORD +0 -110
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
eodag/rest/server.py
CHANGED
|
@@ -20,29 +20,20 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
22
|
import re
|
|
23
|
-
import traceback
|
|
24
23
|
from contextlib import asynccontextmanager
|
|
25
24
|
from importlib.metadata import version
|
|
26
25
|
from json import JSONDecodeError
|
|
27
|
-
from typing import
|
|
28
|
-
TYPE_CHECKING,
|
|
29
|
-
Any,
|
|
30
|
-
AsyncGenerator,
|
|
31
|
-
Awaitable,
|
|
32
|
-
Callable,
|
|
33
|
-
Dict,
|
|
34
|
-
Optional,
|
|
35
|
-
)
|
|
26
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Optional
|
|
36
27
|
|
|
37
28
|
from fastapi import APIRouter as FastAPIRouter
|
|
38
29
|
from fastapi import FastAPI, HTTPException, Request
|
|
30
|
+
from fastapi.concurrency import run_in_threadpool
|
|
39
31
|
from fastapi.middleware.cors import CORSMiddleware
|
|
40
32
|
from fastapi.openapi.utils import get_openapi
|
|
41
33
|
from fastapi.responses import ORJSONResponse
|
|
42
34
|
from pydantic import ValidationError as pydanticValidationError
|
|
43
35
|
from pygeofilter.backends.cql2_json import to_cql2
|
|
44
36
|
from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
|
|
45
|
-
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
46
37
|
|
|
47
38
|
from eodag.config import load_stac_api_config
|
|
48
39
|
from eodag.rest.cache import init_cache
|
|
@@ -59,23 +50,16 @@ from eodag.rest.core import (
|
|
|
59
50
|
get_stac_extension_oseo,
|
|
60
51
|
search_stac_items,
|
|
61
52
|
)
|
|
62
|
-
from eodag.rest.
|
|
53
|
+
from eodag.rest.errors import add_exception_handlers
|
|
63
54
|
from eodag.rest.types.queryables import QueryablesGetParams
|
|
64
55
|
from eodag.rest.types.stac_search import SearchPostRequest, sortby2list
|
|
65
|
-
from eodag.rest.utils import
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
MisconfiguredError,
|
|
71
|
-
NoMatchingProductType,
|
|
72
|
-
NotAvailableError,
|
|
73
|
-
RequestError,
|
|
74
|
-
TimeOutError,
|
|
75
|
-
UnsupportedProductType,
|
|
76
|
-
UnsupportedProvider,
|
|
77
|
-
ValidationError,
|
|
56
|
+
from eodag.rest.utils import (
|
|
57
|
+
LIVENESS_PROBE_PATH,
|
|
58
|
+
format_pydantic_error,
|
|
59
|
+
str2json,
|
|
60
|
+
str2list,
|
|
78
61
|
)
|
|
62
|
+
from eodag.utils import parse_header, update_nested_dict
|
|
79
63
|
|
|
80
64
|
if TYPE_CHECKING:
|
|
81
65
|
from fastapi.types import DecoratedCallable
|
|
@@ -84,12 +68,6 @@ if TYPE_CHECKING:
|
|
|
84
68
|
from starlette.responses import Response as StarletteResponse
|
|
85
69
|
|
|
86
70
|
logger = logging.getLogger("eodag.rest.server")
|
|
87
|
-
ERRORS_WITH_500_STATUS_CODE = {
|
|
88
|
-
"MisconfiguredError",
|
|
89
|
-
"AuthenticationError",
|
|
90
|
-
"DownloadError",
|
|
91
|
-
"RequestError",
|
|
92
|
-
}
|
|
93
71
|
|
|
94
72
|
|
|
95
73
|
class APIRouter(FastAPIRouter):
|
|
@@ -139,10 +117,21 @@ app = FastAPI(lifespan=lifespan, title="EODAG", docs_url="/api.html")
|
|
|
139
117
|
stac_api_config = load_stac_api_config()
|
|
140
118
|
|
|
141
119
|
|
|
120
|
+
@router.api_route(
|
|
121
|
+
methods=["GET", "HEAD"],
|
|
122
|
+
path=LIVENESS_PROBE_PATH,
|
|
123
|
+
include_in_schema=False,
|
|
124
|
+
status_code=200,
|
|
125
|
+
)
|
|
126
|
+
async def liveness_probe(request: Request) -> dict[str, bool]:
|
|
127
|
+
"Endpoint meant to be used as liveness probe by deployment platforms"
|
|
128
|
+
return {"success": True}
|
|
129
|
+
|
|
130
|
+
|
|
142
131
|
@router.api_route(
|
|
143
132
|
methods=["GET", "HEAD"], path="/api", tags=["Capabilities"], include_in_schema=False
|
|
144
133
|
)
|
|
145
|
-
async def eodag_openapi(request: Request) ->
|
|
134
|
+
async def eodag_openapi(request: Request) -> dict[str, Any]:
|
|
146
135
|
"""Customized openapi"""
|
|
147
136
|
logger.debug("URL: /api")
|
|
148
137
|
if app.openapi_schema:
|
|
@@ -197,6 +186,8 @@ app.add_middleware(
|
|
|
197
186
|
allow_headers=["*"],
|
|
198
187
|
)
|
|
199
188
|
|
|
189
|
+
add_exception_handlers(app)
|
|
190
|
+
|
|
200
191
|
|
|
201
192
|
@app.middleware("http")
|
|
202
193
|
async def forward_middleware(
|
|
@@ -221,141 +212,10 @@ async def forward_middleware(
|
|
|
221
212
|
return response
|
|
222
213
|
|
|
223
214
|
|
|
224
|
-
@app.exception_handler(StarletteHTTPException)
|
|
225
|
-
async def default_exception_handler(
|
|
226
|
-
request: Request, error: HTTPException
|
|
227
|
-
) -> ORJSONResponse:
|
|
228
|
-
"""Default errors handle"""
|
|
229
|
-
description = (
|
|
230
|
-
getattr(error, "description", None)
|
|
231
|
-
or getattr(error, "detail", None)
|
|
232
|
-
or str(error)
|
|
233
|
-
)
|
|
234
|
-
return ORJSONResponse(
|
|
235
|
-
status_code=error.status_code,
|
|
236
|
-
content={"description": description},
|
|
237
|
-
)
|
|
238
|
-
|
|
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
|
-
|
|
259
|
-
@app.exception_handler(NoMatchingProductType)
|
|
260
|
-
@app.exception_handler(UnsupportedProductType)
|
|
261
|
-
@app.exception_handler(UnsupportedProvider)
|
|
262
|
-
async def handle_invalid_usage(request: Request, error: Exception) -> ORJSONResponse:
|
|
263
|
-
"""Invalid usage [400] errors handle"""
|
|
264
|
-
return await default_exception_handler(
|
|
265
|
-
request,
|
|
266
|
-
HTTPException(
|
|
267
|
-
status_code=400,
|
|
268
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
269
|
-
),
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
@app.exception_handler(NotAvailableError)
|
|
274
|
-
async def handle_resource_not_found(
|
|
275
|
-
request: Request, error: Exception
|
|
276
|
-
) -> ORJSONResponse:
|
|
277
|
-
"""Not found [404] errors handle"""
|
|
278
|
-
return await default_exception_handler(
|
|
279
|
-
request,
|
|
280
|
-
HTTPException(
|
|
281
|
-
status_code=404,
|
|
282
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
283
|
-
),
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@app.exception_handler(MisconfiguredError)
|
|
288
|
-
@app.exception_handler(AuthenticationError)
|
|
289
|
-
async def handle_auth_error(request: Request, error: Exception) -> ORJSONResponse:
|
|
290
|
-
"""These errors should be sent as internal server error to the client"""
|
|
291
|
-
logger.error("%s: %s", type(error).__name__, str(error))
|
|
292
|
-
return await default_exception_handler(
|
|
293
|
-
request,
|
|
294
|
-
HTTPException(
|
|
295
|
-
status_code=500,
|
|
296
|
-
detail="Internal server error: please contact the administrator",
|
|
297
|
-
),
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
314
|
-
@app.exception_handler(RequestError)
|
|
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)
|
|
332
|
-
logger.error(f"{type(error).__name__}: {str(error)}")
|
|
333
|
-
return await default_exception_handler(
|
|
334
|
-
request,
|
|
335
|
-
HTTPException(
|
|
336
|
-
status_code=500,
|
|
337
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
338
|
-
),
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
@app.exception_handler(TimeOutError)
|
|
343
|
-
async def handle_timeout(request: Request, error: Exception) -> ORJSONResponse:
|
|
344
|
-
"""Timeout [504] errors handle"""
|
|
345
|
-
logger.error(f"{type(error).__name__}: {str(error)}")
|
|
346
|
-
return await default_exception_handler(
|
|
347
|
-
request,
|
|
348
|
-
HTTPException(
|
|
349
|
-
status_code=504,
|
|
350
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
351
|
-
),
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
355
215
|
@router.api_route(methods=["GET", "HEAD"], path="/", tags=["Capabilities"])
|
|
356
216
|
async def catalogs_root(request: Request) -> ORJSONResponse:
|
|
357
217
|
"""STAC catalogs root"""
|
|
358
|
-
logger.
|
|
218
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
359
219
|
|
|
360
220
|
response = await get_stac_catalogs(
|
|
361
221
|
request=request,
|
|
@@ -367,9 +227,9 @@ async def catalogs_root(request: Request) -> ORJSONResponse:
|
|
|
367
227
|
|
|
368
228
|
|
|
369
229
|
@router.api_route(methods=["GET", "HEAD"], path="/conformance", tags=["Capabilities"])
|
|
370
|
-
def conformance() -> ORJSONResponse:
|
|
230
|
+
def conformance(request: Request) -> ORJSONResponse:
|
|
371
231
|
"""STAC conformance"""
|
|
372
|
-
logger.
|
|
232
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
373
233
|
response = get_stac_conformance()
|
|
374
234
|
|
|
375
235
|
return ORJSONResponse(response)
|
|
@@ -382,7 +242,7 @@ def conformance() -> ORJSONResponse:
|
|
|
382
242
|
)
|
|
383
243
|
def stac_extension_oseo(request: Request) -> ORJSONResponse:
|
|
384
244
|
"""STAC OGC / OpenSearch extension for EO"""
|
|
385
|
-
logger.
|
|
245
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
386
246
|
response = get_stac_extension_oseo(url=request.state.url)
|
|
387
247
|
|
|
388
248
|
return ORJSONResponse(response)
|
|
@@ -398,14 +258,14 @@ def stac_collections_item_download(
|
|
|
398
258
|
collection_id: str, item_id: str, request: Request
|
|
399
259
|
) -> StarletteResponse:
|
|
400
260
|
"""STAC collection item download"""
|
|
401
|
-
logger.
|
|
261
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
402
262
|
|
|
403
263
|
arguments = dict(request.query_params)
|
|
404
264
|
provider = arguments.pop("provider", None)
|
|
405
265
|
|
|
406
266
|
return download_stac_item(
|
|
407
267
|
request=request,
|
|
408
|
-
|
|
268
|
+
collection_id=collection_id,
|
|
409
269
|
item_id=item_id,
|
|
410
270
|
provider=provider,
|
|
411
271
|
**arguments,
|
|
@@ -422,14 +282,14 @@ def stac_collections_item_download_asset(
|
|
|
422
282
|
collection_id: str, item_id: str, asset: str, request: Request
|
|
423
283
|
):
|
|
424
284
|
"""STAC collection item asset download"""
|
|
425
|
-
logger.
|
|
285
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
426
286
|
|
|
427
287
|
arguments = dict(request.query_params)
|
|
428
288
|
provider = arguments.pop("provider", None)
|
|
429
289
|
|
|
430
290
|
return download_stac_item(
|
|
431
291
|
request=request,
|
|
432
|
-
|
|
292
|
+
collection_id=collection_id,
|
|
433
293
|
item_id=item_id,
|
|
434
294
|
provider=provider,
|
|
435
295
|
asset=asset,
|
|
@@ -446,7 +306,7 @@ def stac_collections_item(
|
|
|
446
306
|
collection_id: str, item_id: str, request: Request, provider: Optional[str] = None
|
|
447
307
|
) -> ORJSONResponse:
|
|
448
308
|
"""STAC collection item by id"""
|
|
449
|
-
logger.
|
|
309
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
450
310
|
|
|
451
311
|
search_request = SearchPostRequest(
|
|
452
312
|
provider=provider, ids=[item_id], collections=[collection_id], limit=1
|
|
@@ -522,14 +382,25 @@ async def list_collection_queryables(
|
|
|
522
382
|
:param collection_id: The identifier of the collection for which to retrieve queryable properties.
|
|
523
383
|
:returns: A json object containing the list of available queryable properties for the specified collection.
|
|
524
384
|
"""
|
|
525
|
-
logger.
|
|
526
|
-
|
|
385
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
386
|
+
# split by `,` to handle list of parameters
|
|
387
|
+
additional_params = {k: v.split(",") for k, v in dict(request.query_params).items()}
|
|
527
388
|
provider = additional_params.pop("provider", None)
|
|
528
389
|
|
|
390
|
+
datetime = additional_params.pop("datetime", None)
|
|
391
|
+
|
|
529
392
|
queryables = await get_queryables(
|
|
530
393
|
request,
|
|
531
|
-
QueryablesGetParams(
|
|
532
|
-
|
|
394
|
+
QueryablesGetParams.model_validate(
|
|
395
|
+
{
|
|
396
|
+
**additional_params,
|
|
397
|
+
**{
|
|
398
|
+
"collection": collection_id,
|
|
399
|
+
"datetime": datetime[0] if datetime else None,
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
),
|
|
403
|
+
provider=provider[0] if provider else None,
|
|
533
404
|
)
|
|
534
405
|
|
|
535
406
|
return ORJSONResponse(queryables)
|
|
@@ -545,7 +416,7 @@ async def collection_by_id(
|
|
|
545
416
|
collection_id: str, request: Request, provider: Optional[str] = None
|
|
546
417
|
) -> ORJSONResponse:
|
|
547
418
|
"""STAC collection by id"""
|
|
548
|
-
logger.
|
|
419
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
549
420
|
|
|
550
421
|
response = await get_collection(
|
|
551
422
|
request=request,
|
|
@@ -576,7 +447,7 @@ async def collections(
|
|
|
576
447
|
Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
|
|
577
448
|
processingLevel
|
|
578
449
|
"""
|
|
579
|
-
logger.
|
|
450
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
580
451
|
|
|
581
452
|
collections = await all_collections(
|
|
582
453
|
request, provider, q, platform, instrument, constellation, datetime
|
|
@@ -584,161 +455,6 @@ async def collections(
|
|
|
584
455
|
return ORJSONResponse(collections)
|
|
585
456
|
|
|
586
457
|
|
|
587
|
-
@router.api_route(
|
|
588
|
-
methods=["GET", "HEAD"],
|
|
589
|
-
path="/catalogs/{catalogs:path}/items/{item_id}/download",
|
|
590
|
-
tags=["Data"],
|
|
591
|
-
include_in_schema=False,
|
|
592
|
-
)
|
|
593
|
-
def stac_catalogs_item_download(
|
|
594
|
-
catalogs: str, item_id: str, request: Request
|
|
595
|
-
) -> StarletteResponse:
|
|
596
|
-
"""STAC Catalog item download"""
|
|
597
|
-
logger.debug("URL: %s", request.url)
|
|
598
|
-
|
|
599
|
-
arguments = dict(request.query_params)
|
|
600
|
-
provider = arguments.pop("provider", None)
|
|
601
|
-
|
|
602
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
603
|
-
|
|
604
|
-
return download_stac_item(
|
|
605
|
-
request=request,
|
|
606
|
-
catalogs=list_catalog,
|
|
607
|
-
item_id=item_id,
|
|
608
|
-
provider=provider,
|
|
609
|
-
**arguments,
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
@router.api_route(
|
|
614
|
-
methods=["GET", "HEAD"],
|
|
615
|
-
path="/catalogs/{catalogs:path}/items/{item_id}/download/{asset_filter}",
|
|
616
|
-
tags=["Data"],
|
|
617
|
-
include_in_schema=False,
|
|
618
|
-
)
|
|
619
|
-
def stac_catalogs_item_download_asset(
|
|
620
|
-
catalogs: str, item_id: str, asset_filter: str, request: Request
|
|
621
|
-
):
|
|
622
|
-
"""STAC Catalog item asset download"""
|
|
623
|
-
logger.debug("URL: %s", request.url)
|
|
624
|
-
|
|
625
|
-
arguments = dict(request.query_params)
|
|
626
|
-
provider = arguments.pop("provider", None)
|
|
627
|
-
|
|
628
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
629
|
-
|
|
630
|
-
return download_stac_item(
|
|
631
|
-
request,
|
|
632
|
-
catalogs=list_catalog,
|
|
633
|
-
item_id=item_id,
|
|
634
|
-
provider=provider,
|
|
635
|
-
asset=asset_filter,
|
|
636
|
-
**arguments,
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
@router.api_route(
|
|
641
|
-
methods=["GET", "HEAD"],
|
|
642
|
-
path="/catalogs/{catalogs:path}/items/{item_id}",
|
|
643
|
-
tags=["Data"],
|
|
644
|
-
include_in_schema=False,
|
|
645
|
-
)
|
|
646
|
-
def stac_catalogs_item(
|
|
647
|
-
catalogs: str, item_id: str, request: Request, provider: Optional[str] = None
|
|
648
|
-
):
|
|
649
|
-
"""Fetch catalog's single features."""
|
|
650
|
-
logger.debug("URL: %s", request.url)
|
|
651
|
-
|
|
652
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
653
|
-
|
|
654
|
-
search_request = SearchPostRequest(provider=provider, ids=[item_id], limit=1)
|
|
655
|
-
|
|
656
|
-
item_collection = search_stac_items(request, search_request, catalogs=list_catalog)
|
|
657
|
-
|
|
658
|
-
if not item_collection["features"]:
|
|
659
|
-
raise HTTPException(
|
|
660
|
-
status_code=404,
|
|
661
|
-
detail=f"Item {item_id} in Catalog {catalogs} does not exist.",
|
|
662
|
-
)
|
|
663
|
-
|
|
664
|
-
return ORJSONResponse(item_collection["features"][0])
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
@router.api_route(
|
|
668
|
-
methods=["GET", "HEAD"],
|
|
669
|
-
path="/catalogs/{catalogs:path}/items",
|
|
670
|
-
tags=["Data"],
|
|
671
|
-
include_in_schema=False,
|
|
672
|
-
)
|
|
673
|
-
def stac_catalogs_items(
|
|
674
|
-
catalogs: str,
|
|
675
|
-
request: Request,
|
|
676
|
-
provider: Optional[str] = None,
|
|
677
|
-
bbox: Optional[str] = None,
|
|
678
|
-
datetime: Optional[str] = None,
|
|
679
|
-
limit: Optional[int] = None,
|
|
680
|
-
page: Optional[int] = None,
|
|
681
|
-
sortby: Optional[str] = None,
|
|
682
|
-
crunch: Optional[str] = None,
|
|
683
|
-
) -> ORJSONResponse:
|
|
684
|
-
"""Fetch catalog's features"""
|
|
685
|
-
logger.debug("URL: %s", request.state.url)
|
|
686
|
-
|
|
687
|
-
base_args = {
|
|
688
|
-
"provider": provider,
|
|
689
|
-
"datetime": datetime,
|
|
690
|
-
"bbox": str2list(bbox),
|
|
691
|
-
"limit": limit,
|
|
692
|
-
"page": page,
|
|
693
|
-
"sortby": sortby2list(sortby),
|
|
694
|
-
"crunch": crunch,
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
clean = {k: v for k, v in base_args.items() if v is not None and v != []}
|
|
698
|
-
|
|
699
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
700
|
-
|
|
701
|
-
try:
|
|
702
|
-
search_request = SearchPostRequest.model_validate(clean)
|
|
703
|
-
except pydanticValidationError as e:
|
|
704
|
-
raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
|
|
705
|
-
|
|
706
|
-
response = search_stac_items(
|
|
707
|
-
request=request,
|
|
708
|
-
search_request=search_request,
|
|
709
|
-
catalogs=list_catalog,
|
|
710
|
-
)
|
|
711
|
-
return ORJSONResponse(response)
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
@router.api_route(
|
|
715
|
-
methods=["GET", "HEAD"],
|
|
716
|
-
path="/catalogs/{catalogs:path}",
|
|
717
|
-
tags=["Capabilities"],
|
|
718
|
-
include_in_schema=False,
|
|
719
|
-
)
|
|
720
|
-
async def stac_catalogs(
|
|
721
|
-
catalogs: str, request: Request, provider: Optional[str] = None
|
|
722
|
-
) -> ORJSONResponse:
|
|
723
|
-
"""Describe the given catalog and list available sub-catalogs"""
|
|
724
|
-
logger.debug("URL: %s", request.url)
|
|
725
|
-
|
|
726
|
-
if not catalogs:
|
|
727
|
-
raise HTTPException(
|
|
728
|
-
status_code=404,
|
|
729
|
-
detail="Not found",
|
|
730
|
-
)
|
|
731
|
-
|
|
732
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
733
|
-
response = await get_stac_catalogs(
|
|
734
|
-
request=request,
|
|
735
|
-
url=request.state.url,
|
|
736
|
-
catalogs=tuple(list_catalog),
|
|
737
|
-
provider=provider,
|
|
738
|
-
)
|
|
739
|
-
return ORJSONResponse(response)
|
|
740
|
-
|
|
741
|
-
|
|
742
458
|
@router.api_route(
|
|
743
459
|
methods=["GET", "HEAD"],
|
|
744
460
|
path="/queryables",
|
|
@@ -756,7 +472,7 @@ async def list_queryables(request: Request) -> ORJSONResponse:
|
|
|
756
472
|
:param request: The incoming request object.
|
|
757
473
|
:returns: A json object containing the list of available queryable terms.
|
|
758
474
|
"""
|
|
759
|
-
logger.
|
|
475
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
760
476
|
additional_params = dict(request.query_params.items())
|
|
761
477
|
provider = additional_params.pop("provider", None)
|
|
762
478
|
queryables = await get_queryables(
|
|
@@ -789,7 +505,7 @@ def get_search(
|
|
|
789
505
|
crunch: Optional[str] = None,
|
|
790
506
|
) -> ORJSONResponse:
|
|
791
507
|
"""Handler for GET /search"""
|
|
792
|
-
logger.
|
|
508
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
793
509
|
|
|
794
510
|
query_params = str(request.query_params)
|
|
795
511
|
|
|
@@ -843,7 +559,7 @@ def get_search(
|
|
|
843
559
|
)
|
|
844
560
|
async def post_search(request: Request) -> ORJSONResponse:
|
|
845
561
|
"""STAC post search"""
|
|
846
|
-
logger.
|
|
562
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
847
563
|
|
|
848
564
|
content_type = request.headers.get("Content-Type")
|
|
849
565
|
|
|
@@ -864,10 +580,12 @@ async def post_search(request: Request) -> ORJSONResponse:
|
|
|
864
580
|
|
|
865
581
|
logger.debug("Body: %s", search_request.model_dump(exclude_none=True))
|
|
866
582
|
|
|
867
|
-
response =
|
|
868
|
-
|
|
869
|
-
|
|
583
|
+
response = await run_in_threadpool(
|
|
584
|
+
search_stac_items,
|
|
585
|
+
request,
|
|
586
|
+
search_request,
|
|
870
587
|
)
|
|
588
|
+
|
|
871
589
|
return ORJSONResponse(content=response, media_type="application/json")
|
|
872
590
|
|
|
873
591
|
|