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
@@ -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,13 @@ 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
+ from typing_extensions import Self
53
+
41
54
  Geometry = Union[
42
55
  Dict[str, Any],
43
56
  Point,
@@ -61,17 +74,25 @@ class EODAGSearch(BaseModel):
61
74
  productType: Optional[str] = Field(None, alias="collections", validate_default=True)
62
75
  provider: Optional[str] = Field(None)
63
76
  ids: Optional[List[str]] = Field(None)
64
- id: Optional[List[str]] = Field(None, alias="ids")
77
+ id: Optional[List[str]] = Field(
78
+ None, alias="ids"
79
+ ) # TODO: remove when updating queryables
65
80
  geom: Optional[Geometry] = Field(None, alias="geometry")
66
81
  start: Optional[str] = Field(None, alias="start_datetime")
67
82
  end: Optional[str] = Field(None, alias="end_datetime")
83
+ startTimeFromAscendingNode: Optional[str] = Field(
84
+ None,
85
+ alias="start_datetime",
86
+ validation_alias=AliasChoices("start_datetime", "datetime"),
87
+ )
88
+ completionTimeFromAscendingNode: Optional[str] = Field(None, alias="end_datetime")
68
89
  publicationDate: Optional[str] = Field(None, alias="published")
69
90
  creationDate: Optional[str] = Field(None, alias="created")
70
91
  modificationDate: Optional[str] = Field(None, alias="updated")
71
92
  platformSerialIdentifier: Optional[str] = Field(None, alias="platform")
72
93
  instrument: Optional[str] = Field(None, alias="instruments")
73
94
  platform: Optional[str] = Field(None, alias="constellation")
74
- resolution: Optional[int] = Field(None, alias="gsd")
95
+ resolution: Optional[Union[int, str]] = Field(None, alias="gsd")
75
96
  cloudCover: Optional[int] = Field(None, alias="eo:cloud_cover")
76
97
  snowCover: Optional[int] = Field(None, alias="eo:snow_cover")
77
98
  processingLevel: Optional[str] = Field(None, alias="processing:level")
@@ -83,40 +104,198 @@ class EODAGSearch(BaseModel):
83
104
  polarizationChannels: Optional[List[str]] = Field(None, alias="sar:polarizations")
84
105
  dopplerFrequency: Optional[str] = Field(None, alias="sar:frequency_band")
85
106
  doi: Optional[str] = Field(None, alias="sci:doi")
86
- productVersion: Optional[str] = Field(None, alias="version")
87
107
  illuminationElevationAngle: Optional[float] = Field(
88
108
  None, alias="view:sun_elevation"
89
109
  )
90
110
  illuminationAzimuthAngle: Optional[float] = Field(None, alias="view:sun_azimuth")
91
111
  page: Optional[int] = Field(1)
92
112
  items_per_page: int = Field(DEFAULT_ITEMS_PER_PAGE, alias="limit")
93
- sortBy: Optional[List[Tuple[str, str]]] = Field(None, alias="sortby")
113
+ sort_by: Optional[List[Tuple[str, str]]] = Field(None, alias="sortby")
114
+ raise_errors: bool = False
94
115
 
95
- @model_validator(mode="before")
96
- @classmethod
97
- def remove_custom_extensions(cls, values: Dict[str, Any]) -> Dict[str, Any]:
116
+ _to_eodag_map: Dict[str, str]
117
+
118
+ @model_validator(mode="after")
119
+ def remove_timeFromAscendingNode(self) -> Self: # pylint: disable=invalid-name
120
+ """TimeFromAscendingNode are just used for translation and not for search"""
121
+ self.startTimeFromAscendingNode = None # pylint: disable=invalid-name
122
+ self.completionTimeFromAscendingNode = None # pylint: disable=invalid-name
123
+ return self
124
+
125
+ @model_validator(mode="after")
126
+ def parse_extra_fields(self) -> Self:
98
127
  """process unknown and oseo EODAG custom extensions fields"""
99
128
  # Transform EODAG custom extensions OSEO and UNK.
129
+ if not self.__pydantic_extra__:
130
+ return self
131
+
100
132
  keys_to_update: Dict[str, str] = {}
101
- for key in values.keys():
133
+ for key in self.__pydantic_extra__.keys():
102
134
  if key.startswith("unk:"):
103
135
  keys_to_update[key] = key[len("unk:") :]
104
136
  elif key.startswith("oseo:"):
105
137
  keys_to_update[key] = key[len("oseo:") :]
106
138
 
107
139
  for old_key, new_key in keys_to_update.items():
108
- values[cls.snake_to_camel(new_key)] = values.pop(old_key)
140
+ self.__pydantic_extra__[
141
+ to_camel(to_snake(new_key))
142
+ ] = self.__pydantic_extra__.pop(old_key)
109
143
 
110
- return values
144
+ return self
111
145
 
112
146
  @model_validator(mode="before")
113
147
  @classmethod
114
148
  def remove_keys(cls, values: Dict[str, Any]) -> Dict[str, Any]:
115
149
  """Remove 'datetime', 'crunch', 'intersects', and 'bbox' keys"""
116
- for key in ["datetime", "crunch", "intersects", "bbox"]:
150
+ for key in ["datetime", "crunch", "intersects", "bbox", "filter_lang"]:
117
151
  values.pop(key, None)
118
152
  return values
119
153
 
154
+ @model_validator(mode="before")
155
+ @classmethod
156
+ def parse_collections(
157
+ cls, values: Dict[str, Any], info: ValidationInfo
158
+ ) -> Dict[str, Any]:
159
+ """convert collections to productType"""
160
+
161
+ if collections := values.pop("collections", None):
162
+ if len(collections) > 1:
163
+ raise ValueError("Only one collection is supported per search")
164
+ values["productType"] = collections[0]
165
+ else:
166
+ if not getattr(info, "context", None) or not info.context.get( # type: ignore
167
+ "isCatalog"
168
+ ):
169
+ raise ValueError("A collection is required")
170
+
171
+ return values
172
+
173
+ @model_validator(mode="before")
174
+ @classmethod
175
+ def parse_query(cls, values: Dict[str, Any]) -> Dict[str, Any]:
176
+ """
177
+ Convert a STAC query parameter filter with the "eq" operator to a dict.
178
+ """
179
+
180
+ def add_error(error_message: str, input: Any) -> None:
181
+ errors.append(
182
+ InitErrorDetails(
183
+ type=PydanticCustomError("invalid_query", error_message), # type: ignore
184
+ loc=("query",),
185
+ input=input,
186
+ )
187
+ )
188
+
189
+ query = values.pop("query", None)
190
+ if not query:
191
+ return values
192
+
193
+ query_props: Dict[str, Any] = {}
194
+ errors: List[InitErrorDetails] = []
195
+ for property_name, conditions in cast(Dict[str, Any], query).items():
196
+ # Remove the prefix "properties." if present
197
+ prop = property_name.replace("properties.", "", 1)
198
+
199
+ # Check if exactly one operator is specified per property
200
+ if not is_dict_str_any(conditions) or len(conditions) != 1: # type: ignore
201
+ add_error(
202
+ "Exactly 1 operator must be specified per property",
203
+ query[property_name],
204
+ )
205
+ continue
206
+
207
+ # Retrieve the operator and its value
208
+ operator, value = next(iter(cast(Dict[str, Any], conditions).items()))
209
+
210
+ # Validate the operator
211
+ # only eq, in and lte are allowed
212
+ # lte is only supported with eo:cloud_cover
213
+ # eo:cloud_cover only accept lte operator
214
+ if (
215
+ operator not in ("eq", "lte", "in")
216
+ or (operator == "lte" and prop != "eo:cloud_cover")
217
+ or (prop == "eo:cloud_cover" and operator != "lte")
218
+ ):
219
+ add_error(
220
+ f'operator "{operator}" is not supported for property "{prop}"',
221
+ query[property_name],
222
+ )
223
+ continue
224
+ if operator == "in" and not isinstance(value, list):
225
+ add_error(
226
+ f'operator "{operator}" requires a value of type list for property "{prop}"',
227
+ query[property_name],
228
+ )
229
+ continue
230
+
231
+ query_props[prop] = value
232
+
233
+ if errors:
234
+ raise ValidationError.from_exception_data(
235
+ title=cls.__name__, line_errors=errors
236
+ )
237
+
238
+ return {**values, **query_props}
239
+
240
+ @model_validator(mode="before")
241
+ @classmethod
242
+ def parse_cql(cls, values: Dict[str, Any]) -> Dict[str, Any]:
243
+ """
244
+ Process cql2 filter
245
+ """
246
+
247
+ def add_error(error_message: str) -> None:
248
+ errors.append(
249
+ InitErrorDetails(
250
+ type=PydanticCustomError("invalid_filter", error_message), # type: ignore
251
+ loc=("filter",),
252
+ )
253
+ )
254
+
255
+ filter_ = values.pop("filter", None)
256
+ if not filter_:
257
+ return values
258
+
259
+ errors: List[InitErrorDetails] = []
260
+ try:
261
+ parsing_result = EodagEvaluator().evaluate(parse_json(filter_)) # type: ignore
262
+ except (ValueError, NotImplementedError) as e:
263
+ add_error(str(e))
264
+ raise ValidationError.from_exception_data(
265
+ title=cls.__name__, line_errors=errors
266
+ ) from e
267
+
268
+ if not is_dict_str_any(parsing_result):
269
+ add_error("The parsed filter is not a proper dictionary")
270
+ raise ValidationError.from_exception_data(
271
+ title=cls.__name__, line_errors=errors
272
+ )
273
+
274
+ cql_args: Dict[str, Any] = cast(Dict[str, Any], parsing_result)
275
+
276
+ invalid_keys = {
277
+ "collections": 'Use "collection" instead of "collections"',
278
+ "ids": 'Use "id" instead of "ids"',
279
+ }
280
+ for k, m in invalid_keys.items():
281
+ if k in cql_args:
282
+ add_error(m)
283
+
284
+ if errors:
285
+ raise ValidationError.from_exception_data(
286
+ title=cls.__name__, line_errors=errors
287
+ )
288
+
289
+ # convert collection to EODAG collections
290
+ if col := cql_args.pop("collection", None):
291
+ cql_args["collections"] = col if isinstance(col, list) else [col]
292
+
293
+ # convert id to EODAG ids
294
+ if id := cql_args.pop("id", None):
295
+ cql_args["ids"] = id if isinstance(id, list) else [id]
296
+
297
+ return {**values, **cql_args}
298
+
120
299
  @field_validator("instrument", mode="before")
121
300
  @classmethod
122
301
  def join_instruments(cls, v: Union[str, List[str]]) -> str:
@@ -125,35 +304,29 @@ class EODAGSearch(BaseModel):
125
304
  return ",".join(v)
126
305
  return v
127
306
 
128
- @field_validator("sortBy", mode="before")
307
+ @field_validator("sort_by", mode="before")
129
308
  @classmethod
130
- def convert_stac_to_eodag_sortby(
309
+ def parse_sortby(
131
310
  cls,
132
311
  sortby_post_params: List[Dict[str, str]],
133
312
  ) -> List[Tuple[str, str]]:
134
313
  """
135
- Convert STAC POST sortby to EODAG sortby
314
+ Convert STAC POST sortby to EODAG sort_by
136
315
  """
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
316
+ special_fields = {
317
+ "start": "startTimeFromAscendingNode",
318
+ "end": "completionTimeFromAscendingNode",
319
+ }
320
+ return [
321
+ (
322
+ special_fields.get(
323
+ to_camel(to_snake(cls.to_eodag(param["field"]))),
324
+ to_camel(to_snake(cls.to_eodag(param["field"]))),
325
+ ),
326
+ param["direction"],
327
+ )
328
+ for param in sortby_post_params
329
+ ]
157
330
 
158
331
  @field_validator("start", "end")
159
332
  @classmethod
@@ -164,26 +337,44 @@ class EODAGSearch(BaseModel):
164
337
  return v
165
338
 
166
339
  @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:])
340
+ def _create_to_eodag_map(cls) -> None:
341
+ """Create mapping to convert fields from STAC to EODAG"""
342
+ cls._to_eodag_map = {}
343
+ for name, field_info in cls.model_fields.items():
344
+ if field_info.validation_alias:
345
+ if isinstance(field_info.validation_alias, (AliasChoices, AliasPath)):
346
+ for a in list_to_str_list(
347
+ flatten_list(field_info.validation_alias.convert_to_aliases())
348
+ ):
349
+ cls._to_eodag_map[a] = name
350
+ else:
351
+ cls._to_eodag_map[field_info.validation_alias] = name
352
+ elif field_info.alias:
353
+ cls._to_eodag_map[field_info.alias] = name
172
354
 
173
355
  @classmethod
174
356
  def to_eodag(cls, value: str) -> str:
175
357
  """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)
358
+ if not isinstance(cls._to_eodag_map, dict) or not cls._to_eodag_map:
359
+ cls._create_to_eodag_map()
360
+ return cls._to_eodag_map.get(value, value)
182
361
 
183
362
  @classmethod
184
- def to_stac(cls, field_name: str) -> str:
363
+ def to_stac(
364
+ cls,
365
+ field_name: str,
366
+ stac_item_properties: Optional[List[str]] = None,
367
+ provider: Optional[str] = None,
368
+ ) -> str:
185
369
  """Get the alias of a field in a Pydantic model"""
186
370
  field = cls.model_fields.get(field_name)
187
371
  if field is not None and field.alias is not None:
188
372
  return field.alias
373
+ if (
374
+ provider
375
+ and ":" not in field_name
376
+ and stac_item_properties
377
+ and field_name not in stac_item_properties
378
+ ):
379
+ return f"{provider}:{field_name}"
189
380
  return field_name
@@ -0,0 +1,164 @@
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 __future__ import annotations
19
+
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
34
+ from eodag.types import python_field_definition_to_json
35
+ from eodag.utils import Annotated
36
+
37
+ if TYPE_CHECKING:
38
+ from pydantic.fields import FieldInfo
39
+
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
+ # use [prop-decorator] mypy error code when mypy==1.12 is released
55
+ @computed_field # type: ignore[misc]
56
+ @property
57
+ def start_datetime(self) -> Optional[str]:
58
+ """Extract start_datetime property from datetime"""
59
+ start = str_to_interval(self.datetime)[0]
60
+ return start.strftime("%Y-%m-%dT%H:%M:%SZ") if start else None
61
+
62
+ # use [prop-decorator] mypy error code when mypy==1.12 is released
63
+ @computed_field # type: ignore[misc]
64
+ @property
65
+ def end_datetime(self) -> Optional[str]:
66
+ """Extract end_datetime property from datetime"""
67
+ end = str_to_interval(self.datetime)[1]
68
+ return end.strftime("%Y-%m-%dT%H:%M:%SZ") if end else None
69
+
70
+
71
+ class StacQueryableProperty(BaseModel):
72
+ """A class representing a queryable property.
73
+
74
+ :param description: The description of the queryables property
75
+ :param ref: (optional) A reference link to the schema of the property.
76
+ :param type: (optional) possible types of the property
77
+ """
78
+
79
+ description: str
80
+ ref: Optional[str] = Field(default=None, serialization_alias="$ref")
81
+ type: Optional[Union[str, List[str]]] = None
82
+ enum: Optional[List[Any]] = None
83
+ value: Optional[Any] = None
84
+ min: Optional[Union[int, List[Union[int, None]]]] = None
85
+ max: Optional[Union[int, List[Union[int, None]]]] = None
86
+ oneOf: Optional[List[Any]] = None
87
+ items: Optional[Any] = None
88
+
89
+ @classmethod
90
+ def from_python_field_definition(
91
+ cls, id: str, python_field_definition: Annotated[Any, FieldInfo]
92
+ ) -> StacQueryableProperty:
93
+ """Build Model from python_field_definition"""
94
+ def_dict = python_field_definition_to_json(python_field_definition)
95
+
96
+ if not def_dict.get("description", None):
97
+ def_dict["description"] = def_dict.get("title", None) or id
98
+
99
+ return cls(**def_dict)
100
+
101
+ @model_serializer(mode="wrap")
102
+ def remove_none(
103
+ self,
104
+ handler: SerializerFunctionWrapHandler,
105
+ _: SerializationInfo,
106
+ ):
107
+ """Remove none value property fields during serialization"""
108
+ props: Dict[str, Any] = handler(self)
109
+ return {k: v for k, v in props.items() if v is not None}
110
+
111
+
112
+ class StacQueryables(BaseModel):
113
+ """A class representing queryable properties for the STAC API.
114
+
115
+ :param json_schema: The URL of the JSON schema.
116
+ :param q_id: (optional) The identifier of the queryables.
117
+ :param q_type: The type of the object.
118
+ :param title: The title of the queryables.
119
+ :param description: The description of the queryables
120
+ :param properties: A dictionary of queryable properties.
121
+ :param additional_properties: Whether additional properties are allowed.
122
+ """
123
+
124
+ json_schema: str = Field(
125
+ default="https://json-schema.org/draft/2019-09/schema",
126
+ serialization_alias="$schema",
127
+ )
128
+ q_id: Optional[str] = Field(default=None, serialization_alias="$id")
129
+ q_type: str = Field(default="object", serialization_alias="type")
130
+ title: str = Field(default="Queryables for EODAG STAC API")
131
+ description: str = Field(
132
+ default="Queryable names for the EODAG STAC API Item Search filter."
133
+ )
134
+ default_properties: ClassVar[Dict[str, StacQueryableProperty]] = {
135
+ "id": StacQueryableProperty(
136
+ description="ID",
137
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
138
+ ),
139
+ "collection": StacQueryableProperty(
140
+ description="Collection",
141
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
142
+ ),
143
+ "geometry": StacQueryableProperty(
144
+ description="Geometry",
145
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
146
+ ),
147
+ "datetime": StacQueryableProperty(
148
+ description="Datetime - use parameters year, month, day, time instead if available",
149
+ ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
150
+ ),
151
+ "bbox": StacQueryableProperty(
152
+ description="BBox",
153
+ type="array",
154
+ oneOf=[{"minItems": 4, "maxItems": 4}, {"minItems": 6, "maxItems": 6}],
155
+ items={"type": "number"},
156
+ ),
157
+ }
158
+ properties: Dict[str, StacQueryableProperty] = Field()
159
+ additional_properties: bool = Field(
160
+ default=True, serialization_alias="additionalProperties"
161
+ )
162
+
163
+ def __contains__(self, name: str) -> bool:
164
+ return name in self.properties