eodag 3.0.0b3__py3-none-any.whl → 3.0.1__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.
Files changed (71) hide show
  1. eodag/api/core.py +189 -125
  2. eodag/api/product/metadata_mapping.py +12 -3
  3. eodag/api/search_result.py +29 -3
  4. eodag/cli.py +35 -19
  5. eodag/config.py +412 -116
  6. eodag/plugins/apis/base.py +10 -4
  7. eodag/plugins/apis/ecmwf.py +14 -4
  8. eodag/plugins/apis/usgs.py +25 -2
  9. eodag/plugins/authentication/aws_auth.py +14 -5
  10. eodag/plugins/authentication/base.py +10 -1
  11. eodag/plugins/authentication/generic.py +14 -3
  12. eodag/plugins/authentication/header.py +12 -4
  13. eodag/plugins/authentication/keycloak.py +41 -22
  14. eodag/plugins/authentication/oauth.py +11 -1
  15. eodag/plugins/authentication/openid_connect.py +178 -163
  16. eodag/plugins/authentication/qsauth.py +12 -4
  17. eodag/plugins/authentication/sas_auth.py +19 -2
  18. eodag/plugins/authentication/token.py +57 -10
  19. eodag/plugins/authentication/token_exchange.py +19 -19
  20. eodag/plugins/crunch/base.py +4 -1
  21. eodag/plugins/crunch/filter_date.py +5 -2
  22. eodag/plugins/crunch/filter_latest_intersect.py +5 -4
  23. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  24. eodag/plugins/crunch/filter_overlap.py +5 -7
  25. eodag/plugins/crunch/filter_property.py +4 -3
  26. eodag/plugins/download/aws.py +39 -22
  27. eodag/plugins/download/base.py +11 -11
  28. eodag/plugins/download/creodias_s3.py +11 -2
  29. eodag/plugins/download/http.py +86 -52
  30. eodag/plugins/download/s3rest.py +20 -18
  31. eodag/plugins/manager.py +168 -23
  32. eodag/plugins/search/base.py +33 -14
  33. eodag/plugins/search/build_search_result.py +55 -51
  34. eodag/plugins/search/cop_marine.py +112 -29
  35. eodag/plugins/search/creodias_s3.py +20 -5
  36. eodag/plugins/search/csw.py +41 -1
  37. eodag/plugins/search/data_request_search.py +109 -9
  38. eodag/plugins/search/qssearch.py +532 -152
  39. eodag/plugins/search/static_stac_search.py +20 -21
  40. eodag/resources/ext_product_types.json +1 -1
  41. eodag/resources/product_types.yml +187 -56
  42. eodag/resources/providers.yml +1610 -1701
  43. eodag/resources/stac.yml +3 -163
  44. eodag/resources/user_conf_template.yml +112 -97
  45. eodag/rest/config.py +1 -2
  46. eodag/rest/constants.py +0 -1
  47. eodag/rest/core.py +61 -51
  48. eodag/rest/errors.py +181 -0
  49. eodag/rest/server.py +24 -325
  50. eodag/rest/stac.py +93 -544
  51. eodag/rest/types/eodag_search.py +13 -8
  52. eodag/rest/types/queryables.py +1 -2
  53. eodag/rest/types/stac_search.py +11 -2
  54. eodag/types/__init__.py +15 -3
  55. eodag/types/download_args.py +1 -1
  56. eodag/types/queryables.py +1 -2
  57. eodag/types/search_args.py +3 -3
  58. eodag/utils/__init__.py +77 -57
  59. eodag/utils/exceptions.py +23 -9
  60. eodag/utils/logging.py +37 -77
  61. eodag/utils/requests.py +1 -3
  62. eodag/utils/stac_reader.py +1 -1
  63. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/METADATA +11 -12
  64. eodag-3.0.1.dist-info/RECORD +109 -0
  65. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/WHEEL +1 -1
  66. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/entry_points.txt +1 -0
  67. eodag/resources/constraints/climate-dt.json +0 -13
  68. eodag/resources/constraints/extremes-dt.json +0 -8
  69. eodag-3.0.0b3.dist-info/RECORD +0 -110
  70. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/LICENSE +0 -0
  71. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/top_level.txt +0 -0
eodag/rest/server.py CHANGED
@@ -20,7 +20,6 @@ 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
@@ -36,13 +35,13 @@ from typing import (
36
35
 
37
36
  from fastapi import APIRouter as FastAPIRouter
38
37
  from fastapi import FastAPI, HTTPException, Request
38
+ from fastapi.concurrency import run_in_threadpool
39
39
  from fastapi.middleware.cors import CORSMiddleware
40
40
  from fastapi.openapi.utils import get_openapi
41
41
  from fastapi.responses import ORJSONResponse
42
42
  from pydantic import ValidationError as pydanticValidationError
43
43
  from pygeofilter.backends.cql2_json import to_cql2
44
44
  from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
45
- from starlette.exceptions import HTTPException as StarletteHTTPException
46
45
 
47
46
  from eodag.config import load_stac_api_config
48
47
  from eodag.rest.cache import init_cache
@@ -59,23 +58,11 @@ from eodag.rest.core import (
59
58
  get_stac_extension_oseo,
60
59
  search_stac_items,
61
60
  )
62
- from eodag.rest.types.eodag_search import EODAGSearch
61
+ from eodag.rest.errors import add_exception_handlers
63
62
  from eodag.rest.types.queryables import QueryablesGetParams
64
63
  from eodag.rest.types.stac_search import SearchPostRequest, sortby2list
65
64
  from eodag.rest.utils import format_pydantic_error, str2json, str2list
66
65
  from eodag.utils import parse_header, update_nested_dict
67
- from eodag.utils.exceptions import (
68
- AuthenticationError,
69
- DownloadError,
70
- MisconfiguredError,
71
- NoMatchingProductType,
72
- NotAvailableError,
73
- RequestError,
74
- TimeOutError,
75
- UnsupportedProductType,
76
- UnsupportedProvider,
77
- ValidationError,
78
- )
79
66
 
80
67
  if TYPE_CHECKING:
81
68
  from fastapi.types import DecoratedCallable
@@ -84,12 +71,6 @@ if TYPE_CHECKING:
84
71
  from starlette.responses import Response as StarletteResponse
85
72
 
86
73
  logger = logging.getLogger("eodag.rest.server")
87
- ERRORS_WITH_500_STATUS_CODE = {
88
- "MisconfiguredError",
89
- "AuthenticationError",
90
- "DownloadError",
91
- "RequestError",
92
- }
93
74
 
94
75
 
95
76
  class APIRouter(FastAPIRouter):
@@ -197,6 +178,8 @@ app.add_middleware(
197
178
  allow_headers=["*"],
198
179
  )
199
180
 
181
+ add_exception_handlers(app)
182
+
200
183
 
201
184
  @app.middleware("http")
202
185
  async def forward_middleware(
@@ -221,141 +204,10 @@ async def forward_middleware(
221
204
  return response
222
205
 
223
206
 
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
207
  @router.api_route(methods=["GET", "HEAD"], path="/", tags=["Capabilities"])
356
208
  async def catalogs_root(request: Request) -> ORJSONResponse:
357
209
  """STAC catalogs root"""
358
- logger.debug("URL: %s", request.url)
210
+ logger.info(f"{request.method} {request.state.url}")
359
211
 
360
212
  response = await get_stac_catalogs(
361
213
  request=request,
@@ -367,9 +219,9 @@ async def catalogs_root(request: Request) -> ORJSONResponse:
367
219
 
368
220
 
369
221
  @router.api_route(methods=["GET", "HEAD"], path="/conformance", tags=["Capabilities"])
370
- def conformance() -> ORJSONResponse:
222
+ def conformance(request: Request) -> ORJSONResponse:
371
223
  """STAC conformance"""
372
- logger.debug("URL: /conformance")
224
+ logger.info(f"{request.method} {request.state.url}")
373
225
  response = get_stac_conformance()
374
226
 
375
227
  return ORJSONResponse(response)
@@ -382,7 +234,7 @@ def conformance() -> ORJSONResponse:
382
234
  )
383
235
  def stac_extension_oseo(request: Request) -> ORJSONResponse:
384
236
  """STAC OGC / OpenSearch extension for EO"""
385
- logger.debug("URL: %s", request.url)
237
+ logger.info(f"{request.method} {request.state.url}")
386
238
  response = get_stac_extension_oseo(url=request.state.url)
387
239
 
388
240
  return ORJSONResponse(response)
@@ -398,14 +250,14 @@ def stac_collections_item_download(
398
250
  collection_id: str, item_id: str, request: Request
399
251
  ) -> StarletteResponse:
400
252
  """STAC collection item download"""
401
- logger.debug("URL: %s", request.url)
253
+ logger.info(f"{request.method} {request.state.url}")
402
254
 
403
255
  arguments = dict(request.query_params)
404
256
  provider = arguments.pop("provider", None)
405
257
 
406
258
  return download_stac_item(
407
259
  request=request,
408
- catalogs=[collection_id],
260
+ collection_id=collection_id,
409
261
  item_id=item_id,
410
262
  provider=provider,
411
263
  **arguments,
@@ -422,14 +274,14 @@ def stac_collections_item_download_asset(
422
274
  collection_id: str, item_id: str, asset: str, request: Request
423
275
  ):
424
276
  """STAC collection item asset download"""
425
- logger.debug("URL: %s", request.url)
277
+ logger.info(f"{request.method} {request.state.url}")
426
278
 
427
279
  arguments = dict(request.query_params)
428
280
  provider = arguments.pop("provider", None)
429
281
 
430
282
  return download_stac_item(
431
283
  request=request,
432
- catalogs=[collection_id],
284
+ collection_id=collection_id,
433
285
  item_id=item_id,
434
286
  provider=provider,
435
287
  asset=asset,
@@ -446,7 +298,7 @@ def stac_collections_item(
446
298
  collection_id: str, item_id: str, request: Request, provider: Optional[str] = None
447
299
  ) -> ORJSONResponse:
448
300
  """STAC collection item by id"""
449
- logger.debug("URL: %s", request.url)
301
+ logger.info(f"{request.method} {request.state.url}")
450
302
 
451
303
  search_request = SearchPostRequest(
452
304
  provider=provider, ids=[item_id], collections=[collection_id], limit=1
@@ -522,7 +374,7 @@ async def list_collection_queryables(
522
374
  :param collection_id: The identifier of the collection for which to retrieve queryable properties.
523
375
  :returns: A json object containing the list of available queryable properties for the specified collection.
524
376
  """
525
- logger.debug(f"URL: {request.url}")
377
+ logger.info(f"{request.method} {request.state.url}")
526
378
  additional_params = dict(request.query_params)
527
379
  provider = additional_params.pop("provider", None)
528
380
 
@@ -545,7 +397,7 @@ async def collection_by_id(
545
397
  collection_id: str, request: Request, provider: Optional[str] = None
546
398
  ) -> ORJSONResponse:
547
399
  """STAC collection by id"""
548
- logger.debug("URL: %s", request.url)
400
+ logger.info(f"{request.method} {request.state.url}")
549
401
 
550
402
  response = await get_collection(
551
403
  request=request,
@@ -576,7 +428,7 @@ async def collections(
576
428
  Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
577
429
  processingLevel
578
430
  """
579
- logger.debug("URL: %s", request.url)
431
+ logger.info(f"{request.method} {request.state.url}")
580
432
 
581
433
  collections = await all_collections(
582
434
  request, provider, q, platform, instrument, constellation, datetime
@@ -584,161 +436,6 @@ async def collections(
584
436
  return ORJSONResponse(collections)
585
437
 
586
438
 
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
439
  @router.api_route(
743
440
  methods=["GET", "HEAD"],
744
441
  path="/queryables",
@@ -756,7 +453,7 @@ async def list_queryables(request: Request) -> ORJSONResponse:
756
453
  :param request: The incoming request object.
757
454
  :returns: A json object containing the list of available queryable terms.
758
455
  """
759
- logger.debug(f"URL: {request.url}")
456
+ logger.info(f"{request.method} {request.state.url}")
760
457
  additional_params = dict(request.query_params.items())
761
458
  provider = additional_params.pop("provider", None)
762
459
  queryables = await get_queryables(
@@ -789,7 +486,7 @@ def get_search(
789
486
  crunch: Optional[str] = None,
790
487
  ) -> ORJSONResponse:
791
488
  """Handler for GET /search"""
792
- logger.debug("URL: %s", request.state.url)
489
+ logger.info(f"{request.method} {request.state.url}")
793
490
 
794
491
  query_params = str(request.query_params)
795
492
 
@@ -843,7 +540,7 @@ def get_search(
843
540
  )
844
541
  async def post_search(request: Request) -> ORJSONResponse:
845
542
  """STAC post search"""
846
- logger.debug("URL: %s", request.url)
543
+ logger.info(f"{request.method} {request.state.url}")
847
544
 
848
545
  content_type = request.headers.get("Content-Type")
849
546
 
@@ -864,10 +561,12 @@ async def post_search(request: Request) -> ORJSONResponse:
864
561
 
865
562
  logger.debug("Body: %s", search_request.model_dump(exclude_none=True))
866
563
 
867
- response = search_stac_items(
868
- request=request,
869
- search_request=search_request,
564
+ response = await run_in_threadpool(
565
+ search_stac_items,
566
+ request,
567
+ search_request,
870
568
  )
569
+
871
570
  return ORJSONResponse(content=response, media_type="application/json")
872
571
 
873
572