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.
Files changed (94) hide show
  1. eodag/api/core.py +347 -247
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +129 -93
  10. eodag/api/search_result.py +28 -12
  11. eodag/cli.py +61 -24
  12. eodag/config.py +457 -167
  13. eodag/plugins/apis/base.py +10 -4
  14. eodag/plugins/apis/ecmwf.py +53 -23
  15. eodag/plugins/apis/usgs.py +41 -17
  16. eodag/plugins/authentication/aws_auth.py +30 -18
  17. eodag/plugins/authentication/base.py +14 -3
  18. eodag/plugins/authentication/generic.py +14 -3
  19. eodag/plugins/authentication/header.py +14 -6
  20. eodag/plugins/authentication/keycloak.py +44 -25
  21. eodag/plugins/authentication/oauth.py +18 -4
  22. eodag/plugins/authentication/openid_connect.py +192 -171
  23. eodag/plugins/authentication/qsauth.py +12 -4
  24. eodag/plugins/authentication/sas_auth.py +22 -5
  25. eodag/plugins/authentication/token.py +95 -17
  26. eodag/plugins/authentication/token_exchange.py +19 -19
  27. eodag/plugins/base.py +4 -4
  28. eodag/plugins/crunch/base.py +8 -5
  29. eodag/plugins/crunch/filter_date.py +9 -6
  30. eodag/plugins/crunch/filter_latest_intersect.py +9 -8
  31. eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
  32. eodag/plugins/crunch/filter_overlap.py +9 -11
  33. eodag/plugins/crunch/filter_property.py +10 -10
  34. eodag/plugins/download/aws.py +181 -105
  35. eodag/plugins/download/base.py +49 -67
  36. eodag/plugins/download/creodias_s3.py +40 -2
  37. eodag/plugins/download/http.py +247 -223
  38. eodag/plugins/download/s3rest.py +29 -28
  39. eodag/plugins/manager.py +176 -41
  40. eodag/plugins/search/__init__.py +6 -5
  41. eodag/plugins/search/base.py +123 -60
  42. eodag/plugins/search/build_search_result.py +1046 -355
  43. eodag/plugins/search/cop_marine.py +132 -39
  44. eodag/plugins/search/creodias_s3.py +19 -68
  45. eodag/plugins/search/csw.py +48 -8
  46. eodag/plugins/search/data_request_search.py +124 -23
  47. eodag/plugins/search/qssearch.py +531 -310
  48. eodag/plugins/search/stac_list_assets.py +85 -0
  49. eodag/plugins/search/static_stac_search.py +23 -24
  50. eodag/resources/ext_product_types.json +1 -1
  51. eodag/resources/product_types.yml +1295 -355
  52. eodag/resources/providers.yml +1819 -3010
  53. eodag/resources/stac.yml +3 -163
  54. eodag/resources/stac_api.yml +2 -2
  55. eodag/resources/user_conf_template.yml +115 -99
  56. eodag/rest/cache.py +2 -2
  57. eodag/rest/config.py +3 -4
  58. eodag/rest/constants.py +0 -1
  59. eodag/rest/core.py +157 -117
  60. eodag/rest/errors.py +181 -0
  61. eodag/rest/server.py +57 -339
  62. eodag/rest/stac.py +133 -581
  63. eodag/rest/types/collections_search.py +3 -3
  64. eodag/rest/types/eodag_search.py +41 -30
  65. eodag/rest/types/queryables.py +42 -32
  66. eodag/rest/types/stac_search.py +15 -16
  67. eodag/rest/utils/__init__.py +14 -21
  68. eodag/rest/utils/cql_evaluate.py +6 -6
  69. eodag/rest/utils/rfc3339.py +2 -2
  70. eodag/types/__init__.py +153 -32
  71. eodag/types/bbox.py +2 -2
  72. eodag/types/download_args.py +4 -4
  73. eodag/types/queryables.py +183 -73
  74. eodag/types/search_args.py +6 -6
  75. eodag/types/whoosh.py +127 -3
  76. eodag/utils/__init__.py +228 -106
  77. eodag/utils/exceptions.py +47 -26
  78. eodag/utils/import_system.py +2 -2
  79. eodag/utils/logging.py +37 -77
  80. eodag/utils/repr.py +65 -6
  81. eodag/utils/requests.py +13 -15
  82. eodag/utils/rest.py +2 -2
  83. eodag/utils/s3.py +231 -0
  84. eodag/utils/stac_reader.py +11 -11
  85. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
  86. eodag-3.1.0.dist-info/RECORD +113 -0
  87. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  88. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
  89. eodag/resources/constraints/climate-dt.json +0 -13
  90. eodag/resources/constraints/extremes-dt.json +0 -8
  91. eodag/utils/constraints.py +0 -244
  92. eodag-3.0.0b3.dist-info/RECORD +0 -110
  93. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  94. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
eodag/utils/__init__.py CHANGED
@@ -54,14 +54,10 @@ from typing import (
54
54
  TYPE_CHECKING,
55
55
  Any,
56
56
  Callable,
57
- Dict,
58
57
  Iterable,
59
58
  Iterator,
60
- List,
61
59
  Mapping,
62
60
  Optional,
63
- Tuple,
64
- Type,
65
61
  Union,
66
62
  cast,
67
63
  )
@@ -79,16 +75,12 @@ from urllib.parse import ( # noqa; noqa
79
75
  )
80
76
  from urllib.request import url2pathname
81
77
 
82
- if sys.version_info >= (3, 9):
83
- from typing import Annotated, get_args, get_origin # noqa
84
- else:
85
- from typing_extensions import Annotated, get_args, get_origin # type: ignore # noqa
86
-
87
78
  if sys.version_info >= (3, 12):
88
79
  from typing import Unpack # type: ignore # noqa
89
80
  else:
90
81
  from typing_extensions import Unpack # noqa
91
82
 
83
+
92
84
  import click
93
85
  import orjson
94
86
  import shapefile
@@ -99,7 +91,7 @@ from dateutil.tz import UTC
99
91
  from jsonpath_ng import jsonpath
100
92
  from jsonpath_ng.ext import parse
101
93
  from jsonpath_ng.jsonpath import Child, Fields, Index, Root, Slice
102
- from requests import HTTPError
94
+ from requests import HTTPError, Response
103
95
  from shapely.geometry import Polygon, shape
104
96
  from shapely.geometry.base import GEOMETRY_TYPES, BaseGeometry
105
97
  from tqdm.auto import tqdm
@@ -110,7 +102,7 @@ from eodag.utils.exceptions import MisconfiguredError
110
102
  if TYPE_CHECKING:
111
103
  from jsonpath_ng import JSONPath
112
104
 
113
- from eodag.api.product import EOProduct
105
+ from eodag.api.product._product import EOProduct
114
106
 
115
107
 
116
108
  logger = py_logging.getLogger("eodag.utils")
@@ -123,11 +115,16 @@ eodag_version = metadata("eodag")["Version"]
123
115
  USER_AGENT = {"User-Agent": f"eodag/{eodag_version}"}
124
116
 
125
117
  HTTP_REQ_TIMEOUT = 5 # in seconds
118
+ DEFAULT_SEARCH_TIMEOUT = 20 # in seconds
126
119
  DEFAULT_STREAM_REQUESTS_TIMEOUT = 60 # in seconds
127
120
 
121
+ REQ_RETRY_TOTAL = 3
122
+ REQ_RETRY_BACKOFF_FACTOR = 2
123
+ REQ_RETRY_STATUS_FORCELIST = [401, 429, 500, 502, 503, 504]
124
+
128
125
  # default wait times in minutes
129
- DEFAULT_DOWNLOAD_WAIT = 2 # in minutes
130
- DEFAULT_DOWNLOAD_TIMEOUT = 20 # in minutes
126
+ DEFAULT_DOWNLOAD_WAIT = 0.2 # in minutes
127
+ DEFAULT_DOWNLOAD_TIMEOUT = 10 # in minutes
131
128
 
132
129
  JSONPATH_MATCH = re.compile(r"^[\{\(]*\$(\..*)*$")
133
130
  WORKABLE_JSONPATH_MATCH = re.compile(r"^\$(\.[a-zA-Z0-9-_:\.\[\]\"\(\)=\?\*]+)*$")
@@ -143,6 +140,13 @@ DEFAULT_MAX_ITEMS_PER_PAGE = 50
143
140
  # default product-types start date
144
141
  DEFAULT_MISSION_START_DATE = "2015-01-01T00:00:00Z"
145
142
 
143
+ # update missing mimetypes
144
+ mimetypes.add_type("text/xml", ".xsd")
145
+ mimetypes.add_type("application/x-grib", ".grib")
146
+ mimetypes.add_type("application/x-grib2", ".grib2")
147
+ # jp2 is missing on windows
148
+ mimetypes.add_type("image/jp2", ".jp2")
149
+
146
150
 
147
151
  def _deprecated(reason: str = "", version: Optional[str] = None) -> Callable[..., Any]:
148
152
  """Simple decorator to mark functions/methods/classes as deprecated.
@@ -237,9 +241,10 @@ class FloatRange(click.types.FloatParamType):
237
241
  def slugify(value: Any, allow_unicode: bool = False) -> str:
238
242
  """Copied from Django Source code, only modifying last line (no need for safe
239
243
  strings).
244
+
240
245
  source: https://github.com/django/django/blob/master/django/utils/text.py
241
246
 
242
- Convert to ASCII if 'allow_unicode' is False. Convert spaces to hyphens.
247
+ Convert to ASCII if ``allow_unicode`` is ``False``. Convert spaces to hyphens.
243
248
  Remove characters that aren't alphanumerics, underscores, or hyphens.
244
249
  Convert to lowercase. Also strip leading and trailing whitespace.
245
250
  """
@@ -298,7 +303,7 @@ def strip_accents(s: str) -> str:
298
303
 
299
304
  def uri_to_path(uri: str) -> str:
300
305
  """
301
- Convert a file URI (e.g. 'file:///tmp') to a local path (e.g. '/tmp')
306
+ Convert a file URI (e.g. ``file:///tmp``) to a local path (e.g. ``/tmp``)
302
307
  """
303
308
  if not uri.startswith("file"):
304
309
  raise ValueError("A file URI must be provided (e.g. 'file:///tmp'")
@@ -314,7 +319,7 @@ def path_to_uri(path: str) -> str:
314
319
  return Path(path).as_uri()
315
320
 
316
321
 
317
- def mutate_dict_in_place(func: Callable[[Any], Any], mapping: Dict[Any, Any]) -> None:
322
+ def mutate_dict_in_place(func: Callable[[Any], Any], mapping: dict[Any, Any]) -> None:
318
323
  """Apply func to values of mapping.
319
324
 
320
325
  The mapping object's values are modified in-place. The function is recursive,
@@ -332,11 +337,11 @@ def mutate_dict_in_place(func: Callable[[Any], Any], mapping: Dict[Any, Any]) ->
332
337
  mapping[key] = func(value)
333
338
 
334
339
 
335
- def merge_mappings(mapping1: Dict[Any, Any], mapping2: Dict[Any, Any]) -> None:
336
- """Merge two mappings with string keys, values from `mapping2` overriding values
337
- from `mapping1`.
340
+ def merge_mappings(mapping1: dict[Any, Any], mapping2: dict[Any, Any]) -> None:
341
+ """Merge two mappings with string keys, values from ``mapping2`` overriding values
342
+ from ``mapping1``.
338
343
 
339
- Do its best to detect the key in `mapping1` to override. For example::
344
+ Do its best to detect the key in ``mapping1`` to override. For example:
340
345
 
341
346
  >>> mapping2 = {"keya": "new"}
342
347
  >>> mapping1 = {"keyA": "obsolete"}
@@ -344,12 +349,11 @@ def merge_mappings(mapping1: Dict[Any, Any], mapping2: Dict[Any, Any]) -> None:
344
349
  >>> mapping1
345
350
  {'keyA': 'new'}
346
351
 
347
- If mapping2 has a key that cannot be detected in mapping1, this new key is added
348
- to mapping1 as is.
352
+ If ``mapping2`` has a key that cannot be detected in ``mapping1``, this new key is
353
+ added to ``mapping1`` as is.
349
354
 
350
355
  :param mapping1: The mapping containing values to be overridden
351
- :param mapping2: The mapping containing values that will override the
352
- first mapping
356
+ :param mapping2: The mapping containing values that will override the first mapping
353
357
  """
354
358
  # A mapping between mapping1 keys as lowercase strings and original mapping1 keys
355
359
  m1_keys_lowercase = {key.lower(): key for key in mapping1}
@@ -416,7 +420,7 @@ def get_timestamp(date_time: str) -> float:
416
420
  If the datetime has no offset, it is assumed to be an UTC datetime.
417
421
 
418
422
  :param date_time: The datetime string to return as timestamp
419
- :returns: The timestamp corresponding to the date_time string in seconds
423
+ :returns: The timestamp corresponding to the ``date_time`` string in seconds
420
424
  """
421
425
  dt = isoparse(date_time)
422
426
  if not dt.tzinfo:
@@ -425,12 +429,39 @@ def get_timestamp(date_time: str) -> float:
425
429
 
426
430
 
427
431
  def datetime_range(start: dt, end: dt) -> Iterator[dt]:
428
- """Generator function for all dates in-between start and end date."""
432
+ """Generator function for all dates in-between ``start`` and ``end`` date."""
429
433
  delta = end - start
430
434
  for nday in range(delta.days + 1):
431
435
  yield start + datetime.timedelta(days=nday)
432
436
 
433
437
 
438
+ def is_range_in_range(valid_range: str, check_range: str) -> bool:
439
+ """Check if the check_range is completely within the valid_range.
440
+
441
+ This function checks if both the start and end dates of the check_range
442
+ are within the start and end dates of the valid_range.
443
+
444
+ :param valid_range: The valid date range in the format 'YYYY-MM-DD/YYYY-MM-DD'.
445
+ :param check_range: The date range to check in the format 'YYYY-MM-DD/YYYY-MM-DD'.
446
+ :returns: True if check_range is within valid_range, otherwise False.
447
+ """
448
+ if "/" not in valid_range or "/" not in check_range:
449
+ return False
450
+
451
+ # Split the date ranges into start and end dates
452
+ start_valid, end_valid = valid_range.split("/")
453
+ start_check, end_check = check_range.split("/")
454
+
455
+ # Convert the strings to datetime objects using fromisoformat
456
+ start_valid_dt = datetime.datetime.fromisoformat(start_valid)
457
+ end_valid_dt = datetime.datetime.fromisoformat(end_valid)
458
+ start_check_dt = datetime.datetime.fromisoformat(start_check)
459
+ end_check_dt = datetime.datetime.fromisoformat(end_check)
460
+
461
+ # Check if check_range is within valid_range
462
+ return start_valid_dt <= start_check_dt and end_valid_dt >= end_check_dt
463
+
464
+
434
465
  class DownloadedCallback:
435
466
  """Example class for callback after each download in :meth:`~eodag.api.core.EODataAccessGateway.download_all`"""
436
467
 
@@ -445,15 +476,15 @@ class DownloadedCallback:
445
476
  class ProgressCallback(tqdm):
446
477
  """A callable used to render progress to users for long running processes.
447
478
 
448
- It inherits from `tqdm.auto.tqdm`, and accepts the same arguments on
449
- instantiation: `iterable`, `desc`, `total`, `leave`, `file`, `ncols`,
450
- `mininterval`, `maxinterval`, `miniters`, `ascii`, `disable`, `unit`,
451
- `unit_scale`, `dynamic_ncols`, `smoothing`, `bar_format`, `initial`,
452
- `position`, `postfix`, `unit_divisor`.
479
+ It inherits from :class:`tqdm.auto.tqdm`, and accepts the same arguments on
480
+ instantiation: ``iterable``, ``desc``, ``total``, ``leave``, ``file``, ``ncols``,
481
+ ``mininterval``, ``maxinterval``, ``miniters``, ``ascii``, ``disable``, ``unit``,
482
+ ``unit_scale``, ``dynamic_ncols``, ``smoothing``, ``bar_format``, ``initial``,
483
+ ``position``, ``postfix``, ``unit_divisor``.
453
484
 
454
- It can be globally disabled using `eodag.utils.logging.setup_logging(0)` or
455
- `eodag.utils.logging.setup_logging(level, no_progress_bar=True)`, and
456
- individually disabled using `disable=True`.
485
+ It can be globally disabled using ``eodag.utils.logging.setup_logging(0)`` or
486
+ ``eodag.utils.logging.setup_logging(level, no_progress_bar=True)``, and
487
+ individually disabled using ``disable=True``.
457
488
  """
458
489
 
459
490
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -488,8 +519,8 @@ class ProgressCallback(tqdm):
488
519
  """Returns another progress callback using the same initial
489
520
  keyword-arguments.
490
521
 
491
- Optional `args` and `kwargs` parameters will be used to create a
492
- new `~eodag.utils.ProgressCallback` instance, overriding initial
522
+ Optional ``args`` and ``kwargs`` parameters will be used to create a
523
+ new :class:`~eodag.utils.ProgressCallback` instance, overriding initial
493
524
  `kwargs`.
494
525
  """
495
526
 
@@ -511,7 +542,7 @@ def get_progress_callback() -> tqdm:
511
542
 
512
543
 
513
544
  def repeatfunc(func: Callable[..., Any], n: int, *args: Any) -> starmap:
514
- """Call `func` `n` times with `args`"""
545
+ """Call ``func`` ``n`` times with ``args``"""
515
546
  return starmap(func, repeat(args, n))
516
547
 
517
548
 
@@ -526,12 +557,12 @@ def makedirs(dirpath: str) -> None:
526
557
 
527
558
 
528
559
  def rename_subfolder(dirpath: str, name: str) -> None:
529
- """Rename first subfolder found in dirpath with given name,
530
- raise RuntimeError if no subfolder can be found
560
+ """Rename first subfolder found in ``dirpath`` with given ``name``,
561
+ raise :class:`RuntimeError` if no subfolder can be found
531
562
 
532
563
  :param dirpath: path to the directory containing the subfolder
533
564
  :param name: new name of the subfolder
534
- :raises: RuntimeError
565
+ :raises: :class:`RuntimeError`
535
566
 
536
567
  Example:
537
568
 
@@ -545,16 +576,20 @@ def rename_subfolder(dirpath: str, name: str) -> None:
545
576
  ... rename_subfolder(tmpdir, "otherfolder")
546
577
  ... assert not os.path.isdir(somefolder) and os.path.isdir(otherfolder)
547
578
 
548
- Before:
579
+ Before::
580
+
549
581
  $ tree <tmp-folder>
550
582
  <tmp-folder>
551
583
  └── somefolder
552
584
  └── somefile
553
- After:
585
+
586
+ After::
587
+
554
588
  $ tree <tmp-folder>
555
589
  <tmp-folder>
556
590
  └── otherfolder
557
591
  └── somefile
592
+
558
593
  """
559
594
  try:
560
595
  subdir, *_ = (p for p in glob(os.path.join(dirpath, "*")) if os.path.isdir(p))
@@ -567,10 +602,50 @@ def rename_subfolder(dirpath: str, name: str) -> None:
567
602
  )
568
603
 
569
604
 
605
+ def rename_with_version(file_path: str, suffix: str = "old") -> str:
606
+ """
607
+ Renames a file by appending and incrementing a version number if a conflict exists.
608
+
609
+ :param file_path: full path of the file to rename
610
+ :param suffix: suffix preceding version number in case of name conflict
611
+ :returns: new file path with the version appended or incremented
612
+
613
+ Example:
614
+
615
+ >>> import tempfile
616
+ >>> from pathlib import Path
617
+ >>> with tempfile.TemporaryDirectory() as tmpdir:
618
+ ... file_path = (Path(tmpdir) / "foo.txt")
619
+ ... file_path.touch()
620
+ ... (Path(tmpdir) / "foo_old1.txt").touch()
621
+ ... expected = str(Path(tmpdir) / "foo_old2.txt")
622
+ ... assert expected == rename_with_version(str(file_path))
623
+
624
+ """
625
+ if not os.path.isfile(file_path):
626
+ raise FileNotFoundError(f"The file '{file_path}' does not exist.")
627
+
628
+ dir_path, file_name = os.path.split(file_path)
629
+ file_base, file_ext = os.path.splitext(file_name)
630
+
631
+ new_file_path = file_path
632
+
633
+ # loop and iterate on conflicting existing files
634
+ version = 0
635
+ while os.path.exists(new_file_path):
636
+ version += 1
637
+ new_file_name = f"{file_base}_{suffix}{version}{file_ext}"
638
+ new_file_path = os.path.join(dir_path, new_file_name)
639
+
640
+ # Rename the file
641
+ os.rename(file_path, new_file_path)
642
+ return new_file_path
643
+
644
+
570
645
  def format_dict_items(
571
- config_dict: Dict[str, Any], **format_variables: Any
572
- ) -> Dict[Any, Any]:
573
- r"""Recursive apply string.format(\**format_variables) to dict elements
646
+ config_dict: dict[str, Any], **format_variables: Any
647
+ ) -> dict[Any, Any]:
648
+ r"""Recursively apply :meth:`str.format` to ``**format_variables`` on ``config_dict`` values
574
649
 
575
650
  >>> format_dict_items(
576
651
  ... {"foo": {"bar": "{a}"}, "baz": ["{b}?", "{b}!"]},
@@ -578,7 +653,7 @@ def format_dict_items(
578
653
  ... ) == {"foo": {"bar": "qux"}, "baz": ["quux?", "quux!"]}
579
654
  True
580
655
 
581
- :param config_dict: Dictionnary having values that need to be parsed
656
+ :param config_dict: Dictionary having values that need to be parsed
582
657
  :param format_variables: Variables used as args for parsing
583
658
  :returns: Updated dict
584
659
  """
@@ -586,9 +661,9 @@ def format_dict_items(
586
661
 
587
662
 
588
663
  def jsonpath_parse_dict_items(
589
- jsonpath_dict: Dict[str, Any], values_dict: Dict[str, Any]
590
- ) -> Dict[Any, Any]:
591
- """Recursive parse jsonpath elements in dict
664
+ jsonpath_dict: dict[str, Any], values_dict: dict[str, Any]
665
+ ) -> dict[Any, Any]:
666
+ """Recursively parse :class:`jsonpath_ng.JSONPath` elements in dict
592
667
 
593
668
  >>> import jsonpath_ng.ext as jsonpath
594
669
  >>> jsonpath_parse_dict_items(
@@ -597,7 +672,7 @@ def jsonpath_parse_dict_items(
597
672
  ... ) == {'foo': {'bar': 'baz'}, 'qux': ['quux', 'quux']}
598
673
  True
599
674
 
600
- :param jsonpath_dict: Dictionnary having values that need to be parsed
675
+ :param jsonpath_dict: Dictionary having :class:`jsonpath_ng.JSONPath` values that need to be parsed
601
676
  :param values_dict: Values dict used as args for parsing
602
677
  :returns: Updated dict
603
678
  """
@@ -605,13 +680,13 @@ def jsonpath_parse_dict_items(
605
680
 
606
681
 
607
682
  def update_nested_dict(
608
- old_dict: Dict[Any, Any],
609
- new_dict: Dict[Any, Any],
683
+ old_dict: dict[Any, Any],
684
+ new_dict: dict[Any, Any],
610
685
  extend_list_values: bool = False,
611
686
  allow_empty_values: bool = False,
612
687
  allow_extend_duplicates: bool = True,
613
- ) -> Dict[Any, Any]:
614
- """Update recursively old_dict items with new_dict ones
688
+ ) -> dict[Any, Any]:
689
+ """Update recursively ``old_dict`` items with ``new_dict`` ones
615
690
 
616
691
  >>> update_nested_dict(
617
692
  ... {"a": {"a.a": 1, "a.b": 2}, "b": 3},
@@ -690,10 +765,10 @@ def update_nested_dict(
690
765
 
691
766
 
692
767
  def items_recursive_apply(
693
- input_obj: Union[Dict[Any, Any], List[Any]],
768
+ input_obj: Union[dict[Any, Any], list[Any]],
694
769
  apply_method: Callable[..., Any],
695
770
  **apply_method_parameters: Any,
696
- ) -> Union[Dict[Any, Any], List[Any]]:
771
+ ) -> Union[dict[Any, Any], list[Any]]:
697
772
  """Recursive apply method to items contained in input object (dict or list)
698
773
 
699
774
  >>> items_recursive_apply(
@@ -731,10 +806,10 @@ def items_recursive_apply(
731
806
 
732
807
 
733
808
  def dict_items_recursive_apply(
734
- config_dict: Dict[Any, Any],
809
+ config_dict: dict[Any, Any],
735
810
  apply_method: Callable[..., Any],
736
811
  **apply_method_parameters: Any,
737
- ) -> Dict[Any, Any]:
812
+ ) -> dict[Any, Any]:
738
813
  """Recursive apply method to dict elements
739
814
 
740
815
  >>> dict_items_recursive_apply(
@@ -743,12 +818,12 @@ def dict_items_recursive_apply(
743
818
  ... ) == {'foo': {'bar': 'BAZ!'}, 'qux': ['A!', 'B!']}
744
819
  True
745
820
 
746
- :param config_dict: Input nested dictionnary
821
+ :param config_dict: Input nested dictionary
747
822
  :param apply_method: Method to be applied to dict elements
748
823
  :param apply_method_parameters: Optional parameters passed to the method
749
824
  :returns: Updated dict
750
825
  """
751
- result_dict: Dict[Any, Any] = deepcopy(config_dict)
826
+ result_dict: dict[Any, Any] = deepcopy(config_dict)
752
827
  for dict_k, dict_v in result_dict.items():
753
828
  if isinstance(dict_v, dict):
754
829
  result_dict[dict_k] = dict_items_recursive_apply(
@@ -756,7 +831,7 @@ def dict_items_recursive_apply(
756
831
  )
757
832
  elif any(isinstance(dict_v, t) for t in (list, tuple)):
758
833
  result_dict[dict_k] = list_items_recursive_apply(
759
- dict_v, apply_method, **apply_method_parameters
834
+ list(dict_v), apply_method, **apply_method_parameters
760
835
  )
761
836
  else:
762
837
  result_dict[dict_k] = apply_method(
@@ -767,10 +842,10 @@ def dict_items_recursive_apply(
767
842
 
768
843
 
769
844
  def list_items_recursive_apply(
770
- config_list: List[Any],
845
+ config_list: list[Any],
771
846
  apply_method: Callable[..., Any],
772
847
  **apply_method_parameters: Any,
773
- ) -> List[Any]:
848
+ ) -> list[Any]:
774
849
  """Recursive apply method to list elements
775
850
 
776
851
  >>> list_items_recursive_apply(
@@ -803,8 +878,8 @@ def list_items_recursive_apply(
803
878
 
804
879
 
805
880
  def items_recursive_sort(
806
- input_obj: Union[List[Any], Dict[Any, Any]],
807
- ) -> Union[List[Any], Dict[Any, Any]]:
881
+ input_obj: Union[list[Any], dict[Any, Any]],
882
+ ) -> Union[list[Any], dict[Any, Any]]:
808
883
  """Recursive sort dict items contained in input object (dict or list)
809
884
 
810
885
  >>> items_recursive_sort(
@@ -828,7 +903,7 @@ def items_recursive_sort(
828
903
  return input_obj
829
904
 
830
905
 
831
- def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
906
+ def dict_items_recursive_sort(config_dict: dict[Any, Any]) -> dict[Any, Any]:
832
907
  """Recursive sort dict elements
833
908
 
834
909
  >>> dict_items_recursive_sort(
@@ -836,10 +911,10 @@ def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
836
911
  ... ) == {"a": ["b", {0: 1, 1: 2, 2: 0}], "b": {"a": 0, "b": "c"}}
837
912
  True
838
913
 
839
- :param config_dict: Input nested dictionnary
914
+ :param config_dict: Input nested dictionary
840
915
  :returns: Updated dict
841
916
  """
842
- result_dict: Dict[Any, Any] = deepcopy(config_dict)
917
+ result_dict: dict[Any, Any] = deepcopy(config_dict)
843
918
  for dict_k, dict_v in result_dict.items():
844
919
  if isinstance(dict_v, dict):
845
920
  result_dict[dict_k] = dict_items_recursive_sort(dict_v)
@@ -851,7 +926,7 @@ def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
851
926
  return dict(sorted(result_dict.items()))
852
927
 
853
928
 
854
- def list_items_recursive_sort(config_list: List[Any]) -> List[Any]:
929
+ def list_items_recursive_sort(config_list: list[Any]) -> list[Any]:
855
930
  """Recursive sort dicts in list elements
856
931
 
857
932
  >>> list_items_recursive_sort(["b", {2: 0, 0: 1, 1: 2}])
@@ -860,7 +935,7 @@ def list_items_recursive_sort(config_list: List[Any]) -> List[Any]:
860
935
  :param config_list: Input list containing nested lists/dicts
861
936
  :returns: Updated list
862
937
  """
863
- result_list: List[Any] = deepcopy(config_list)
938
+ result_list: list[Any] = deepcopy(config_list)
864
939
  for list_idx, list_v in enumerate(result_list):
865
940
  if isinstance(list_v, dict):
866
941
  result_list[list_idx] = dict_items_recursive_sort(list_v)
@@ -873,7 +948,7 @@ def list_items_recursive_sort(config_list: List[Any]) -> List[Any]:
873
948
 
874
949
 
875
950
  def string_to_jsonpath(*args: Any, force: bool = False) -> Union[str, JSONPath]:
876
- """Get jsonpath for "$.foo.bar" like string
951
+ """Get :class:`jsonpath_ng.JSONPath` for ``$.foo.bar`` like string
877
952
 
878
953
  >>> string_to_jsonpath(None, "$.foo.bar")
879
954
  Child(Child(Root(), Fields('foo')), Fields('bar'))
@@ -887,7 +962,7 @@ def string_to_jsonpath(*args: Any, force: bool = False) -> Union[str, JSONPath]:
887
962
  Fields('foo')
888
963
 
889
964
  :param args: Last arg as input string value, to be converted
890
- :param force: force conversion even if input string is not detected as a jsonpath
965
+ :param force: force conversion even if input string is not detected as a :class:`jsonpath_ng.JSONPath`
891
966
  :returns: Parsed value
892
967
  """
893
968
  path_str: str = args[-1]
@@ -950,7 +1025,7 @@ def string_to_jsonpath(*args: Any, force: bool = False) -> Union[str, JSONPath]:
950
1025
 
951
1026
 
952
1027
  def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
953
- """Format "{foo}" like string
1028
+ """Format ``"{foo}"``-like string
954
1029
 
955
1030
  >>> format_string(None, "foo {bar}, {baz} ?", **{"bar": "qux", "baz": "quux"})
956
1031
  'foo qux, quux ?'
@@ -973,7 +1048,7 @@ def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
973
1048
  # defaultdict usage will return "" for missing keys in format_args
974
1049
  try:
975
1050
  result = str_to_format.format_map(defaultdict(str, **format_variables))
976
- except TypeError as e:
1051
+ except (ValueError, TypeError) as e:
977
1052
  raise MisconfiguredError(
978
1053
  f"Unable to format str={str_to_format} using {str(format_variables)}: {str(e)}"
979
1054
  )
@@ -986,9 +1061,9 @@ def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
986
1061
 
987
1062
 
988
1063
  def parse_jsonpath(
989
- key: str, jsonpath_obj: Union[str, jsonpath.Child], **values_dict: Dict[str, Any]
1064
+ key: str, jsonpath_obj: Union[str, jsonpath.Child], **values_dict: dict[str, Any]
990
1065
  ) -> Optional[str]:
991
- """Parse jsonpah in jsonpath_obj using values_dict
1066
+ """Parse jsonpah in ``jsonpath_obj`` using ``values_dict``
992
1067
 
993
1068
  >>> import jsonpath_ng.ext as jsonpath
994
1069
  >>> parse_jsonpath(None, parse("$.foo.bar"), **{"foo": {"bar": "baz"}})
@@ -1006,7 +1081,7 @@ def parse_jsonpath(
1006
1081
  return jsonpath_obj
1007
1082
 
1008
1083
 
1009
- def nested_pairs2dict(pairs: Union[List[Any], Any]) -> Union[Any, Dict[Any, Any]]:
1084
+ def nested_pairs2dict(pairs: Union[list[Any], Any]) -> Union[Any, dict[Any, Any]]:
1010
1085
  """Create a dict using nested pairs
1011
1086
 
1012
1087
  >>> nested_pairs2dict([["foo", [["bar", "baz"]]]])
@@ -1028,12 +1103,12 @@ def nested_pairs2dict(pairs: Union[List[Any], Any]) -> Union[Any, Dict[Any, Any]
1028
1103
 
1029
1104
 
1030
1105
  def get_geometry_from_various(
1031
- locations_config: List[Dict[str, Any]] = [], **query_args: Any
1106
+ locations_config: list[dict[str, Any]] = [], **query_args: Any
1032
1107
  ) -> BaseGeometry:
1033
- """Creates a shapely geometry using given query kwargs arguments
1108
+ """Creates a ``shapely.geometry`` using given query kwargs arguments
1034
1109
 
1035
1110
  :param locations_config: (optional) EODAG locations configuration
1036
- :param query_args: Query kwargs arguments from core.search() method
1111
+ :param query_args: Query kwargs arguments from :meth:`~eodag.api.core.EODataAccessGateway.search`
1037
1112
  :returns: shapely Geometry found
1038
1113
  :raises: :class:`ValueError`
1039
1114
  """
@@ -1117,7 +1192,7 @@ def get_geometry_from_various(
1117
1192
  class MockResponse:
1118
1193
  """Fake requests response"""
1119
1194
 
1120
- def __init__(self, json_data: Any, status_code: int) -> None:
1195
+ def __init__(self, json_data: Any = None, status_code: int = 200) -> None:
1121
1196
  self.json_data = json_data
1122
1197
  self.status_code = status_code
1123
1198
  self.content = json_data
@@ -1126,10 +1201,21 @@ class MockResponse:
1126
1201
  """Return json data"""
1127
1202
  return self.json_data
1128
1203
 
1204
+ def __iter__(self):
1205
+ yield self
1206
+
1207
+ def __enter__(self):
1208
+ return self
1209
+
1210
+ def __exit__(self, exc_type, exc_val, exc_tb):
1211
+ pass
1212
+
1129
1213
  def raise_for_status(self) -> None:
1130
1214
  """raises an exception when the status is not ok"""
1131
1215
  if self.status_code != 200:
1132
- raise HTTPError()
1216
+ response = Response()
1217
+ response.status_code = self.status_code
1218
+ raise HTTPError(response=response)
1133
1219
 
1134
1220
 
1135
1221
  def md5sum(file_path: str) -> str:
@@ -1163,7 +1249,7 @@ def obj_md5sum(data: Any) -> str:
1163
1249
 
1164
1250
  @functools.lru_cache()
1165
1251
  def cached_parse(str_to_parse: str) -> JSONPath:
1166
- """Cached jsonpath_ng.ext.parse
1252
+ """Cached :func:`jsonpath_ng.ext.parse`
1167
1253
 
1168
1254
  >>> cached_parse.cache_clear()
1169
1255
  >>> cached_parse("$.foo")
@@ -1179,8 +1265,8 @@ def cached_parse(str_to_parse: str) -> JSONPath:
1179
1265
  >>> cached_parse.cache_info()
1180
1266
  CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)
1181
1267
 
1182
- :param str_to_parse: string to parse as jsonpath
1183
- :returns: parsed jsonpath
1268
+ :param str_to_parse: string to parse as :class:`jsonpath_ng.JSONPath`
1269
+ :returns: parsed :class:`jsonpath_ng.JSONPath`
1184
1270
  """
1185
1271
  return parse(str_to_parse)
1186
1272
 
@@ -1193,8 +1279,8 @@ def _mutable_cached_yaml_load(config_path: str) -> Any:
1193
1279
  return yaml.load(fh, Loader=yaml.SafeLoader)
1194
1280
 
1195
1281
 
1196
- def cached_yaml_load(config_path: str) -> Dict[str, Any]:
1197
- """Cached yaml.load
1282
+ def cached_yaml_load(config_path: str) -> dict[str, Any]:
1283
+ """Cached :func:`yaml.load`
1198
1284
 
1199
1285
  :param config_path: path to the yaml configuration file
1200
1286
  :returns: loaded yaml configuration
@@ -1203,13 +1289,13 @@ def cached_yaml_load(config_path: str) -> Dict[str, Any]:
1203
1289
 
1204
1290
 
1205
1291
  @functools.lru_cache()
1206
- def _mutable_cached_yaml_load_all(config_path: str) -> List[Any]:
1292
+ def _mutable_cached_yaml_load_all(config_path: str) -> list[Any]:
1207
1293
  with open(config_path, "r") as fh:
1208
1294
  return list(yaml.load_all(fh, Loader=yaml.Loader))
1209
1295
 
1210
1296
 
1211
- def cached_yaml_load_all(config_path: str) -> List[Any]:
1212
- """Cached yaml.load_all
1297
+ def cached_yaml_load_all(config_path: str) -> list[Any]:
1298
+ """Cached :func:`yaml.load_all`
1213
1299
 
1214
1300
  Load all configurations stored in the configuration file as separated yaml documents
1215
1301
 
@@ -1221,7 +1307,7 @@ def cached_yaml_load_all(config_path: str) -> List[Any]:
1221
1307
 
1222
1308
  def get_bucket_name_and_prefix(
1223
1309
  url: str, bucket_path_level: Optional[int] = None
1224
- ) -> Tuple[Optional[str], Optional[str]]:
1310
+ ) -> tuple[Optional[str], Optional[str]]:
1225
1311
  """Extract bucket name and prefix from URL
1226
1312
 
1227
1313
  :param url: (optional) URL to use as product.location
@@ -1234,7 +1320,9 @@ def get_bucket_name_and_prefix(
1234
1320
  subdomain = netloc.split(".")[0]
1235
1321
  path = path.strip("/")
1236
1322
 
1237
- if scheme and bucket_path_level is None:
1323
+ if "/" in path and scheme and subdomain == "s3" and bucket_path_level is None:
1324
+ bucket, prefix = path.split("/", 1)
1325
+ elif scheme and bucket_path_level is None:
1238
1326
  bucket = subdomain
1239
1327
  prefix = path
1240
1328
  elif not scheme and bucket_path_level is None:
@@ -1274,15 +1362,16 @@ def flatten_top_directories(
1274
1362
 
1275
1363
  def deepcopy(sth: Any) -> Any:
1276
1364
  """Customized and faster deepcopy inspired by https://stackoverflow.com/a/45858907
1277
- `_copy_list` and `_copy_dict` available for the moment
1365
+
1366
+ ``_copy_list`` and ``_copy_dict`` dispatchers available for the moment
1278
1367
 
1279
1368
  :param sth: Object to copy
1280
1369
  :returns: Copied object
1281
1370
  """
1282
- _dispatcher: Dict[Type[Any], Callable[..., Any]] = {}
1371
+ _dispatcher: dict[type[Any], Callable[..., Any]] = {}
1283
1372
 
1284
1373
  def _copy_list(
1285
- input_list: List[Any], dispatch: Dict[Type[Any], Callable[..., Any]]
1374
+ input_list: list[Any], dispatch: dict[type[Any], Callable[..., Any]]
1286
1375
  ):
1287
1376
  ret = input_list.copy()
1288
1377
  for idx, item in enumerate(ret):
@@ -1292,7 +1381,7 @@ def deepcopy(sth: Any) -> Any:
1292
1381
  return ret
1293
1382
 
1294
1383
  def _copy_dict(
1295
- input_dict: Dict[Any, Any], dispatch: Dict[Type[Any], Callable[..., Any]]
1384
+ input_dict: dict[Any, Any], dispatch: dict[type[Any], Callable[..., Any]]
1296
1385
  ):
1297
1386
  ret = input_dict.copy()
1298
1387
  for key, value in ret.items():
@@ -1339,7 +1428,7 @@ def cast_scalar_value(value: Any, new_type: Any) -> Any:
1339
1428
 
1340
1429
  :param value: the scalar value to convert
1341
1430
  :param new_type: the wanted type
1342
- :returns: scalar value converted to new_type
1431
+ :returns: scalar ``value`` converted to ``new_type``
1343
1432
  """
1344
1433
  if isinstance(value, str) and new_type is bool:
1345
1434
  # Bool is a type with special meaning in Python, thus the special
@@ -1369,24 +1458,42 @@ class StreamResponse:
1369
1458
 
1370
1459
 
1371
1460
  def guess_file_type(file: str) -> Optional[str]:
1372
- """guess the mime type of a file or URL based on its extension"""
1373
- mimetypes.add_type("text/xml", ".xsd")
1374
- mimetypes.add_type("application/x-grib", ".grib")
1461
+ """Guess the mime type of a file or URL based on its extension,
1462
+ using eodag extended mimetypes definition
1463
+
1464
+ >>> guess_file_type('foo.tiff')
1465
+ 'image/tiff'
1466
+ >>> guess_file_type('foo.grib')
1467
+ 'application/x-grib'
1468
+
1469
+ :param file: file url or path
1470
+ :returns: guessed mime type
1471
+ """
1375
1472
  mime_type, _ = mimetypes.guess_type(file, False)
1473
+ if mime_type == "text/xml":
1474
+ return "application/xml"
1376
1475
  return mime_type
1377
1476
 
1378
1477
 
1379
1478
  def guess_extension(type: str) -> Optional[str]:
1380
- """guess extension from mime type"""
1381
- mimetypes.add_type("text/xml", ".xsd")
1382
- mimetypes.add_type("application/x-grib", ".grib")
1479
+ """Guess extension from mime type, using eodag extended mimetypes definition
1480
+
1481
+ >>> guess_extension('image/tiff')
1482
+ '.tiff'
1483
+ >>> guess_extension('application/x-grib')
1484
+ '.grib'
1485
+
1486
+ :param type: mime type
1487
+ :returns: guessed file extension
1488
+ """
1383
1489
  return mimetypes.guess_extension(type, strict=False)
1384
1490
 
1385
1491
 
1386
1492
  def get_ssl_context(ssl_verify: bool) -> ssl.SSLContext:
1387
1493
  """
1388
- Returns an SSL context based on ssl_verify argument.
1389
- :param ssl_verify: ssl_verify parameter
1494
+ Returns an SSL context based on ``ssl_verify`` argument.
1495
+
1496
+ :param ssl_verify: :attr:`~eodag.config.PluginConfig.ssl_verify` parameter
1390
1497
  :returns: An SSL context object.
1391
1498
  """
1392
1499
  ctx = ssl.create_default_context()
@@ -1399,7 +1506,7 @@ def get_ssl_context(ssl_verify: bool) -> ssl.SSLContext:
1399
1506
  return ctx
1400
1507
 
1401
1508
 
1402
- def sort_dict(input_dict: Dict[str, Any]) -> Dict[str, Any]:
1509
+ def sort_dict(input_dict: dict[str, Any]) -> dict[str, Any]:
1403
1510
  """
1404
1511
  Recursively sorts a dict by keys.
1405
1512
 
@@ -1413,3 +1520,18 @@ def sort_dict(input_dict: Dict[str, Any]) -> Dict[str, Any]:
1413
1520
  k: sort_dict(v) if isinstance(v, dict) else v
1414
1521
  for k, v in sorted(input_dict.items())
1415
1522
  }
1523
+
1524
+
1525
+ def dict_md5sum(input_dict: dict[str, Any]) -> str:
1526
+ """
1527
+ Hash nested dictionary
1528
+
1529
+ :param input_dict: input dict
1530
+ :returns: hash
1531
+
1532
+ >>> hd = dict_md5sum({"b": {"c": 1, "a": 2, "b": 3}, "a": 4})
1533
+ >>> hd
1534
+ 'a195bcef1bb3b419e9e74b7cc5db8098'
1535
+ >>> assert(dict_md5sum({"a": 4, "b": {"b": 3, "c": 1, "a": 2}}) == hd)
1536
+ """
1537
+ return obj_md5sum(sort_dict(input_dict))