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.
- eodag/api/core.py +347 -247
- eodag/api/product/_assets.py +44 -15
- eodag/api/product/_product.py +58 -47
- eodag/api/product/drivers/__init__.py +81 -4
- eodag/api/product/drivers/base.py +65 -4
- eodag/api/product/drivers/generic.py +65 -0
- eodag/api/product/drivers/sentinel1.py +97 -0
- eodag/api/product/drivers/sentinel2.py +95 -0
- eodag/api/product/metadata_mapping.py +129 -93
- eodag/api/search_result.py +28 -12
- eodag/cli.py +61 -24
- eodag/config.py +457 -167
- eodag/plugins/apis/base.py +10 -4
- eodag/plugins/apis/ecmwf.py +53 -23
- eodag/plugins/apis/usgs.py +41 -17
- eodag/plugins/authentication/aws_auth.py +30 -18
- eodag/plugins/authentication/base.py +14 -3
- eodag/plugins/authentication/generic.py +14 -3
- eodag/plugins/authentication/header.py +14 -6
- eodag/plugins/authentication/keycloak.py +44 -25
- eodag/plugins/authentication/oauth.py +18 -4
- eodag/plugins/authentication/openid_connect.py +192 -171
- eodag/plugins/authentication/qsauth.py +12 -4
- eodag/plugins/authentication/sas_auth.py +22 -5
- eodag/plugins/authentication/token.py +95 -17
- eodag/plugins/authentication/token_exchange.py +19 -19
- eodag/plugins/base.py +4 -4
- eodag/plugins/crunch/base.py +8 -5
- eodag/plugins/crunch/filter_date.py +9 -6
- eodag/plugins/crunch/filter_latest_intersect.py +9 -8
- eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
- eodag/plugins/crunch/filter_overlap.py +9 -11
- eodag/plugins/crunch/filter_property.py +10 -10
- eodag/plugins/download/aws.py +181 -105
- eodag/plugins/download/base.py +49 -67
- eodag/plugins/download/creodias_s3.py +40 -2
- eodag/plugins/download/http.py +247 -223
- eodag/plugins/download/s3rest.py +29 -28
- eodag/plugins/manager.py +176 -41
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +123 -60
- eodag/plugins/search/build_search_result.py +1046 -355
- eodag/plugins/search/cop_marine.py +132 -39
- eodag/plugins/search/creodias_s3.py +19 -68
- eodag/plugins/search/csw.py +48 -8
- eodag/plugins/search/data_request_search.py +124 -23
- eodag/plugins/search/qssearch.py +531 -310
- eodag/plugins/search/stac_list_assets.py +85 -0
- eodag/plugins/search/static_stac_search.py +23 -24
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1295 -355
- eodag/resources/providers.yml +1819 -3010
- eodag/resources/stac.yml +3 -163
- eodag/resources/stac_api.yml +2 -2
- eodag/resources/user_conf_template.yml +115 -99
- eodag/rest/cache.py +2 -2
- eodag/rest/config.py +3 -4
- eodag/rest/constants.py +0 -1
- eodag/rest/core.py +157 -117
- eodag/rest/errors.py +181 -0
- eodag/rest/server.py +57 -339
- eodag/rest/stac.py +133 -581
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +41 -30
- eodag/rest/types/queryables.py +42 -32
- eodag/rest/types/stac_search.py +15 -16
- eodag/rest/utils/__init__.py +14 -21
- eodag/rest/utils/cql_evaluate.py +6 -6
- eodag/rest/utils/rfc3339.py +2 -2
- eodag/types/__init__.py +153 -32
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +4 -4
- eodag/types/queryables.py +183 -73
- eodag/types/search_args.py +6 -6
- eodag/types/whoosh.py +127 -3
- eodag/utils/__init__.py +228 -106
- eodag/utils/exceptions.py +47 -26
- eodag/utils/import_system.py +2 -2
- eodag/utils/logging.py +37 -77
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +13 -15
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +231 -0
- eodag/utils/stac_reader.py +11 -11
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
- eodag-3.1.0.dist-info/RECORD +113 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
- eodag/resources/constraints/climate-dt.json +0 -13
- eodag/resources/constraints/extremes-dt.json +0 -8
- eodag/utils/constraints.py +0 -244
- eodag-3.0.0b3.dist-info/RECORD +0 -110
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
- {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 =
|
|
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
|
|
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.
|
|
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:
|
|
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:
|
|
336
|
-
"""Merge two mappings with string keys, values from
|
|
337
|
-
from
|
|
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
|
|
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
|
|
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
|
|
449
|
-
instantiation:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
455
|
-
|
|
456
|
-
individually disabled using
|
|
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
|
|
492
|
-
new
|
|
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
|
|
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
|
-
|
|
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:
|
|
572
|
-
) ->
|
|
573
|
-
r"""
|
|
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:
|
|
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:
|
|
590
|
-
) ->
|
|
591
|
-
"""
|
|
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:
|
|
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:
|
|
609
|
-
new_dict:
|
|
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
|
-
) ->
|
|
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[
|
|
768
|
+
input_obj: Union[dict[Any, Any], list[Any]],
|
|
694
769
|
apply_method: Callable[..., Any],
|
|
695
770
|
**apply_method_parameters: Any,
|
|
696
|
-
) -> Union[
|
|
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:
|
|
809
|
+
config_dict: dict[Any, Any],
|
|
735
810
|
apply_method: Callable[..., Any],
|
|
736
811
|
**apply_method_parameters: Any,
|
|
737
|
-
) ->
|
|
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
|
|
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:
|
|
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:
|
|
845
|
+
config_list: list[Any],
|
|
771
846
|
apply_method: Callable[..., Any],
|
|
772
847
|
**apply_method_parameters: Any,
|
|
773
|
-
) ->
|
|
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[
|
|
807
|
-
) -> Union[
|
|
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:
|
|
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
|
|
914
|
+
:param config_dict: Input nested dictionary
|
|
840
915
|
:returns: Updated dict
|
|
841
916
|
"""
|
|
842
|
-
result_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:
|
|
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:
|
|
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
|
|
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
|
|
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}"
|
|
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:
|
|
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[
|
|
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:
|
|
1106
|
+
locations_config: list[dict[str, Any]] = [], **query_args: Any
|
|
1032
1107
|
) -> BaseGeometry:
|
|
1033
|
-
"""Creates a shapely
|
|
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
|
|
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
|
-
|
|
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
|
|
1183
|
-
:returns: parsed
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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:
|
|
1371
|
+
_dispatcher: dict[type[Any], Callable[..., Any]] = {}
|
|
1283
1372
|
|
|
1284
1373
|
def _copy_list(
|
|
1285
|
-
input_list:
|
|
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:
|
|
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
|
-
"""
|
|
1373
|
-
mimetypes
|
|
1374
|
-
|
|
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
|
-
"""
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
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:
|
|
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))
|