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
eodag/types/__init__.py
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
"""EODAG types"""
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
-
from typing import Any, Dict, List, Literal, Optional, Union
|
|
21
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
|
|
22
22
|
|
|
23
|
+
from annotated_types import Gt, Lt
|
|
23
24
|
from pydantic import Field
|
|
24
25
|
from pydantic.fields import FieldInfo
|
|
25
26
|
|
|
@@ -56,13 +57,51 @@ def json_type_to_python(json_type: Union[str, List[str]]) -> type:
|
|
|
56
57
|
return type(None)
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def
|
|
60
|
+
def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> Tuple[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
checks if the value from an Annotated object is a minimum or maximum
|
|
63
|
+
:param type_info: info from Annotated
|
|
64
|
+
:return: "min" or "max"
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(type_info, Gt):
|
|
67
|
+
return "min", type_info.gt
|
|
68
|
+
if isinstance(type_info, Lt):
|
|
69
|
+
return "max", type_info.lt
|
|
70
|
+
return "", None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_type_info_from_annotated(
|
|
74
|
+
annotated_type: Annotated[type, Any]
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
retrieves type information from an annotated object
|
|
78
|
+
:param annotated_type: annotated object
|
|
79
|
+
:return: dict containing type and min/max if available
|
|
80
|
+
"""
|
|
81
|
+
type_args = get_args(annotated_type)
|
|
82
|
+
type_data = {
|
|
83
|
+
"type": list(JSON_TYPES_MAPPING.keys())[
|
|
84
|
+
list(JSON_TYPES_MAPPING.values()).index(type_args[0])
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
if len(type_args) >= 2:
|
|
88
|
+
min_or_max, value = _get_min_or_max(type_args[1])
|
|
89
|
+
type_data[min_or_max] = value
|
|
90
|
+
if len(type_args) > 2:
|
|
91
|
+
min_or_max, value = _get_min_or_max(type_args[2])
|
|
92
|
+
type_data[min_or_max] = value
|
|
93
|
+
return type_data
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def python_type_to_json(
|
|
97
|
+
python_type: type,
|
|
98
|
+
) -> Optional[Union[str, List[Dict[str, Any]]]]:
|
|
60
99
|
"""Get json type from python https://spec.openapis.org/oas/v3.1.0#data-types
|
|
61
100
|
|
|
62
101
|
>>> python_type_to_json(int)
|
|
63
102
|
'integer'
|
|
64
103
|
>>> python_type_to_json(Union[float, str])
|
|
65
|
-
['number', 'string']
|
|
104
|
+
[{'type': 'number'}, {'type': 'string'}]
|
|
66
105
|
|
|
67
106
|
:param python_type: the python type
|
|
68
107
|
:returns: the json type
|
|
@@ -70,18 +109,25 @@ def python_type_to_json(python_type: type) -> Optional[Union[str, List[str]]]:
|
|
|
70
109
|
if get_origin(python_type) is Union:
|
|
71
110
|
json_type = list()
|
|
72
111
|
for single_python_type in get_args(python_type):
|
|
112
|
+
type_data = {}
|
|
73
113
|
if single_python_type in JSON_TYPES_MAPPING.values():
|
|
74
114
|
# JSON_TYPES_MAPPING key from given value
|
|
75
115
|
single_json_type = list(JSON_TYPES_MAPPING.keys())[
|
|
76
116
|
list(JSON_TYPES_MAPPING.values()).index(single_python_type)
|
|
77
117
|
]
|
|
78
|
-
|
|
118
|
+
type_data["type"] = single_json_type
|
|
119
|
+
json_type.append(type_data)
|
|
120
|
+
elif get_origin(single_python_type) == Annotated:
|
|
121
|
+
type_data = _get_type_info_from_annotated(single_python_type)
|
|
122
|
+
json_type.append(type_data)
|
|
79
123
|
return json_type
|
|
80
124
|
elif python_type in JSON_TYPES_MAPPING.values():
|
|
81
125
|
# JSON_TYPES_MAPPING key from given value
|
|
82
126
|
return list(JSON_TYPES_MAPPING.keys())[
|
|
83
127
|
list(JSON_TYPES_MAPPING.values()).index(python_type)
|
|
84
128
|
]
|
|
129
|
+
elif get_origin(python_type) == Annotated:
|
|
130
|
+
return [_get_type_info_from_annotated(python_type)]
|
|
85
131
|
else:
|
|
86
132
|
return None
|
|
87
133
|
|
|
@@ -99,7 +145,9 @@ def json_field_definition_to_python(
|
|
|
99
145
|
... 'title': 'Foo parameter'
|
|
100
146
|
... }
|
|
101
147
|
... )
|
|
102
|
-
>>> str(result).replace('_extensions', '') # python3.8 compatibility
|
|
148
|
+
>>> res_repr = str(result).replace('_extensions', '') # python3.8 compatibility
|
|
149
|
+
>>> res_repr = res_repr.replace(', default=None', '') # pydantic >= 2.7.0 compatibility
|
|
150
|
+
>>> res_repr
|
|
103
151
|
"typing.Annotated[bool, FieldInfo(annotation=NoneType, required=False, title='Foo parameter')]"
|
|
104
152
|
|
|
105
153
|
:param json_field_definition: the json field definition
|
|
@@ -152,20 +200,54 @@ def python_field_definition_to_json(
|
|
|
152
200
|
"%s must be an instance of Annotated" % python_field_definition
|
|
153
201
|
)
|
|
154
202
|
|
|
155
|
-
json_field_definition = dict()
|
|
203
|
+
json_field_definition: Dict[str, Any] = dict()
|
|
156
204
|
|
|
157
205
|
python_field_args = get_args(python_field_definition)
|
|
158
206
|
|
|
159
207
|
# enum & type
|
|
160
208
|
if get_origin(python_field_args[0]) is Literal:
|
|
161
209
|
enum_args = get_args(python_field_args[0])
|
|
162
|
-
|
|
210
|
+
type_data = python_type_to_json(type(enum_args[0]))
|
|
211
|
+
if isinstance(type_data, str):
|
|
212
|
+
json_field_definition["type"] = type_data
|
|
213
|
+
elif type_data is None:
|
|
214
|
+
json_field_definition["type"] = json_field_definition[
|
|
215
|
+
"min"
|
|
216
|
+
] = json_field_definition["max"] = None
|
|
217
|
+
else:
|
|
218
|
+
json_field_definition["type"] = [row["type"] for row in type_data]
|
|
219
|
+
json_field_definition["min"] = [
|
|
220
|
+
row["min"] if "min" in row else None for row in type_data
|
|
221
|
+
]
|
|
222
|
+
json_field_definition["max"] = [
|
|
223
|
+
row["max"] if "max" in row else None for row in type_data
|
|
224
|
+
]
|
|
163
225
|
json_field_definition["enum"] = list(enum_args)
|
|
164
226
|
# type
|
|
165
227
|
else:
|
|
166
228
|
field_type = python_type_to_json(python_field_args[0])
|
|
167
|
-
if field_type
|
|
168
|
-
json_field_definition["type"] =
|
|
229
|
+
if isinstance(field_type, str):
|
|
230
|
+
json_field_definition["type"] = field_type
|
|
231
|
+
elif field_type is None:
|
|
232
|
+
json_field_definition["type"] = json_field_definition[
|
|
233
|
+
"min"
|
|
234
|
+
] = json_field_definition["max"] = None
|
|
235
|
+
else:
|
|
236
|
+
json_field_definition["type"] = [row["type"] for row in field_type]
|
|
237
|
+
json_field_definition["min"] = [
|
|
238
|
+
row["min"] if "min" in row else None for row in field_type
|
|
239
|
+
]
|
|
240
|
+
json_field_definition["max"] = [
|
|
241
|
+
row["max"] if "max" in row else None for row in field_type
|
|
242
|
+
]
|
|
243
|
+
if "min" in json_field_definition and json_field_definition["min"].count(
|
|
244
|
+
None
|
|
245
|
+
) == len(json_field_definition["min"]):
|
|
246
|
+
json_field_definition.pop("min")
|
|
247
|
+
if "max" in json_field_definition and json_field_definition["max"].count(
|
|
248
|
+
None
|
|
249
|
+
) == len(json_field_definition["max"]):
|
|
250
|
+
json_field_definition.pop("max")
|
|
169
251
|
|
|
170
252
|
if len(python_field_args) < 2:
|
|
171
253
|
return json_field_definition
|
|
@@ -204,7 +286,9 @@ def model_fields_to_annotated(
|
|
|
204
286
|
>>> from pydantic import create_model
|
|
205
287
|
>>> some_model = create_model("some_model", foo=(str, None))
|
|
206
288
|
>>> fields_definitions = model_fields_to_annotated(some_model.model_fields)
|
|
207
|
-
>>> str(fields_definitions).replace('_extensions', '') # python3.8 compatibility
|
|
289
|
+
>>> fd_repr = str(fields_definitions).replace('_extensions', '') # python3.8 compatibility
|
|
290
|
+
>>> fd_repr = fd_repr.replace(', default=None', '') # pydantic >= 2.7.0 compatibility
|
|
291
|
+
>>> fd_repr
|
|
208
292
|
"{'foo': typing.Annotated[str, FieldInfo(annotation=NoneType, required=False)]}"
|
|
209
293
|
|
|
210
294
|
:param model_fields: BaseModel.model_fields to convert
|
|
@@ -217,3 +301,15 @@ def model_fields_to_annotated(
|
|
|
217
301
|
new_field_info.annotation = None
|
|
218
302
|
annotated_model_fields[param] = Annotated[field_type, new_field_info]
|
|
219
303
|
return annotated_model_fields
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class ProviderSortables(TypedDict):
|
|
307
|
+
"""A class representing sortable parameter(s) of a provider and the allowed
|
|
308
|
+
maximum number of used sortable(s) in a search request with the provider
|
|
309
|
+
|
|
310
|
+
:param sortables: The list of sortable parameter(s) of a provider
|
|
311
|
+
:param max_sort_params: (optional) The allowed maximum number of sortable(s) in a search request with the provider
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
sortables: List[str]
|
|
315
|
+
max_sort_params: Annotated[Optional[int], Gt(0)]
|
eodag/types/bbox.py
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
|
-
from typing import
|
|
18
|
+
from typing import Dict, List, Tuple, Union
|
|
19
19
|
|
|
20
|
-
from pydantic import BaseModel,
|
|
20
|
+
from pydantic import BaseModel, ValidationInfo, field_validator
|
|
21
21
|
from shapely.geometry.polygon import Polygon
|
|
22
22
|
|
|
23
23
|
NumType = Union[float, int]
|
|
@@ -36,7 +36,7 @@ class BBox(BaseModel):
|
|
|
36
36
|
lonmax: NumType
|
|
37
37
|
latmax: NumType
|
|
38
38
|
|
|
39
|
-
def __init__(__pydantic_self__, bboxArgs: BBoxArgs) -> None: # type: ignore
|
|
39
|
+
def __init__(__pydantic_self__, bboxArgs: BBoxArgs) -> None: # type: ignore # pylint: disable=no-self-argument
|
|
40
40
|
"""
|
|
41
41
|
Constructs all the necessary attributes for the BBox object.
|
|
42
42
|
|
|
@@ -55,11 +55,12 @@ class BBox(BaseModel):
|
|
|
55
55
|
values = bboxArgs
|
|
56
56
|
else:
|
|
57
57
|
raise ValueError(
|
|
58
|
-
"Expected a dictionary,
|
|
58
|
+
"Expected a dictionary,",
|
|
59
|
+
" list or tuple with 4 values for lonmin, latmin, lonmax, latmax",
|
|
59
60
|
)
|
|
60
61
|
super().__init__(**values)
|
|
61
62
|
|
|
62
|
-
@
|
|
63
|
+
@field_validator("lonmin", "lonmax")
|
|
63
64
|
@classmethod
|
|
64
65
|
def validate_longitude(cls, v: NumType) -> NumType:
|
|
65
66
|
"""
|
|
@@ -72,7 +73,7 @@ class BBox(BaseModel):
|
|
|
72
73
|
raise ValueError("Longitude values must be between -180 and 180")
|
|
73
74
|
return v
|
|
74
75
|
|
|
75
|
-
@
|
|
76
|
+
@field_validator("latmin", "latmax")
|
|
76
77
|
@classmethod
|
|
77
78
|
def validate_latitude(cls, v: NumType) -> NumType:
|
|
78
79
|
"""
|
|
@@ -85,31 +86,31 @@ class BBox(BaseModel):
|
|
|
85
86
|
raise ValueError("Latitude values must be between -90 and 90")
|
|
86
87
|
return v
|
|
87
88
|
|
|
88
|
-
@
|
|
89
|
+
@field_validator("lonmax")
|
|
89
90
|
@classmethod
|
|
90
|
-
def validate_lonmax(cls, v: NumType,
|
|
91
|
+
def validate_lonmax(cls, v: NumType, info: ValidationInfo) -> NumType:
|
|
91
92
|
"""
|
|
92
93
|
Validates that lonmax is greater than lonmin.
|
|
93
94
|
|
|
94
95
|
:param v: The lonmax value to be validated.
|
|
95
|
-
:param
|
|
96
|
+
:param info: Additional validation informations.
|
|
96
97
|
:return: The validated lonmax value.
|
|
97
98
|
"""
|
|
98
|
-
if "lonmin" in
|
|
99
|
+
if "lonmin" in info.data and v < info.data["lonmin"]:
|
|
99
100
|
raise ValueError("lonmax must be greater than lonmin")
|
|
100
101
|
return v
|
|
101
102
|
|
|
102
|
-
@
|
|
103
|
+
@field_validator("latmax")
|
|
103
104
|
@classmethod
|
|
104
|
-
def validate_latmax(cls, v: NumType,
|
|
105
|
+
def validate_latmax(cls, v: NumType, info: ValidationInfo) -> NumType:
|
|
105
106
|
"""
|
|
106
107
|
Validates that latmax is greater than latmin.
|
|
107
108
|
|
|
108
109
|
:param v: The latmax value to be validated.
|
|
109
|
-
:param
|
|
110
|
+
:param info: Additional validation informations.
|
|
110
111
|
:return: The validated latmax value.
|
|
111
112
|
"""
|
|
112
|
-
if "latmin" in
|
|
113
|
+
if "latmin" in info.data and v < info.data["latmin"]:
|
|
113
114
|
raise ValueError("latmax must be greater than latmin")
|
|
114
115
|
return v
|
|
115
116
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024, CS GROUP - France, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Dict, Optional, TypedDict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DownloadConf(TypedDict, total=False):
|
|
24
|
+
"""Download configuration
|
|
25
|
+
|
|
26
|
+
:cvar output_prefix: where to store downloaded products, as an absolute file path
|
|
27
|
+
(Default: local temporary directory)
|
|
28
|
+
:cvar output_extension: downloaded file extension
|
|
29
|
+
:cvar extract: whether to extract the downloaded products, only applies to archived products
|
|
30
|
+
:cvar dl_url_params: additional parameters to pass over to the download url as an url parameter
|
|
31
|
+
:cvar delete_archive: whether to delete the downloaded archives
|
|
32
|
+
:cvar asset: regex filter to identify assets to download
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
output_dir: str
|
|
36
|
+
output_extension: str
|
|
37
|
+
extract: bool
|
|
38
|
+
dl_url_params: Dict[str, str]
|
|
39
|
+
delete_archive: bool
|
|
40
|
+
asset: Optional[str]
|
eodag/types/search_args.py
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
|
+
import re
|
|
18
19
|
from datetime import datetime
|
|
19
|
-
from typing import Dict, List, Optional, Tuple, Union, cast
|
|
20
|
+
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
|
20
21
|
|
|
22
|
+
from annotated_types import MinLen
|
|
21
23
|
from pydantic import BaseModel, ConfigDict, Field, conint, field_validator
|
|
22
24
|
from shapely import wkt
|
|
23
25
|
from shapely.errors import GEOSException
|
|
@@ -25,12 +27,14 @@ from shapely.geometry import Polygon, shape
|
|
|
25
27
|
from shapely.geometry.base import GEOMETRY_TYPES, BaseGeometry
|
|
26
28
|
|
|
27
29
|
from eodag.types.bbox import BBox
|
|
28
|
-
from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE
|
|
30
|
+
from eodag.utils import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE, Annotated
|
|
31
|
+
from eodag.utils.exceptions import ValidationError
|
|
29
32
|
|
|
30
33
|
NumType = Union[float, int]
|
|
31
34
|
GeomArgs = Union[List[NumType], Tuple[NumType], Dict[str, NumType], str, BaseGeometry]
|
|
32
35
|
|
|
33
36
|
PositiveInt = conint(gt=0)
|
|
37
|
+
SortByList = Annotated[List[Tuple[str, str]], MinLen(1)]
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
class SearchArgs(BaseModel):
|
|
@@ -47,6 +51,7 @@ class SearchArgs(BaseModel):
|
|
|
47
51
|
locations: Optional[Dict[str, str]] = Field(None)
|
|
48
52
|
page: Optional[int] = Field(DEFAULT_PAGE, gt=0) # type: ignore
|
|
49
53
|
items_per_page: Optional[PositiveInt] = Field(DEFAULT_ITEMS_PER_PAGE) # type: ignore
|
|
54
|
+
sort_by: Optional[SortByList] = Field(None) # type: ignore
|
|
50
55
|
|
|
51
56
|
@field_validator("start", "end", mode="before")
|
|
52
57
|
@classmethod
|
|
@@ -60,24 +65,69 @@ class SearchArgs(BaseModel):
|
|
|
60
65
|
|
|
61
66
|
@field_validator("geom", mode="before")
|
|
62
67
|
@classmethod
|
|
63
|
-
def check_geom(cls, v:
|
|
68
|
+
def check_geom(cls, v: Any) -> BaseGeometry:
|
|
64
69
|
"""Validate geom"""
|
|
65
70
|
# GeoJSON geometry
|
|
66
|
-
if isinstance(v, dict) and v.get("type") in GEOMETRY_TYPES:
|
|
71
|
+
if isinstance(v, dict) and v.get("type") in GEOMETRY_TYPES: # type: ignore
|
|
67
72
|
return cast(BaseGeometry, shape(v))
|
|
68
73
|
|
|
69
74
|
# Bounding Box
|
|
70
75
|
if isinstance(v, (list, tuple, dict)):
|
|
71
|
-
return BBox(v).to_polygon()
|
|
76
|
+
return BBox(v).to_polygon() # type: ignore
|
|
72
77
|
|
|
73
78
|
if isinstance(v, str):
|
|
74
79
|
# WKT geometry
|
|
75
80
|
try:
|
|
76
|
-
return cast(Polygon, wkt.loads(v))
|
|
77
|
-
except GEOSException as e:
|
|
81
|
+
return cast(Polygon, wkt.loads(v)) # type: ignore
|
|
82
|
+
except GEOSException as e: # type: ignore
|
|
78
83
|
raise ValueError(f"Invalid geometry WKT string: {v}") from e
|
|
79
84
|
|
|
80
85
|
if isinstance(v, BaseGeometry):
|
|
81
86
|
return v
|
|
82
87
|
|
|
83
88
|
raise TypeError(f"Invalid geometry type: {type(v)}")
|
|
89
|
+
|
|
90
|
+
@field_validator("sort_by", mode="before")
|
|
91
|
+
@classmethod
|
|
92
|
+
def check_sort_by_arg(
|
|
93
|
+
cls,
|
|
94
|
+
sort_by_arg: Optional[SortByList], # type: ignore
|
|
95
|
+
) -> Optional[SortByList]: # type: ignore
|
|
96
|
+
"""Check if the sort_by argument is correct
|
|
97
|
+
|
|
98
|
+
:param sort_by_arg: The sort_by argument
|
|
99
|
+
:returns: The sort_by argument with sorting order parsed (whitespace(s) are
|
|
100
|
+
removed and only the 3 first letters in uppercase are kept)
|
|
101
|
+
"""
|
|
102
|
+
if sort_by_arg is None:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
assert isinstance(
|
|
106
|
+
sort_by_arg, list
|
|
107
|
+
), f"Sort argument must be a list of tuple(s), got a '{type(sort_by_arg)}' instead"
|
|
108
|
+
sort_order_pattern = r"^(ASC|DES)[a-zA-Z]*$"
|
|
109
|
+
for i, sort_by_tuple in enumerate(sort_by_arg):
|
|
110
|
+
assert isinstance(
|
|
111
|
+
sort_by_tuple, tuple
|
|
112
|
+
), f"Sort argument must be a list of tuple(s), got a list of '{type(sort_by_tuple)}' instead"
|
|
113
|
+
# get sorting elements by removing leading and trailing whitespace(s) if exist
|
|
114
|
+
sort_param = sort_by_tuple[0].strip()
|
|
115
|
+
sort_order = sort_by_tuple[1].strip().upper()
|
|
116
|
+
assert re.match(sort_order_pattern, sort_order) is not None, (
|
|
117
|
+
"Sorting order must be set to 'ASC' (ASCENDING) or 'DESC' (DESCENDING), "
|
|
118
|
+
f"got '{sort_order}' with '{sort_param}' instead"
|
|
119
|
+
)
|
|
120
|
+
sort_by_arg[i] = (sort_param, sort_order[:3])
|
|
121
|
+
# remove duplicates
|
|
122
|
+
pruned_sort_by_arg: SortByList = list(dict.fromkeys(sort_by_arg)) # type: ignore
|
|
123
|
+
for i, sort_by_tuple in enumerate(pruned_sort_by_arg):
|
|
124
|
+
for j, sort_by_tuple_tmp in enumerate(pruned_sort_by_arg):
|
|
125
|
+
# since duplicated tuples or dictionnaries have been removed, if two sorting parameters are equal,
|
|
126
|
+
# then their sorting order is different and there is a contradiction that would raise an error
|
|
127
|
+
if i != j and sort_by_tuple[0] == sort_by_tuple_tmp[0]:
|
|
128
|
+
raise ValidationError(
|
|
129
|
+
f"'{sort_by_tuple[0]}' parameter is called several times to sort results with different "
|
|
130
|
+
"sorting orders. Please set it to only one ('ASC' (ASCENDING) or 'DESC' (DESCENDING))",
|
|
131
|
+
set([sort_by_tuple[0]]),
|
|
132
|
+
)
|
|
133
|
+
return pruned_sort_by_arg
|
eodag/types/whoosh.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2024, CS Systemes d'Information, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
from typing import List
|
|
19
|
+
|
|
20
|
+
from whoosh.fields import Schema
|
|
21
|
+
from whoosh.matching import NullMatcher
|
|
22
|
+
from whoosh.qparser import OrGroup, QueryParser, plugins
|
|
23
|
+
from whoosh.query.positional import Phrase
|
|
24
|
+
from whoosh.query.qcore import QueryError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RobustPhrase(Phrase):
|
|
28
|
+
"""
|
|
29
|
+
Matches documents containing a given phrase.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def matcher(self, searcher, context=None):
|
|
33
|
+
"""
|
|
34
|
+
Override the default to not raise error on match exception but simply return not found
|
|
35
|
+
Needed to handle phrase search in whoosh.fields.IDLIST
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
return super().matcher(searcher, context)
|
|
39
|
+
except QueryError:
|
|
40
|
+
return NullMatcher()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EODAGQueryParser(QueryParser):
|
|
44
|
+
"""
|
|
45
|
+
A hand-written query parser built on modular plug-ins.
|
|
46
|
+
|
|
47
|
+
Override the default to include specific EODAG configuration
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
filters: List[str],
|
|
53
|
+
schema: Schema,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
EODAG QueryParser initialization
|
|
57
|
+
|
|
58
|
+
:param filters: list of fieldnames to filter on
|
|
59
|
+
:param schema: Whoosh Schema
|
|
60
|
+
"""
|
|
61
|
+
super().__init__(
|
|
62
|
+
None,
|
|
63
|
+
schema=schema,
|
|
64
|
+
plugins=[
|
|
65
|
+
plugins.SingleQuotePlugin(),
|
|
66
|
+
plugins.FieldsPlugin(),
|
|
67
|
+
plugins.WildcardPlugin(),
|
|
68
|
+
plugins.PhrasePlugin(),
|
|
69
|
+
plugins.GroupPlugin(),
|
|
70
|
+
plugins.OperatorsPlugin(),
|
|
71
|
+
plugins.BoostPlugin(),
|
|
72
|
+
plugins.EveryPlugin(),
|
|
73
|
+
plugins.RangePlugin(),
|
|
74
|
+
plugins.PlusMinusPlugin(),
|
|
75
|
+
plugins.MultifieldPlugin(filters, fieldboosts=None),
|
|
76
|
+
],
|
|
77
|
+
phraseclass=RobustPhrase,
|
|
78
|
+
group=OrGroup,
|
|
79
|
+
)
|