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.
- eodag/api/core.py +292 -198
- eodag/api/product/_assets.py +6 -6
- eodag/api/product/_product.py +18 -18
- eodag/api/product/metadata_mapping.py +51 -14
- eodag/api/search_result.py +29 -3
- eodag/cli.py +57 -20
- eodag/config.py +413 -117
- eodag/plugins/apis/base.py +10 -4
- eodag/plugins/apis/ecmwf.py +49 -16
- eodag/plugins/apis/usgs.py +30 -7
- eodag/plugins/authentication/aws_auth.py +14 -5
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +14 -3
- eodag/plugins/authentication/header.py +12 -4
- eodag/plugins/authentication/keycloak.py +41 -22
- eodag/plugins/authentication/oauth.py +11 -1
- eodag/plugins/authentication/openid_connect.py +178 -163
- eodag/plugins/authentication/qsauth.py +12 -4
- eodag/plugins/authentication/sas_auth.py +19 -2
- eodag/plugins/authentication/token.py +93 -15
- eodag/plugins/authentication/token_exchange.py +19 -19
- eodag/plugins/crunch/base.py +4 -1
- eodag/plugins/crunch/filter_date.py +5 -2
- eodag/plugins/crunch/filter_latest_intersect.py +5 -4
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
- eodag/plugins/crunch/filter_overlap.py +5 -7
- eodag/plugins/crunch/filter_property.py +6 -6
- eodag/plugins/download/aws.py +50 -34
- eodag/plugins/download/base.py +41 -50
- eodag/plugins/download/creodias_s3.py +40 -2
- eodag/plugins/download/http.py +221 -195
- eodag/plugins/download/s3rest.py +25 -25
- eodag/plugins/manager.py +168 -23
- eodag/plugins/search/base.py +106 -39
- eodag/plugins/search/build_search_result.py +1065 -324
- eodag/plugins/search/cop_marine.py +112 -29
- eodag/plugins/search/creodias_s3.py +45 -24
- eodag/plugins/search/csw.py +41 -1
- eodag/plugins/search/data_request_search.py +109 -9
- eodag/plugins/search/qssearch.py +549 -257
- eodag/plugins/search/static_stac_search.py +20 -21
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +577 -87
- eodag/resources/providers.yml +1619 -2776
- eodag/resources/stac.yml +3 -163
- eodag/resources/user_conf_template.yml +112 -97
- eodag/rest/config.py +1 -2
- eodag/rest/constants.py +0 -1
- eodag/rest/core.py +138 -98
- eodag/rest/errors.py +181 -0
- eodag/rest/server.py +55 -329
- eodag/rest/stac.py +93 -544
- eodag/rest/types/eodag_search.py +19 -8
- eodag/rest/types/queryables.py +6 -8
- eodag/rest/types/stac_search.py +11 -2
- eodag/rest/utils/__init__.py +3 -0
- eodag/types/__init__.py +71 -18
- eodag/types/download_args.py +3 -3
- eodag/types/queryables.py +180 -73
- eodag/types/search_args.py +3 -3
- eodag/types/whoosh.py +126 -0
- eodag/utils/__init__.py +147 -66
- eodag/utils/exceptions.py +47 -26
- eodag/utils/logging.py +37 -77
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +11 -13
- eodag/utils/stac_reader.py +1 -1
- {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/METADATA +80 -81
- eodag-3.1.0b1.dist-info/RECORD +108 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
- {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +4 -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.0b1.dist-info}/LICENSE +0 -0
- {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 =
|
|
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
|
|
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.
|
|
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
|
|
337
|
-
from
|
|
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
|
|
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
|
|
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
|
|
449
|
-
instantiation:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
455
|
-
|
|
456
|
-
individually disabled using
|
|
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
|
|
492
|
-
new
|
|
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
|
|
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
|
-
|
|
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"""
|
|
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:
|
|
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
|
-
"""
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1183
|
-
:returns: parsed
|
|
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
|
-
|
|
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
|
-
"""
|
|
1373
|
-
mimetypes
|
|
1374
|
-
|
|
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
|
-
"""
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
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))
|