eodag 2.12.0__py3-none-any.whl → 3.0.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.
Files changed (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -165
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.0.dist-info/RECORD +0 -94
  92. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
eodag/rest/core.py ADDED
@@ -0,0 +1,735 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2023, 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
+ from __future__ import annotations
19
+
20
+ import datetime
21
+ import logging
22
+ import os
23
+ import re
24
+ from typing import TYPE_CHECKING, cast
25
+ from unittest.mock import Mock
26
+
27
+ import dateutil
28
+ from cachetools.func import lru_cache
29
+ from fastapi.responses import ORJSONResponse, StreamingResponse
30
+ from pydantic import ValidationError as pydanticValidationError
31
+ from requests.models import Response as RequestsResponse
32
+
33
+ import eodag
34
+ from eodag import EOProduct
35
+ from eodag.api.product.metadata_mapping import (
36
+ DEFAULT_METADATA_MAPPING,
37
+ NOT_AVAILABLE,
38
+ OFFLINE_STATUS,
39
+ ONLINE_STATUS,
40
+ OSEO_METADATA_MAPPING,
41
+ STAGING_STATUS,
42
+ )
43
+ from eodag.api.search_result import SearchResult
44
+ from eodag.config import load_stac_config
45
+ from eodag.plugins.crunch.filter_latest_intersect import FilterLatestIntersect
46
+ from eodag.plugins.crunch.filter_latest_tpl_name import FilterLatestByName
47
+ from eodag.plugins.crunch.filter_overlap import FilterOverlap
48
+ from eodag.rest.cache import cached
49
+ from eodag.rest.constants import (
50
+ CACHE_KEY_COLLECTION,
51
+ CACHE_KEY_COLLECTIONS,
52
+ CACHE_KEY_QUERYABLES,
53
+ )
54
+ from eodag.rest.errors import ResponseSearchError
55
+ from eodag.rest.stac import StacCatalog, StacCollection, StacCommon, StacItem
56
+ from eodag.rest.types.eodag_search import EODAGSearch
57
+ from eodag.rest.types.queryables import (
58
+ QueryablesGetParams,
59
+ StacQueryableProperty,
60
+ StacQueryables,
61
+ )
62
+ from eodag.rest.types.stac_search import SearchPostRequest
63
+ from eodag.rest.utils import (
64
+ Cruncher,
65
+ file_to_stream,
66
+ format_pydantic_error,
67
+ get_next_link,
68
+ )
69
+ from eodag.rest.utils.rfc3339 import rfc3339_str_to_datetime
70
+ from eodag.utils import (
71
+ _deprecated,
72
+ deepcopy,
73
+ dict_items_recursive_apply,
74
+ format_dict_items,
75
+ obj_md5sum,
76
+ urlencode,
77
+ )
78
+ from eodag.utils.exceptions import (
79
+ MisconfiguredError,
80
+ NotAvailableError,
81
+ ValidationError,
82
+ )
83
+
84
+ if TYPE_CHECKING:
85
+ from typing import Any, Dict, List, Optional, Union
86
+
87
+ from fastapi import Request
88
+ from requests.auth import AuthBase
89
+ from starlette.responses import Response
90
+
91
+
92
+ eodag_api = eodag.EODataAccessGateway()
93
+
94
+ logger = logging.getLogger("eodag.rest.core")
95
+
96
+ stac_config = load_stac_config()
97
+
98
+ crunchers = {
99
+ "filterLatestIntersect": Cruncher(FilterLatestIntersect, []),
100
+ "filterLatestByName": Cruncher(FilterLatestByName, ["name_pattern"]),
101
+ "filterOverlap": Cruncher(FilterOverlap, ["minimum_overlap"]),
102
+ }
103
+
104
+
105
+ @_deprecated(reason="No more needed with STAC API + Swagger", version="2.6.1")
106
+ def get_home_page_content(base_url: str, ipp: Optional[int] = None) -> str:
107
+ """Compute eodag service home page content
108
+
109
+ :param base_url: The service root URL
110
+ :param ipp: (optional) Items per page number
111
+ """
112
+ base_url = base_url.rstrip("/") + "/"
113
+ content = f"""<h1>EODAG Server</h1><br />
114
+ <a href='{base_url}'>root</a><br />
115
+ <a href='{base_url}service-doc'>service-doc</a><br />
116
+ """
117
+ return content
118
+
119
+
120
+ @_deprecated(
121
+ reason="Function internally used by get_home_page_content, also deprecated",
122
+ version="2.6.1",
123
+ )
124
+ def format_product_types(product_types: List[Dict[str, Any]]) -> str:
125
+ """Format product_types
126
+
127
+ :param product_types: A list of EODAG product types as returned by the core api
128
+ """
129
+ result: List[str] = []
130
+ for pt in product_types:
131
+ result.append(f'* *__{pt["ID"]}__*: {pt["abstract"]}')
132
+ return "\n".join(sorted(result))
133
+
134
+
135
+ def search_stac_items(
136
+ request: Request,
137
+ search_request: SearchPostRequest,
138
+ ) -> Dict[str, Any]:
139
+ """
140
+ Search and retrieve STAC items based on the given search request.
141
+
142
+ This function takes a search request, performs a search using EODAG API, and returns a
143
+ dictionary of STAC items.
144
+
145
+ :param request: The incoming HTTP request with state information.
146
+ :param search_request: The search criteria for STAC items
147
+ :returns: A dictionary containing the STAC items and related metadata.
148
+
149
+ The function handles the conversion of search criteria into STAC and EODAG compatible formats, validates the input
150
+ using pydantic, and constructs the appropriate URLs for querying the STAC API. It also manages pagination and the
151
+ construction of the 'next' link for the response.
152
+
153
+ If specific item IDs are provided, it retrieves the corresponding products. Otherwise, it performs a search based on
154
+ the provided criteria and time interval overlap checks.
155
+
156
+ The results are then formatted into STAC items and returned as part of the response dictionary, which includes the
157
+ items themselves, total count, and the next link if applicable.
158
+ """
159
+
160
+ stac_args = search_request.model_dump(exclude_none=True)
161
+ if search_request.start_date:
162
+ stac_args["start_datetime"] = search_request.start_date
163
+ if search_request.end_date:
164
+ stac_args["end_datetime"] = search_request.end_date
165
+ if search_request.spatial_filter:
166
+ stac_args["geometry"] = search_request.spatial_filter
167
+ try:
168
+ eodag_args = EODAGSearch.model_validate(stac_args)
169
+ except pydanticValidationError as e:
170
+ raise ValidationError(format_pydantic_error(e)) from e
171
+
172
+ catalog_url = re.sub("/items.*", "", request.state.url)
173
+ catalog = StacCatalog(
174
+ url=catalog_url.replace("/search", f"/collections/{eodag_args.productType}"),
175
+ stac_config=stac_config,
176
+ root=request.state.url_root,
177
+ provider=eodag_args.provider,
178
+ eodag_api=eodag_api,
179
+ collection=eodag_args.productType, # type: ignore
180
+ )
181
+
182
+ # get products by ids
183
+ if eodag_args.ids:
184
+ results = SearchResult([])
185
+ for item_id in eodag_args.ids:
186
+ results.extend(
187
+ eodag_api.search(
188
+ id=item_id,
189
+ productType=eodag_args.productType,
190
+ provider=eodag_args.provider,
191
+ )
192
+ )
193
+ results.number_matched = len(results)
194
+ total = len(results)
195
+
196
+ elif time_interval_overlap(eodag_args, catalog):
197
+ criteria = {
198
+ **catalog.search_args,
199
+ **eodag_args.model_dump(exclude_none=True),
200
+ }
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
214
+ else:
215
+ # return empty results
216
+ results = SearchResult([], 0)
217
+ total = 0
218
+
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:
226
+ record.product_type = eodag_api.get_alias_from_product_type(record.product_type)
227
+
228
+ items = StacItem(
229
+ url=request.state.url,
230
+ stac_config=stac_config,
231
+ provider=eodag_args.provider,
232
+ eodag_api=eodag_api,
233
+ root=request.state.url_root,
234
+ ).get_stac_items(
235
+ search_results=results,
236
+ total=total,
237
+ next_link=get_next_link(
238
+ request, search_request, total, eodag_args.items_per_page
239
+ ),
240
+ catalog={
241
+ **catalog.data,
242
+ **{"url": catalog.url, "root": catalog.root},
243
+ },
244
+ )
245
+ return items
246
+
247
+
248
+ def download_stac_item(
249
+ request: Request,
250
+ collection_id: str,
251
+ item_id: str,
252
+ provider: Optional[str] = None,
253
+ asset: Optional[str] = None,
254
+ **kwargs: Any,
255
+ ) -> Response:
256
+ """Download item
257
+
258
+ :param collection_id: id of the product type
259
+ :param item_id: Product ID
260
+ :param provider: (optional) Chosen provider
261
+ :param kwargs: additional download parameters
262
+ :returns: a stream of the downloaded data (zip file)
263
+ """
264
+ product_type = collection_id
265
+
266
+ search_results = eodag_api.search(
267
+ id=item_id, productType=product_type, provider=provider, **kwargs
268
+ )
269
+ if len(search_results) > 0:
270
+ product = cast(EOProduct, search_results[0])
271
+
272
+ else:
273
+ raise NotAvailableError(
274
+ f"Could not find {item_id} item in {product_type} collection"
275
+ + (f" for provider {provider}" if provider else "")
276
+ )
277
+ auth = product.downloader_auth.authenticate() if product.downloader_auth else None
278
+
279
+ try:
280
+ if product.properties.get("orderLink"):
281
+ _order_and_update(product, auth, kwargs)
282
+
283
+ download_stream = product.downloader._stream_download_dict(
284
+ product,
285
+ auth=auth,
286
+ asset=asset,
287
+ wait=-1,
288
+ timeout=-1,
289
+ )
290
+ except NotImplementedError:
291
+ logger.warning(
292
+ "Download streaming not supported for %s: downloading locally then delete",
293
+ product.downloader,
294
+ )
295
+ download_stream = file_to_stream(
296
+ eodag_api.download(product, extract=False, asset=asset)
297
+ )
298
+ except NotAvailableError:
299
+ if product.properties.get("storageStatus") != ONLINE_STATUS:
300
+ kwargs["orderId"] = kwargs.get("orderId") or product.properties.get(
301
+ "orderId"
302
+ )
303
+ kwargs["provider"] = provider
304
+ qs = urlencode(kwargs, doseq=True)
305
+ download_link = f"{request.state.url}?{qs}"
306
+ return ORJSONResponse(
307
+ status_code=202,
308
+ headers={"Location": download_link},
309
+ content={
310
+ "description": "Product is not available yet, please try again using given updated location",
311
+ "status": product.properties.get("orderStatus"),
312
+ "location": download_link,
313
+ },
314
+ )
315
+ else:
316
+ raise
317
+
318
+ return StreamingResponse(
319
+ content=download_stream.content,
320
+ headers=download_stream.headers,
321
+ media_type=download_stream.media_type,
322
+ )
323
+
324
+
325
+ def _order_and_update(
326
+ product: EOProduct,
327
+ auth: Union[AuthBase, Dict[str, str], None],
328
+ query_args: Dict[str, Any],
329
+ ) -> None:
330
+ """Order product if needed and update given kwargs with order-status-dict"""
331
+ if product.properties.get("storageStatus") != ONLINE_STATUS and hasattr(
332
+ product.downloader, "order_response_process"
333
+ ):
334
+ # update product (including orderStatusLink) if product was previously ordered
335
+ logger.debug("Use given download query arguments to parse order link")
336
+ response = Mock(spec=RequestsResponse)
337
+ response.status_code = 200
338
+ response.json.return_value = query_args
339
+ response.headers = {}
340
+ product.downloader.order_response_process(response, product)
341
+
342
+ if (
343
+ product.properties.get("storageStatus") != ONLINE_STATUS
344
+ and NOT_AVAILABLE in product.properties.get("orderStatusLink", "")
345
+ and hasattr(product.downloader, "order_download")
346
+ ):
347
+ # first order
348
+ logger.debug("Order product")
349
+ order_status_dict = product.downloader.order_download(
350
+ product=product, auth=auth
351
+ )
352
+ query_args.update(order_status_dict or {})
353
+
354
+ if (
355
+ product.properties.get("storageStatus") == OFFLINE_STATUS
356
+ and product.properties.get("orderStatusLink")
357
+ and NOT_AVAILABLE not in product.properties.get("orderStatusLink", "")
358
+ ):
359
+ product.properties["storageStatus"] = STAGING_STATUS
360
+
361
+ if product.properties.get("storageStatus") == STAGING_STATUS and hasattr(
362
+ product.downloader, "order_download_status"
363
+ ):
364
+ # check order status if needed
365
+ logger.debug("Checking product order status")
366
+ product.downloader.order_download_status(product=product, auth=auth)
367
+
368
+ if product.properties.get("storageStatus") != ONLINE_STATUS:
369
+ raise NotAvailableError("Product is not available yet")
370
+
371
+
372
+ @lru_cache(maxsize=1)
373
+ def get_detailled_collections_list() -> List[Dict[str, Any]]:
374
+ """Returns detailled collections / product_types list as a list of
375
+ config dicts
376
+
377
+ :returns: List of config dicts
378
+ """
379
+ return eodag_api.list_product_types(fetch_providers=False)
380
+
381
+
382
+ async def all_collections(
383
+ request: Request,
384
+ provider: Optional[str] = None,
385
+ q: Optional[str] = None,
386
+ platform: Optional[str] = None,
387
+ instrument: Optional[str] = None,
388
+ constellation: Optional[str] = None,
389
+ datetime: Optional[str] = None,
390
+ ) -> Dict[str, Any]:
391
+ """Build STAC collections
392
+
393
+ :param url: Requested URL
394
+ :param root: The API root
395
+ :param filters: Search collections filters
396
+ :param provider: (optional) Chosen provider
397
+ :returns: Collections dictionary
398
+ """
399
+
400
+ async def _fetch() -> Dict[str, Any]:
401
+ stac_collection = StacCollection(
402
+ url=request.state.url,
403
+ stac_config=stac_config,
404
+ provider=provider,
405
+ eodag_api=eodag_api,
406
+ root=request.state.url_root,
407
+ )
408
+ collections = deepcopy(stac_config["collections"])
409
+ collections["collections"] = stac_collection.get_collection_list(
410
+ q=q,
411
+ platform=platform,
412
+ instrument=instrument,
413
+ constellation=constellation,
414
+ datetime=datetime,
415
+ )
416
+
417
+ # # parse f-strings
418
+ format_args = deepcopy(stac_config)
419
+ format_args["collections"].update(
420
+ {"url": stac_collection.url, "root": stac_collection.root}
421
+ )
422
+
423
+ collections["links"] = [
424
+ format_dict_items(link, **format_args) for link in collections["links"]
425
+ ]
426
+
427
+ collections = format_dict_items(collections, **format_args)
428
+ return collections
429
+
430
+ hashed_collections = hash(
431
+ f"{provider}:{q}:{platform}:{instrument}:{constellation}:{datetime}"
432
+ )
433
+ cache_key = f"{CACHE_KEY_COLLECTIONS}:{hashed_collections}"
434
+ return await cached(_fetch, cache_key, request)
435
+
436
+
437
+ async def get_collection(
438
+ request: Request, collection_id: str, provider: Optional[str] = None
439
+ ) -> Dict[str, Any]:
440
+ """Build STAC collection by id
441
+
442
+ :param url: Requested URL
443
+ :param root: API root
444
+ :param collection_id: Product_type as ID of the collection
445
+ :param provider: (optional) Chosen provider
446
+ :returns: Collection dictionary
447
+ """
448
+
449
+ async def _fetch() -> Dict[str, Any]:
450
+ stac_collection = StacCollection(
451
+ url=request.state.url,
452
+ stac_config=stac_config,
453
+ provider=provider,
454
+ eodag_api=eodag_api,
455
+ root=request.state.url_root,
456
+ )
457
+ collection_list = stac_collection.get_collection_list(collection=collection_id)
458
+
459
+ if not collection_list:
460
+ raise NotAvailableError(f"Collection {collection_id} does not exist.")
461
+
462
+ return collection_list[0]
463
+
464
+ cache_key = f"{CACHE_KEY_COLLECTION}:{provider}:{collection_id}"
465
+ return await cached(_fetch, cache_key, request)
466
+
467
+
468
+ async def get_stac_catalogs(
469
+ request: Request,
470
+ url: str,
471
+ provider: Optional[str] = None,
472
+ ) -> Dict[str, Any]:
473
+ """Build STAC catalog
474
+
475
+ :param url: Requested URL
476
+ :param root: (optional) API root
477
+ :param provider: (optional) Chosen provider
478
+ :returns: Catalog dictionary
479
+ """
480
+
481
+ async def _fetch() -> Dict[str, Any]:
482
+ return StacCatalog(
483
+ url=url,
484
+ stac_config=stac_config,
485
+ root=request.state.url_root,
486
+ provider=provider,
487
+ eodag_api=eodag_api,
488
+ ).data
489
+
490
+ return await cached(_fetch, f"{CACHE_KEY_COLLECTION}:{provider}", request)
491
+
492
+
493
+ def time_interval_overlap(eodag_args: EODAGSearch, catalog: StacCatalog) -> bool:
494
+ """fix search date filter based on catalog date range"""
495
+ # check if time filtering appears both in search arguments and catalog
496
+ # (for catalogs built by date: i.e. `year/2020/month/05`)
497
+ if not set(["start", "end"]) <= set(eodag_args.model_dump().keys()) or not set(
498
+ ["start", "end"]
499
+ ) <= set(catalog.search_args.keys()):
500
+ return True
501
+
502
+ search_date_min = cast(
503
+ datetime.datetime,
504
+ (
505
+ dateutil.parser.parse(eodag_args.start) # type: ignore
506
+ if eodag_args.start
507
+ else datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
508
+ ),
509
+ )
510
+ search_date_max = cast(
511
+ datetime.datetime,
512
+ (
513
+ dateutil.parser.parse(eodag_args.end) # type: ignore
514
+ if eodag_args.end
515
+ else datetime.datetime.now(tz=datetime.timezone.utc)
516
+ ),
517
+ )
518
+
519
+ catalog_date_min = rfc3339_str_to_datetime(catalog.search_args["start"])
520
+ catalog_date_max = rfc3339_str_to_datetime(catalog.search_args["end"])
521
+ # check if date intervals overlap
522
+ if (search_date_min <= catalog_date_max) and (search_date_max >= catalog_date_min):
523
+ # use intersection
524
+ eodag_args.start = (
525
+ max(search_date_min, catalog_date_min).isoformat().replace("+00:00", "Z")
526
+ )
527
+ eodag_args.end = (
528
+ min(search_date_max, catalog_date_max).isoformat().replace("+00:00", "Z")
529
+ )
530
+ return True
531
+
532
+ logger.warning("Time intervals do not overlap")
533
+ return False
534
+
535
+
536
+ @lru_cache(maxsize=1)
537
+ def get_stac_conformance() -> Dict[str, str]:
538
+ """Build STAC conformance
539
+
540
+ :returns: conformance dictionary
541
+ """
542
+ return stac_config["conformance"]
543
+
544
+
545
+ def get_stac_api_version() -> str:
546
+ """Get STAC API version
547
+
548
+ :returns: STAC API version
549
+ """
550
+ return stac_config["stac_api_version"]
551
+
552
+
553
+ @lru_cache(maxsize=1)
554
+ def get_stac_extension_oseo(url: str) -> Dict[str, str]:
555
+ """Build STAC OGC / OpenSearch Extension for EO
556
+
557
+ :param url: Requested URL
558
+ :returns: Catalog dictionary
559
+ """
560
+
561
+ def apply_method(_: str, x: str) -> str:
562
+ return str(x).replace("$.product.", "$.")
563
+
564
+ item_mapping = dict_items_recursive_apply(stac_config["item"], apply_method)
565
+
566
+ # all properties as string type by default
567
+ oseo_properties = {
568
+ "oseo:{}".format(k): {
569
+ "type": "string",
570
+ "title": k[0].upper() + re.sub(r"([A-Z][a-z]+)", r" \1", k[1:]),
571
+ }
572
+ for k, v in OSEO_METADATA_MAPPING.items()
573
+ if v not in str(item_mapping)
574
+ }
575
+
576
+ return StacCommon.get_stac_extension(
577
+ url=url, stac_config=stac_config, extension="oseo", properties=oseo_properties
578
+ )
579
+
580
+
581
+ async def get_queryables(
582
+ request: Request,
583
+ params: QueryablesGetParams,
584
+ provider: Optional[str] = None,
585
+ ) -> Dict[str, Any]:
586
+ """Fetch the queryable properties for a collection.
587
+
588
+ :param collection_id: The ID of the collection.
589
+ :returns: A set containing the STAC standardized queryable properties for a collection.
590
+ """
591
+
592
+ async def _fetch() -> Dict[str, Any]:
593
+ python_queryables = eodag_api.list_queryables(
594
+ provider=provider, **params.model_dump(exclude_none=True, by_alias=True)
595
+ )
596
+ python_queryables.pop("start")
597
+ python_queryables.pop("end")
598
+
599
+ # productType and id are already default in stac collection and id
600
+ python_queryables.pop("productType", None)
601
+ python_queryables.pop("id", None)
602
+
603
+ stac_queryables: Dict[str, StacQueryableProperty] = deepcopy(
604
+ StacQueryables.default_properties
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()
612
+ for param, queryable in python_queryables.items():
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)
618
+ # only keep "datetime" queryable for dates
619
+ if stac_param in stac_queryables or stac_param in (
620
+ "start_datetime",
621
+ "end_datetime",
622
+ ):
623
+ continue
624
+
625
+ stac_queryables[
626
+ stac_param
627
+ ] = StacQueryableProperty.from_python_field_definition(
628
+ stac_param, queryable
629
+ )
630
+
631
+ if params.collection:
632
+ stac_queryables.pop("collection")
633
+
634
+ return StacQueryables(
635
+ q_id=request.state.url,
636
+ additional_properties=bool(not params.collection),
637
+ properties=stac_queryables,
638
+ ).model_dump(mode="json", by_alias=True)
639
+
640
+ hashed_queryables = hash(params.model_dump_json())
641
+ return await cached(
642
+ _fetch, f"{CACHE_KEY_QUERYABLES}:{provider}:{hashed_queryables}", request
643
+ )
644
+
645
+
646
+ @_deprecated(
647
+ reason="Used to format output from deprecated function get_home_page_content",
648
+ version="2.6.1",
649
+ )
650
+ def get_templates_path() -> str:
651
+ """Returns Jinja templates path"""
652
+ return os.path.join(os.path.dirname(__file__), "templates")
653
+
654
+
655
+ def crunch_products(
656
+ products: SearchResult, cruncher_name: str, **kwargs: Any
657
+ ) -> SearchResult:
658
+ """Apply an eodag cruncher to filter products"""
659
+ cruncher = crunchers.get(cruncher_name)
660
+ if not cruncher:
661
+ raise ValidationError(
662
+ f'Unknown crunch name. Use one of: {", ".join(crunchers.keys())}'
663
+ )
664
+
665
+ cruncher_config: Dict[str, Any] = {}
666
+ for config_param in cruncher.config_params:
667
+ config_param_value = kwargs.get(config_param)
668
+ if not config_param_value:
669
+ raise ValidationError(
670
+ (
671
+ f"cruncher {cruncher} require additional parameters:"
672
+ f' {", ".join(cruncher.config_params)}'
673
+ )
674
+ )
675
+ cruncher_config[config_param] = config_param_value
676
+
677
+ try:
678
+ products = products.crunch(cruncher.clazz(cruncher_config), **kwargs)
679
+ except MisconfiguredError as e:
680
+ raise ValidationError(str(e)) from e
681
+
682
+ return products
683
+
684
+
685
+ def eodag_api_init() -> None:
686
+ """Init EODataAccessGateway server instance, pre-running all time consuming tasks"""
687
+ eodag_api.fetch_product_types_list()
688
+ StacCollection.fetch_external_stac_collections(eodag_api)
689
+
690
+ # update eodag product_types config form external stac collections
691
+ for p, p_f in eodag_api.product_types_config.source.items():
692
+ for key in (p, p_f.get("alias")):
693
+ if key is None:
694
+ continue
695
+ ext_col = StacCollection.ext_stac_collections.get(key)
696
+ if not ext_col:
697
+ continue
698
+ platform: Union[str, List[str]] = ext_col.get("summaries", {}).get(
699
+ "platform"
700
+ )
701
+ constellation: Union[str, List[str]] = ext_col.get("summaries", {}).get(
702
+ "constellation"
703
+ )
704
+ # Check if platform or constellation are lists and join them into a string if they are
705
+ if isinstance(platform, list):
706
+ platform = ",".join(platform)
707
+ if isinstance(constellation, list):
708
+ constellation = ",".join(constellation)
709
+
710
+ update_fields = {
711
+ "title": ext_col.get("title"),
712
+ "abstract": ext_col["description"],
713
+ "keywords": ext_col.get("keywords"),
714
+ "instrument": ",".join(
715
+ ext_col.get("summaries", {}).get("instruments", [])
716
+ ),
717
+ "platform": constellation,
718
+ "platformSerialIdentifier": platform,
719
+ "processingLevel": ext_col.get("summaries", {}).get("processing:level"),
720
+ "license": ext_col["license"],
721
+ "missionStartDate": ext_col["extent"]["temporal"]["interval"][0][0],
722
+ "missionEndDate": ext_col["extent"]["temporal"]["interval"][-1][1],
723
+ }
724
+ clean = {k: v for k, v in update_fields.items() if v}
725
+ p_f.update(clean)
726
+
727
+ eodag_api.product_types_config_md5 = obj_md5sum(
728
+ eodag_api.product_types_config.source
729
+ )
730
+
731
+ eodag_api.build_index()
732
+
733
+ # pre-build search plugins
734
+ for provider in eodag_api.available_providers():
735
+ next(eodag_api._plugins_manager.get_search_plugins(provider=provider))