eodag 3.0.0b2__py3-none-any.whl → 3.0.1__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/__init__.py +6 -8
- eodag/api/core.py +295 -287
- eodag/api/product/__init__.py +10 -4
- eodag/api/product/_assets.py +2 -14
- eodag/api/product/_product.py +16 -30
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +12 -31
- eodag/api/search_result.py +33 -12
- eodag/cli.py +35 -19
- eodag/config.py +455 -155
- eodag/plugins/apis/base.py +13 -7
- eodag/plugins/apis/ecmwf.py +16 -7
- eodag/plugins/apis/usgs.py +68 -16
- eodag/plugins/authentication/aws_auth.py +25 -7
- 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 +183 -167
- eodag/plugins/authentication/qsauth.py +12 -4
- eodag/plugins/authentication/sas_auth.py +19 -2
- eodag/plugins/authentication/token.py +59 -11
- eodag/plugins/authentication/token_exchange.py +19 -19
- eodag/plugins/crunch/base.py +7 -2
- eodag/plugins/crunch/filter_date.py +8 -11
- eodag/plugins/crunch/filter_latest_intersect.py +5 -7
- eodag/plugins/crunch/filter_latest_tpl_name.py +2 -5
- eodag/plugins/crunch/filter_overlap.py +9 -15
- eodag/plugins/crunch/filter_property.py +9 -14
- eodag/plugins/download/aws.py +84 -99
- eodag/plugins/download/base.py +36 -77
- eodag/plugins/download/creodias_s3.py +11 -2
- eodag/plugins/download/http.py +134 -109
- eodag/plugins/download/s3rest.py +37 -43
- eodag/plugins/manager.py +173 -41
- eodag/plugins/search/__init__.py +9 -9
- eodag/plugins/search/base.py +35 -35
- eodag/plugins/search/build_search_result.py +55 -64
- eodag/plugins/search/cop_marine.py +113 -32
- eodag/plugins/search/creodias_s3.py +20 -8
- eodag/plugins/search/csw.py +41 -1
- eodag/plugins/search/data_request_search.py +119 -14
- eodag/plugins/search/qssearch.py +619 -197
- eodag/plugins/search/static_stac_search.py +25 -23
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +211 -56
- eodag/resources/providers.yml +1762 -1809
- eodag/resources/stac.yml +3 -163
- eodag/resources/user_conf_template.yml +134 -119
- eodag/rest/config.py +1 -2
- eodag/rest/constants.py +0 -1
- eodag/rest/core.py +70 -92
- eodag/rest/errors.py +181 -0
- eodag/rest/server.py +24 -330
- eodag/rest/stac.py +105 -630
- eodag/rest/types/eodag_search.py +17 -15
- eodag/rest/types/queryables.py +5 -14
- eodag/rest/types/stac_search.py +18 -13
- eodag/rest/utils/rfc3339.py +0 -1
- eodag/types/__init__.py +24 -6
- eodag/types/download_args.py +14 -5
- eodag/types/queryables.py +1 -2
- eodag/types/search_args.py +10 -11
- eodag/types/whoosh.py +0 -2
- eodag/utils/__init__.py +97 -136
- eodag/utils/constraints.py +0 -8
- eodag/utils/exceptions.py +23 -9
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/requests.py +13 -23
- eodag/utils/rest.py +0 -4
- eodag/utils/stac_reader.py +3 -15
- {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/METADATA +41 -24
- eodag-3.0.1.dist-info/RECORD +109 -0
- {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/WHEEL +1 -1
- {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/entry_points.txt +1 -0
- eodag/resources/constraints/climate-dt.json +0 -13
- eodag/resources/constraints/extremes-dt.json +0 -8
- eodag-3.0.0b2.dist-info/RECORD +0 -110
- {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/LICENSE +0 -0
- {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/top_level.txt +0 -0
eodag/plugins/search/base.py
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
from typing import TYPE_CHECKING
|
|
21
|
+
from typing import TYPE_CHECKING, Annotated, get_args
|
|
22
22
|
|
|
23
23
|
import orjson
|
|
24
24
|
from pydantic.fields import Field, FieldInfo
|
|
@@ -35,11 +35,9 @@ from eodag.types.queryables import Queryables
|
|
|
35
35
|
from eodag.types.search_args import SortByList
|
|
36
36
|
from eodag.utils import (
|
|
37
37
|
GENERIC_PRODUCT_TYPE,
|
|
38
|
-
Annotated,
|
|
39
38
|
copy_deepcopy,
|
|
40
39
|
deepcopy,
|
|
41
40
|
format_dict_items,
|
|
42
|
-
get_args,
|
|
43
41
|
update_nested_dict,
|
|
44
42
|
)
|
|
45
43
|
from eodag.utils.exceptions import ValidationError
|
|
@@ -59,9 +57,7 @@ class Search(PluginTopic):
|
|
|
59
57
|
"""Base Search Plugin.
|
|
60
58
|
|
|
61
59
|
:param provider: An EODAG provider name
|
|
62
|
-
:type provider: str
|
|
63
60
|
:param config: An EODAG plugin configuration
|
|
64
|
-
:type config: :class:`~eodag.config.PluginConfig`
|
|
65
61
|
"""
|
|
66
62
|
|
|
67
63
|
auth: Union[AuthBase, Dict[str, str]]
|
|
@@ -97,9 +93,9 @@ class Search(PluginTopic):
|
|
|
97
93
|
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
98
94
|
"""Implementation of how the products must be searched goes here.
|
|
99
95
|
|
|
100
|
-
This method must return a tuple with (1) a list of
|
|
101
|
-
which will be processed by a Download plugin (2) and the total number of
|
|
102
|
-
the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
|
|
96
|
+
This method must return a tuple with (1) a list of :class:`~eodag.api.product._product.EOProduct` instances
|
|
97
|
+
which will be processed by a :class:`~eodag.plugins.download.base.Download` plugin (2) and the total number of
|
|
98
|
+
products matching the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
|
|
103
99
|
"""
|
|
104
100
|
raise NotImplementedError("A Search plugin must implement a method named query")
|
|
105
101
|
|
|
@@ -110,13 +106,11 @@ class Search(PluginTopic):
|
|
|
110
106
|
def discover_queryables(
|
|
111
107
|
self, **kwargs: Any
|
|
112
108
|
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
|
|
113
|
-
"""Fetch queryables list from provider using
|
|
109
|
+
"""Fetch queryables list from provider using :attr:`~eodag.config.PluginConfig.discover_queryables` conf
|
|
114
110
|
|
|
115
|
-
:param kwargs: additional filters for queryables (
|
|
111
|
+
:param kwargs: additional filters for queryables (``productType`` and other search
|
|
116
112
|
arguments)
|
|
117
|
-
:type kwargs: Any
|
|
118
113
|
:returns: fetched queryable parameters dict
|
|
119
|
-
:rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
|
|
120
114
|
"""
|
|
121
115
|
raise NotImplementedError(
|
|
122
116
|
f"discover_queryables is not implemeted for plugin {self.__class__.__name__}"
|
|
@@ -129,9 +123,7 @@ class Search(PluginTopic):
|
|
|
129
123
|
Return given product type default settings as queryables
|
|
130
124
|
|
|
131
125
|
:param product_type: given product type
|
|
132
|
-
:type product_type: str
|
|
133
126
|
:returns: queryable parameters dict
|
|
134
|
-
:rtype: Dict[str, Annotated[Any, FieldInfo]]
|
|
135
127
|
"""
|
|
136
128
|
defaults = deepcopy(self.config.products.get(product_type, {}))
|
|
137
129
|
defaults.pop("metadata_mapping", None)
|
|
@@ -147,9 +139,7 @@ class Search(PluginTopic):
|
|
|
147
139
|
"""Get the provider product type from eodag product type
|
|
148
140
|
|
|
149
141
|
:param product_type: eodag product type
|
|
150
|
-
:type product_type: str
|
|
151
142
|
:returns: provider product type
|
|
152
|
-
:rtype: str
|
|
153
143
|
"""
|
|
154
144
|
if product_type is None:
|
|
155
145
|
return None
|
|
@@ -164,9 +154,7 @@ class Search(PluginTopic):
|
|
|
164
154
|
"""Get the provider product type definition parameters and specific settings
|
|
165
155
|
|
|
166
156
|
:param product_type: the desired product type
|
|
167
|
-
:type product_type: str
|
|
168
157
|
:returns: The product type definition parameters
|
|
169
|
-
:rtype: dict
|
|
170
158
|
"""
|
|
171
159
|
if product_type in self.config.products.keys():
|
|
172
160
|
logger.debug(
|
|
@@ -189,15 +177,34 @@ class Search(PluginTopic):
|
|
|
189
177
|
else:
|
|
190
178
|
return {}
|
|
191
179
|
|
|
180
|
+
def get_product_type_cfg_value(self, key: str, default: Any = None) -> Any:
|
|
181
|
+
"""
|
|
182
|
+
Get the value of a configuration option specific to the current product type.
|
|
183
|
+
|
|
184
|
+
This method retrieves the value of a configuration option from the
|
|
185
|
+
``product_type_config`` attribute. If the option is not found, the provided
|
|
186
|
+
default value is returned.
|
|
187
|
+
|
|
188
|
+
:param key: The configuration option key.
|
|
189
|
+
:type key: str
|
|
190
|
+
:param default: The default value to be returned if the option is not found (default is None).
|
|
191
|
+
:type default: Any
|
|
192
|
+
|
|
193
|
+
:return: The value of the specified configuration option or the default value.
|
|
194
|
+
:rtype: Any
|
|
195
|
+
"""
|
|
196
|
+
product_type_cfg = getattr(self.config, "product_type_config", {})
|
|
197
|
+
non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
|
|
198
|
+
|
|
199
|
+
return non_none_cfg.get(key, default)
|
|
200
|
+
|
|
192
201
|
def get_metadata_mapping(
|
|
193
202
|
self, product_type: Optional[str] = None
|
|
194
203
|
) -> Dict[str, Union[str, List[str]]]:
|
|
195
204
|
"""Get the plugin metadata mapping configuration (product type specific if exists)
|
|
196
205
|
|
|
197
206
|
:param product_type: the desired product type
|
|
198
|
-
:type product_type: str
|
|
199
207
|
:returns: The product type specific metadata-mapping
|
|
200
|
-
:rtype: dict
|
|
201
208
|
"""
|
|
202
209
|
if product_type:
|
|
203
210
|
return self.config.products.get(product_type, {}).get(
|
|
@@ -206,16 +213,14 @@ class Search(PluginTopic):
|
|
|
206
213
|
return self.config.metadata_mapping
|
|
207
214
|
|
|
208
215
|
def get_sort_by_arg(self, kwargs: Dict[str, Any]) -> Optional[SortByList]:
|
|
209
|
-
"""Extract the
|
|
216
|
+
"""Extract the ``sort_by`` argument from the kwargs or the provider default sort configuration
|
|
210
217
|
|
|
211
218
|
:param kwargs: Search arguments
|
|
212
|
-
:
|
|
213
|
-
:returns: The "sortBy" argument from the kwargs or the provider default sort configuration
|
|
214
|
-
:rtype: :class:`~eodag.types.search_args.SortByList`
|
|
219
|
+
:returns: The ``sort_by`` argument from the kwargs or the provider default sort configuration
|
|
215
220
|
"""
|
|
216
|
-
# remove "
|
|
221
|
+
# remove "sort_by" from search args if exists because it is not part of metadata mapping,
|
|
217
222
|
# it will complete the query string or body once metadata mapping will be done
|
|
218
|
-
sort_by_arg_tmp = kwargs.pop("
|
|
223
|
+
sort_by_arg_tmp = kwargs.pop("sort_by", None)
|
|
219
224
|
sort_by_arg = sort_by_arg_tmp or getattr(self.config, "sort", {}).get(
|
|
220
225
|
"sort_by_default", None
|
|
221
226
|
)
|
|
@@ -230,18 +235,16 @@ class Search(PluginTopic):
|
|
|
230
235
|
self, sort_by_arg: SortByList
|
|
231
236
|
) -> Tuple[str, Dict[str, List[Dict[str, str]]]]:
|
|
232
237
|
"""Build the sorting part of the query string or body by transforming
|
|
233
|
-
the
|
|
238
|
+
the ``sort_by`` argument into a provider-specific string or dictionary
|
|
234
239
|
|
|
235
|
-
:param sort_by_arg: the
|
|
236
|
-
:
|
|
237
|
-
:returns: The "sortBy" argument in provider-specific format
|
|
238
|
-
:rtype: Union[str, Dict[str, List[Dict[str, str]]]]
|
|
240
|
+
:param sort_by_arg: the ``sort_by`` argument in EODAG format
|
|
241
|
+
:returns: The ``sort_by`` argument in provider-specific format
|
|
239
242
|
"""
|
|
240
243
|
if not hasattr(self.config, "sort"):
|
|
241
244
|
raise ValidationError(f"{self.provider} does not support sorting feature")
|
|
242
245
|
# TODO: remove this code block when search args model validation is embeded
|
|
243
246
|
# remove duplicates
|
|
244
|
-
sort_by_arg = list(
|
|
247
|
+
sort_by_arg = list(dict.fromkeys(sort_by_arg))
|
|
245
248
|
|
|
246
249
|
sort_by_qs: str = ""
|
|
247
250
|
sort_by_qp: Dict[str, Any] = {}
|
|
@@ -331,13 +334,10 @@ class Search(PluginTopic):
|
|
|
331
334
|
Get queryables
|
|
332
335
|
|
|
333
336
|
:param filters: Additional filters for queryables.
|
|
334
|
-
:type filters: Dict[str, Any]
|
|
335
337
|
:param product_type: (optional) The product type.
|
|
336
|
-
:type product_type: Optional[str]
|
|
337
338
|
|
|
338
339
|
:return: A dictionary containing the queryable properties, associating parameters to their
|
|
339
340
|
annotated type.
|
|
340
|
-
:rtype: Dict[str, Annotated[Any, FieldInfo]]
|
|
341
341
|
"""
|
|
342
342
|
default_values: Dict[str, Any] = deepcopy(
|
|
343
343
|
getattr(self.config, "products", {}).get(product_type, {})
|
|
@@ -20,16 +20,27 @@ from __future__ import annotations
|
|
|
20
20
|
import hashlib
|
|
21
21
|
import logging
|
|
22
22
|
from datetime import datetime, timedelta, timezone
|
|
23
|
-
from typing import
|
|
23
|
+
from typing import (
|
|
24
|
+
TYPE_CHECKING,
|
|
25
|
+
Annotated,
|
|
26
|
+
Any,
|
|
27
|
+
Dict,
|
|
28
|
+
List,
|
|
29
|
+
Optional,
|
|
30
|
+
Set,
|
|
31
|
+
Tuple,
|
|
32
|
+
cast,
|
|
33
|
+
get_args,
|
|
34
|
+
)
|
|
24
35
|
from urllib.parse import quote_plus, unquote_plus
|
|
25
36
|
|
|
26
37
|
import geojson
|
|
27
38
|
import orjson
|
|
28
39
|
from dateutil.parser import isoparse
|
|
40
|
+
from dateutil.tz import tzutc
|
|
29
41
|
from jsonpath_ng import Child, Fields, Root
|
|
30
42
|
from pydantic import create_model
|
|
31
43
|
from pydantic.fields import FieldInfo
|
|
32
|
-
from typing_extensions import get_args
|
|
33
44
|
|
|
34
45
|
from eodag.api.product import EOProduct
|
|
35
46
|
from eodag.api.product.metadata_mapping import (
|
|
@@ -47,7 +58,6 @@ from eodag.types import json_field_definition_to_python, model_fields_to_annotat
|
|
|
47
58
|
from eodag.types.queryables import CommonQueryables
|
|
48
59
|
from eodag.utils import (
|
|
49
60
|
DEFAULT_MISSION_START_DATE,
|
|
50
|
-
Annotated,
|
|
51
61
|
deepcopy,
|
|
52
62
|
dict_items_recursive_sort,
|
|
53
63
|
get_geometry_from_various,
|
|
@@ -71,23 +81,16 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
71
81
|
performs a POST request and uses its result to build a single :class:`~eodag.api.search_result.SearchResult`
|
|
72
82
|
object.
|
|
73
83
|
|
|
74
|
-
The available configuration parameters
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- **api_endpoint**: (mandatory) The endpoint of the provider's search interface
|
|
84
|
+
The available configuration parameters are inherited from parent classes
|
|
85
|
+
(:class:`~eodag.plugins.search.qssearch.PostJsonSearch` and
|
|
86
|
+
:class:`~eodag.plugins.search.qssearch.QueryStringSearch`), with particularly for this plugin:
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
:param provider: provider name
|
|
89
|
+
:param config: Search plugin configuration:
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
formatted like `{{"foo":"bar"}}` because it will be passed to a `.format()`
|
|
85
|
-
method before being loaded as json.
|
|
91
|
+
* :attr:`~eodag.config.PluginConfig.remove_from_query` (``List[str]``): List of parameters
|
|
92
|
+
used to parse metadata but that must not be included to the query
|
|
86
93
|
|
|
87
|
-
:param provider: An eodag providers configuration dictionary
|
|
88
|
-
:type provider: dict
|
|
89
|
-
:param config: Path to the user configuration file
|
|
90
|
-
:type config: str
|
|
91
94
|
"""
|
|
92
95
|
|
|
93
96
|
def count_hits(
|
|
@@ -112,8 +115,8 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
112
115
|
prep.url = prep.search_urls[0]
|
|
113
116
|
prep.info_message = f"Sending search request: {prep.url}"
|
|
114
117
|
prep.exception_message = (
|
|
115
|
-
f"Skipping error while searching for {self.provider}
|
|
116
|
-
f"{self.__class__.__name__} instance
|
|
118
|
+
f"Skipping error while searching for {self.provider}"
|
|
119
|
+
f" {self.__class__.__name__} instance"
|
|
117
120
|
)
|
|
118
121
|
response = self._request(prep)
|
|
119
122
|
|
|
@@ -125,11 +128,8 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
125
128
|
"""Build :class:`~eodag.api.product._product.EOProduct` from provider result
|
|
126
129
|
|
|
127
130
|
:param results: Raw provider result as single dict in list
|
|
128
|
-
:type results: list
|
|
129
131
|
:param kwargs: Search arguments
|
|
130
|
-
:type kwargs: Union[int, str, bool, dict, list]
|
|
131
132
|
:returns: list of single :class:`~eodag.api.product._product.EOProduct`
|
|
132
|
-
:rtype: list
|
|
133
133
|
"""
|
|
134
134
|
product_type = kwargs.get("productType")
|
|
135
135
|
|
|
@@ -183,7 +183,7 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
183
183
|
result.update(results.product_type_def_params)
|
|
184
184
|
result = dict(result, **{k: v for k, v in kwargs.items() if v is not None})
|
|
185
185
|
|
|
186
|
-
# parse
|
|
186
|
+
# parse properties
|
|
187
187
|
parsed_properties = properties_from_json(
|
|
188
188
|
result,
|
|
189
189
|
self.config.metadata_mapping,
|
|
@@ -244,21 +244,18 @@ class BuildSearchResult(BuildPostSearchResult):
|
|
|
244
244
|
This plugin builds a single :class:`~eodag.api.search_result.SearchResult` object
|
|
245
245
|
using given query parameters as product properties.
|
|
246
246
|
|
|
247
|
-
The available configuration parameters inherits from parent classes
|
|
248
|
-
|
|
247
|
+
The available configuration parameters inherits from parent classes
|
|
248
|
+
(:class:`~eodag.plugins.search.build_search_result.BuildPostSearchResult`,
|
|
249
|
+
:class:`~eodag.plugins.search.qssearch.PostJsonSearch` and
|
|
250
|
+
:class:`~eodag.plugins.search.qssearch.QueryStringSearch`), with particularly for this plugin:
|
|
249
251
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
:param provider: provider name
|
|
253
|
+
:param config: Search plugin configuration:
|
|
252
254
|
|
|
253
|
-
|
|
254
|
-
not
|
|
255
|
+
* :attr:`~eodag.config.PluginConfig.end_date_excluded` (``bool``): Set to ``False`` if provider
|
|
256
|
+
does not include end date in the search request; In this case, if the end date is at midnight,
|
|
257
|
+
the previous day will be used. default: ``True``
|
|
255
258
|
|
|
256
|
-
- **constraints_file_url**: url of the constraint file used to build queryables
|
|
257
|
-
|
|
258
|
-
:param provider: An eodag providers configuration dictionary
|
|
259
|
-
:type provider: dict
|
|
260
|
-
:param config: Path to the user configuration file
|
|
261
|
-
:type config: str
|
|
262
259
|
"""
|
|
263
260
|
|
|
264
261
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
@@ -346,27 +343,6 @@ class BuildSearchResult(BuildPostSearchResult):
|
|
|
346
343
|
self, product_type=product_type, **available_properties
|
|
347
344
|
)
|
|
348
345
|
|
|
349
|
-
def get_product_type_cfg(self, key: str, default: Any = None) -> Any:
|
|
350
|
-
"""
|
|
351
|
-
Get the value of a configuration option specific to the current product type.
|
|
352
|
-
|
|
353
|
-
This method retrieves the value of a configuration option from the
|
|
354
|
-
`_product_type_config` attribute. If the option is not found, the provided
|
|
355
|
-
default value is returned.
|
|
356
|
-
|
|
357
|
-
:param key: The configuration option key.
|
|
358
|
-
:type key: str
|
|
359
|
-
:param default: The default value to be returned if the option is not found (default is None).
|
|
360
|
-
:type default: Any
|
|
361
|
-
|
|
362
|
-
:return: The value of the specified configuration option or the default value.
|
|
363
|
-
:rtype: Any
|
|
364
|
-
"""
|
|
365
|
-
product_type_cfg = getattr(self.config, "product_type_config", {})
|
|
366
|
-
non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
|
|
367
|
-
|
|
368
|
-
return non_none_cfg.get(key, default)
|
|
369
|
-
|
|
370
346
|
def _preprocess_search_params(self, params: Dict[str, Any]) -> None:
|
|
371
347
|
"""Preprocess search parameters before making a request to the CDS API.
|
|
372
348
|
|
|
@@ -376,7 +352,6 @@ class BuildSearchResult(BuildPostSearchResult):
|
|
|
376
352
|
in the input parameters, default values or values from the configuration are used.
|
|
377
353
|
|
|
378
354
|
:param params: Search parameters to be preprocessed.
|
|
379
|
-
:type params: dict
|
|
380
355
|
"""
|
|
381
356
|
_dc_qs = params.get("_dc_qs", None)
|
|
382
357
|
if _dc_qs is not None:
|
|
@@ -408,7 +383,7 @@ class BuildSearchResult(BuildPostSearchResult):
|
|
|
408
383
|
|
|
409
384
|
# dates
|
|
410
385
|
mission_start_dt = datetime.fromisoformat(
|
|
411
|
-
self.
|
|
386
|
+
self.get_product_type_cfg_value(
|
|
412
387
|
"missionStartDate", DEFAULT_MISSION_START_DATE
|
|
413
388
|
).replace(
|
|
414
389
|
"Z", "+00:00"
|
|
@@ -434,11 +409,29 @@ class BuildSearchResult(BuildPostSearchResult):
|
|
|
434
409
|
"completionTimeFromAscendingNode", default_end_str
|
|
435
410
|
)
|
|
436
411
|
|
|
437
|
-
#
|
|
412
|
+
# adapt end date if it is midnight
|
|
438
413
|
end_date_excluded = getattr(self.config, "end_date_excluded", True)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
414
|
+
is_datetime = True
|
|
415
|
+
try:
|
|
416
|
+
end_date = datetime.strptime(
|
|
417
|
+
params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
|
|
418
|
+
)
|
|
419
|
+
end_date = end_date.replace(tzinfo=tzutc())
|
|
420
|
+
except ValueError:
|
|
421
|
+
try:
|
|
422
|
+
end_date = datetime.strptime(
|
|
423
|
+
params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
424
|
+
)
|
|
425
|
+
end_date = end_date.replace(tzinfo=tzutc())
|
|
426
|
+
except ValueError:
|
|
427
|
+
end_date = isoparse(params["completionTimeFromAscendingNode"])
|
|
428
|
+
is_datetime = False
|
|
429
|
+
start_date = isoparse(params["startTimeFromAscendingNode"])
|
|
430
|
+
if (
|
|
431
|
+
not end_date_excluded
|
|
432
|
+
and is_datetime
|
|
433
|
+
and end_date > start_date
|
|
434
|
+
and end_date == end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
442
435
|
):
|
|
443
436
|
end_date += timedelta(days=-1)
|
|
444
437
|
params["completionTimeFromAscendingNode"] = end_date.isoformat()
|
|
@@ -454,9 +447,7 @@ class BuildSearchResult(BuildPostSearchResult):
|
|
|
454
447
|
|
|
455
448
|
:param kwargs: additional filters for queryables (`productType` and other search
|
|
456
449
|
arguments)
|
|
457
|
-
:type kwargs: Any
|
|
458
450
|
:returns: fetched queryable parameters dict
|
|
459
|
-
:rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
|
|
460
451
|
"""
|
|
461
452
|
constraints_file_url = getattr(self.config, "constraints_file_url", "")
|
|
462
453
|
if not constraints_file_url:
|
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import copy
|
|
21
21
|
import logging
|
|
22
|
+
import os
|
|
22
23
|
import re
|
|
23
24
|
from datetime import datetime
|
|
24
25
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
|
|
@@ -37,7 +38,7 @@ from eodag.config import PluginConfig
|
|
|
37
38
|
from eodag.plugins.search import PreparedSearch
|
|
38
39
|
from eodag.plugins.search.static_stac_search import StaticStacSearch
|
|
39
40
|
from eodag.utils import get_bucket_name_and_prefix
|
|
40
|
-
from eodag.utils.exceptions import UnsupportedProductType, ValidationError
|
|
41
|
+
from eodag.utils.exceptions import RequestError, UnsupportedProductType, ValidationError
|
|
41
42
|
|
|
42
43
|
if TYPE_CHECKING:
|
|
43
44
|
from mypy_boto3_s3 import S3Client
|
|
@@ -67,6 +68,21 @@ def _get_date_from_yyyymmdd(date_str: str, item_key: str) -> Optional[datetime]:
|
|
|
67
68
|
return date
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
def _get_dates_from_dataset_data(
|
|
72
|
+
dataset_item: Dict[str, Any]
|
|
73
|
+
) -> Optional[Dict[str, str]]:
|
|
74
|
+
dates = {}
|
|
75
|
+
if "start_datetime" in dataset_item["properties"]:
|
|
76
|
+
dates["start"] = dataset_item["properties"]["start_datetime"]
|
|
77
|
+
dates["end"] = dataset_item["properties"]["end_datetime"]
|
|
78
|
+
elif "datetime" in dataset_item["properties"]:
|
|
79
|
+
dates["start"] = dataset_item["properties"]["datetime"]
|
|
80
|
+
dates["end"] = dataset_item["properties"]["datetime"]
|
|
81
|
+
else:
|
|
82
|
+
return None
|
|
83
|
+
return dates
|
|
84
|
+
|
|
85
|
+
|
|
70
86
|
def _get_s3_client(endpoint_url: str) -> S3Client:
|
|
71
87
|
s3_session = boto3.Session()
|
|
72
88
|
return s3_session.client(
|
|
@@ -94,7 +110,21 @@ def _check_int_values_properties(properties: Dict[str, Any]):
|
|
|
94
110
|
|
|
95
111
|
|
|
96
112
|
class CopMarineSearch(StaticStacSearch):
|
|
97
|
-
"""class that implements search for the Copernicus Marine provider
|
|
113
|
+
"""class that implements search for the Copernicus Marine provider
|
|
114
|
+
|
|
115
|
+
It calls :meth:`~eodag.plugins.search.static_stac_search.StaticStacSearch.discover_product_types`
|
|
116
|
+
inherited from :class:`~eodag.plugins.search.static_stac_search.StaticStacSearch`
|
|
117
|
+
but for the actual search a special method which fetches the urls of the available products from an S3 storage and
|
|
118
|
+
filters them has been written.
|
|
119
|
+
|
|
120
|
+
The configuration parameters are inherited from the parent and grand-parent classes. The
|
|
121
|
+
:attr:`~eodag.config.PluginConfig.DiscoverMetadata.auto_discovery` parameter in the
|
|
122
|
+
:attr:`~eodag.config.PluginConfig.discover_metadata` section has to be set to ``false`` and the
|
|
123
|
+
:attr:`~eodag.config.PluginConfig.DiscoverQueryables.fetch_url` in the
|
|
124
|
+
:attr:`~eodag.config.PluginConfig.discover_queryables` queryables section has to be set to ``null`` to
|
|
125
|
+
overwrite the default config from the stac provider configuration because those functionalities
|
|
126
|
+
are not available.
|
|
127
|
+
"""
|
|
98
128
|
|
|
99
129
|
def __init__(self, provider: str, config: PluginConfig):
|
|
100
130
|
original_metadata_mapping = copy.deepcopy(config.metadata_mapping)
|
|
@@ -107,12 +137,10 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
107
137
|
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
|
|
108
138
|
"""Fetch product type and associated datasets info"""
|
|
109
139
|
|
|
110
|
-
fetch_url = cast(
|
|
111
|
-
|
|
112
|
-
self.config.discover_product_types["fetch_url"].format(
|
|
113
|
-
**self.config.__dict__
|
|
114
|
-
),
|
|
140
|
+
fetch_url = cast(str, self.config.discover_product_types["fetch_url"]).format(
|
|
141
|
+
**self.config.__dict__
|
|
115
142
|
)
|
|
143
|
+
|
|
116
144
|
logger.debug("fetch data for collection %s", product_type)
|
|
117
145
|
provider_product_type = self.config.products.get(product_type, {}).get(
|
|
118
146
|
"productType", None
|
|
@@ -125,9 +153,14 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
125
153
|
)
|
|
126
154
|
try:
|
|
127
155
|
collection_data = requests.get(collection_url).json()
|
|
128
|
-
except requests.RequestException:
|
|
156
|
+
except requests.RequestException as exc:
|
|
157
|
+
if exc.errno == 404:
|
|
158
|
+
logger.error("product %s not found", product_type)
|
|
159
|
+
raise UnsupportedProductType(product_type)
|
|
129
160
|
logger.error("data for product %s could not be fetched", product_type)
|
|
130
|
-
raise
|
|
161
|
+
raise RequestError.from_error(
|
|
162
|
+
exc, f"data for product {product_type} could not be fetched"
|
|
163
|
+
) from exc
|
|
131
164
|
|
|
132
165
|
datasets = []
|
|
133
166
|
for link in [li for li in collection_data["links"] if li["rel"] == "item"]:
|
|
@@ -170,7 +203,7 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
170
203
|
use_dataset_dates: bool = False,
|
|
171
204
|
) -> Optional[EOProduct]:
|
|
172
205
|
|
|
173
|
-
item_id = item_key.split("/")[-1]
|
|
206
|
+
item_id = os.path.splitext(item_key.split("/")[-1])[0]
|
|
174
207
|
download_url = s3_url + "/" + item_key
|
|
175
208
|
properties = {
|
|
176
209
|
"id": item_id,
|
|
@@ -180,20 +213,16 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
180
213
|
"dataset": dataset_item["id"],
|
|
181
214
|
}
|
|
182
215
|
if use_dataset_dates:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"end_datetime"
|
|
189
|
-
]
|
|
190
|
-
elif "datetime" in dataset_item:
|
|
191
|
-
properties["startTimeFromAscendingNode"] = dataset_item["datetime"]
|
|
192
|
-
properties["completionTimeFromAscendingNode"] = dataset_item["datetime"]
|
|
216
|
+
dates = _get_dates_from_dataset_data(dataset_item)
|
|
217
|
+
if not dates:
|
|
218
|
+
return None
|
|
219
|
+
properties["startTimeFromAscendingNode"] = dates["start"]
|
|
220
|
+
properties["completionTimeFromAscendingNode"] = dates["end"]
|
|
193
221
|
else:
|
|
194
|
-
item_dates = re.findall(r"\d{
|
|
222
|
+
item_dates = re.findall(r"(\d{4})(0[1-9]|1[0-2])([0-3]\d)", item_id)
|
|
195
223
|
if not item_dates:
|
|
196
|
-
item_dates = re.findall(r"\d{
|
|
224
|
+
item_dates = re.findall(r"_(\d{4})(0[1-9]|1[0-2])", item_id)
|
|
225
|
+
item_dates = ["".join(row) for row in item_dates]
|
|
197
226
|
item_start = _get_date_from_yyyymmdd(item_dates[0], item_key)
|
|
198
227
|
if not item_start: # identified pattern was not a valid datetime
|
|
199
228
|
return None
|
|
@@ -209,11 +238,26 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
209
238
|
).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
210
239
|
|
|
211
240
|
for key, value in collection_dict["properties"].items():
|
|
212
|
-
if key not in ["id", "title", "start_datetime", "end_datetime"]:
|
|
241
|
+
if key not in ["id", "title", "start_datetime", "end_datetime", "datetime"]:
|
|
213
242
|
properties[key] = value
|
|
214
243
|
for key, value in dataset_item["properties"].items():
|
|
215
|
-
if key not in ["id", "title", "start_datetime", "end_datetime"]:
|
|
244
|
+
if key not in ["id", "title", "start_datetime", "end_datetime", "datetime"]:
|
|
216
245
|
properties[key] = value
|
|
246
|
+
|
|
247
|
+
code_mapping = self.config.products.get(product_type, {}).get(
|
|
248
|
+
"code_mapping", None
|
|
249
|
+
)
|
|
250
|
+
if code_mapping:
|
|
251
|
+
id_parts = item_id.split("_")
|
|
252
|
+
if len(id_parts) > code_mapping["index"]:
|
|
253
|
+
code = id_parts[code_mapping["index"]]
|
|
254
|
+
if "pattern" not in code_mapping:
|
|
255
|
+
properties[code_mapping["param"]] = code
|
|
256
|
+
elif re.findall(code_mapping["pattern"], code):
|
|
257
|
+
properties[code_mapping["param"]] = re.findall(
|
|
258
|
+
code_mapping["pattern"], code
|
|
259
|
+
)[0]
|
|
260
|
+
|
|
217
261
|
_check_int_values_properties(properties)
|
|
218
262
|
|
|
219
263
|
properties["thumbnail"] = collection_dict["assets"]["thumbnail"]["href"]
|
|
@@ -238,16 +282,14 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
238
282
|
"""
|
|
239
283
|
Implementation of search for the Copernicus Marine provider
|
|
240
284
|
:param prep: object containing search parameterds
|
|
241
|
-
:type prep: PreparedSearch
|
|
242
285
|
:param kwargs: additional search arguments
|
|
243
286
|
:returns: list of products and total number of products
|
|
244
|
-
:rtype: Tuple[List[EOProduct], Optional[int]]
|
|
245
287
|
"""
|
|
246
288
|
page = prep.page
|
|
247
289
|
items_per_page = prep.items_per_page
|
|
248
290
|
|
|
249
291
|
# only return 1 page if pagination is disabled
|
|
250
|
-
if page > 1 and items_per_page <= 0:
|
|
292
|
+
if page is None or items_per_page is None or page > 1 and items_per_page <= 0:
|
|
251
293
|
return ([], 0) if prep.count else ([], None)
|
|
252
294
|
|
|
253
295
|
product_type = kwargs.get("productType", prep.product_type)
|
|
@@ -350,16 +392,54 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
350
392
|
|
|
351
393
|
for obj in s3_objects["Contents"]:
|
|
352
394
|
item_key = obj["Key"]
|
|
395
|
+
item_id = os.path.splitext(item_key.split("/")[-1])[0]
|
|
353
396
|
# filter according to date(s) in item id
|
|
354
|
-
item_dates = re.findall(r"\d{
|
|
397
|
+
item_dates = re.findall(r"(\d{4})(0[1-9]|1[0-2])([0-3]\d)", item_id)
|
|
355
398
|
if not item_dates:
|
|
356
|
-
item_dates = re.findall(r"\d{
|
|
357
|
-
|
|
358
|
-
|
|
399
|
+
item_dates = re.findall(r"_(\d{4})(0[1-9]|1[0-2])", item_id)
|
|
400
|
+
item_dates = [
|
|
401
|
+
"".join(row) for row in item_dates
|
|
402
|
+
] # join tuples returned by findall
|
|
403
|
+
item_start = None
|
|
404
|
+
item_end = None
|
|
405
|
+
use_dataset_dates = False
|
|
406
|
+
if item_dates:
|
|
407
|
+
item_start = _get_date_from_yyyymmdd(item_dates[0], item_key)
|
|
408
|
+
if len(item_dates) > 2: # start, end and created_at timestamps
|
|
409
|
+
item_end = _get_date_from_yyyymmdd(item_dates[1], item_key)
|
|
410
|
+
if not item_start:
|
|
411
|
+
# no valid datetime given in id
|
|
412
|
+
use_dataset_dates = True
|
|
413
|
+
dates = _get_dates_from_dataset_data(dataset_item)
|
|
414
|
+
if dates:
|
|
415
|
+
item_start_str = dates["start"].replace("Z", "+0000")
|
|
416
|
+
item_end_str = dates["end"].replace("Z", "+0000")
|
|
417
|
+
try:
|
|
418
|
+
item_start = datetime.strptime(
|
|
419
|
+
item_start_str, "%Y-%m-%dT%H:%M:%S.%f%z"
|
|
420
|
+
)
|
|
421
|
+
item_end = datetime.strptime(
|
|
422
|
+
item_end_str, "%Y-%m-%dT%H:%M:%S.%f%z"
|
|
423
|
+
)
|
|
424
|
+
except ValueError:
|
|
425
|
+
item_start = datetime.strptime(
|
|
426
|
+
item_start_str, "%Y-%m-%dT%H:%M:%S%z"
|
|
427
|
+
)
|
|
428
|
+
item_end = datetime.strptime(
|
|
429
|
+
item_end_str, "%Y-%m-%dT%H:%M:%S%z"
|
|
430
|
+
)
|
|
431
|
+
if not item_start:
|
|
432
|
+
# no valid datetime in id and dataset data
|
|
359
433
|
continue
|
|
360
434
|
if item_start > end_date:
|
|
361
435
|
stop_search = True
|
|
362
|
-
if
|
|
436
|
+
if (
|
|
437
|
+
(start_date <= item_start <= end_date)
|
|
438
|
+
or (item_end and start_date <= item_end <= end_date)
|
|
439
|
+
or (
|
|
440
|
+
item_end and item_start < start_date and item_end > end_date
|
|
441
|
+
)
|
|
442
|
+
):
|
|
363
443
|
num_total += 1
|
|
364
444
|
if num_total < start_index:
|
|
365
445
|
continue
|
|
@@ -370,6 +450,7 @@ class CopMarineSearch(StaticStacSearch):
|
|
|
370
450
|
endpoint_url + "/" + bucket,
|
|
371
451
|
dataset_item,
|
|
372
452
|
collection_dict,
|
|
453
|
+
use_dataset_dates,
|
|
373
454
|
)
|
|
374
455
|
if product:
|
|
375
456
|
products.append(product)
|