eodag 3.0.0b3__py3-none-any.whl → 3.1.0b1__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 (77) hide show
  1. eodag/api/core.py +292 -198
  2. eodag/api/product/_assets.py +6 -6
  3. eodag/api/product/_product.py +18 -18
  4. eodag/api/product/metadata_mapping.py +51 -14
  5. eodag/api/search_result.py +29 -3
  6. eodag/cli.py +57 -20
  7. eodag/config.py +413 -117
  8. eodag/plugins/apis/base.py +10 -4
  9. eodag/plugins/apis/ecmwf.py +49 -16
  10. eodag/plugins/apis/usgs.py +30 -7
  11. eodag/plugins/authentication/aws_auth.py +14 -5
  12. eodag/plugins/authentication/base.py +10 -1
  13. eodag/plugins/authentication/generic.py +14 -3
  14. eodag/plugins/authentication/header.py +12 -4
  15. eodag/plugins/authentication/keycloak.py +41 -22
  16. eodag/plugins/authentication/oauth.py +11 -1
  17. eodag/plugins/authentication/openid_connect.py +178 -163
  18. eodag/plugins/authentication/qsauth.py +12 -4
  19. eodag/plugins/authentication/sas_auth.py +19 -2
  20. eodag/plugins/authentication/token.py +93 -15
  21. eodag/plugins/authentication/token_exchange.py +19 -19
  22. eodag/plugins/crunch/base.py +4 -1
  23. eodag/plugins/crunch/filter_date.py +5 -2
  24. eodag/plugins/crunch/filter_latest_intersect.py +5 -4
  25. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  26. eodag/plugins/crunch/filter_overlap.py +5 -7
  27. eodag/plugins/crunch/filter_property.py +6 -6
  28. eodag/plugins/download/aws.py +50 -34
  29. eodag/plugins/download/base.py +41 -50
  30. eodag/plugins/download/creodias_s3.py +40 -2
  31. eodag/plugins/download/http.py +221 -195
  32. eodag/plugins/download/s3rest.py +25 -25
  33. eodag/plugins/manager.py +168 -23
  34. eodag/plugins/search/base.py +106 -39
  35. eodag/plugins/search/build_search_result.py +1065 -324
  36. eodag/plugins/search/cop_marine.py +112 -29
  37. eodag/plugins/search/creodias_s3.py +45 -24
  38. eodag/plugins/search/csw.py +41 -1
  39. eodag/plugins/search/data_request_search.py +109 -9
  40. eodag/plugins/search/qssearch.py +549 -257
  41. eodag/plugins/search/static_stac_search.py +20 -21
  42. eodag/resources/ext_product_types.json +1 -1
  43. eodag/resources/product_types.yml +577 -87
  44. eodag/resources/providers.yml +1619 -2776
  45. eodag/resources/stac.yml +3 -163
  46. eodag/resources/user_conf_template.yml +112 -97
  47. eodag/rest/config.py +1 -2
  48. eodag/rest/constants.py +0 -1
  49. eodag/rest/core.py +138 -98
  50. eodag/rest/errors.py +181 -0
  51. eodag/rest/server.py +55 -329
  52. eodag/rest/stac.py +93 -544
  53. eodag/rest/types/eodag_search.py +19 -8
  54. eodag/rest/types/queryables.py +6 -8
  55. eodag/rest/types/stac_search.py +11 -2
  56. eodag/rest/utils/__init__.py +3 -0
  57. eodag/types/__init__.py +71 -18
  58. eodag/types/download_args.py +3 -3
  59. eodag/types/queryables.py +180 -73
  60. eodag/types/search_args.py +3 -3
  61. eodag/types/whoosh.py +126 -0
  62. eodag/utils/__init__.py +147 -66
  63. eodag/utils/exceptions.py +47 -26
  64. eodag/utils/logging.py +37 -77
  65. eodag/utils/repr.py +65 -6
  66. eodag/utils/requests.py +11 -13
  67. eodag/utils/stac_reader.py +1 -1
  68. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/METADATA +80 -81
  69. eodag-3.1.0b1.dist-info/RECORD +108 -0
  70. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
  71. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +4 -2
  72. eodag/resources/constraints/climate-dt.json +0 -13
  73. eodag/resources/constraints/extremes-dt.json +0 -8
  74. eodag/utils/constraints.py +0 -244
  75. eodag-3.0.0b3.dist-info/RECORD +0 -110
  76. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
eodag/types/whoosh.py CHANGED
@@ -18,10 +18,13 @@
18
18
  from typing import List
19
19
 
20
20
  from whoosh.fields import Schema
21
+ from whoosh.index import _DEF_INDEX_NAME, FileIndex
21
22
  from whoosh.matching import NullMatcher
22
23
  from whoosh.qparser import OrGroup, QueryParser, plugins
23
24
  from whoosh.query.positional import Phrase
24
25
  from whoosh.query.qcore import QueryError
26
+ from whoosh.util.text import utf8encode
27
+ from whoosh.writing import SegmentWriter
25
28
 
26
29
 
27
30
  class RobustPhrase(Phrase):
@@ -77,3 +80,126 @@ class EODAGQueryParser(QueryParser):
77
80
  phraseclass=RobustPhrase,
78
81
  group=OrGroup,
79
82
  )
83
+
84
+
85
+ class CleanSegmentWriter(SegmentWriter):
86
+ """Override to clean up writer for failed document add when exceptions were absorbed
87
+ cf: https://github.com/whoosh-community/whoosh/pull/543
88
+ """
89
+
90
+ def add_document(self, **fields):
91
+ """Add document"""
92
+ self._check_state()
93
+ perdocwriter = self.perdocwriter
94
+ schema = self.schema
95
+ docnum = self.docnum
96
+ add_post = self.pool.add
97
+
98
+ docboost = self._doc_boost(fields)
99
+ fieldnames = sorted(
100
+ [name for name in fields.keys() if not name.startswith("_")]
101
+ )
102
+ self._check_fields(schema, fieldnames)
103
+
104
+ perdocwriter.start_doc(docnum)
105
+
106
+ try:
107
+ for fieldname in fieldnames:
108
+ value = fields.get(fieldname)
109
+ if value is None:
110
+ continue
111
+ field = schema[fieldname]
112
+
113
+ length = 0
114
+ if field.indexed:
115
+ # TODO: Method for adding progressive field values, ie
116
+ # setting start_pos/start_char?
117
+ fieldboost = self._field_boost(fields, fieldname, docboost)
118
+ # Ask the field to return a list of (text, weight, vbytes)
119
+ # tuples
120
+ items = field.index(value)
121
+ # Only store the length if the field is marked scorable
122
+ scorable = field.scorable
123
+ # Add the terms to the pool
124
+ for tbytes, freq, weight, vbytes in items:
125
+ weight *= fieldboost
126
+ if scorable:
127
+ length += freq
128
+ add_post((fieldname, tbytes, docnum, weight, vbytes))
129
+
130
+ if field.separate_spelling():
131
+ spellfield = field.spelling_fieldname(fieldname)
132
+ for word in field.spellable_words(value):
133
+ word = utf8encode(word)[0]
134
+ add_post((spellfield, word, 0, 1, vbytes))
135
+
136
+ vformat = field.vector
137
+ if vformat:
138
+ analyzer = field.analyzer
139
+ # Call the format's word_values method to get posting values
140
+ vitems = vformat.word_values(value, analyzer, mode="index")
141
+ # Remove unused frequency field from the tuple
142
+ vitems = sorted(
143
+ (text, weight, vbytes) for text, _, weight, vbytes in vitems
144
+ )
145
+ perdocwriter.add_vector_items(fieldname, field, vitems)
146
+
147
+ # Allow a custom value for stored field/column
148
+ customval = fields.get("_stored_%s" % fieldname, value)
149
+
150
+ # Add the stored value and length for this field to the per-
151
+ # document writer
152
+ sv = customval if field.stored else None
153
+ perdocwriter.add_field(fieldname, field, sv, length)
154
+
155
+ column = field.column_type
156
+ if column and customval is not None:
157
+ cv = field.to_column_value(customval)
158
+ perdocwriter.add_column_value(fieldname, column, cv)
159
+ except Exception as ex:
160
+ # cancel doc
161
+ perdocwriter._doccount -= 1
162
+ perdocwriter._indoc = False
163
+ raise ex
164
+
165
+ perdocwriter.finish_doc()
166
+ self._added = True
167
+ self.docnum += 1
168
+
169
+
170
+ class CleanFileIndex(FileIndex):
171
+ """Override to call CleanSegmentWriter"""
172
+
173
+ def writer(self, procs=1, **kwargs):
174
+ """file index writer"""
175
+ if procs > 1:
176
+ from whoosh.multiproc import MpWriter
177
+
178
+ return MpWriter(self, procs=procs, **kwargs)
179
+ else:
180
+ return CleanSegmentWriter(self, **kwargs)
181
+
182
+
183
+ def create_in(dirname, schema, indexname=None):
184
+ """
185
+ Override to call the CleanFileIndex.
186
+
187
+ Convenience function to create an index in a directory. Takes care of
188
+ creating a FileStorage object for you.
189
+
190
+ :param dirname: the path string of the directory in which to create the
191
+ index.
192
+ :param schema: a :class:`whoosh.fields.Schema` object describing the
193
+ index's fields.
194
+ :param indexname: the name of the index to create; you only need to specify
195
+ this if you are creating multiple indexes within the same storage
196
+ object.
197
+ :returns: :class:`Index`
198
+ """
199
+
200
+ from whoosh.filedb.filestore import FileStorage
201
+
202
+ if not indexname:
203
+ indexname = _DEF_INDEX_NAME
204
+ storage = FileStorage(dirname)
205
+ return CleanFileIndex.create(storage, schema, indexname)
eodag/utils/__init__.py CHANGED
@@ -79,16 +79,12 @@ from urllib.parse import ( # noqa; noqa
79
79
  )
80
80
  from urllib.request import url2pathname
81
81
 
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
82
  if sys.version_info >= (3, 12):
88
83
  from typing import Unpack # type: ignore # noqa
89
84
  else:
90
85
  from typing_extensions import Unpack # noqa
91
86
 
87
+
92
88
  import click
93
89
  import orjson
94
90
  import shapefile
@@ -99,7 +95,7 @@ from dateutil.tz import UTC
99
95
  from jsonpath_ng import jsonpath
100
96
  from jsonpath_ng.ext import parse
101
97
  from jsonpath_ng.jsonpath import Child, Fields, Index, Root, Slice
102
- from requests import HTTPError
98
+ from requests import HTTPError, Response
103
99
  from shapely.geometry import Polygon, shape
104
100
  from shapely.geometry.base import GEOMETRY_TYPES, BaseGeometry
105
101
  from tqdm.auto import tqdm
@@ -110,7 +106,7 @@ from eodag.utils.exceptions import MisconfiguredError
110
106
  if TYPE_CHECKING:
111
107
  from jsonpath_ng import JSONPath
112
108
 
113
- from eodag.api.product import EOProduct
109
+ from eodag.api.product._product import EOProduct
114
110
 
115
111
 
116
112
  logger = py_logging.getLogger("eodag.utils")
@@ -125,9 +121,13 @@ USER_AGENT = {"User-Agent": f"eodag/{eodag_version}"}
125
121
  HTTP_REQ_TIMEOUT = 5 # in seconds
126
122
  DEFAULT_STREAM_REQUESTS_TIMEOUT = 60 # in seconds
127
123
 
124
+ REQ_RETRY_TOTAL = 3
125
+ REQ_RETRY_BACKOFF_FACTOR = 2
126
+ REQ_RETRY_STATUS_FORCELIST = [401, 429, 500, 502, 503, 504]
127
+
128
128
  # default wait times in minutes
129
- DEFAULT_DOWNLOAD_WAIT = 2 # in minutes
130
- DEFAULT_DOWNLOAD_TIMEOUT = 20 # in minutes
129
+ DEFAULT_DOWNLOAD_WAIT = 0.2 # in minutes
130
+ DEFAULT_DOWNLOAD_TIMEOUT = 10 # in minutes
131
131
 
132
132
  JSONPATH_MATCH = re.compile(r"^[\{\(]*\$(\..*)*$")
133
133
  WORKABLE_JSONPATH_MATCH = re.compile(r"^\$(\.[a-zA-Z0-9-_:\.\[\]\"\(\)=\?\*]+)*$")
@@ -143,6 +143,13 @@ DEFAULT_MAX_ITEMS_PER_PAGE = 50
143
143
  # default product-types start date
144
144
  DEFAULT_MISSION_START_DATE = "2015-01-01T00:00:00Z"
145
145
 
146
+ # update missing mimetypes
147
+ mimetypes.add_type("text/xml", ".xsd")
148
+ mimetypes.add_type("application/x-grib", ".grib")
149
+ mimetypes.add_type("application/x-grib2", ".grib2")
150
+ # jp2 is missing on windows
151
+ mimetypes.add_type("image/jp2", ".jp2")
152
+
146
153
 
147
154
  def _deprecated(reason: str = "", version: Optional[str] = None) -> Callable[..., Any]:
148
155
  """Simple decorator to mark functions/methods/classes as deprecated.
@@ -237,9 +244,10 @@ class FloatRange(click.types.FloatParamType):
237
244
  def slugify(value: Any, allow_unicode: bool = False) -> str:
238
245
  """Copied from Django Source code, only modifying last line (no need for safe
239
246
  strings).
247
+
240
248
  source: https://github.com/django/django/blob/master/django/utils/text.py
241
249
 
242
- Convert to ASCII if 'allow_unicode' is False. Convert spaces to hyphens.
250
+ Convert to ASCII if ``allow_unicode`` is ``False``. Convert spaces to hyphens.
243
251
  Remove characters that aren't alphanumerics, underscores, or hyphens.
244
252
  Convert to lowercase. Also strip leading and trailing whitespace.
245
253
  """
@@ -298,7 +306,7 @@ def strip_accents(s: str) -> str:
298
306
 
299
307
  def uri_to_path(uri: str) -> str:
300
308
  """
301
- Convert a file URI (e.g. 'file:///tmp') to a local path (e.g. '/tmp')
309
+ Convert a file URI (e.g. ``file:///tmp``) to a local path (e.g. ``/tmp``)
302
310
  """
303
311
  if not uri.startswith("file"):
304
312
  raise ValueError("A file URI must be provided (e.g. 'file:///tmp'")
@@ -333,10 +341,10 @@ def mutate_dict_in_place(func: Callable[[Any], Any], mapping: Dict[Any, Any]) ->
333
341
 
334
342
 
335
343
  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`.
344
+ """Merge two mappings with string keys, values from ``mapping2`` overriding values
345
+ from ``mapping1``.
338
346
 
339
- Do its best to detect the key in `mapping1` to override. For example::
347
+ Do its best to detect the key in ``mapping1`` to override. For example:
340
348
 
341
349
  >>> mapping2 = {"keya": "new"}
342
350
  >>> mapping1 = {"keyA": "obsolete"}
@@ -344,12 +352,11 @@ def merge_mappings(mapping1: Dict[Any, Any], mapping2: Dict[Any, Any]) -> None:
344
352
  >>> mapping1
345
353
  {'keyA': 'new'}
346
354
 
347
- If mapping2 has a key that cannot be detected in mapping1, this new key is added
348
- to mapping1 as is.
355
+ If ``mapping2`` has a key that cannot be detected in ``mapping1``, this new key is
356
+ added to ``mapping1`` as is.
349
357
 
350
358
  :param mapping1: The mapping containing values to be overridden
351
- :param mapping2: The mapping containing values that will override the
352
- first mapping
359
+ :param mapping2: The mapping containing values that will override the first mapping
353
360
  """
354
361
  # A mapping between mapping1 keys as lowercase strings and original mapping1 keys
355
362
  m1_keys_lowercase = {key.lower(): key for key in mapping1}
@@ -416,7 +423,7 @@ def get_timestamp(date_time: str) -> float:
416
423
  If the datetime has no offset, it is assumed to be an UTC datetime.
417
424
 
418
425
  :param date_time: The datetime string to return as timestamp
419
- :returns: The timestamp corresponding to the date_time string in seconds
426
+ :returns: The timestamp corresponding to the ``date_time`` string in seconds
420
427
  """
421
428
  dt = isoparse(date_time)
422
429
  if not dt.tzinfo:
@@ -425,12 +432,39 @@ def get_timestamp(date_time: str) -> float:
425
432
 
426
433
 
427
434
  def datetime_range(start: dt, end: dt) -> Iterator[dt]:
428
- """Generator function for all dates in-between start and end date."""
435
+ """Generator function for all dates in-between ``start`` and ``end`` date."""
429
436
  delta = end - start
430
437
  for nday in range(delta.days + 1):
431
438
  yield start + datetime.timedelta(days=nday)
432
439
 
433
440
 
441
+ def is_range_in_range(valid_range: str, check_range: str) -> bool:
442
+ """Check if the check_range is completely within the valid_range.
443
+
444
+ This function checks if both the start and end dates of the check_range
445
+ are within the start and end dates of the valid_range.
446
+
447
+ :param valid_range: The valid date range in the format 'YYYY-MM-DD/YYYY-MM-DD'.
448
+ :param check_range: The date range to check in the format 'YYYY-MM-DD/YYYY-MM-DD'.
449
+ :returns: True if check_range is within valid_range, otherwise False.
450
+ """
451
+ if "/" not in valid_range or "/" not in check_range:
452
+ return False
453
+
454
+ # Split the date ranges into start and end dates
455
+ start_valid, end_valid = valid_range.split("/")
456
+ start_check, end_check = check_range.split("/")
457
+
458
+ # Convert the strings to datetime objects using fromisoformat
459
+ start_valid_dt = datetime.datetime.fromisoformat(start_valid)
460
+ end_valid_dt = datetime.datetime.fromisoformat(end_valid)
461
+ start_check_dt = datetime.datetime.fromisoformat(start_check)
462
+ end_check_dt = datetime.datetime.fromisoformat(end_check)
463
+
464
+ # Check if check_range is within valid_range
465
+ return start_valid_dt <= start_check_dt and end_valid_dt >= end_check_dt
466
+
467
+
434
468
  class DownloadedCallback:
435
469
  """Example class for callback after each download in :meth:`~eodag.api.core.EODataAccessGateway.download_all`"""
436
470
 
@@ -445,15 +479,15 @@ class DownloadedCallback:
445
479
  class ProgressCallback(tqdm):
446
480
  """A callable used to render progress to users for long running processes.
447
481
 
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`.
482
+ It inherits from :class:`tqdm.auto.tqdm`, and accepts the same arguments on
483
+ instantiation: ``iterable``, ``desc``, ``total``, ``leave``, ``file``, ``ncols``,
484
+ ``mininterval``, ``maxinterval``, ``miniters``, ``ascii``, ``disable``, ``unit``,
485
+ ``unit_scale``, ``dynamic_ncols``, ``smoothing``, ``bar_format``, ``initial``,
486
+ ``position``, ``postfix``, ``unit_divisor``.
453
487
 
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`.
488
+ It can be globally disabled using ``eodag.utils.logging.setup_logging(0)`` or
489
+ ``eodag.utils.logging.setup_logging(level, no_progress_bar=True)``, and
490
+ individually disabled using ``disable=True``.
457
491
  """
458
492
 
459
493
  def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -488,8 +522,8 @@ class ProgressCallback(tqdm):
488
522
  """Returns another progress callback using the same initial
489
523
  keyword-arguments.
490
524
 
491
- Optional `args` and `kwargs` parameters will be used to create a
492
- new `~eodag.utils.ProgressCallback` instance, overriding initial
525
+ Optional ``args`` and ``kwargs`` parameters will be used to create a
526
+ new :class:`~eodag.utils.ProgressCallback` instance, overriding initial
493
527
  `kwargs`.
494
528
  """
495
529
 
@@ -511,7 +545,7 @@ def get_progress_callback() -> tqdm:
511
545
 
512
546
 
513
547
  def repeatfunc(func: Callable[..., Any], n: int, *args: Any) -> starmap:
514
- """Call `func` `n` times with `args`"""
548
+ """Call ``func`` ``n`` times with ``args``"""
515
549
  return starmap(func, repeat(args, n))
516
550
 
517
551
 
@@ -526,12 +560,12 @@ def makedirs(dirpath: str) -> None:
526
560
 
527
561
 
528
562
  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
563
+ """Rename first subfolder found in ``dirpath`` with given ``name``,
564
+ raise :class:`RuntimeError` if no subfolder can be found
531
565
 
532
566
  :param dirpath: path to the directory containing the subfolder
533
567
  :param name: new name of the subfolder
534
- :raises: RuntimeError
568
+ :raises: :class:`RuntimeError`
535
569
 
536
570
  Example:
537
571
 
@@ -545,16 +579,20 @@ def rename_subfolder(dirpath: str, name: str) -> None:
545
579
  ... rename_subfolder(tmpdir, "otherfolder")
546
580
  ... assert not os.path.isdir(somefolder) and os.path.isdir(otherfolder)
547
581
 
548
- Before:
582
+ Before::
583
+
549
584
  $ tree <tmp-folder>
550
585
  <tmp-folder>
551
586
  └── somefolder
552
587
  └── somefile
553
- After:
588
+
589
+ After::
590
+
554
591
  $ tree <tmp-folder>
555
592
  <tmp-folder>
556
593
  └── otherfolder
557
594
  └── somefile
595
+
558
596
  """
559
597
  try:
560
598
  subdir, *_ = (p for p in glob(os.path.join(dirpath, "*")) if os.path.isdir(p))
@@ -570,7 +608,7 @@ def rename_subfolder(dirpath: str, name: str) -> None:
570
608
  def format_dict_items(
571
609
  config_dict: Dict[str, Any], **format_variables: Any
572
610
  ) -> Dict[Any, Any]:
573
- r"""Recursive apply string.format(\**format_variables) to dict elements
611
+ r"""Recursively apply :meth:`str.format` to ``**format_variables`` on ``config_dict`` values
574
612
 
575
613
  >>> format_dict_items(
576
614
  ... {"foo": {"bar": "{a}"}, "baz": ["{b}?", "{b}!"]},
@@ -578,7 +616,7 @@ def format_dict_items(
578
616
  ... ) == {"foo": {"bar": "qux"}, "baz": ["quux?", "quux!"]}
579
617
  True
580
618
 
581
- :param config_dict: Dictionnary having values that need to be parsed
619
+ :param config_dict: Dictionary having values that need to be parsed
582
620
  :param format_variables: Variables used as args for parsing
583
621
  :returns: Updated dict
584
622
  """
@@ -588,7 +626,7 @@ def format_dict_items(
588
626
  def jsonpath_parse_dict_items(
589
627
  jsonpath_dict: Dict[str, Any], values_dict: Dict[str, Any]
590
628
  ) -> Dict[Any, Any]:
591
- """Recursive parse jsonpath elements in dict
629
+ """Recursively parse :class:`jsonpath_ng.JSONPath` elements in dict
592
630
 
593
631
  >>> import jsonpath_ng.ext as jsonpath
594
632
  >>> jsonpath_parse_dict_items(
@@ -597,7 +635,7 @@ def jsonpath_parse_dict_items(
597
635
  ... ) == {'foo': {'bar': 'baz'}, 'qux': ['quux', 'quux']}
598
636
  True
599
637
 
600
- :param jsonpath_dict: Dictionnary having values that need to be parsed
638
+ :param jsonpath_dict: Dictionary having :class:`jsonpath_ng.JSONPath` values that need to be parsed
601
639
  :param values_dict: Values dict used as args for parsing
602
640
  :returns: Updated dict
603
641
  """
@@ -611,7 +649,7 @@ def update_nested_dict(
611
649
  allow_empty_values: bool = False,
612
650
  allow_extend_duplicates: bool = True,
613
651
  ) -> Dict[Any, Any]:
614
- """Update recursively old_dict items with new_dict ones
652
+ """Update recursively ``old_dict`` items with ``new_dict`` ones
615
653
 
616
654
  >>> update_nested_dict(
617
655
  ... {"a": {"a.a": 1, "a.b": 2}, "b": 3},
@@ -743,7 +781,7 @@ def dict_items_recursive_apply(
743
781
  ... ) == {'foo': {'bar': 'BAZ!'}, 'qux': ['A!', 'B!']}
744
782
  True
745
783
 
746
- :param config_dict: Input nested dictionnary
784
+ :param config_dict: Input nested dictionary
747
785
  :param apply_method: Method to be applied to dict elements
748
786
  :param apply_method_parameters: Optional parameters passed to the method
749
787
  :returns: Updated dict
@@ -836,7 +874,7 @@ def dict_items_recursive_sort(config_dict: Dict[Any, Any]) -> Dict[Any, Any]:
836
874
  ... ) == {"a": ["b", {0: 1, 1: 2, 2: 0}], "b": {"a": 0, "b": "c"}}
837
875
  True
838
876
 
839
- :param config_dict: Input nested dictionnary
877
+ :param config_dict: Input nested dictionary
840
878
  :returns: Updated dict
841
879
  """
842
880
  result_dict: Dict[Any, Any] = deepcopy(config_dict)
@@ -873,7 +911,7 @@ def list_items_recursive_sort(config_list: List[Any]) -> List[Any]:
873
911
 
874
912
 
875
913
  def string_to_jsonpath(*args: Any, force: bool = False) -> Union[str, JSONPath]:
876
- """Get jsonpath for "$.foo.bar" like string
914
+ """Get :class:`jsonpath_ng.JSONPath` for ``$.foo.bar`` like string
877
915
 
878
916
  >>> string_to_jsonpath(None, "$.foo.bar")
879
917
  Child(Child(Root(), Fields('foo')), Fields('bar'))
@@ -887,7 +925,7 @@ def string_to_jsonpath(*args: Any, force: bool = False) -> Union[str, JSONPath]:
887
925
  Fields('foo')
888
926
 
889
927
  :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
928
+ :param force: force conversion even if input string is not detected as a :class:`jsonpath_ng.JSONPath`
891
929
  :returns: Parsed value
892
930
  """
893
931
  path_str: str = args[-1]
@@ -950,7 +988,7 @@ def string_to_jsonpath(*args: Any, force: bool = False) -> Union[str, JSONPath]:
950
988
 
951
989
 
952
990
  def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
953
- """Format "{foo}" like string
991
+ """Format ``"{foo}"``-like string
954
992
 
955
993
  >>> format_string(None, "foo {bar}, {baz} ?", **{"bar": "qux", "baz": "quux"})
956
994
  'foo qux, quux ?'
@@ -988,7 +1026,7 @@ def format_string(key: str, str_to_format: Any, **format_variables: Any) -> Any:
988
1026
  def parse_jsonpath(
989
1027
  key: str, jsonpath_obj: Union[str, jsonpath.Child], **values_dict: Dict[str, Any]
990
1028
  ) -> Optional[str]:
991
- """Parse jsonpah in jsonpath_obj using values_dict
1029
+ """Parse jsonpah in ``jsonpath_obj`` using ``values_dict``
992
1030
 
993
1031
  >>> import jsonpath_ng.ext as jsonpath
994
1032
  >>> parse_jsonpath(None, parse("$.foo.bar"), **{"foo": {"bar": "baz"}})
@@ -1030,10 +1068,10 @@ def nested_pairs2dict(pairs: Union[List[Any], Any]) -> Union[Any, Dict[Any, Any]
1030
1068
  def get_geometry_from_various(
1031
1069
  locations_config: List[Dict[str, Any]] = [], **query_args: Any
1032
1070
  ) -> BaseGeometry:
1033
- """Creates a shapely geometry using given query kwargs arguments
1071
+ """Creates a ``shapely.geometry`` using given query kwargs arguments
1034
1072
 
1035
1073
  :param locations_config: (optional) EODAG locations configuration
1036
- :param query_args: Query kwargs arguments from core.search() method
1074
+ :param query_args: Query kwargs arguments from :meth:`~eodag.api.core.EODataAccessGateway.search`
1037
1075
  :returns: shapely Geometry found
1038
1076
  :raises: :class:`ValueError`
1039
1077
  """
@@ -1117,7 +1155,7 @@ def get_geometry_from_various(
1117
1155
  class MockResponse:
1118
1156
  """Fake requests response"""
1119
1157
 
1120
- def __init__(self, json_data: Any, status_code: int) -> None:
1158
+ def __init__(self, json_data: Any = None, status_code: int = 200) -> None:
1121
1159
  self.json_data = json_data
1122
1160
  self.status_code = status_code
1123
1161
  self.content = json_data
@@ -1126,10 +1164,21 @@ class MockResponse:
1126
1164
  """Return json data"""
1127
1165
  return self.json_data
1128
1166
 
1167
+ def __iter__(self):
1168
+ yield self
1169
+
1170
+ def __enter__(self):
1171
+ return self
1172
+
1173
+ def __exit__(self, exc_type, exc_val, exc_tb):
1174
+ pass
1175
+
1129
1176
  def raise_for_status(self) -> None:
1130
1177
  """raises an exception when the status is not ok"""
1131
1178
  if self.status_code != 200:
1132
- raise HTTPError()
1179
+ response = Response()
1180
+ response.status_code = self.status_code
1181
+ raise HTTPError(response=response)
1133
1182
 
1134
1183
 
1135
1184
  def md5sum(file_path: str) -> str:
@@ -1163,7 +1212,7 @@ def obj_md5sum(data: Any) -> str:
1163
1212
 
1164
1213
  @functools.lru_cache()
1165
1214
  def cached_parse(str_to_parse: str) -> JSONPath:
1166
- """Cached jsonpath_ng.ext.parse
1215
+ """Cached :func:`jsonpath_ng.ext.parse`
1167
1216
 
1168
1217
  >>> cached_parse.cache_clear()
1169
1218
  >>> cached_parse("$.foo")
@@ -1179,8 +1228,8 @@ def cached_parse(str_to_parse: str) -> JSONPath:
1179
1228
  >>> cached_parse.cache_info()
1180
1229
  CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)
1181
1230
 
1182
- :param str_to_parse: string to parse as jsonpath
1183
- :returns: parsed jsonpath
1231
+ :param str_to_parse: string to parse as :class:`jsonpath_ng.JSONPath`
1232
+ :returns: parsed :class:`jsonpath_ng.JSONPath`
1184
1233
  """
1185
1234
  return parse(str_to_parse)
1186
1235
 
@@ -1194,7 +1243,7 @@ def _mutable_cached_yaml_load(config_path: str) -> Any:
1194
1243
 
1195
1244
 
1196
1245
  def cached_yaml_load(config_path: str) -> Dict[str, Any]:
1197
- """Cached yaml.load
1246
+ """Cached :func:`yaml.load`
1198
1247
 
1199
1248
  :param config_path: path to the yaml configuration file
1200
1249
  :returns: loaded yaml configuration
@@ -1209,7 +1258,7 @@ def _mutable_cached_yaml_load_all(config_path: str) -> List[Any]:
1209
1258
 
1210
1259
 
1211
1260
  def cached_yaml_load_all(config_path: str) -> List[Any]:
1212
- """Cached yaml.load_all
1261
+ """Cached :func:`yaml.load_all`
1213
1262
 
1214
1263
  Load all configurations stored in the configuration file as separated yaml documents
1215
1264
 
@@ -1274,7 +1323,8 @@ def flatten_top_directories(
1274
1323
 
1275
1324
  def deepcopy(sth: Any) -> Any:
1276
1325
  """Customized and faster deepcopy inspired by https://stackoverflow.com/a/45858907
1277
- `_copy_list` and `_copy_dict` available for the moment
1326
+
1327
+ ``_copy_list`` and ``_copy_dict`` dispatchers available for the moment
1278
1328
 
1279
1329
  :param sth: Object to copy
1280
1330
  :returns: Copied object
@@ -1339,7 +1389,7 @@ def cast_scalar_value(value: Any, new_type: Any) -> Any:
1339
1389
 
1340
1390
  :param value: the scalar value to convert
1341
1391
  :param new_type: the wanted type
1342
- :returns: scalar value converted to new_type
1392
+ :returns: scalar ``value`` converted to ``new_type``
1343
1393
  """
1344
1394
  if isinstance(value, str) and new_type is bool:
1345
1395
  # Bool is a type with special meaning in Python, thus the special
@@ -1369,24 +1419,40 @@ class StreamResponse:
1369
1419
 
1370
1420
 
1371
1421
  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")
1422
+ """Guess the mime type of a file or URL based on its extension,
1423
+ using eodag extended mimetypes definition
1424
+
1425
+ >>> guess_file_type('foo.tiff')
1426
+ 'image/tiff'
1427
+ >>> guess_file_type('foo.grib')
1428
+ 'application/x-grib'
1429
+
1430
+ :param file: file url or path
1431
+ :returns: guessed mime type
1432
+ """
1375
1433
  mime_type, _ = mimetypes.guess_type(file, False)
1376
1434
  return mime_type
1377
1435
 
1378
1436
 
1379
1437
  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")
1438
+ """Guess extension from mime type, using eodag extended mimetypes definition
1439
+
1440
+ >>> guess_extension('image/tiff')
1441
+ '.tiff'
1442
+ >>> guess_extension('application/x-grib')
1443
+ '.grib'
1444
+
1445
+ :param type: mime type
1446
+ :returns: guessed file extension
1447
+ """
1383
1448
  return mimetypes.guess_extension(type, strict=False)
1384
1449
 
1385
1450
 
1386
1451
  def get_ssl_context(ssl_verify: bool) -> ssl.SSLContext:
1387
1452
  """
1388
- Returns an SSL context based on ssl_verify argument.
1389
- :param ssl_verify: ssl_verify parameter
1453
+ Returns an SSL context based on ``ssl_verify`` argument.
1454
+
1455
+ :param ssl_verify: :attr:`~eodag.config.PluginConfig.ssl_verify` parameter
1390
1456
  :returns: An SSL context object.
1391
1457
  """
1392
1458
  ctx = ssl.create_default_context()
@@ -1413,3 +1479,18 @@ def sort_dict(input_dict: Dict[str, Any]) -> Dict[str, Any]:
1413
1479
  k: sort_dict(v) if isinstance(v, dict) else v
1414
1480
  for k, v in sorted(input_dict.items())
1415
1481
  }
1482
+
1483
+
1484
+ def dict_md5sum(input_dict: Dict[str, Any]) -> str:
1485
+ """
1486
+ Hash nested dictionary
1487
+
1488
+ :param input_dict: input dict
1489
+ :returns: hash
1490
+
1491
+ >>> hd = dict_md5sum({"b": {"c": 1, "a": 2, "b": 3}, "a": 4})
1492
+ >>> hd
1493
+ 'a195bcef1bb3b419e9e74b7cc5db8098'
1494
+ >>> assert(dict_md5sum({"a": 4, "b": {"b": 3, "c": 1, "a": 2}}) == hd)
1495
+ """
1496
+ return obj_md5sum(sort_dict(input_dict))