eodag 3.6.0__py3-none-any.whl → 3.8.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 +110 -189
- eodag/api/product/metadata_mapping.py +42 -3
- eodag/cli.py +6 -3
- eodag/config.py +7 -1
- eodag/plugins/authentication/openid_connect.py +1 -2
- eodag/plugins/download/aws.py +145 -178
- eodag/plugins/download/base.py +3 -2
- eodag/plugins/download/creodias_s3.py +10 -5
- eodag/plugins/download/http.py +14 -6
- eodag/plugins/download/s3rest.py +7 -3
- eodag/plugins/manager.py +1 -1
- eodag/plugins/search/base.py +34 -4
- eodag/plugins/search/build_search_result.py +3 -0
- eodag/plugins/search/cop_marine.py +2 -0
- eodag/plugins/search/data_request_search.py +6 -1
- eodag/plugins/search/qssearch.py +64 -25
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +30 -171
- eodag/resources/providers.yml +87 -328
- eodag/resources/stac.yml +1 -2
- eodag/resources/stac_provider.yml +1 -1
- eodag/resources/user_conf_template.yml +0 -11
- eodag/rest/core.py +5 -16
- eodag/rest/stac.py +0 -4
- eodag/utils/__init__.py +41 -27
- eodag/utils/exceptions.py +4 -0
- eodag/utils/free_text_search.py +229 -0
- eodag/utils/s3.py +605 -65
- {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/METADATA +7 -9
- {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/RECORD +34 -34
- eodag/types/whoosh.py +0 -203
- {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/WHEEL +0 -0
- {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/entry_points.txt +0 -0
- {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.6.0.dist-info → eodag-3.8.0.dist-info}/top_level.txt +0 -0
eodag/api/core.py
CHANGED
|
@@ -30,10 +30,6 @@ from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
|
|
|
30
30
|
|
|
31
31
|
import geojson
|
|
32
32
|
import yaml.parser
|
|
33
|
-
from whoosh import analysis, fields
|
|
34
|
-
from whoosh.fields import Schema
|
|
35
|
-
from whoosh.index import exists_in, open_dir
|
|
36
|
-
from whoosh.qparser import QueryParser
|
|
37
33
|
|
|
38
34
|
from eodag.api.product.metadata_mapping import (
|
|
39
35
|
NOT_AVAILABLE,
|
|
@@ -61,7 +57,6 @@ from eodag.plugins.search.build_search_result import MeteoblueSearch
|
|
|
61
57
|
from eodag.plugins.search.qssearch import PostJsonSearch
|
|
62
58
|
from eodag.types import model_fields_to_annotated
|
|
63
59
|
from eodag.types.queryables import CommonQueryables, QueryablesDict
|
|
64
|
-
from eodag.types.whoosh import EODAGQueryParser, create_in
|
|
65
60
|
from eodag.utils import (
|
|
66
61
|
DEFAULT_DOWNLOAD_TIMEOUT,
|
|
67
62
|
DEFAULT_DOWNLOAD_WAIT,
|
|
@@ -75,7 +70,6 @@ from eodag.utils import (
|
|
|
75
70
|
_deprecated,
|
|
76
71
|
get_geometry_from_various,
|
|
77
72
|
makedirs,
|
|
78
|
-
obj_md5sum,
|
|
79
73
|
sort_dict,
|
|
80
74
|
string_to_jsonpath,
|
|
81
75
|
uri_to_path,
|
|
@@ -83,19 +77,18 @@ from eodag.utils import (
|
|
|
83
77
|
from eodag.utils.env import is_env_var_true
|
|
84
78
|
from eodag.utils.exceptions import (
|
|
85
79
|
AuthenticationError,
|
|
86
|
-
EodagError,
|
|
87
80
|
NoMatchingProductType,
|
|
88
81
|
PluginImplementationError,
|
|
89
82
|
RequestError,
|
|
90
83
|
UnsupportedProductType,
|
|
91
84
|
UnsupportedProvider,
|
|
92
85
|
)
|
|
86
|
+
from eodag.utils.free_text_search import compile_free_text_query
|
|
93
87
|
from eodag.utils.rest import rfc3339_str_to_datetime
|
|
94
88
|
from eodag.utils.stac_reader import fetch_stac_items
|
|
95
89
|
|
|
96
90
|
if TYPE_CHECKING:
|
|
97
91
|
from shapely.geometry.base import BaseGeometry
|
|
98
|
-
from whoosh.index import Index
|
|
99
92
|
|
|
100
93
|
from eodag.api.product import EOProduct
|
|
101
94
|
from eodag.plugins.apis.base import Api
|
|
@@ -125,7 +118,6 @@ class EODataAccessGateway:
|
|
|
125
118
|
res_files("eodag") / "resources" / "product_types.yml"
|
|
126
119
|
)
|
|
127
120
|
self.product_types_config = SimpleYamlProxyConfig(product_types_config_path)
|
|
128
|
-
self.product_types_config_md5 = obj_md5sum(self.product_types_config.source)
|
|
129
121
|
self.providers_config = load_default_config()
|
|
130
122
|
|
|
131
123
|
env_var_cfg_dir = "EODAG_CFG_DIR"
|
|
@@ -189,6 +181,8 @@ class EODataAccessGateway:
|
|
|
189
181
|
self._sync_provider_product_types(
|
|
190
182
|
provider, available_product_types, strict_mode
|
|
191
183
|
)
|
|
184
|
+
# init product types configuration
|
|
185
|
+
self._product_types_config_init()
|
|
192
186
|
|
|
193
187
|
# re-build _plugins_manager using up-to-date providers_config
|
|
194
188
|
self._plugins_manager.rebuild(self.providers_config)
|
|
@@ -201,10 +195,6 @@ class EODataAccessGateway:
|
|
|
201
195
|
# Sort providers taking into account of possible new priority orders
|
|
202
196
|
self._plugins_manager.sort_providers()
|
|
203
197
|
|
|
204
|
-
# Build a search index for product types
|
|
205
|
-
self._product_types_index: Optional[Index] = None
|
|
206
|
-
self.build_index()
|
|
207
|
-
|
|
208
198
|
# set locations configuration
|
|
209
199
|
if locations_conf_path is None:
|
|
210
200
|
locations_conf_path = os.getenv("EODAG_LOCS_CFG_FILE")
|
|
@@ -235,6 +225,11 @@ class EODataAccessGateway:
|
|
|
235
225
|
)
|
|
236
226
|
self.set_locations_conf(locations_conf_path)
|
|
237
227
|
|
|
228
|
+
def _product_types_config_init(self) -> None:
|
|
229
|
+
"""Initialize product types configuration."""
|
|
230
|
+
for pt_id, pd_dict in self.product_types_config.source.items():
|
|
231
|
+
self.product_types_config.source[pt_id].setdefault("_id", pt_id)
|
|
232
|
+
|
|
238
233
|
def _sync_provider_product_types(
|
|
239
234
|
self,
|
|
240
235
|
provider: str,
|
|
@@ -294,95 +289,6 @@ class EODataAccessGateway:
|
|
|
294
289
|
"""Get eodag package version"""
|
|
295
290
|
return version("eodag")
|
|
296
291
|
|
|
297
|
-
def build_index(self) -> None:
|
|
298
|
-
"""Build a `Whoosh <https://whoosh.readthedocs.io/en/latest/index.html>`_
|
|
299
|
-
index for product types searches.
|
|
300
|
-
"""
|
|
301
|
-
index_dir = os.path.join(self.conf_dir, ".index")
|
|
302
|
-
|
|
303
|
-
try:
|
|
304
|
-
create_index = not exists_in(index_dir)
|
|
305
|
-
except ValueError as ve:
|
|
306
|
-
# Whoosh uses pickle internally. New versions of Python sometimes introduce
|
|
307
|
-
# a new pickle protocol (e.g. 3.4 -> 4, 3.8 -> 5), the new version not
|
|
308
|
-
# being supported by previous versions of Python (e.g. Python 3.7 doesn't
|
|
309
|
-
# support Protocol 5). In that case, we need to recreate the .index.
|
|
310
|
-
if "unsupported pickle protocol" in str(ve):
|
|
311
|
-
logger.debug("Need to recreate whoosh .index: '%s'", ve)
|
|
312
|
-
create_index = True
|
|
313
|
-
# Unexpected error
|
|
314
|
-
else:
|
|
315
|
-
logger.error(
|
|
316
|
-
"Error while opening .index using whoosh, "
|
|
317
|
-
"please report this issue and try to delete '%s' manually",
|
|
318
|
-
index_dir,
|
|
319
|
-
)
|
|
320
|
-
raise
|
|
321
|
-
# check index version
|
|
322
|
-
if not create_index:
|
|
323
|
-
if self._product_types_index is None:
|
|
324
|
-
logger.debug("Opening product types index in %s", index_dir)
|
|
325
|
-
self._product_types_index = open_dir(index_dir)
|
|
326
|
-
|
|
327
|
-
with self._product_types_index.searcher() as searcher:
|
|
328
|
-
p = QueryParser("md5", self._product_types_index.schema, plugins=[])
|
|
329
|
-
query = p.parse(self.product_types_config_md5)
|
|
330
|
-
results = searcher.search(query, limit=1)
|
|
331
|
-
|
|
332
|
-
if not results:
|
|
333
|
-
create_index = True
|
|
334
|
-
logger.debug(
|
|
335
|
-
"Out-of-date product types index removed from %s", index_dir
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
if create_index:
|
|
339
|
-
logger.debug("Creating product types index in %s", index_dir)
|
|
340
|
-
makedirs(index_dir)
|
|
341
|
-
|
|
342
|
-
kw_analyzer = (
|
|
343
|
-
analysis.CommaSeparatedTokenizer()
|
|
344
|
-
| analysis.LowercaseFilter()
|
|
345
|
-
| analysis.SubstitutionFilter("-", "")
|
|
346
|
-
| analysis.SubstitutionFilter("_", "")
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
product_types_schema = Schema(
|
|
350
|
-
ID=fields.ID(stored=True),
|
|
351
|
-
abstract=fields.TEXT,
|
|
352
|
-
instrument=fields.IDLIST,
|
|
353
|
-
platform=fields.ID,
|
|
354
|
-
platformSerialIdentifier=fields.IDLIST,
|
|
355
|
-
processingLevel=fields.ID,
|
|
356
|
-
sensorType=fields.ID,
|
|
357
|
-
md5=fields.ID,
|
|
358
|
-
license=fields.ID,
|
|
359
|
-
title=fields.TEXT,
|
|
360
|
-
missionStartDate=fields.STORED,
|
|
361
|
-
missionEndDate=fields.STORED,
|
|
362
|
-
keywords=fields.KEYWORD(analyzer=kw_analyzer),
|
|
363
|
-
stacCollection=fields.STORED,
|
|
364
|
-
)
|
|
365
|
-
self._product_types_index = create_in(index_dir, product_types_schema)
|
|
366
|
-
ix_writer = self._product_types_index.writer()
|
|
367
|
-
for product_type in self.list_product_types(fetch_providers=False):
|
|
368
|
-
versioned_product_type = dict(
|
|
369
|
-
product_type, **{"md5": self.product_types_config_md5}
|
|
370
|
-
)
|
|
371
|
-
# add to index
|
|
372
|
-
try:
|
|
373
|
-
ix_writer.add_document(
|
|
374
|
-
**{
|
|
375
|
-
k: v
|
|
376
|
-
for k, v in versioned_product_type.items()
|
|
377
|
-
if k in product_types_schema.names()
|
|
378
|
-
}
|
|
379
|
-
)
|
|
380
|
-
except TypeError as e:
|
|
381
|
-
logger.error(
|
|
382
|
-
f"Cannot write product type {product_type['ID']} into index. e={e} product_type={product_type}"
|
|
383
|
-
)
|
|
384
|
-
ix_writer.commit()
|
|
385
|
-
|
|
386
292
|
def set_preferred_provider(self, provider: str) -> None:
|
|
387
293
|
"""Set max priority for the given provider.
|
|
388
294
|
|
|
@@ -674,8 +580,6 @@ class EODataAccessGateway:
|
|
|
674
580
|
continue
|
|
675
581
|
|
|
676
582
|
config = self.product_types_config[product_type_id]
|
|
677
|
-
config["_id"] = product_type_id
|
|
678
|
-
|
|
679
583
|
if "alias" in config:
|
|
680
584
|
product_type_id = config["alias"]
|
|
681
585
|
product_type = {"ID": product_type_id, **config}
|
|
@@ -977,14 +881,12 @@ class EODataAccessGateway:
|
|
|
977
881
|
# to self.product_types_config
|
|
978
882
|
self.product_types_config.source.update(
|
|
979
883
|
{
|
|
980
|
-
new_product_type:
|
|
981
|
-
|
|
982
|
-
|
|
884
|
+
new_product_type: {"_id": new_product_type}
|
|
885
|
+
| new_product_types_conf["product_types_config"][
|
|
886
|
+
new_product_type
|
|
887
|
+
]
|
|
983
888
|
}
|
|
984
889
|
)
|
|
985
|
-
self.product_types_config_md5 = obj_md5sum(
|
|
986
|
-
self.product_types_config.source
|
|
987
|
-
)
|
|
988
890
|
ext_product_types_conf[provider] = new_product_types_conf
|
|
989
891
|
new_product_types.append(new_product_type)
|
|
990
892
|
if new_product_types:
|
|
@@ -1000,9 +902,6 @@ class EODataAccessGateway:
|
|
|
1000
902
|
# re-create _plugins_manager using up-to-date providers_config
|
|
1001
903
|
self._plugins_manager.build_product_type_to_provider_config_map()
|
|
1002
904
|
|
|
1003
|
-
# rebuild index after product types list update
|
|
1004
|
-
self.build_index()
|
|
1005
|
-
|
|
1006
905
|
def available_providers(
|
|
1007
906
|
self, product_type: Optional[str] = None, by_group: bool = False
|
|
1008
907
|
) -> list[str]:
|
|
@@ -1103,11 +1002,11 @@ class EODataAccessGateway:
|
|
|
1103
1002
|
"""
|
|
1104
1003
|
Find EODAG product type IDs that best match a set of search parameters.
|
|
1105
1004
|
|
|
1106
|
-
|
|
1107
|
-
for syntax.
|
|
1005
|
+
When using several filters, product types that match most of them will be returned at first.
|
|
1108
1006
|
|
|
1109
|
-
:param free_text:
|
|
1110
|
-
|
|
1007
|
+
:param free_text: Free text search filter used to search accross all the following parameters. Handles logical
|
|
1008
|
+
operators with parenthesis (``AND``/``OR``/``NOT``), quoted phrases (``"exact phrase"``),
|
|
1009
|
+
``*`` and ``?`` wildcards.
|
|
1111
1010
|
:param intersect: Join results for each parameter using INTERSECT instead of UNION.
|
|
1112
1011
|
:param instrument: Instrument parameter.
|
|
1113
1012
|
:param platform: Platform parameter.
|
|
@@ -1125,69 +1024,105 @@ class EODataAccessGateway:
|
|
|
1125
1024
|
if productType := kwargs.get("productType"):
|
|
1126
1025
|
return [productType]
|
|
1127
1026
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1027
|
+
filters: dict[str, str] = {
|
|
1028
|
+
k: v
|
|
1029
|
+
for k, v in {
|
|
1030
|
+
"instrument": instrument,
|
|
1031
|
+
"platform": platform,
|
|
1032
|
+
"platformSerialIdentifier": platformSerialIdentifier,
|
|
1033
|
+
"processingLevel": processingLevel,
|
|
1034
|
+
"sensorType": sensorType,
|
|
1035
|
+
"keywords": keywords,
|
|
1036
|
+
"abstract": abstract,
|
|
1037
|
+
"title": title,
|
|
1038
|
+
}.items()
|
|
1039
|
+
if v is not None
|
|
1140
1040
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1041
|
+
|
|
1042
|
+
only_dates = (
|
|
1043
|
+
True
|
|
1044
|
+
if (not free_text and not filters and (missionStartDate or missionEndDate))
|
|
1045
|
+
else False
|
|
1144
1046
|
)
|
|
1145
1047
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1048
|
+
free_text_evaluator = (
|
|
1049
|
+
compile_free_text_query(free_text) if free_text else lambda _: True
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
guesses_with_score: list[tuple[str, int]] = []
|
|
1053
|
+
|
|
1054
|
+
for pt_id, pt_dict in self.product_types_config.source.items():
|
|
1055
|
+
if (
|
|
1056
|
+
pt_id == GENERIC_PRODUCT_TYPE
|
|
1057
|
+
or pt_id
|
|
1058
|
+
not in self._plugins_manager.product_type_to_provider_config_map
|
|
1059
|
+
):
|
|
1060
|
+
continue
|
|
1061
|
+
|
|
1062
|
+
score = 0 # how many filters matched
|
|
1063
|
+
|
|
1064
|
+
# free text search
|
|
1065
|
+
if free_text:
|
|
1066
|
+
match = free_text_evaluator(pt_dict)
|
|
1067
|
+
if match:
|
|
1068
|
+
score += 1
|
|
1069
|
+
elif intersect:
|
|
1070
|
+
continue # must match all filters
|
|
1071
|
+
|
|
1072
|
+
# individual filters
|
|
1073
|
+
if filters:
|
|
1074
|
+
filters_matching_method = all if intersect else any
|
|
1075
|
+
filters_evaluators = {
|
|
1076
|
+
filter_name: compile_free_text_query(value)
|
|
1077
|
+
for filter_name, value in filters.items()
|
|
1078
|
+
if value is not None
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
filter_matches = [
|
|
1082
|
+
filters_evaluators[filter_name]({filter_name: pt_dict[filter_name]})
|
|
1083
|
+
for filter_name, value in filters.items()
|
|
1084
|
+
if filter_name in pt_dict
|
|
1085
|
+
]
|
|
1086
|
+
|
|
1087
|
+
if filters_matching_method(filter_matches):
|
|
1088
|
+
# add number of True matches to score
|
|
1089
|
+
score += sum(filter_matches)
|
|
1090
|
+
elif intersect:
|
|
1091
|
+
continue # must match all filters
|
|
1092
|
+
|
|
1093
|
+
if score == 0 and not only_dates:
|
|
1094
|
+
continue
|
|
1095
|
+
|
|
1096
|
+
# datetime filtering
|
|
1097
|
+
if missionStartDate or missionEndDate:
|
|
1098
|
+
min_aware = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
|
|
1099
|
+
max_aware = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
|
|
1100
|
+
|
|
1101
|
+
max_start = max(
|
|
1102
|
+
rfc3339_str_to_datetime(missionStartDate)
|
|
1103
|
+
if missionStartDate
|
|
1104
|
+
else min_aware,
|
|
1105
|
+
rfc3339_str_to_datetime(pt_dict["missionStartDate"])
|
|
1106
|
+
if pt_dict.get("missionStartDate")
|
|
1107
|
+
else min_aware,
|
|
1186
1108
|
)
|
|
1187
|
-
|
|
1109
|
+
min_end = min(
|
|
1110
|
+
rfc3339_str_to_datetime(missionEndDate)
|
|
1111
|
+
if missionEndDate
|
|
1112
|
+
else max_aware,
|
|
1113
|
+
rfc3339_str_to_datetime(pt_dict["missionEndDate"])
|
|
1114
|
+
if pt_dict.get("missionEndDate")
|
|
1115
|
+
else max_aware,
|
|
1116
|
+
)
|
|
1117
|
+
if not (max_start <= min_end):
|
|
1118
|
+
continue
|
|
1188
1119
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1120
|
+
guesses_with_score.append((pt_id, score))
|
|
1121
|
+
|
|
1122
|
+
if guesses_with_score:
|
|
1123
|
+
# sort by score descending, then pt_id for stability
|
|
1124
|
+
guesses_with_score.sort(key=lambda x: (-x[1], x[0]))
|
|
1125
|
+
return [pt_id for pt_id, _ in guesses_with_score]
|
|
1191
1126
|
|
|
1192
1127
|
raise NoMatchingProductType()
|
|
1193
1128
|
|
|
@@ -2007,20 +1942,6 @@ class EODataAccessGateway:
|
|
|
2007
1942
|
nb_res,
|
|
2008
1943
|
search_plugin.provider,
|
|
2009
1944
|
)
|
|
2010
|
-
# Hitting for instance
|
|
2011
|
-
# https://theia.cnes.fr/atdistrib/resto2/api/collections/SENTINEL2/
|
|
2012
|
-
# search.json?startDate=2019-03-01&completionDate=2019-06-15
|
|
2013
|
-
# &processingLevel=LEVEL2A&maxRecords=1&page=1
|
|
2014
|
-
# returns a number (properties.totalResults) that is the number of
|
|
2015
|
-
# products in the collection (here SENTINEL2) instead of the estimated
|
|
2016
|
-
# total number of products matching the search criteria (start/end date).
|
|
2017
|
-
# Remove this warning when this is fixed upstream by THEIA.
|
|
2018
|
-
if search_plugin.provider == "theia":
|
|
2019
|
-
logger.warning(
|
|
2020
|
-
"Results found on provider 'theia' is the total number of products "
|
|
2021
|
-
"available in the searched collection (e.g. SENTINEL2) instead of "
|
|
2022
|
-
"the total number of products matching the search criteria"
|
|
2023
|
-
)
|
|
2024
1945
|
except Exception as e:
|
|
2025
1946
|
if raise_errors:
|
|
2026
1947
|
# Raise the error, letting the application wrapping eodag know that
|
|
@@ -42,6 +42,7 @@ from shapely.ops import transform
|
|
|
42
42
|
from eodag.types.queryables import Queryables
|
|
43
43
|
from eodag.utils import (
|
|
44
44
|
DEFAULT_PROJ,
|
|
45
|
+
_deprecated,
|
|
45
46
|
deepcopy,
|
|
46
47
|
dict_items_recursive_apply,
|
|
47
48
|
format_string,
|
|
@@ -180,6 +181,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
180
181
|
- ``to_datetime_dict``: convert a datetime string to a dictionary where values are either a string or a list
|
|
181
182
|
- ``get_ecmwf_time``: get the time of a datetime string in the ECMWF format
|
|
182
183
|
- ``sanitize``: sanitize string
|
|
184
|
+
- ``ceda_collection_name``: generate a CEDA collection name from a string
|
|
183
185
|
|
|
184
186
|
:param search_param: The string to be formatted
|
|
185
187
|
:param args: (optional) Additional arguments to use in the formatting process
|
|
@@ -219,7 +221,7 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
219
221
|
elif value is not None:
|
|
220
222
|
converted = self.custom_converter(value)
|
|
221
223
|
else:
|
|
222
|
-
converted =
|
|
224
|
+
converted = None
|
|
223
225
|
# Clear this state variable in case the same converter is used to
|
|
224
226
|
# resolve other named arguments
|
|
225
227
|
self.custom_converter = None
|
|
@@ -374,6 +376,18 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
374
376
|
def convert_to_geojson(value: Any) -> str:
|
|
375
377
|
return geojson.dumps(value)
|
|
376
378
|
|
|
379
|
+
@staticmethod
|
|
380
|
+
def convert_to_geojson_polytope(
|
|
381
|
+
value: BaseGeometry,
|
|
382
|
+
) -> Union[dict[Any, Any], str]:
|
|
383
|
+
# ECMWF Polytope uses non-geojson structure for features
|
|
384
|
+
if isinstance(value, Polygon):
|
|
385
|
+
return {
|
|
386
|
+
"type": "polygon",
|
|
387
|
+
"shape": [[y, x] for x, y in value.exterior.coords],
|
|
388
|
+
}
|
|
389
|
+
raise ValidationError("to_geojson_polytope only accepts shapely Polygon")
|
|
390
|
+
|
|
377
391
|
@staticmethod
|
|
378
392
|
def convert_from_ewkt(ewkt_string: str) -> Union[BaseGeometry, str]:
|
|
379
393
|
"""Convert EWKT (Extended Well-Known text) to shapely geometry"""
|
|
@@ -488,10 +502,14 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
488
502
|
|
|
489
503
|
@staticmethod
|
|
490
504
|
def convert_get_group_name(string: str, pattern: str) -> str:
|
|
505
|
+
sanitized_pattern = pattern.replace(" ", "_SPACE_")
|
|
491
506
|
try:
|
|
492
|
-
match = re.search(
|
|
507
|
+
match = re.search(sanitized_pattern, str(string))
|
|
493
508
|
if match:
|
|
494
|
-
|
|
509
|
+
if result := match.lastgroup:
|
|
510
|
+
return result.replace("_SPACE_", " ")
|
|
511
|
+
else:
|
|
512
|
+
return NOT_AVAILABLE
|
|
495
513
|
except AttributeError:
|
|
496
514
|
pass
|
|
497
515
|
logger.warning(
|
|
@@ -511,6 +529,14 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
511
529
|
old, new = ast.literal_eval(args)
|
|
512
530
|
return re.sub(old, new, value)
|
|
513
531
|
|
|
532
|
+
@staticmethod
|
|
533
|
+
def convert_ceda_collection_name(value: str) -> str:
|
|
534
|
+
data_regex = re.compile(r"/data/(?P<name>.+?)/?$")
|
|
535
|
+
match = data_regex.search(value)
|
|
536
|
+
if match:
|
|
537
|
+
return match.group("name").replace("/", "_").upper()
|
|
538
|
+
return "NOT_AVAILABLE"
|
|
539
|
+
|
|
514
540
|
@staticmethod
|
|
515
541
|
def convert_recursive_sub_str(
|
|
516
542
|
input_obj: Union[dict[Any, Any], list[Any]], args: str
|
|
@@ -615,6 +641,10 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
615
641
|
return NOT_AVAILABLE
|
|
616
642
|
|
|
617
643
|
@staticmethod
|
|
644
|
+
@_deprecated(
|
|
645
|
+
reason="Method that was used in previous wekeo provider configuration, but not used anymore",
|
|
646
|
+
version="3.7.1",
|
|
647
|
+
)
|
|
618
648
|
def convert_split_id_into_s1_params(product_id: str) -> dict[str, str]:
|
|
619
649
|
parts: list[str] = re.split(r"_(?!_)", product_id)
|
|
620
650
|
if len(parts) < 9:
|
|
@@ -667,6 +697,10 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
667
697
|
return params
|
|
668
698
|
|
|
669
699
|
@staticmethod
|
|
700
|
+
@_deprecated(
|
|
701
|
+
reason="Method that was used in previous wekeo provider configuration, but not used anymore",
|
|
702
|
+
version="3.7.1",
|
|
703
|
+
)
|
|
670
704
|
def convert_split_id_into_s5p_params(product_id: str) -> dict[str, str]:
|
|
671
705
|
parts: list[str] = re.split(r"_(?!_)", product_id)
|
|
672
706
|
params = {
|
|
@@ -685,6 +719,10 @@ def format_metadata(search_param: str, *args: Any, **kwargs: Any) -> str:
|
|
|
685
719
|
return params
|
|
686
720
|
|
|
687
721
|
@staticmethod
|
|
722
|
+
@_deprecated(
|
|
723
|
+
reason="Method that was used in previous wekeo provider configuration, but not used anymore",
|
|
724
|
+
version="3.7.1",
|
|
725
|
+
)
|
|
688
726
|
def convert_split_cop_dem_id(product_id: str) -> list[int]:
|
|
689
727
|
parts = product_id.split("_")
|
|
690
728
|
lattitude = parts[3]
|
|
@@ -1342,6 +1380,7 @@ def format_query_params(
|
|
|
1342
1380
|
formatted_query_param = remove_str_array_quotes(
|
|
1343
1381
|
formatted_query_param
|
|
1344
1382
|
)
|
|
1383
|
+
|
|
1345
1384
|
# json query string (for POST request)
|
|
1346
1385
|
update_nested_dict(
|
|
1347
1386
|
query_params,
|
eodag/cli.py
CHANGED
|
@@ -49,11 +49,12 @@ import sys
|
|
|
49
49
|
import textwrap
|
|
50
50
|
from importlib.metadata import metadata
|
|
51
51
|
from typing import TYPE_CHECKING, Any, Mapping
|
|
52
|
+
from urllib.parse import parse_qs
|
|
52
53
|
|
|
53
54
|
import click
|
|
54
55
|
|
|
55
56
|
from eodag.api.core import EODataAccessGateway, SearchResult
|
|
56
|
-
from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE
|
|
57
|
+
from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE
|
|
57
58
|
from eodag.utils.exceptions import NoMatchingProductType, UnsupportedProvider
|
|
58
59
|
from eodag.utils.logging import setup_logging
|
|
59
60
|
|
|
@@ -109,8 +110,9 @@ class MutuallyExclusiveOption(click.Option):
|
|
|
109
110
|
"""Raise error or use parent handle_parse_result()"""
|
|
110
111
|
if self.mutually_exclusive.intersection(opts) and self.name in opts:
|
|
111
112
|
raise click.UsageError(
|
|
112
|
-
"Illegal usage: `{}` is mutually exclusive with "
|
|
113
|
-
|
|
113
|
+
"Illegal usage: `{}` is mutually exclusive with arguments `{}`.".format(
|
|
114
|
+
self.name, ", ".join(self.mutually_exclusive)
|
|
115
|
+
)
|
|
114
116
|
)
|
|
115
117
|
|
|
116
118
|
return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args)
|
|
@@ -687,6 +689,7 @@ def serve_rest(
|
|
|
687
689
|
setup_logging(verbose=ctx.obj["verbosity"])
|
|
688
690
|
try:
|
|
689
691
|
import uvicorn
|
|
692
|
+
import uvicorn.config
|
|
690
693
|
except ImportError:
|
|
691
694
|
raise ImportError(
|
|
692
695
|
"Feature not available, please install eodag[server] or eodag[all]"
|
eodag/config.py
CHANGED
|
@@ -307,7 +307,8 @@ class PluginConfig(yaml.YAMLObject):
|
|
|
307
307
|
single_collection_fetch_url: str
|
|
308
308
|
#: Query string to be added to the fetch_url to filter for a collection
|
|
309
309
|
single_collection_fetch_qs: str
|
|
310
|
-
#: Mapping for product type metadata returned by the endpoint given in single_collection_fetch_url
|
|
310
|
+
#: Mapping for product type metadata returned by the endpoint given in single_collection_fetch_url. If ``ID``
|
|
311
|
+
#: is redefined in this mapping, it will replace ``generic_product_type_id`` value
|
|
311
312
|
single_product_type_parsable_metadata: dict[str, str]
|
|
312
313
|
|
|
313
314
|
class DiscoverQueryables(TypedDict, total=False):
|
|
@@ -452,6 +453,11 @@ class PluginConfig(yaml.YAMLObject):
|
|
|
452
453
|
discover_queryables: PluginConfig.DiscoverQueryables
|
|
453
454
|
#: :class:`~eodag.plugins.search.base.Search` The mapping between eodag metadata and the plugin specific metadata
|
|
454
455
|
metadata_mapping: dict[str, Union[str, list[str]]]
|
|
456
|
+
#: :class:`~eodag.plugins.search.base.Search` :attr:`~eodag.config.PluginConfig.metadata_mapping` got from the given
|
|
457
|
+
#: product type
|
|
458
|
+
metadata_mapping_from_product: str
|
|
459
|
+
#: :class:`~eodag.plugins.search.base.Search` A mapping for the metadata of individual assets
|
|
460
|
+
assets_mapping: dict[str, dict[str, Any]]
|
|
455
461
|
#: :class:`~eodag.plugins.search.base.Search` Parameters to remove from queryables
|
|
456
462
|
remove_from_queryables: list[str]
|
|
457
463
|
#: :class:`~eodag.plugins.search.base.Search` Parameters to be passed as is in the search url query string
|
|
@@ -23,6 +23,7 @@ import string
|
|
|
23
23
|
from datetime import datetime, timedelta, timezone
|
|
24
24
|
from random import SystemRandom
|
|
25
25
|
from typing import TYPE_CHECKING, Any, Optional
|
|
26
|
+
from urllib.parse import parse_qs, urlparse
|
|
26
27
|
|
|
27
28
|
import jwt
|
|
28
29
|
import requests
|
|
@@ -34,9 +35,7 @@ from eodag.utils import (
|
|
|
34
35
|
DEFAULT_TOKEN_EXPIRATION_MARGIN,
|
|
35
36
|
HTTP_REQ_TIMEOUT,
|
|
36
37
|
USER_AGENT,
|
|
37
|
-
parse_qs,
|
|
38
38
|
repeatfunc,
|
|
39
|
-
urlparse,
|
|
40
39
|
)
|
|
41
40
|
from eodag.utils.exceptions import (
|
|
42
41
|
AuthenticationError,
|