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/core.py CHANGED
@@ -33,6 +33,7 @@ from requests.models import Response as RequestsResponse
33
33
  import eodag
34
34
  from eodag import EOProduct
35
35
  from eodag.api.product.metadata_mapping import (
36
+ DEFAULT_METADATA_MAPPING,
36
37
  NOT_AVAILABLE,
37
38
  OFFLINE_STATUS,
38
39
  ONLINE_STATUS,
@@ -50,6 +51,7 @@ from eodag.rest.constants import (
50
51
  CACHE_KEY_COLLECTIONS,
51
52
  CACHE_KEY_QUERYABLES,
52
53
  )
54
+ from eodag.rest.errors import ResponseSearchError
53
55
  from eodag.rest.stac import StacCatalog, StacCollection, StacCommon, StacItem
54
56
  from eodag.rest.types.eodag_search import EODAGSearch
55
57
  from eodag.rest.types.queryables import (
@@ -80,7 +82,7 @@ from eodag.utils.exceptions import (
80
82
  )
81
83
 
82
84
  if TYPE_CHECKING:
83
- from typing import Any, Dict, List, Optional, Tuple, Union
85
+ from typing import Any, Dict, List, Optional, Union
84
86
 
85
87
  from fastapi import Request
86
88
  from requests.auth import AuthBase
@@ -133,17 +135,15 @@ def format_product_types(product_types: List[Dict[str, Any]]) -> str:
133
135
  def search_stac_items(
134
136
  request: Request,
135
137
  search_request: SearchPostRequest,
136
- catalogs: Optional[List[str]] = None,
137
138
  ) -> Dict[str, Any]:
138
139
  """
139
- Search and retrieve STAC items from the given catalogs.
140
+ Search and retrieve STAC items based on the given search request.
140
141
 
141
- This function takes a search request and optional catalogs list, performs a search using EODAG API, and returns a
142
+ This function takes a search request, performs a search using EODAG API, and returns a
142
143
  dictionary of STAC items.
143
144
 
144
145
  :param request: The incoming HTTP request with state information.
145
- :param search_request: The search criteria for STAC items.
146
- :param catalogs: (optional) A list of catalogs to search within. Defaults to None.
146
+ :param search_request: The search criteria for STAC items
147
147
  :returns: A dictionary containing the STAC items and related metadata.
148
148
 
149
149
  The function handles the conversion of search criteria into STAC and EODAG compatible formats, validates the input
@@ -165,60 +165,64 @@ def search_stac_items(
165
165
  if search_request.spatial_filter:
166
166
  stac_args["geometry"] = search_request.spatial_filter
167
167
  try:
168
- eodag_args = EODAGSearch.model_validate(
169
- stac_args, context={"isCatalog": bool(catalogs)}
170
- )
168
+ eodag_args = EODAGSearch.model_validate(stac_args)
171
169
  except pydanticValidationError as e:
172
170
  raise ValidationError(format_pydantic_error(e)) from e
173
171
 
174
172
  catalog_url = re.sub("/items.*", "", request.state.url)
175
-
176
173
  catalog = StacCatalog(
177
- url=(
178
- catalog_url
179
- if catalogs
180
- else catalog_url.replace(
181
- "/search", f"/collections/{eodag_args.productType}"
182
- )
183
- ),
174
+ url=catalog_url.replace("/search", f"/collections/{eodag_args.productType}"),
184
175
  stac_config=stac_config,
185
176
  root=request.state.url_root,
186
177
  provider=eodag_args.provider,
187
178
  eodag_api=eodag_api,
188
- catalogs=catalogs or [eodag_args.productType], # type: ignore
179
+ collection=eodag_args.productType, # type: ignore
189
180
  )
190
181
 
191
182
  # get products by ids
192
183
  if eodag_args.ids:
193
- search_results = SearchResult([])
184
+ results = SearchResult([])
194
185
  for item_id in eodag_args.ids:
195
- sr = eodag_api.search(
196
- id=item_id,
197
- productType=catalogs[0] if catalogs else eodag_args.productType,
198
- provider=eodag_args.provider,
186
+ results.extend(
187
+ eodag_api.search(
188
+ id=item_id,
189
+ productType=eodag_args.productType,
190
+ provider=eodag_args.provider,
191
+ )
199
192
  )
200
- search_results.extend(sr)
201
- search_results.number_matched = len(search_results)
202
- total = len(search_results)
193
+ results.number_matched = len(results)
194
+ total = len(results)
203
195
 
204
196
  elif time_interval_overlap(eodag_args, catalog):
205
197
  criteria = {
206
198
  **catalog.search_args,
207
199
  **eodag_args.model_dump(exclude_none=True),
208
200
  }
209
-
210
- search_results = eodag_api.search(count=True, **criteria)
211
- total = search_results.number_matched or 0
212
- if search_request.crunch:
213
- search_results = crunch_products(
214
- search_results, search_request.crunch, **criteria
215
- )
201
+ # remove provider prefixes
202
+ stac_extensions = stac_config["extensions"]
203
+ keys_to_update = {}
204
+ for key in criteria:
205
+ if ":" in key and key.split(":")[0] not in stac_extensions:
206
+ new_key = key.split(":")[1]
207
+ keys_to_update[key] = new_key
208
+ for key, new_key in keys_to_update.items():
209
+ criteria[new_key] = criteria[key]
210
+ criteria.pop(key)
211
+
212
+ results = eodag_api.search(count=True, **criteria)
213
+ total = results.number_matched or 0
216
214
  else:
217
215
  # return empty results
218
- search_results = SearchResult([], 0)
216
+ results = SearchResult([], 0)
219
217
  total = 0
220
218
 
221
- for record in search_results:
219
+ if len(results) == 0 and results.errors:
220
+ raise ResponseSearchError(results.errors)
221
+
222
+ if search_request.crunch:
223
+ results = crunch_products(results, search_request.crunch, **criteria)
224
+
225
+ for record in results:
222
226
  record.product_type = eodag_api.get_alias_from_product_type(record.product_type)
223
227
 
224
228
  items = StacItem(
@@ -228,7 +232,7 @@ def search_stac_items(
228
232
  eodag_api=eodag_api,
229
233
  root=request.state.url_root,
230
234
  ).get_stac_items(
231
- search_results=search_results,
235
+ search_results=results,
232
236
  total=total,
233
237
  next_link=get_next_link(
234
238
  request, search_request, total, eodag_args.items_per_page
@@ -243,7 +247,7 @@ def search_stac_items(
243
247
 
244
248
  def download_stac_item(
245
249
  request: Request,
246
- catalogs: List[str],
250
+ collection_id: str,
247
251
  item_id: str,
248
252
  provider: Optional[str] = None,
249
253
  asset: Optional[str] = None,
@@ -251,13 +255,13 @@ def download_stac_item(
251
255
  ) -> Response:
252
256
  """Download item
253
257
 
254
- :param catalogs: Catalogs list (only first is used as product_type)
258
+ :param collection_id: id of the product type
255
259
  :param item_id: Product ID
256
260
  :param provider: (optional) Chosen provider
257
261
  :param kwargs: additional download parameters
258
262
  :returns: a stream of the downloaded data (zip file)
259
263
  """
260
- product_type = catalogs[0]
264
+ product_type = collection_id
261
265
 
262
266
  search_results = eodag_api.search(
263
267
  id=item_id, productType=product_type, provider=provider, **kwargs
@@ -390,7 +394,7 @@ async def all_collections(
390
394
  :param root: The API root
391
395
  :param filters: Search collections filters
392
396
  :param provider: (optional) Chosen provider
393
- :returns: Collections dictionnary
397
+ :returns: Collections dictionary
394
398
  """
395
399
 
396
400
  async def _fetch() -> Dict[str, Any]:
@@ -423,7 +427,9 @@ async def all_collections(
423
427
  collections = format_dict_items(collections, **format_args)
424
428
  return collections
425
429
 
426
- hashed_collections = hash(f"{provider}:{q}:{platform}:{instrument}:{constellation}")
430
+ hashed_collections = hash(
431
+ f"{provider}:{q}:{platform}:{instrument}:{constellation}:{datetime}"
432
+ )
427
433
  cache_key = f"{CACHE_KEY_COLLECTIONS}:{hashed_collections}"
428
434
  return await cached(_fetch, cache_key, request)
429
435
 
@@ -462,14 +468,12 @@ async def get_collection(
462
468
  async def get_stac_catalogs(
463
469
  request: Request,
464
470
  url: str,
465
- catalogs: Optional[Tuple[str, ...]] = None,
466
471
  provider: Optional[str] = None,
467
472
  ) -> Dict[str, Any]:
468
473
  """Build STAC catalog
469
474
 
470
475
  :param url: Requested URL
471
476
  :param root: (optional) API root
472
- :param catalogs: (optional) Catalogs list
473
477
  :param provider: (optional) Chosen provider
474
478
  :returns: Catalog dictionary
475
479
  """
@@ -481,13 +485,9 @@ async def get_stac_catalogs(
481
485
  root=request.state.url_root,
482
486
  provider=provider,
483
487
  eodag_api=eodag_api,
484
- catalogs=list(catalogs) if catalogs else None,
485
488
  ).data
486
489
 
487
- hashed_catalogs = hash(":".join(catalogs) if catalogs else None)
488
- return await cached(
489
- _fetch, f"{CACHE_KEY_COLLECTION}:{provider}:{hashed_catalogs}", request
490
- )
490
+ return await cached(_fetch, f"{CACHE_KEY_COLLECTION}:{provider}", request)
491
491
 
492
492
 
493
493
  def time_interval_overlap(eodag_args: EODAGSearch, catalog: StacCatalog) -> bool:
@@ -537,7 +537,7 @@ def time_interval_overlap(eodag_args: EODAGSearch, catalog: StacCatalog) -> bool
537
537
  def get_stac_conformance() -> Dict[str, str]:
538
538
  """Build STAC conformance
539
539
 
540
- :returns: conformance dictionnary
540
+ :returns: conformance dictionary
541
541
  """
542
542
  return stac_config["conformance"]
543
543
 
@@ -555,7 +555,7 @@ def get_stac_extension_oseo(url: str) -> Dict[str, str]:
555
555
  """Build STAC OGC / OpenSearch Extension for EO
556
556
 
557
557
  :param url: Requested URL
558
- :returns: Catalog dictionnary
558
+ :returns: Catalog dictionary
559
559
  """
560
560
 
561
561
  def apply_method(_: str, x: str) -> str:
@@ -603,8 +603,18 @@ async def get_queryables(
603
603
  stac_queryables: Dict[str, StacQueryableProperty] = deepcopy(
604
604
  StacQueryables.default_properties
605
605
  )
606
+ # get stac default properties to set prefixes
607
+ stac_item_properties = list(stac_config["item"]["properties"].values())
608
+ stac_item_properties.extend(list(stac_queryables.keys()))
609
+ ignore = stac_config["metadata_ignore"]
610
+ stac_item_properties.extend(ignore)
611
+ default_mapping = DEFAULT_METADATA_MAPPING.keys()
606
612
  for param, queryable in python_queryables.items():
607
- stac_param = EODAGSearch.to_stac(param)
613
+ if param in default_mapping and not any(
614
+ param in str(prop) for prop in stac_item_properties
615
+ ):
616
+ param = f"oseo:{param}"
617
+ stac_param = EODAGSearch.to_stac(param, stac_item_properties, provider)
608
618
  # only keep "datetime" queryable for dates
609
619
  if stac_param in stac_queryables or stac_param in (
610
620
  "start_datetime",
eodag/rest/errors.py ADDED
@@ -0,0 +1,181 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
3
+ #
4
+ # This file is part of EODAG project
5
+ # https://www.github.com/CS-SI/EODAG
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ import logging
19
+ from typing import Dict, List, Tuple, Union
20
+
21
+ from fastapi import FastAPI, Request
22
+ from fastapi.responses import ORJSONResponse
23
+ from starlette import status
24
+ from starlette.exceptions import HTTPException as StarletteHTTPException
25
+
26
+ from eodag.rest.types.eodag_search import EODAGSearch
27
+ from eodag.utils.exceptions import (
28
+ AuthenticationError,
29
+ DownloadError,
30
+ EodagError,
31
+ MisconfiguredError,
32
+ NoMatchingProductType,
33
+ NotAvailableError,
34
+ RequestError,
35
+ TimeOutError,
36
+ UnsupportedProductType,
37
+ UnsupportedProvider,
38
+ ValidationError,
39
+ )
40
+
41
+ EODAG_DEFAULT_STATUS_CODES = {
42
+ AuthenticationError: status.HTTP_500_INTERNAL_SERVER_ERROR,
43
+ DownloadError: status.HTTP_500_INTERNAL_SERVER_ERROR,
44
+ MisconfiguredError: status.HTTP_500_INTERNAL_SERVER_ERROR,
45
+ NotAvailableError: status.HTTP_404_NOT_FOUND,
46
+ NoMatchingProductType: status.HTTP_404_NOT_FOUND,
47
+ TimeOutError: status.HTTP_504_GATEWAY_TIMEOUT,
48
+ UnsupportedProductType: status.HTTP_404_NOT_FOUND,
49
+ UnsupportedProvider: status.HTTP_404_NOT_FOUND,
50
+ ValidationError: status.HTTP_400_BAD_REQUEST,
51
+ }
52
+
53
+ logger = logging.getLogger("eodag.rest.server")
54
+
55
+
56
+ class ResponseSearchError(Exception):
57
+ """Represent a EODAG search error response"""
58
+
59
+ def __init__(self, errors: List[Tuple[str, Exception]]) -> None:
60
+ self._errors = errors
61
+
62
+ @property
63
+ def errors(self) -> List[Dict[str, Union[str, int]]]:
64
+ """return errors as a list of dict"""
65
+ error_list: List[Dict[str, Union[str, int]]] = []
66
+ for name, exception in self._errors:
67
+
68
+ error_dict: Dict[str, Union[str, int]] = {
69
+ "provider": name,
70
+ "error": exception.__class__.__name__,
71
+ }
72
+
73
+ if exception.args:
74
+ error_dict["message"] = exception.args[0]
75
+
76
+ if len(exception.args) > 1:
77
+ error_dict["detail"] = " ".join([str(i) for i in exception.args[1:]])
78
+
79
+ error_dict["status_code"] = EODAG_DEFAULT_STATUS_CODES.get(
80
+ type(exception), getattr(exception, "status_code", 500)
81
+ )
82
+
83
+ if type(exception) in (MisconfiguredError, AuthenticationError):
84
+ logger.error("%s: %s", type(exception).__name__, str(exception))
85
+ error_dict[
86
+ "message"
87
+ ] = "Internal server error: please contact the administrator"
88
+ error_dict.pop("detail", None)
89
+
90
+ if type(exception) is ValidationError:
91
+ for error_param in exception.parameters:
92
+ stac_param = EODAGSearch.to_stac(error_param)
93
+ exception.message = exception.message.replace(
94
+ error_param, stac_param
95
+ )
96
+ error_dict["message"] = exception.message
97
+
98
+ error_list.append(error_dict)
99
+
100
+ return error_list
101
+
102
+ @property
103
+ def status_code(self) -> int:
104
+ """get global errors status code"""
105
+ if len(self._errors) == 1 and type(self.errors[0]["status_code"]) is int:
106
+ return self.errors[0]["status_code"]
107
+
108
+ return 400
109
+
110
+
111
+ async def response_search_error_handler(
112
+ request: Request, exc: Exception
113
+ ) -> ORJSONResponse:
114
+ """Handle ResponseSearchError exceptions"""
115
+ if not isinstance(exc, ResponseSearchError):
116
+ return starlette_exception_handler(request, exc)
117
+
118
+ return ORJSONResponse(
119
+ status_code=exc.status_code,
120
+ content={"errors": exc.errors},
121
+ )
122
+
123
+
124
+ async def eodag_errors_handler(request: Request, exc: Exception) -> ORJSONResponse:
125
+ """Handler for EODAG errors"""
126
+ if not isinstance(exc, EodagError):
127
+ return starlette_exception_handler(request, exc)
128
+
129
+ exception_status_code = getattr(exc, "status_code", None)
130
+ default_status_code = exception_status_code or 500
131
+ code = EODAG_DEFAULT_STATUS_CODES.get(type(exc), default_status_code)
132
+
133
+ detail = f"{type(exc).__name__}: {str(exc)}"
134
+
135
+ if type(exc) in (MisconfiguredError, AuthenticationError, TimeOutError):
136
+ logger.error("%s: %s", type(exc).__name__, str(exc))
137
+
138
+ if type(exc) in (MisconfiguredError, AuthenticationError):
139
+ detail = "Internal server error: please contact the administrator"
140
+
141
+ if type(exc) is ValidationError:
142
+ for error_param in exc.parameters:
143
+ stac_param = EODAGSearch.to_stac(error_param)
144
+ exc.message = exc.message.replace(error_param, stac_param)
145
+ detail = exc.message
146
+
147
+ return ORJSONResponse(
148
+ status_code=code,
149
+ content={"description": detail},
150
+ )
151
+
152
+
153
+ def starlette_exception_handler(request: Request, error: Exception) -> ORJSONResponse:
154
+ """Default errors handle"""
155
+ description = (
156
+ getattr(error, "description", None)
157
+ or getattr(error, "detail", None)
158
+ or str(error)
159
+ )
160
+ return ORJSONResponse(
161
+ status_code=getattr(error, "status_code", 500),
162
+ content={"description": description},
163
+ )
164
+
165
+
166
+ def add_exception_handlers(app: FastAPI) -> None:
167
+ """Add exception handlers to the FastAPI application.
168
+
169
+ Args:
170
+ app: the FastAPI application.
171
+
172
+ Returns:
173
+ None
174
+ """
175
+ app.add_exception_handler(StarletteHTTPException, starlette_exception_handler)
176
+
177
+ app.add_exception_handler(RequestError, eodag_errors_handler)
178
+ for exc in EODAG_DEFAULT_STATUS_CODES:
179
+ app.add_exception_handler(exc, eodag_errors_handler)
180
+
181
+ app.add_exception_handler(ResponseSearchError, response_search_error_handler)