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.
Files changed (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -165
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.0.dist-info/RECORD +0 -94
  92. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {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 python_type_to_json(python_type: type) -> Optional[Union[str, List[str]]]:
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
- json_type.append(single_json_type)
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
- json_field_definition["type"] = python_type_to_json(type(enum_args[0]))
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 is not None:
168
- json_field_definition["type"] = python_type_to_json(python_field_args[0])
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 Any, Dict, List, Tuple, Union
18
+ from typing import Dict, List, Tuple, Union
19
19
 
20
- from pydantic import BaseModel, validator
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, list or tuple with 4 values for lonmin, latmin, lonmax, latmax"
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
- @validator("lonmin", "lonmax")
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
- @validator("latmin", "latmax")
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
- @validator("lonmax")
89
+ @field_validator("lonmax")
89
90
  @classmethod
90
- def validate_lonmax(cls, v: NumType, values: Dict[str, Any]) -> 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 values: A dictionary containing the current attribute values.
96
+ :param info: Additional validation informations.
96
97
  :return: The validated lonmax value.
97
98
  """
98
- if "lonmin" in values and v < values["lonmin"]:
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
- @validator("latmax")
103
+ @field_validator("latmax")
103
104
  @classmethod
104
- def validate_latmax(cls, v: NumType, values: Dict[str, Any]) -> 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 values: A dictionary containing the current attribute values.
110
+ :param info: Additional validation informations.
110
111
  :return: The validated latmax value.
111
112
  """
112
- if "latmin" in values and v < values["latmin"]:
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]
@@ -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: GeomArgs) -> BaseGeometry:
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
+ )