eodag 2.12.0__py3-none-any.whl → 3.0.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/__init__.py +6 -8
- eodag/api/core.py +654 -538
- eodag/api/product/__init__.py +12 -2
- eodag/api/product/_assets.py +59 -16
- eodag/api/product/_product.py +100 -93
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +192 -96
- eodag/api/search_result.py +69 -10
- eodag/cli.py +55 -25
- eodag/config.py +391 -116
- eodag/plugins/apis/base.py +11 -165
- eodag/plugins/apis/ecmwf.py +36 -25
- eodag/plugins/apis/usgs.py +80 -35
- eodag/plugins/authentication/aws_auth.py +13 -4
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +17 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +268 -49
- eodag/plugins/authentication/qsauth.py +4 -1
- eodag/plugins/authentication/sas_auth.py +9 -2
- eodag/plugins/authentication/token.py +98 -47
- eodag/plugins/authentication/token_exchange.py +122 -0
- eodag/plugins/crunch/base.py +3 -1
- eodag/plugins/crunch/filter_date.py +3 -9
- eodag/plugins/crunch/filter_latest_intersect.py +0 -3
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
- eodag/plugins/crunch/filter_overlap.py +4 -8
- eodag/plugins/crunch/filter_property.py +5 -11
- eodag/plugins/download/aws.py +149 -185
- eodag/plugins/download/base.py +88 -97
- eodag/plugins/download/creodias_s3.py +1 -1
- eodag/plugins/download/http.py +638 -310
- eodag/plugins/download/s3rest.py +47 -45
- eodag/plugins/manager.py +228 -88
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +239 -30
- eodag/plugins/search/build_search_result.py +382 -37
- eodag/plugins/search/cop_marine.py +441 -0
- eodag/plugins/search/creodias_s3.py +25 -20
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +61 -30
- eodag/plugins/search/qssearch.py +713 -255
- eodag/plugins/search/static_stac_search.py +106 -40
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1921 -34
- eodag/resources/providers.yml +4091 -3655
- eodag/resources/stac.yml +50 -216
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +89 -32
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +26 -0
- eodag/rest/core.py +735 -0
- eodag/rest/errors.py +178 -0
- eodag/rest/server.py +264 -431
- eodag/rest/stac.py +442 -836
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +238 -47
- eodag/rest/types/queryables.py +164 -0
- eodag/rest/types/stac_search.py +273 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +64 -0
- eodag/types/__init__.py +106 -10
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +40 -0
- eodag/types/search_args.py +57 -7
- eodag/types/whoosh.py +79 -0
- eodag/utils/__init__.py +110 -91
- eodag/utils/constraints.py +37 -45
- eodag/utils/exceptions.py +39 -22
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +128 -0
- eodag/utils/rest.py +100 -0
- eodag/utils/stac_reader.py +93 -21
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
- eodag-3.0.0.dist-info/RECORD +109 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/types/stac_queryables.py +0 -134
- eodag/rest/utils.py +0 -1133
- eodag-2.12.0.dist-info/RECORD +0 -94
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -19,17 +19,48 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import hashlib
|
|
21
21
|
import logging
|
|
22
|
-
from
|
|
22
|
+
from datetime import datetime, timedelta, timezone
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, cast
|
|
23
24
|
from urllib.parse import quote_plus, unquote_plus
|
|
24
25
|
|
|
25
26
|
import geojson
|
|
26
27
|
import orjson
|
|
27
|
-
from
|
|
28
|
+
from dateutil.parser import isoparse
|
|
29
|
+
from dateutil.tz import tzutc
|
|
30
|
+
from jsonpath_ng import Child, Fields, Root
|
|
31
|
+
from pydantic import create_model
|
|
32
|
+
from pydantic.fields import FieldInfo
|
|
33
|
+
from typing_extensions import get_args
|
|
28
34
|
|
|
29
35
|
from eodag.api.product import EOProduct
|
|
30
|
-
from eodag.api.product.metadata_mapping import
|
|
36
|
+
from eodag.api.product.metadata_mapping import (
|
|
37
|
+
NOT_AVAILABLE,
|
|
38
|
+
NOT_MAPPED,
|
|
39
|
+
get_queryable_from_provider,
|
|
40
|
+
mtd_cfg_as_conversion_and_querypath,
|
|
41
|
+
properties_from_json,
|
|
42
|
+
)
|
|
43
|
+
from eodag.api.search_result import RawSearchResult
|
|
44
|
+
from eodag.plugins.search import PreparedSearch
|
|
45
|
+
from eodag.plugins.search.base import Search
|
|
31
46
|
from eodag.plugins.search.qssearch import PostJsonSearch
|
|
32
|
-
from eodag.
|
|
47
|
+
from eodag.types import json_field_definition_to_python, model_fields_to_annotated
|
|
48
|
+
from eodag.types.queryables import CommonQueryables
|
|
49
|
+
from eodag.utils import (
|
|
50
|
+
DEFAULT_MISSION_START_DATE,
|
|
51
|
+
Annotated,
|
|
52
|
+
deepcopy,
|
|
53
|
+
dict_items_recursive_sort,
|
|
54
|
+
get_geometry_from_various,
|
|
55
|
+
)
|
|
56
|
+
from eodag.utils.constraints import (
|
|
57
|
+
fetch_constraints,
|
|
58
|
+
get_constraint_queryables_with_additional_params,
|
|
59
|
+
)
|
|
60
|
+
from eodag.utils.exceptions import ValidationError
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from eodag.config import PluginConfig
|
|
33
64
|
|
|
34
65
|
logger = logging.getLogger("eodag.search.build_search_result")
|
|
35
66
|
|
|
@@ -55,9 +86,7 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
55
86
|
method before being loaded as json.
|
|
56
87
|
|
|
57
88
|
:param provider: An eodag providers configuration dictionary
|
|
58
|
-
:type provider: dict
|
|
59
89
|
:param config: Path to the user configuration file
|
|
60
|
-
:type config: str
|
|
61
90
|
"""
|
|
62
91
|
|
|
63
92
|
def count_hits(
|
|
@@ -68,39 +97,35 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
68
97
|
|
|
69
98
|
def collect_search_urls(
|
|
70
99
|
self,
|
|
71
|
-
|
|
72
|
-
items_per_page: Optional[int] = None,
|
|
73
|
-
count: bool = True,
|
|
100
|
+
prep: PreparedSearch = PreparedSearch(),
|
|
74
101
|
**kwargs: Any,
|
|
75
102
|
) -> Tuple[List[str], int]:
|
|
76
103
|
"""Wraps PostJsonSearch.collect_search_urls to force product count to 1"""
|
|
77
|
-
urls, _ = super(BuildPostSearchResult, self).collect_search_urls(
|
|
78
|
-
page=page, items_per_page=items_per_page, count=count, **kwargs
|
|
79
|
-
)
|
|
104
|
+
urls, _ = super(BuildPostSearchResult, self).collect_search_urls(prep, **kwargs)
|
|
80
105
|
return urls, 1
|
|
81
106
|
|
|
82
|
-
def do_search(
|
|
107
|
+
def do_search(
|
|
108
|
+
self, prep: PreparedSearch = PreparedSearch(items_per_page=None), **kwargs: Any
|
|
109
|
+
) -> List[Dict[str, Any]]:
|
|
83
110
|
"""Perform the actual search request, and return result in a single element."""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
f"{self.__class__.__name__} instance:",
|
|
111
|
+
prep.url = prep.search_urls[0]
|
|
112
|
+
prep.info_message = f"Sending search request: {prep.url}"
|
|
113
|
+
prep.exception_message = (
|
|
114
|
+
f"Skipping error while searching for {self.provider}"
|
|
115
|
+
f" {self.__class__.__name__} instance"
|
|
90
116
|
)
|
|
117
|
+
response = self._request(prep)
|
|
118
|
+
|
|
91
119
|
return [response.json()]
|
|
92
120
|
|
|
93
121
|
def normalize_results(
|
|
94
|
-
self, results:
|
|
122
|
+
self, results: RawSearchResult, **kwargs: Any
|
|
95
123
|
) -> List[EOProduct]:
|
|
96
124
|
"""Build :class:`~eodag.api.product._product.EOProduct` from provider result
|
|
97
125
|
|
|
98
126
|
:param results: Raw provider result as single dict in list
|
|
99
|
-
:type results: list
|
|
100
127
|
:param kwargs: Search arguments
|
|
101
|
-
:type kwargs: Union[int, str, bool, dict, list]
|
|
102
128
|
:returns: list of single :class:`~eodag.api.product._product.EOProduct`
|
|
103
|
-
:rtype: list
|
|
104
129
|
"""
|
|
105
130
|
product_type = kwargs.get("productType")
|
|
106
131
|
|
|
@@ -110,22 +135,20 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
110
135
|
_dc_qs = kwargs.pop("_dc_qs", None)
|
|
111
136
|
if _dc_qs is not None:
|
|
112
137
|
qs = unquote_plus(unquote_plus(_dc_qs))
|
|
113
|
-
|
|
114
|
-
qs
|
|
115
|
-
)
|
|
138
|
+
sorted_unpaginated_query_params = geojson.loads(qs)
|
|
116
139
|
else:
|
|
117
140
|
# update result with query parameters without pagination (or search-only params)
|
|
118
141
|
if isinstance(
|
|
119
142
|
self.config.pagination["next_page_query_obj"], str
|
|
120
|
-
) and hasattr(
|
|
121
|
-
unpaginated_query_params =
|
|
143
|
+
) and hasattr(results, "query_params_unpaginated"):
|
|
144
|
+
unpaginated_query_params = results.query_params_unpaginated
|
|
122
145
|
elif isinstance(self.config.pagination["next_page_query_obj"], str):
|
|
123
146
|
next_page_query_obj = orjson.loads(
|
|
124
147
|
self.config.pagination["next_page_query_obj"].format()
|
|
125
148
|
)
|
|
126
149
|
unpaginated_query_params = {
|
|
127
150
|
k: v[0] if (isinstance(v, list) and len(v) == 1) else v
|
|
128
|
-
for k, v in
|
|
151
|
+
for k, v in results.query_params.items()
|
|
129
152
|
if (k, v) not in next_page_query_obj.items()
|
|
130
153
|
}
|
|
131
154
|
else:
|
|
@@ -135,14 +158,25 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
135
158
|
sorted_unpaginated_query_params = dict_items_recursive_sort(
|
|
136
159
|
unpaginated_query_params
|
|
137
160
|
)
|
|
138
|
-
qs = geojson.dumps(sorted_unpaginated_query_params)
|
|
139
161
|
|
|
140
|
-
|
|
162
|
+
# use all available query_params to parse properties
|
|
163
|
+
result = dict(
|
|
164
|
+
result,
|
|
165
|
+
**sorted_unpaginated_query_params,
|
|
166
|
+
qs=sorted_unpaginated_query_params,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# remove unwanted query params
|
|
170
|
+
for param in getattr(self.config, "remove_from_query", []):
|
|
171
|
+
sorted_unpaginated_query_params.pop(param, None)
|
|
141
172
|
|
|
142
|
-
|
|
173
|
+
qs = geojson.dumps(sorted_unpaginated_query_params)
|
|
174
|
+
|
|
175
|
+
query_hash = hashlib.sha1(str(qs).encode("UTF-8")).hexdigest()
|
|
143
176
|
|
|
144
|
-
# update result with search args if not None (and not auth)
|
|
177
|
+
# update result with product_type_def_params and search args if not None (and not auth)
|
|
145
178
|
kwargs.pop("auth", None)
|
|
179
|
+
result.update(results.product_type_def_params)
|
|
146
180
|
result = dict(result, **{k: v for k, v in kwargs.items() if v is not None})
|
|
147
181
|
|
|
148
182
|
# parse porperties
|
|
@@ -157,22 +191,28 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
157
191
|
|
|
158
192
|
# build product id
|
|
159
193
|
id_prefix = (product_type or self.provider).upper()
|
|
160
|
-
product_id = "%s_%s_%s" % (
|
|
194
|
+
product_id = "%s_%s_%s_%s" % (
|
|
161
195
|
id_prefix,
|
|
162
196
|
parsed_properties["startTimeFromAscendingNode"]
|
|
163
197
|
.split("T")[0]
|
|
164
198
|
.replace("-", ""),
|
|
199
|
+
parsed_properties["completionTimeFromAscendingNode"]
|
|
200
|
+
.split("T")[0]
|
|
201
|
+
.replace("-", ""),
|
|
165
202
|
query_hash,
|
|
166
203
|
)
|
|
167
204
|
parsed_properties["id"] = parsed_properties["title"] = product_id
|
|
168
205
|
|
|
169
|
-
# update downloadLink
|
|
170
|
-
parsed_properties["downloadLink"] += f"?{qs}"
|
|
206
|
+
# update downloadLink and orderLink
|
|
171
207
|
parsed_properties["_dc_qs"] = quote_plus(qs)
|
|
208
|
+
if parsed_properties["downloadLink"] != "Not Available":
|
|
209
|
+
parsed_properties["downloadLink"] += f"?{qs}"
|
|
172
210
|
|
|
173
211
|
# parse metadata needing downloadLink
|
|
212
|
+
dl_path = Fields("downloadLink")
|
|
213
|
+
dl_path_from_root = Child(Root(), dl_path)
|
|
174
214
|
for param, mapping in self.config.metadata_mapping.items():
|
|
175
|
-
if
|
|
215
|
+
if dl_path in mapping or dl_path_from_root in mapping:
|
|
176
216
|
parsed_properties.update(
|
|
177
217
|
properties_from_json(parsed_properties, {param: mapping})
|
|
178
218
|
)
|
|
@@ -192,3 +232,308 @@ class BuildPostSearchResult(PostJsonSearch):
|
|
|
192
232
|
return [
|
|
193
233
|
product,
|
|
194
234
|
]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class BuildSearchResult(BuildPostSearchResult):
|
|
238
|
+
"""BuildSearchResult search plugin.
|
|
239
|
+
|
|
240
|
+
This plugin builds a single :class:`~eodag.api.search_result.SearchResult` object
|
|
241
|
+
using given query parameters as product properties.
|
|
242
|
+
|
|
243
|
+
The available configuration parameters inherits from parent classes, with particularly
|
|
244
|
+
for this plugin:
|
|
245
|
+
|
|
246
|
+
- **end_date_excluded**: Set to `False` if provider does not include end date to
|
|
247
|
+
search
|
|
248
|
+
|
|
249
|
+
- **remove_from_query**: List of parameters used to parse metadata but that must
|
|
250
|
+
not be included to the query
|
|
251
|
+
|
|
252
|
+
- **constraints_file_url**: url of the constraint file used to build queryables
|
|
253
|
+
|
|
254
|
+
:param provider: An eodag providers configuration dictionary
|
|
255
|
+
:param config: Path to the user configuration file
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
259
|
+
# init self.config.metadata_mapping using Search Base plugin
|
|
260
|
+
Search.__init__(self, provider, config)
|
|
261
|
+
|
|
262
|
+
self.config.__dict__.setdefault("api_endpoint", "")
|
|
263
|
+
|
|
264
|
+
# needed by QueryStringSearch.build_query_string / format_free_text_search
|
|
265
|
+
self.config.__dict__.setdefault("free_text_search_operations", {})
|
|
266
|
+
# needed for compatibility
|
|
267
|
+
self.config.__dict__.setdefault("pagination", {"next_page_query_obj": "{{}}"})
|
|
268
|
+
|
|
269
|
+
# parse jsonpath on init: product type specific metadata-mapping
|
|
270
|
+
for product_type in self.config.products.keys():
|
|
271
|
+
if "metadata_mapping" in self.config.products[product_type].keys():
|
|
272
|
+
self.config.products[product_type][
|
|
273
|
+
"metadata_mapping"
|
|
274
|
+
] = mtd_cfg_as_conversion_and_querypath(
|
|
275
|
+
self.config.products[product_type]["metadata_mapping"]
|
|
276
|
+
)
|
|
277
|
+
# Complete and ready to use product type specific metadata-mapping
|
|
278
|
+
product_type_metadata_mapping = deepcopy(self.config.metadata_mapping)
|
|
279
|
+
|
|
280
|
+
# update config using provider product type definition metadata_mapping
|
|
281
|
+
# from another product
|
|
282
|
+
other_product_for_mapping = cast(
|
|
283
|
+
str,
|
|
284
|
+
self.config.products[product_type].get(
|
|
285
|
+
"metadata_mapping_from_product", ""
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
if other_product_for_mapping:
|
|
289
|
+
other_product_type_def_params = self.get_product_type_def_params(
|
|
290
|
+
other_product_for_mapping,
|
|
291
|
+
)
|
|
292
|
+
product_type_metadata_mapping.update(
|
|
293
|
+
other_product_type_def_params.get("metadata_mapping", {})
|
|
294
|
+
)
|
|
295
|
+
# from current product
|
|
296
|
+
product_type_metadata_mapping.update(
|
|
297
|
+
self.config.products[product_type]["metadata_mapping"]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
self.config.products[product_type][
|
|
301
|
+
"metadata_mapping"
|
|
302
|
+
] = product_type_metadata_mapping
|
|
303
|
+
|
|
304
|
+
def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
305
|
+
"""Should perform the actual search request."""
|
|
306
|
+
return [{}]
|
|
307
|
+
|
|
308
|
+
def query(
|
|
309
|
+
self,
|
|
310
|
+
prep: PreparedSearch = PreparedSearch(),
|
|
311
|
+
**kwargs: Any,
|
|
312
|
+
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
313
|
+
"""Build ready-to-download SearchResult"""
|
|
314
|
+
|
|
315
|
+
self._preprocess_search_params(kwargs)
|
|
316
|
+
|
|
317
|
+
return BuildPostSearchResult.query(self, prep, **kwargs)
|
|
318
|
+
|
|
319
|
+
def clear(self) -> None:
|
|
320
|
+
"""Clear search context"""
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
def build_query_string(
|
|
324
|
+
self, product_type: str, **kwargs: Any
|
|
325
|
+
) -> Tuple[Dict[str, Any], str]:
|
|
326
|
+
"""Build The query string using the search parameters"""
|
|
327
|
+
# parse kwargs as properties as they might be needed to build the query
|
|
328
|
+
parsed_properties = properties_from_json(
|
|
329
|
+
kwargs,
|
|
330
|
+
self.config.metadata_mapping,
|
|
331
|
+
)
|
|
332
|
+
available_properties = {
|
|
333
|
+
k: v
|
|
334
|
+
for k, v in parsed_properties.items()
|
|
335
|
+
if v not in [NOT_AVAILABLE, NOT_MAPPED]
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# build and return the query
|
|
339
|
+
return BuildPostSearchResult.build_query_string(
|
|
340
|
+
self, product_type=product_type, **available_properties
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def _preprocess_search_params(self, params: Dict[str, Any]) -> None:
|
|
344
|
+
"""Preprocess search parameters before making a request to the CDS API.
|
|
345
|
+
|
|
346
|
+
This method is responsible for checking and updating the provided search parameters
|
|
347
|
+
to ensure that required parameters like 'productType', 'startTimeFromAscendingNode',
|
|
348
|
+
'completionTimeFromAscendingNode', and 'geometry' are properly set. If not specified
|
|
349
|
+
in the input parameters, default values or values from the configuration are used.
|
|
350
|
+
|
|
351
|
+
:param params: Search parameters to be preprocessed.
|
|
352
|
+
"""
|
|
353
|
+
_dc_qs = params.get("_dc_qs", None)
|
|
354
|
+
if _dc_qs is not None:
|
|
355
|
+
# if available, update search params using datacube query-string
|
|
356
|
+
_dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
|
|
357
|
+
if "/to/" in _dc_qp.get("date", ""):
|
|
358
|
+
(
|
|
359
|
+
params["startTimeFromAscendingNode"],
|
|
360
|
+
params["completionTimeFromAscendingNode"],
|
|
361
|
+
) = _dc_qp["date"].split("/to/")
|
|
362
|
+
elif "/" in _dc_qp.get("date", ""):
|
|
363
|
+
(
|
|
364
|
+
params["startTimeFromAscendingNode"],
|
|
365
|
+
params["completionTimeFromAscendingNode"],
|
|
366
|
+
) = _dc_qp["date"].split("/")
|
|
367
|
+
elif _dc_qp.get("date", None):
|
|
368
|
+
params["startTimeFromAscendingNode"] = params[
|
|
369
|
+
"completionTimeFromAscendingNode"
|
|
370
|
+
] = _dc_qp["date"]
|
|
371
|
+
|
|
372
|
+
if "/" in _dc_qp.get("area", ""):
|
|
373
|
+
params["geometry"] = _dc_qp["area"].split("/")
|
|
374
|
+
|
|
375
|
+
non_none_params = {k: v for k, v in params.items() if v}
|
|
376
|
+
|
|
377
|
+
# productType
|
|
378
|
+
dataset = params.get("dataset", None)
|
|
379
|
+
params["productType"] = non_none_params.get("productType", dataset)
|
|
380
|
+
|
|
381
|
+
# dates
|
|
382
|
+
mission_start_dt = datetime.fromisoformat(
|
|
383
|
+
self.get_product_type_cfg_value(
|
|
384
|
+
"missionStartDate", DEFAULT_MISSION_START_DATE
|
|
385
|
+
).replace(
|
|
386
|
+
"Z", "+00:00"
|
|
387
|
+
) # before 3.11
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
default_end_from_cfg = self.config.products.get(params["productType"], {}).get(
|
|
391
|
+
"_default_end_date", None
|
|
392
|
+
)
|
|
393
|
+
default_end_str = (
|
|
394
|
+
default_end_from_cfg
|
|
395
|
+
or (
|
|
396
|
+
datetime.now(timezone.utc)
|
|
397
|
+
if params.get("startTimeFromAscendingNode")
|
|
398
|
+
else mission_start_dt + timedelta(days=1)
|
|
399
|
+
).isoformat()
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
params["startTimeFromAscendingNode"] = non_none_params.get(
|
|
403
|
+
"startTimeFromAscendingNode", mission_start_dt.isoformat()
|
|
404
|
+
)
|
|
405
|
+
params["completionTimeFromAscendingNode"] = non_none_params.get(
|
|
406
|
+
"completionTimeFromAscendingNode", default_end_str
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# adapt end date if it is midnight
|
|
410
|
+
end_date_excluded = getattr(self.config, "end_date_excluded", True)
|
|
411
|
+
is_datetime = True
|
|
412
|
+
try:
|
|
413
|
+
end_date = datetime.strptime(
|
|
414
|
+
params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
|
|
415
|
+
)
|
|
416
|
+
end_date = end_date.replace(tzinfo=tzutc())
|
|
417
|
+
except ValueError:
|
|
418
|
+
try:
|
|
419
|
+
end_date = datetime.strptime(
|
|
420
|
+
params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
421
|
+
)
|
|
422
|
+
end_date = end_date.replace(tzinfo=tzutc())
|
|
423
|
+
except ValueError:
|
|
424
|
+
end_date = isoparse(params["completionTimeFromAscendingNode"])
|
|
425
|
+
is_datetime = False
|
|
426
|
+
start_date = isoparse(params["startTimeFromAscendingNode"])
|
|
427
|
+
if (
|
|
428
|
+
not end_date_excluded
|
|
429
|
+
and is_datetime
|
|
430
|
+
and end_date > start_date
|
|
431
|
+
and end_date == end_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
432
|
+
):
|
|
433
|
+
end_date += timedelta(days=-1)
|
|
434
|
+
params["completionTimeFromAscendingNode"] = end_date.isoformat()
|
|
435
|
+
|
|
436
|
+
# geometry
|
|
437
|
+
if "geometry" in params:
|
|
438
|
+
params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
|
|
439
|
+
|
|
440
|
+
def discover_queryables(
|
|
441
|
+
self, **kwargs: Any
|
|
442
|
+
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
|
|
443
|
+
"""Fetch queryables list from provider using its constraints file
|
|
444
|
+
|
|
445
|
+
:param kwargs: additional filters for queryables (`productType` and other search
|
|
446
|
+
arguments)
|
|
447
|
+
:returns: fetched queryable parameters dict
|
|
448
|
+
"""
|
|
449
|
+
constraints_file_url = getattr(self.config, "constraints_file_url", "")
|
|
450
|
+
if not constraints_file_url:
|
|
451
|
+
return {}
|
|
452
|
+
product_type = kwargs.pop("productType", None)
|
|
453
|
+
if not product_type:
|
|
454
|
+
return {}
|
|
455
|
+
|
|
456
|
+
provider_product_type = self.config.products.get(product_type, {}).get(
|
|
457
|
+
"dataset", None
|
|
458
|
+
)
|
|
459
|
+
user_provider_product_type = kwargs.pop("dataset", None)
|
|
460
|
+
if (
|
|
461
|
+
user_provider_product_type
|
|
462
|
+
and user_provider_product_type != provider_product_type
|
|
463
|
+
):
|
|
464
|
+
raise ValidationError(
|
|
465
|
+
f"Cannot change dataset from {provider_product_type} to {user_provider_product_type}"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# defaults
|
|
469
|
+
default_queryables = self._get_defaults_as_queryables(product_type)
|
|
470
|
+
# remove dataset from queryables
|
|
471
|
+
default_queryables.pop("dataset", None)
|
|
472
|
+
|
|
473
|
+
non_empty_kwargs = {k: v for k, v in kwargs.items() if v}
|
|
474
|
+
|
|
475
|
+
if "{" in constraints_file_url:
|
|
476
|
+
constraints_file_url = constraints_file_url.format(
|
|
477
|
+
dataset=provider_product_type
|
|
478
|
+
)
|
|
479
|
+
constraints = fetch_constraints(constraints_file_url, self)
|
|
480
|
+
if not constraints:
|
|
481
|
+
return default_queryables
|
|
482
|
+
|
|
483
|
+
constraint_params: Dict[str, Dict[str, Set[Any]]] = {}
|
|
484
|
+
if len(kwargs) == 0:
|
|
485
|
+
# get values from constraints without additional filters
|
|
486
|
+
for constraint in constraints:
|
|
487
|
+
for key in constraint.keys():
|
|
488
|
+
if key in constraint_params:
|
|
489
|
+
constraint_params[key]["enum"].update(constraint[key])
|
|
490
|
+
else:
|
|
491
|
+
constraint_params[key] = {}
|
|
492
|
+
constraint_params[key]["enum"] = set(constraint[key])
|
|
493
|
+
else:
|
|
494
|
+
# get values from constraints with additional filters
|
|
495
|
+
constraints_input_params = {k: v for k, v in non_empty_kwargs.items()}
|
|
496
|
+
constraint_params = get_constraint_queryables_with_additional_params(
|
|
497
|
+
constraints, constraints_input_params, self, product_type
|
|
498
|
+
)
|
|
499
|
+
# query params that are not in constraints but might be default queryables
|
|
500
|
+
if len(constraint_params) == 1 and "not_available" in constraint_params:
|
|
501
|
+
not_queryables: Set[str] = set()
|
|
502
|
+
for constraint_param in constraint_params["not_available"]["enum"]:
|
|
503
|
+
param = CommonQueryables.get_queryable_from_alias(constraint_param)
|
|
504
|
+
if param in dict(
|
|
505
|
+
CommonQueryables.model_fields, **default_queryables
|
|
506
|
+
):
|
|
507
|
+
non_empty_kwargs.pop(constraint_param)
|
|
508
|
+
else:
|
|
509
|
+
not_queryables.add(constraint_param)
|
|
510
|
+
if not_queryables:
|
|
511
|
+
raise ValidationError(
|
|
512
|
+
f"parameter(s) {not_queryables} not queryable"
|
|
513
|
+
)
|
|
514
|
+
else:
|
|
515
|
+
# get constraints again without common queryables
|
|
516
|
+
constraint_params = (
|
|
517
|
+
get_constraint_queryables_with_additional_params(
|
|
518
|
+
constraints, non_empty_kwargs, self, product_type
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
field_definitions: Dict[str, Any] = {}
|
|
523
|
+
for json_param, json_mtd in constraint_params.items():
|
|
524
|
+
param = (
|
|
525
|
+
get_queryable_from_provider(
|
|
526
|
+
json_param, self.get_metadata_mapping(product_type)
|
|
527
|
+
)
|
|
528
|
+
or json_param
|
|
529
|
+
)
|
|
530
|
+
default = kwargs.get(param, None) or self.config.products.get(
|
|
531
|
+
product_type, {}
|
|
532
|
+
).get(param, None)
|
|
533
|
+
annotated_def = json_field_definition_to_python(
|
|
534
|
+
json_mtd, default_value=default, required=True
|
|
535
|
+
)
|
|
536
|
+
field_definitions[param] = get_args(annotated_def)
|
|
537
|
+
|
|
538
|
+
python_queryables = create_model("m", **field_definitions).model_fields
|
|
539
|
+
return {**default_queryables, **model_fields_to_annotated(python_queryables)}
|