eodag 2.12.1__py3-none-any.whl → 3.0.0b1__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 (77) hide show
  1. eodag/api/core.py +434 -319
  2. eodag/api/product/__init__.py +5 -1
  3. eodag/api/product/_assets.py +7 -2
  4. eodag/api/product/_product.py +46 -68
  5. eodag/api/product/metadata_mapping.py +181 -66
  6. eodag/api/search_result.py +21 -1
  7. eodag/cli.py +20 -6
  8. eodag/config.py +95 -6
  9. eodag/plugins/apis/base.py +8 -165
  10. eodag/plugins/apis/ecmwf.py +36 -24
  11. eodag/plugins/apis/usgs.py +40 -24
  12. eodag/plugins/authentication/aws_auth.py +2 -2
  13. eodag/plugins/authentication/header.py +31 -6
  14. eodag/plugins/authentication/keycloak.py +13 -84
  15. eodag/plugins/authentication/oauth.py +3 -3
  16. eodag/plugins/authentication/openid_connect.py +256 -46
  17. eodag/plugins/authentication/qsauth.py +3 -0
  18. eodag/plugins/authentication/sas_auth.py +8 -1
  19. eodag/plugins/authentication/token.py +92 -46
  20. eodag/plugins/authentication/token_exchange.py +120 -0
  21. eodag/plugins/download/aws.py +86 -91
  22. eodag/plugins/download/base.py +72 -40
  23. eodag/plugins/download/http.py +607 -264
  24. eodag/plugins/download/s3rest.py +28 -15
  25. eodag/plugins/manager.py +73 -57
  26. eodag/plugins/search/__init__.py +36 -0
  27. eodag/plugins/search/base.py +225 -18
  28. eodag/plugins/search/build_search_result.py +389 -32
  29. eodag/plugins/search/cop_marine.py +378 -0
  30. eodag/plugins/search/creodias_s3.py +15 -14
  31. eodag/plugins/search/csw.py +5 -7
  32. eodag/plugins/search/data_request_search.py +44 -20
  33. eodag/plugins/search/qssearch.py +508 -203
  34. eodag/plugins/search/static_stac_search.py +99 -36
  35. eodag/resources/constraints/climate-dt.json +13 -0
  36. eodag/resources/constraints/extremes-dt.json +8 -0
  37. eodag/resources/ext_product_types.json +1 -1
  38. eodag/resources/product_types.yml +1897 -34
  39. eodag/resources/providers.yml +3539 -3277
  40. eodag/resources/stac.yml +48 -54
  41. eodag/resources/stac_api.yml +71 -25
  42. eodag/resources/stac_provider.yml +5 -0
  43. eodag/resources/user_conf_template.yml +51 -3
  44. eodag/rest/__init__.py +6 -0
  45. eodag/rest/cache.py +70 -0
  46. eodag/rest/config.py +68 -0
  47. eodag/rest/constants.py +27 -0
  48. eodag/rest/core.py +757 -0
  49. eodag/rest/server.py +397 -258
  50. eodag/rest/stac.py +438 -307
  51. eodag/rest/types/collections_search.py +44 -0
  52. eodag/rest/types/eodag_search.py +232 -43
  53. eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
  54. eodag/rest/types/stac_search.py +277 -0
  55. eodag/rest/utils/__init__.py +216 -0
  56. eodag/rest/utils/cql_evaluate.py +119 -0
  57. eodag/rest/utils/rfc3339.py +65 -0
  58. eodag/types/__init__.py +99 -9
  59. eodag/types/bbox.py +15 -14
  60. eodag/types/download_args.py +31 -0
  61. eodag/types/search_args.py +58 -7
  62. eodag/types/whoosh.py +81 -0
  63. eodag/utils/__init__.py +72 -9
  64. eodag/utils/constraints.py +37 -37
  65. eodag/utils/exceptions.py +23 -17
  66. eodag/utils/requests.py +138 -0
  67. eodag/utils/rest.py +104 -0
  68. eodag/utils/stac_reader.py +100 -16
  69. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/METADATA +64 -44
  70. eodag-3.0.0b1.dist-info/RECORD +109 -0
  71. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/WHEEL +1 -1
  72. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/entry_points.txt +6 -5
  73. eodag/plugins/apis/cds.py +0 -540
  74. eodag/rest/utils.py +0 -1133
  75. eodag-2.12.1.dist-info/RECORD +0 -94
  76. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2023, 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 typing import Any, Dict, Optional
19
+
20
+ from pydantic import (
21
+ BaseModel,
22
+ ConfigDict,
23
+ Field,
24
+ SerializerFunctionWrapHandler,
25
+ model_serializer,
26
+ )
27
+
28
+ from eodag.rest.types.eodag_search import EODAGSearch
29
+
30
+
31
+ class CollectionsSearchRequest(BaseModel):
32
+ """Search args for GET collections"""
33
+
34
+ model_config = ConfigDict(frozen=True)
35
+
36
+ q: Optional[str] = Field(default=None, serialization_alias="free_text")
37
+ platform: Optional[str] = Field(default=None)
38
+ instrument: Optional[str] = Field(default=None)
39
+ constellation: Optional[str] = Field(default=None)
40
+
41
+ @model_serializer(mode="wrap")
42
+ def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
43
+ dumped: Dict[str, Any] = handler(self)
44
+ return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
@@ -15,16 +15,24 @@
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, Optional, Tuple, Union
18
+ from __future__ import annotations
19
+
20
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
19
21
 
20
22
  from pydantic import (
23
+ AliasChoices,
24
+ AliasPath,
21
25
  BaseModel,
22
26
  ConfigDict,
23
27
  Field,
28
+ ValidationError,
24
29
  ValidationInfo,
25
30
  field_validator,
26
31
  model_validator,
27
32
  )
33
+ from pydantic.alias_generators import to_camel, to_snake
34
+ from pydantic_core import InitErrorDetails, PydanticCustomError
35
+ from pygeofilter.parsers.cql2_json import parse as parse_json
28
36
  from shapely.geometry import (
29
37
  GeometryCollection,
30
38
  LinearRing,
@@ -36,8 +44,16 @@ from shapely.geometry import (
36
44
  Polygon,
37
45
  )
38
46
 
47
+ from eodag.rest.utils import flatten_list, is_dict_str_any, list_to_str_list
48
+ from eodag.rest.utils.cql_evaluate import EodagEvaluator
39
49
  from eodag.utils import DEFAULT_ITEMS_PER_PAGE
40
50
 
51
+ if TYPE_CHECKING:
52
+ try:
53
+ from typing import Self
54
+ except ImportError:
55
+ from _typeshed import Self
56
+
41
57
  Geometry = Union[
42
58
  Dict[str, Any],
43
59
  Point,
@@ -61,17 +77,25 @@ class EODAGSearch(BaseModel):
61
77
  productType: Optional[str] = Field(None, alias="collections", validate_default=True)
62
78
  provider: Optional[str] = Field(None)
63
79
  ids: Optional[List[str]] = Field(None)
64
- id: Optional[List[str]] = Field(None, alias="ids")
80
+ id: Optional[List[str]] = Field(
81
+ None, alias="ids"
82
+ ) # TODO: remove when updating queryables
65
83
  geom: Optional[Geometry] = Field(None, alias="geometry")
66
84
  start: Optional[str] = Field(None, alias="start_datetime")
67
85
  end: Optional[str] = Field(None, alias="end_datetime")
86
+ startTimeFromAscendingNode: Optional[str] = Field(
87
+ None,
88
+ alias="start_datetime",
89
+ validation_alias=AliasChoices("start_datetime", "datetime"),
90
+ )
91
+ completionTimeFromAscendingNode: Optional[str] = Field(None, alias="end_datetime")
68
92
  publicationDate: Optional[str] = Field(None, alias="published")
69
93
  creationDate: Optional[str] = Field(None, alias="created")
70
94
  modificationDate: Optional[str] = Field(None, alias="updated")
71
95
  platformSerialIdentifier: Optional[str] = Field(None, alias="platform")
72
96
  instrument: Optional[str] = Field(None, alias="instruments")
73
97
  platform: Optional[str] = Field(None, alias="constellation")
74
- resolution: Optional[int] = Field(None, alias="gsd")
98
+ resolution: Optional[Union[int, str]] = Field(None, alias="gsd")
75
99
  cloudCover: Optional[int] = Field(None, alias="eo:cloud_cover")
76
100
  snowCover: Optional[int] = Field(None, alias="eo:snow_cover")
77
101
  processingLevel: Optional[str] = Field(None, alias="processing:level")
@@ -83,7 +107,6 @@ class EODAGSearch(BaseModel):
83
107
  polarizationChannels: Optional[List[str]] = Field(None, alias="sar:polarizations")
84
108
  dopplerFrequency: Optional[str] = Field(None, alias="sar:frequency_band")
85
109
  doi: Optional[str] = Field(None, alias="sci:doi")
86
- productVersion: Optional[str] = Field(None, alias="version")
87
110
  illuminationElevationAngle: Optional[float] = Field(
88
111
  None, alias="view:sun_elevation"
89
112
  )
@@ -91,32 +114,198 @@ class EODAGSearch(BaseModel):
91
114
  page: Optional[int] = Field(1)
92
115
  items_per_page: int = Field(DEFAULT_ITEMS_PER_PAGE, alias="limit")
93
116
  sortBy: Optional[List[Tuple[str, str]]] = Field(None, alias="sortby")
117
+ raise_errors: bool = False
94
118
 
95
- @model_validator(mode="before")
96
- @classmethod
97
- def remove_custom_extensions(cls, values: Dict[str, Any]) -> Dict[str, Any]:
119
+ _to_eodag_map: Dict[str, str]
120
+
121
+ @model_validator(mode="after")
122
+ def set_raise_errors(self) -> Self:
123
+ """Set raise_errors to True if provider is set"""
124
+ if self.provider:
125
+ self.raise_errors = True
126
+ return self
127
+
128
+ @model_validator(mode="after")
129
+ def remove_timeFromAscendingNode(self) -> Self: # pylint: disable=invalid-name
130
+ """TimeFromAscendingNode are just used for translation and not for search"""
131
+ self.startTimeFromAscendingNode = None # pylint: disable=invalid-name
132
+ self.completionTimeFromAscendingNode = None # pylint: disable=invalid-name
133
+ return self
134
+
135
+ @model_validator(mode="after")
136
+ def parse_extra_fields(self) -> Self:
98
137
  """process unknown and oseo EODAG custom extensions fields"""
99
138
  # Transform EODAG custom extensions OSEO and UNK.
139
+ if not self.__pydantic_extra__:
140
+ return self
141
+
100
142
  keys_to_update: Dict[str, str] = {}
101
- for key in values.keys():
143
+ for key in self.__pydantic_extra__.keys():
102
144
  if key.startswith("unk:"):
103
145
  keys_to_update[key] = key[len("unk:") :]
104
146
  elif key.startswith("oseo:"):
105
147
  keys_to_update[key] = key[len("oseo:") :]
106
148
 
107
149
  for old_key, new_key in keys_to_update.items():
108
- values[cls.snake_to_camel(new_key)] = values.pop(old_key)
150
+ self.__pydantic_extra__[
151
+ to_camel(to_snake(new_key))
152
+ ] = self.__pydantic_extra__.pop(old_key)
109
153
 
110
- return values
154
+ return self
111
155
 
112
156
  @model_validator(mode="before")
113
157
  @classmethod
114
158
  def remove_keys(cls, values: Dict[str, Any]) -> Dict[str, Any]:
115
159
  """Remove 'datetime', 'crunch', 'intersects', and 'bbox' keys"""
116
- for key in ["datetime", "crunch", "intersects", "bbox"]:
160
+ for key in ["datetime", "crunch", "intersects", "bbox", "filter_lang"]:
117
161
  values.pop(key, None)
118
162
  return values
119
163
 
164
+ @model_validator(mode="before")
165
+ @classmethod
166
+ def parse_collections(
167
+ cls, values: Dict[str, Any], info: ValidationInfo
168
+ ) -> Dict[str, Any]:
169
+ """convert collections to productType"""
170
+
171
+ if collections := values.pop("collections", None):
172
+ if len(collections) > 1:
173
+ raise ValueError("Only one collection is supported per search")
174
+ values["productType"] = collections[0]
175
+ else:
176
+ if not getattr(info, "context", None) or not info.context.get( # type: ignore
177
+ "isCatalog"
178
+ ):
179
+ raise ValueError("A collection is required")
180
+
181
+ return values
182
+
183
+ @model_validator(mode="before")
184
+ @classmethod
185
+ def parse_query(cls, values: Dict[str, Any]) -> Dict[str, Any]:
186
+ """
187
+ Convert a STAC query parameter filter with the "eq" operator to a dict.
188
+ """
189
+
190
+ def add_error(error_message: str, input: Any) -> None:
191
+ errors.append(
192
+ InitErrorDetails(
193
+ type=PydanticCustomError("invalid_query", error_message), # type: ignore
194
+ loc=("query",),
195
+ input=input,
196
+ )
197
+ )
198
+
199
+ query = values.pop("query", None)
200
+ if not query:
201
+ return values
202
+
203
+ query_props: Dict[str, Any] = {}
204
+ errors: List[InitErrorDetails] = []
205
+ for property_name, conditions in cast(Dict[str, Any], query).items():
206
+ # Remove the prefix "properties." if present
207
+ prop = property_name.replace("properties.", "", 1)
208
+
209
+ # Check if exactly one operator is specified per property
210
+ if not is_dict_str_any(conditions) or len(conditions) != 1: # type: ignore
211
+ add_error(
212
+ "Exactly 1 operator must be specified per property",
213
+ query[property_name],
214
+ )
215
+ continue
216
+
217
+ # Retrieve the operator and its value
218
+ operator, value = next(iter(cast(Dict[str, Any], conditions).items()))
219
+
220
+ # Validate the operator
221
+ # only eq, in and lte are allowed
222
+ # lte is only supported with eo:cloud_cover
223
+ # eo:cloud_cover only accept lte operator
224
+ if (
225
+ operator not in ("eq", "lte", "in")
226
+ or (operator == "lte" and prop != "eo:cloud_cover")
227
+ or (prop == "eo:cloud_cover" and operator != "lte")
228
+ ):
229
+ add_error(
230
+ f'operator "{operator}" is not supported for property "{prop}"',
231
+ query[property_name],
232
+ )
233
+ continue
234
+ if operator == "in" and not isinstance(value, list):
235
+ add_error(
236
+ f'operator "{operator}" requires a value of type list for property "{prop}"',
237
+ query[property_name],
238
+ )
239
+ continue
240
+
241
+ query_props[prop] = value
242
+
243
+ if errors:
244
+ raise ValidationError.from_exception_data(
245
+ title=cls.__name__, line_errors=errors
246
+ )
247
+
248
+ return {**values, **query_props}
249
+
250
+ @model_validator(mode="before")
251
+ @classmethod
252
+ def parse_cql(cls, values: Dict[str, Any]) -> Dict[str, Any]:
253
+ """
254
+ Process cql2 filter
255
+ """
256
+
257
+ def add_error(error_message: str) -> None:
258
+ errors.append(
259
+ InitErrorDetails(
260
+ type=PydanticCustomError("invalid_filter", error_message), # type: ignore
261
+ loc=("filter",),
262
+ )
263
+ )
264
+
265
+ filter_ = values.pop("filter", None)
266
+ if not filter_:
267
+ return values
268
+
269
+ errors: List[InitErrorDetails] = []
270
+ try:
271
+ parsing_result = EodagEvaluator().evaluate(parse_json(filter_)) # type: ignore
272
+ except (ValueError, NotImplementedError) as e:
273
+ add_error(str(e))
274
+ raise ValidationError.from_exception_data(
275
+ title=cls.__name__, line_errors=errors
276
+ ) from e
277
+
278
+ if not is_dict_str_any(parsing_result):
279
+ add_error("The parsed filter is not a proper dictionary")
280
+ raise ValidationError.from_exception_data(
281
+ title=cls.__name__, line_errors=errors
282
+ )
283
+
284
+ cql_args: Dict[str, Any] = cast(Dict[str, Any], parsing_result)
285
+
286
+ invalid_keys = {
287
+ "collections": 'Use "collection" instead of "collections"',
288
+ "ids": 'Use "id" instead of "ids"',
289
+ }
290
+ for k, m in invalid_keys.items():
291
+ if k in cql_args:
292
+ add_error(m)
293
+
294
+ if errors:
295
+ raise ValidationError.from_exception_data(
296
+ title=cls.__name__, line_errors=errors
297
+ )
298
+
299
+ # convert collection to EODAG collections
300
+ if col := cql_args.pop("collection", None):
301
+ cql_args["collections"] = col if isinstance(col, list) else [col]
302
+
303
+ # convert id to EODAG ids
304
+ if id := cql_args.pop("id", None):
305
+ cql_args["ids"] = id if isinstance(id, list) else [id]
306
+
307
+ return {**values, **cql_args}
308
+
120
309
  @field_validator("instrument", mode="before")
121
310
  @classmethod
122
311
  def join_instruments(cls, v: Union[str, List[str]]) -> str:
@@ -127,33 +316,27 @@ class EODAGSearch(BaseModel):
127
316
 
128
317
  @field_validator("sortBy", mode="before")
129
318
  @classmethod
130
- def convert_stac_to_eodag_sortby(
319
+ def parse_sortby(
131
320
  cls,
132
321
  sortby_post_params: List[Dict[str, str]],
133
322
  ) -> List[Tuple[str, str]]:
134
323
  """
135
324
  Convert STAC POST sortby to EODAG sortby
136
325
  """
137
- eodag_sortby: List[Tuple[str, str]] = []
138
- for sortby_post_param in sortby_post_params:
139
- field = cls.snake_to_camel(cls.to_eodag(sortby_post_param["field"]))
140
- eodag_sortby.append((field, sortby_post_param["direction"]))
141
- return eodag_sortby
142
-
143
- @field_validator("productType")
144
- @classmethod
145
- def verify_producttype_is_present(
146
- cls, v: Optional[str], info: ValidationInfo
147
- ) -> Optional[str]:
148
- """Verify productType is present when required"""
149
- if not v and (
150
- not info
151
- or not getattr(info, "context", None)
152
- or not info.context.get("isCatalog") # type: ignore
153
- ):
154
- raise ValueError("A collection is required")
155
-
156
- return v
326
+ special_fields = {
327
+ "start": "startTimeFromAscendingNode",
328
+ "end": "completionTimeFromAscendingNode",
329
+ }
330
+ return [
331
+ (
332
+ special_fields.get(
333
+ to_camel(to_snake(cls.to_eodag(param["field"]))),
334
+ to_camel(to_snake(cls.to_eodag(param["field"]))),
335
+ ),
336
+ param["direction"],
337
+ )
338
+ for param in sortby_post_params
339
+ ]
157
340
 
158
341
  @field_validator("start", "end")
159
342
  @classmethod
@@ -164,21 +347,27 @@ class EODAGSearch(BaseModel):
164
347
  return v
165
348
 
166
349
  @classmethod
167
- def snake_to_camel(cls, snake_str: str) -> str:
168
- """Convert snake_case to camelCase"""
169
- # Split the string by underscore and capitalize each component except the first one
170
- components = snake_str.split("_")
171
- return components[0] + "".join(x.title() for x in components[1:])
350
+ def _create_to_eodag_map(cls) -> None:
351
+ """Create mapping to convert fields from STAC to EODAG"""
352
+ cls._to_eodag_map = {}
353
+ for name, field_info in cls.model_fields.items():
354
+ if field_info.validation_alias:
355
+ if isinstance(field_info.validation_alias, (AliasChoices, AliasPath)):
356
+ for a in list_to_str_list(
357
+ flatten_list(field_info.validation_alias.convert_to_aliases())
358
+ ):
359
+ cls._to_eodag_map[a] = name
360
+ else:
361
+ cls._to_eodag_map[field_info.validation_alias] = name
362
+ elif field_info.alias:
363
+ cls._to_eodag_map[field_info.alias] = name
172
364
 
173
365
  @classmethod
174
366
  def to_eodag(cls, value: str) -> str:
175
367
  """Convert a STAC parameter to its matching EODAG name"""
176
- alias_map = {
177
- field_info.alias: name
178
- for name, field_info in cls.model_fields.items()
179
- if field_info.alias
180
- }
181
- return alias_map.get(value, value)
368
+ if not isinstance(cls._to_eodag_map, dict) or not cls._to_eodag_map:
369
+ cls._create_to_eodag_map()
370
+ return cls._to_eodag_map.get(value, value)
182
371
 
183
372
  @classmethod
184
373
  def to_stac(cls, field_name: str) -> str:
@@ -17,10 +17,20 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
21
-
22
- from pydantic import BaseModel, Field
23
-
20
+ from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Union
21
+
22
+ from pydantic import (
23
+ BaseModel,
24
+ ConfigDict,
25
+ Field,
26
+ SerializationInfo,
27
+ SerializerFunctionWrapHandler,
28
+ computed_field,
29
+ model_serializer,
30
+ )
31
+
32
+ from eodag.rest.types.eodag_search import EODAGSearch
33
+ from eodag.rest.utils.rfc3339 import str_to_interval
24
34
  from eodag.types import python_field_definition_to_json
25
35
  from eodag.utils import Annotated
26
36
 
@@ -28,6 +38,34 @@ if TYPE_CHECKING:
28
38
  from pydantic.fields import FieldInfo
29
39
 
30
40
 
41
+ class QueryablesGetParams(BaseModel):
42
+ """Store GET Queryables query params"""
43
+
44
+ collection: Optional[str] = Field(default=None, serialization_alias="productType")
45
+ datetime: Optional[str] = Field(default=None)
46
+
47
+ model_config = ConfigDict(extra="allow", frozen=True)
48
+
49
+ @model_serializer(mode="wrap")
50
+ def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
51
+ dumped: Dict[str, Any] = handler(self)
52
+ return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
53
+
54
+ @computed_field
55
+ @property
56
+ def start_datetime(self) -> Optional[str]:
57
+ """Extract start_datetime property from datetime"""
58
+ start = str_to_interval(self.datetime)[0]
59
+ return start.strftime("%Y-%m-%dT%H:%M:%SZ") if start else None
60
+
61
+ @computed_field
62
+ @property
63
+ def end_datetime(self) -> Optional[str]:
64
+ """Extract end_datetime property from datetime"""
65
+ end = str_to_interval(self.datetime)[1]
66
+ return end.strftime("%Y-%m-%dT%H:%M:%SZ") if end else None
67
+
68
+
31
69
  class StacQueryableProperty(BaseModel):
32
70
  """A class representing a queryable property.
33
71
 
@@ -44,6 +82,10 @@ class StacQueryableProperty(BaseModel):
44
82
  type: Optional[Union[str, List[str]]] = None
45
83
  enum: Optional[List[Any]] = None
46
84
  value: Optional[Any] = None
85
+ min: Optional[Union[int, List[Union[int, None]]]] = None
86
+ max: Optional[Union[int, List[Union[int, None]]]] = None
87
+ oneOf: Optional[List[Any]] = None
88
+ items: Optional[Any] = None
47
89
 
48
90
  @classmethod
49
91
  def from_python_field_definition(
@@ -57,6 +99,16 @@ class StacQueryableProperty(BaseModel):
57
99
 
58
100
  return cls(**def_dict)
59
101
 
102
+ @model_serializer(mode="wrap")
103
+ def remove_none(
104
+ self,
105
+ handler: SerializerFunctionWrapHandler,
106
+ _: SerializationInfo,
107
+ ):
108
+ """Remove none value property fields during serialization"""
109
+ props: Dict[str, Any] = handler(self)
110
+ return {k: v for k, v in props.items() if v is not None}
111
+
60
112
 
61
113
  class StacQueryables(BaseModel):
62
114
  """A class representing queryable properties for the STAC API.
@@ -87,48 +139,34 @@ class StacQueryables(BaseModel):
87
139
  description: str = Field(
88
140
  default="Queryable names for the EODAG STAC API Item Search filter."
89
141
  )
90
- properties: Dict[str, StacQueryableProperty] = Field(
91
- default={
92
- "ids": StacQueryableProperty(
93
- description="ID",
94
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
95
- ),
96
- "collections": StacQueryableProperty(
97
- description="Collection",
98
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
99
- ),
100
- "geometry": StacQueryableProperty(
101
- description="Geometry",
102
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
103
- ),
104
- "datetime": StacQueryableProperty(
105
- description="Datetime - use parameters year, month, day, time instead if available",
106
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
107
- ),
108
- }
109
- )
142
+ default_properties: ClassVar[Dict[str, StacQueryableProperty]] = {
143
+ "id": StacQueryableProperty(
144
+ description="ID",
145
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
146
+ ),
147
+ "collection": StacQueryableProperty(
148
+ description="Collection",
149
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
150
+ ),
151
+ "geometry": StacQueryableProperty(
152
+ description="Geometry",
153
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
154
+ ),
155
+ "datetime": StacQueryableProperty(
156
+ description="Datetime - use parameters year, month, day, time instead if available",
157
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
158
+ ),
159
+ "bbox": StacQueryableProperty(
160
+ description="BBox",
161
+ type="array",
162
+ oneOf=[{"minItems": 4, "maxItems": 4}, {"minItems": 6, "maxItems": 6}],
163
+ items={"type": "number"},
164
+ ),
165
+ }
166
+ properties: Dict[str, StacQueryableProperty] = Field()
110
167
  additional_properties: bool = Field(
111
168
  default=True, serialization_alias="additionalProperties"
112
169
  )
113
170
 
114
- def get_properties(self) -> Dict[str, StacQueryableProperty]:
115
- """Get the queryable properties.
116
-
117
- :returns: A dictionary containing queryable properties.
118
- :rtype: typing.Dict[str, StacQueryableProperty]
119
- """
120
- properties = {}
121
- for key, property in self.properties.items():
122
- property = StacQueryableProperty(
123
- description=property.description, type=property.type
124
- )
125
- properties[key] = property
126
- return properties
127
-
128
171
  def __contains__(self, name: str) -> bool:
129
172
  return name in self.properties
130
-
131
- def __setitem__(self, name: str, qprop: StacQueryableProperty) -> None:
132
- # only keep "datetime" queryable for dates
133
- if name not in ("start_datetime", "end_datetime"):
134
- self.properties[name] = qprop