eodag 3.0.1__py3-none-any.whl → 3.1.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 (87) hide show
  1. eodag/api/core.py +174 -138
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +117 -90
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +26 -5
  12. eodag/config.py +86 -92
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +42 -22
  15. eodag/plugins/apis/usgs.py +17 -16
  16. eodag/plugins/authentication/aws_auth.py +16 -13
  17. eodag/plugins/authentication/base.py +5 -3
  18. eodag/plugins/authentication/header.py +3 -3
  19. eodag/plugins/authentication/keycloak.py +4 -4
  20. eodag/plugins/authentication/oauth.py +7 -3
  21. eodag/plugins/authentication/openid_connect.py +22 -16
  22. eodag/plugins/authentication/sas_auth.py +4 -4
  23. eodag/plugins/authentication/token.py +41 -10
  24. eodag/plugins/authentication/token_exchange.py +1 -1
  25. eodag/plugins/base.py +4 -4
  26. eodag/plugins/crunch/base.py +4 -4
  27. eodag/plugins/crunch/filter_date.py +4 -4
  28. eodag/plugins/crunch/filter_latest_intersect.py +6 -6
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
  30. eodag/plugins/crunch/filter_overlap.py +4 -4
  31. eodag/plugins/crunch/filter_property.py +6 -7
  32. eodag/plugins/download/aws.py +146 -87
  33. eodag/plugins/download/base.py +38 -56
  34. eodag/plugins/download/creodias_s3.py +29 -0
  35. eodag/plugins/download/http.py +173 -183
  36. eodag/plugins/download/s3rest.py +10 -11
  37. eodag/plugins/manager.py +10 -20
  38. eodag/plugins/search/__init__.py +6 -5
  39. eodag/plugins/search/base.py +90 -46
  40. eodag/plugins/search/build_search_result.py +1048 -361
  41. eodag/plugins/search/cop_marine.py +22 -12
  42. eodag/plugins/search/creodias_s3.py +9 -73
  43. eodag/plugins/search/csw.py +11 -11
  44. eodag/plugins/search/data_request_search.py +19 -18
  45. eodag/plugins/search/qssearch.py +99 -258
  46. eodag/plugins/search/stac_list_assets.py +85 -0
  47. eodag/plugins/search/static_stac_search.py +4 -4
  48. eodag/resources/ext_product_types.json +1 -1
  49. eodag/resources/product_types.yml +1134 -325
  50. eodag/resources/providers.yml +906 -2006
  51. eodag/resources/stac_api.yml +2 -2
  52. eodag/resources/user_conf_template.yml +10 -9
  53. eodag/rest/cache.py +2 -2
  54. eodag/rest/config.py +3 -3
  55. eodag/rest/core.py +112 -82
  56. eodag/rest/errors.py +5 -5
  57. eodag/rest/server.py +33 -14
  58. eodag/rest/stac.py +41 -38
  59. eodag/rest/types/collections_search.py +3 -3
  60. eodag/rest/types/eodag_search.py +29 -23
  61. eodag/rest/types/queryables.py +42 -31
  62. eodag/rest/types/stac_search.py +15 -25
  63. eodag/rest/utils/__init__.py +14 -21
  64. eodag/rest/utils/cql_evaluate.py +6 -6
  65. eodag/rest/utils/rfc3339.py +2 -2
  66. eodag/types/__init__.py +141 -32
  67. eodag/types/bbox.py +2 -2
  68. eodag/types/download_args.py +3 -3
  69. eodag/types/queryables.py +183 -72
  70. eodag/types/search_args.py +4 -4
  71. eodag/types/whoosh.py +127 -3
  72. eodag/utils/__init__.py +153 -51
  73. eodag/utils/exceptions.py +28 -21
  74. eodag/utils/import_system.py +2 -2
  75. eodag/utils/repr.py +65 -6
  76. eodag/utils/requests.py +13 -13
  77. eodag/utils/rest.py +2 -2
  78. eodag/utils/s3.py +231 -0
  79. eodag/utils/stac_reader.py +10 -10
  80. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/METADATA +77 -76
  81. eodag-3.1.0.dist-info/RECORD +113 -0
  82. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  83. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +4 -2
  84. eodag/utils/constraints.py +0 -244
  85. eodag-3.0.1.dist-info/RECORD +0 -109
  86. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  87. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
eodag/rest/stac.py CHANGED
@@ -21,7 +21,7 @@ import logging
21
21
  import os
22
22
  from collections import defaultdict
23
23
  from datetime import datetime, timezone
24
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
24
+ from typing import TYPE_CHECKING, Any, Optional
25
25
  from urllib.parse import (
26
26
  parse_qs,
27
27
  quote,
@@ -82,6 +82,8 @@ COLLECTION_PROPERTIES = [
82
82
  "missionEndDate",
83
83
  "keywords",
84
84
  "stacCollection",
85
+ "alias",
86
+ "productType",
85
87
  ]
86
88
  IGNORED_ITEM_PROPERTIES = [
87
89
  "_id",
@@ -95,6 +97,7 @@ IGNORED_ITEM_PROPERTIES = [
95
97
  "qs",
96
98
  "defaultGeometry",
97
99
  "_date",
100
+ "productType",
98
101
  ]
99
102
 
100
103
 
@@ -118,7 +121,7 @@ class StacCommon:
118
121
  def __init__(
119
122
  self,
120
123
  url: str,
121
- stac_config: Dict[str, Any],
124
+ stac_config: dict[str, Any],
122
125
  provider: Optional[str],
123
126
  eodag_api: EODataAccessGateway,
124
127
  root: str = "/",
@@ -129,9 +132,9 @@ class StacCommon:
129
132
  self.eodag_api = eodag_api
130
133
  self.root = root.rstrip("/") if len(root) > 1 else root
131
134
 
132
- self.data: Dict[str, Any] = {}
135
+ self.data: dict[str, Any] = {}
133
136
 
134
- def update_data(self, data: Dict[str, Any]) -> None:
137
+ def update_data(self, data: dict[str, Any]) -> None:
135
138
  """Updates data using given input STAC dict data
136
139
 
137
140
  :param data: Catalog data (parsed STAC dict)
@@ -165,8 +168,8 @@ class StacCommon:
165
168
 
166
169
  @staticmethod
167
170
  def get_stac_extension(
168
- url: str, stac_config: Dict[str, Any], extension: str, **kwargs: Any
169
- ) -> Dict[str, str]:
171
+ url: str, stac_config: dict[str, Any], extension: str, **kwargs: Any
172
+ ) -> dict[str, str]:
170
173
  """Parse STAC extension from config and return as dict
171
174
 
172
175
  :param url: Requested URL
@@ -185,7 +188,7 @@ class StacCommon:
185
188
  }
186
189
  return format_dict_items(extension_model, **format_args)
187
190
 
188
- def get_provider_dict(self, provider: str) -> Dict[str, Any]:
191
+ def get_provider_dict(self, provider: str) -> dict[str, Any]:
189
192
  """Generate STAC provider dict"""
190
193
  provider_config = next(
191
194
  p
@@ -214,7 +217,7 @@ class StacItem(StacCommon):
214
217
  def __init__(
215
218
  self,
216
219
  url: str,
217
- stac_config: Dict[str, Any],
220
+ stac_config: dict[str, Any],
218
221
  provider: Optional[str],
219
222
  eodag_api: EODataAccessGateway,
220
223
  root: str = "/",
@@ -228,8 +231,8 @@ class StacItem(StacCommon):
228
231
  )
229
232
 
230
233
  def __get_item_list(
231
- self, search_results: SearchResult, catalog: Dict[str, Any]
232
- ) -> List[Dict[str, Any]]:
234
+ self, search_results: SearchResult, catalog: dict[str, Any]
235
+ ) -> list[dict[str, Any]]:
233
236
  """Build STAC items list from EODAG search results
234
237
 
235
238
  :param search_results: EODAG search results
@@ -244,7 +247,7 @@ class StacItem(StacCommon):
244
247
  )
245
248
 
246
249
  # check if some items need to be converted
247
- need_conversion: Dict[str, Any] = {}
250
+ need_conversion: dict[str, Any] = {}
248
251
  for k, v in item_model["properties"].items():
249
252
  if isinstance(v, str):
250
253
  conversion, item_model["properties"][k] = get_metadata_path(
@@ -264,11 +267,11 @@ class StacItem(StacCommon):
264
267
  ]
265
268
  ignored_props = COLLECTION_PROPERTIES + item_props + IGNORED_ITEM_PROPERTIES
266
269
 
267
- item_list: List[Dict[str, Any]] = []
270
+ item_list: list[dict[str, Any]] = []
268
271
  for product in search_results:
269
272
  product_dict = deepcopy(product.__dict__)
270
273
 
271
- product_item: Dict[str, Any] = jsonpath_parse_dict_items(
274
+ product_item: dict[str, Any] = jsonpath_parse_dict_items(
272
275
  item_model,
273
276
  {
274
277
  "product": product_dict,
@@ -370,10 +373,10 @@ class StacItem(StacCommon):
370
373
  product: EOProduct,
371
374
  downloadlink_href: str,
372
375
  without_arg_url: str,
373
- query_dict: Optional[Dict[str, Any]] = None,
376
+ query_dict: Optional[dict[str, Any]] = None,
374
377
  _dc_qs: Optional[str] = None,
375
- ) -> Dict[str, Any]:
376
- assets: Dict[str, Any] = {}
378
+ ) -> dict[str, Any]:
379
+ assets: dict[str, Any] = {}
377
380
  settings = Settings.from_environment()
378
381
 
379
382
  if _dc_qs:
@@ -452,9 +455,9 @@ class StacItem(StacCommon):
452
455
  self,
453
456
  search_results: SearchResult,
454
457
  total: int,
455
- catalog: Dict[str, Any],
456
- next_link: Optional[Dict[str, Any]],
457
- ) -> Dict[str, Any]:
458
+ catalog: dict[str, Any],
459
+ next_link: Optional[dict[str, Any]],
460
+ ) -> dict[str, Any]:
458
461
  """Build STAC items from EODAG search results
459
462
 
460
463
  :param search_results: EODAG search results
@@ -503,8 +506,8 @@ class StacItem(StacCommon):
503
506
  return self.data
504
507
 
505
508
  def __filter_item_model_properties(
506
- self, item_model: Dict[str, Any], product_type: str
507
- ) -> Dict[str, Any]:
509
+ self, item_model: dict[str, Any], product_type: str
510
+ ) -> dict[str, Any]:
508
511
  """Filter item model depending on product type metadata and its extensions.
509
512
  Removes not needed parameters, and adds supplementary ones as
510
513
  part of oseo extension.
@@ -568,13 +571,13 @@ class StacItem(StacCommon):
568
571
 
569
572
  return result_item_model
570
573
 
571
- def __filter_item_properties_values(self, item: Dict[str, Any]) -> Dict[str, Any]:
574
+ def __filter_item_properties_values(self, item: dict[str, Any]) -> dict[str, Any]:
572
575
  """Removes empty properties, unused extensions, and add missing extensions
573
576
 
574
577
  :param item: STAC item data
575
578
  :returns: Filtered item model
576
579
  """
577
- all_extensions_dict: Dict[str, str] = deepcopy(
580
+ all_extensions_dict: dict[str, str] = deepcopy(
578
581
  self.stac_config["stac_extensions"]
579
582
  )
580
583
  # parse f-strings with root
@@ -599,7 +602,7 @@ class StacItem(StacCommon):
599
602
 
600
603
  return item
601
604
 
602
- def get_stac_item_from_product(self, product: EOProduct) -> Dict[str, Any]:
605
+ def get_stac_item_from_product(self, product: EOProduct) -> dict[str, Any]:
603
606
  """Build STAC item from EODAG product
604
607
 
605
608
  :param product: EODAG product
@@ -659,7 +662,7 @@ class StacCollection(StacCommon):
659
662
  """
660
663
 
661
664
  # External STAC collections
662
- ext_stac_collections: Dict[str, Dict[str, Any]] = dict()
665
+ ext_stac_collections: dict[str, dict[str, Any]] = dict()
663
666
 
664
667
  @classmethod
665
668
  def fetch_external_stac_collections(cls, eodag_api: EODataAccessGateway) -> None:
@@ -688,7 +691,7 @@ class StacCollection(StacCommon):
688
691
  def __init__(
689
692
  self,
690
693
  url: str,
691
- stac_config: Dict[str, Any],
694
+ stac_config: dict[str, Any],
692
695
  provider: Optional[str],
693
696
  eodag_api: EODataAccessGateway,
694
697
  root: str = "/",
@@ -701,7 +704,7 @@ class StacCollection(StacCommon):
701
704
  root=root,
702
705
  )
703
706
 
704
- def __list_product_type_providers(self, product_type: Dict[str, Any]) -> List[str]:
707
+ def __list_product_type_providers(self, product_type: dict[str, Any]) -> list[str]:
705
708
  """Retrieve a list of providers for a given product type.
706
709
 
707
710
  :param product_type: Dictionary containing information about the product type.
@@ -718,8 +721,8 @@ class StacCollection(StacCommon):
718
721
  ]
719
722
 
720
723
  def __generate_stac_collection(
721
- self, collection_model: Any, product_type: Dict[str, Any]
722
- ) -> Dict[str, Any]:
724
+ self, collection_model: Any, product_type: dict[str, Any]
725
+ ) -> dict[str, Any]:
723
726
  """Generate a STAC collection dictionary for a given product type.
724
727
 
725
728
  :param collection_model: The base model for the STAC collection.
@@ -728,7 +731,7 @@ class StacCollection(StacCommon):
728
731
  """
729
732
  providers = self.__list_product_type_providers(product_type)
730
733
 
731
- providers_dict: Dict[str, Dict[str, Any]] = {}
734
+ providers_dict: dict[str, dict[str, Any]] = {}
732
735
  for provider in providers:
733
736
  p_dict = self.get_provider_dict(provider)
734
737
  providers_dict.setdefault(p_dict["name"], p_dict)
@@ -815,7 +818,7 @@ class StacCollection(StacCommon):
815
818
  instrument: Optional[str] = None,
816
819
  constellation: Optional[str] = None,
817
820
  datetime: Optional[str] = None,
818
- ) -> List[Dict[str, Any]]:
821
+ ) -> list[dict[str, Any]]:
819
822
  """Build STAC collections list
820
823
 
821
824
  :param filters: (optional) Additional filters for collections search
@@ -850,7 +853,7 @@ class StacCollection(StacCommon):
850
853
  product_types = all_pt
851
854
 
852
855
  # list product types with all metadata using guessed ids
853
- collection_list: List[Dict[str, Any]] = []
856
+ collection_list: list[dict[str, Any]] = []
854
857
  for product_type in product_types:
855
858
  stac_collection = self.__generate_stac_collection(
856
859
  collection_model, product_type
@@ -874,7 +877,7 @@ class StacCatalog(StacCommon):
874
877
  def __init__(
875
878
  self,
876
879
  url: str,
877
- stac_config: Dict[str, Any],
880
+ stac_config: dict[str, Any],
878
881
  provider: Optional[str],
879
882
  eodag_api: EODataAccessGateway,
880
883
  root: str = "/",
@@ -890,8 +893,8 @@ class StacCatalog(StacCommon):
890
893
  self.data = {}
891
894
 
892
895
  self.shp_location_config = eodag_api.locations_config
893
- self.search_args: Dict[str, Any] = {}
894
- self.children: List[Dict[str, Any]] = []
896
+ self.search_args: dict[str, Any] = {}
897
+ self.children: list[dict[str, Any]] = []
895
898
 
896
899
  self.catalog_config = deepcopy(stac_config["catalog"])
897
900
 
@@ -907,7 +910,7 @@ class StacCatalog(StacCommon):
907
910
  # build catalog
908
911
  self.__build_stac_catalog(collection)
909
912
 
910
- def __update_data_from_catalog_config(self, catalog_config: Dict[str, Any]) -> bool:
913
+ def __update_data_from_catalog_config(self, catalog_config: dict[str, Any]) -> bool:
911
914
  """Updates configuration and data using given input catalog config
912
915
 
913
916
  :param catalog_config: Catalog config, from yml stac_config[catalogs]
@@ -959,7 +962,7 @@ class StacCatalog(StacCommon):
959
962
 
960
963
  def set_stac_product_type_by_id(
961
964
  self, product_type: str, **_: Any
962
- ) -> Dict[str, Any]:
965
+ ) -> dict[str, Any]:
963
966
  """Updates catalog with given product_type
964
967
 
965
968
  :param product_type: Product type
@@ -991,7 +994,7 @@ class StacCatalog(StacCommon):
991
994
  format_args["catalog"] = defaultdict(str, **self.data)
992
995
  format_args["collection"] = collections[0]
993
996
  try:
994
- parsed_dict: Dict[str, Any] = format_dict_items(cat_model, **format_args)
997
+ parsed_dict: dict[str, Any] = format_dict_items(cat_model, **format_args)
995
998
  except Exception:
996
999
  logger.error("Could not format product_type catalog")
997
1000
  raise
@@ -15,7 +15,7 @@
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, Optional
18
+ from typing import Any, Optional
19
19
 
20
20
  from pydantic import (
21
21
  BaseModel,
@@ -39,6 +39,6 @@ class CollectionsSearchRequest(BaseModel):
39
39
  constellation: Optional[str] = Field(default=None)
40
40
 
41
41
  @model_serializer(mode="wrap")
42
- def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
43
- dumped: Dict[str, Any] = handler(self)
42
+ def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
43
+ dumped: dict[str, Any] = handler(self)
44
44
  return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
@@ -17,7 +17,7 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
20
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
21
21
 
22
22
  from pydantic import (
23
23
  AliasChoices,
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
52
52
  from typing_extensions import Self
53
53
 
54
54
  Geometry = Union[
55
- Dict[str, Any],
55
+ dict[str, Any],
56
56
  Point,
57
57
  MultiPoint,
58
58
  LineString,
@@ -73,8 +73,8 @@ class EODAGSearch(BaseModel):
73
73
 
74
74
  productType: Optional[str] = Field(None, alias="collections", validate_default=True)
75
75
  provider: Optional[str] = Field(None)
76
- ids: Optional[List[str]] = Field(None)
77
- id: Optional[List[str]] = Field(
76
+ ids: Optional[list[str]] = Field(None)
77
+ id: Optional[list[str]] = Field(
78
78
  None, alias="ids"
79
79
  ) # TODO: remove when updating queryables
80
80
  geom: Optional[Geometry] = Field(None, alias="geometry")
@@ -101,7 +101,7 @@ class EODAGSearch(BaseModel):
101
101
  orbitNumber: Optional[int] = Field(None, alias="sat:absolute_orbit")
102
102
  # TODO: colision in property name. Need to handle "sar:product_type"
103
103
  sensorMode: Optional[str] = Field(None, alias="sar:instrument_mode")
104
- polarizationChannels: Optional[List[str]] = Field(None, alias="sar:polarizations")
104
+ polarizationChannels: Optional[list[str]] = Field(None, alias="sar:polarizations")
105
105
  dopplerFrequency: Optional[str] = Field(None, alias="sar:frequency_band")
106
106
  doi: Optional[str] = Field(None, alias="sci:doi")
107
107
  illuminationElevationAngle: Optional[float] = Field(
@@ -110,10 +110,10 @@ class EODAGSearch(BaseModel):
110
110
  illuminationAzimuthAngle: Optional[float] = Field(None, alias="view:sun_azimuth")
111
111
  page: Optional[int] = Field(1)
112
112
  items_per_page: int = Field(DEFAULT_ITEMS_PER_PAGE, alias="limit")
113
- sort_by: Optional[List[Tuple[str, str]]] = Field(None, alias="sortby")
113
+ sort_by: Optional[list[tuple[str, str]]] = Field(None, alias="sortby")
114
114
  raise_errors: bool = False
115
115
 
116
- _to_eodag_map: Dict[str, str]
116
+ _to_eodag_map: dict[str, str]
117
117
 
118
118
  @model_validator(mode="after")
119
119
  def remove_timeFromAscendingNode(self) -> Self: # pylint: disable=invalid-name
@@ -129,7 +129,7 @@ class EODAGSearch(BaseModel):
129
129
  if not self.__pydantic_extra__:
130
130
  return self
131
131
 
132
- keys_to_update: Dict[str, str] = {}
132
+ keys_to_update: dict[str, str] = {}
133
133
  for key in self.__pydantic_extra__.keys():
134
134
  if key.startswith("unk:"):
135
135
  keys_to_update[key] = key[len("unk:") :]
@@ -145,7 +145,7 @@ class EODAGSearch(BaseModel):
145
145
 
146
146
  @model_validator(mode="before")
147
147
  @classmethod
148
- def remove_keys(cls, values: Dict[str, Any]) -> Dict[str, Any]:
148
+ def remove_keys(cls, values: dict[str, Any]) -> dict[str, Any]:
149
149
  """Remove 'datetime', 'crunch', 'intersects', and 'bbox' keys"""
150
150
  for key in ["datetime", "crunch", "intersects", "bbox", "filter_lang"]:
151
151
  values.pop(key, None)
@@ -154,8 +154,8 @@ class EODAGSearch(BaseModel):
154
154
  @model_validator(mode="before")
155
155
  @classmethod
156
156
  def parse_collections(
157
- cls, values: Dict[str, Any], info: ValidationInfo
158
- ) -> Dict[str, Any]:
157
+ cls, values: dict[str, Any], info: ValidationInfo
158
+ ) -> dict[str, Any]:
159
159
  """convert collections to productType"""
160
160
 
161
161
  if collections := values.pop("collections", None):
@@ -172,7 +172,7 @@ class EODAGSearch(BaseModel):
172
172
 
173
173
  @model_validator(mode="before")
174
174
  @classmethod
175
- def parse_query(cls, values: Dict[str, Any]) -> Dict[str, Any]:
175
+ def parse_query(cls, values: dict[str, Any]) -> dict[str, Any]:
176
176
  """
177
177
  Convert a STAC query parameter filter with the "eq" operator to a dict.
178
178
  """
@@ -190,9 +190,9 @@ class EODAGSearch(BaseModel):
190
190
  if not query:
191
191
  return values
192
192
 
193
- query_props: Dict[str, Any] = {}
194
- errors: List[InitErrorDetails] = []
195
- for property_name, conditions in cast(Dict[str, Any], query).items():
193
+ query_props: dict[str, Any] = {}
194
+ errors: list[InitErrorDetails] = []
195
+ for property_name, conditions in cast(dict[str, Any], query).items():
196
196
  # Remove the prefix "properties." if present
197
197
  prop = property_name.replace("properties.", "", 1)
198
198
 
@@ -205,7 +205,7 @@ class EODAGSearch(BaseModel):
205
205
  continue
206
206
 
207
207
  # Retrieve the operator and its value
208
- operator, value = next(iter(cast(Dict[str, Any], conditions).items()))
208
+ operator, value = next(iter(cast(dict[str, Any], conditions).items()))
209
209
 
210
210
  # Validate the operator
211
211
  # only eq, in and lte are allowed
@@ -239,7 +239,7 @@ class EODAGSearch(BaseModel):
239
239
 
240
240
  @model_validator(mode="before")
241
241
  @classmethod
242
- def parse_cql(cls, values: Dict[str, Any]) -> Dict[str, Any]:
242
+ def parse_cql(cls, values: dict[str, Any]) -> dict[str, Any]:
243
243
  """
244
244
  Process cql2 filter
245
245
  """
@@ -256,7 +256,7 @@ class EODAGSearch(BaseModel):
256
256
  if not filter_:
257
257
  return values
258
258
 
259
- errors: List[InitErrorDetails] = []
259
+ errors: list[InitErrorDetails] = []
260
260
  try:
261
261
  parsing_result = EodagEvaluator().evaluate(parse_json(filter_)) # type: ignore
262
262
  except (ValueError, NotImplementedError) as e:
@@ -271,7 +271,7 @@ class EODAGSearch(BaseModel):
271
271
  title=cls.__name__, line_errors=errors
272
272
  )
273
273
 
274
- cql_args: Dict[str, Any] = cast(Dict[str, Any], parsing_result)
274
+ cql_args: dict[str, Any] = cast(dict[str, Any], parsing_result)
275
275
 
276
276
  invalid_keys = {
277
277
  "collections": 'Use "collection" instead of "collections"',
@@ -298,7 +298,7 @@ class EODAGSearch(BaseModel):
298
298
 
299
299
  @field_validator("instrument", mode="before")
300
300
  @classmethod
301
- def join_instruments(cls, v: Union[str, List[str]]) -> str:
301
+ def join_instruments(cls, v: Union[str, list[str]]) -> str:
302
302
  """convert instruments to instrument"""
303
303
  if isinstance(v, list):
304
304
  return ",".join(v)
@@ -308,8 +308,8 @@ class EODAGSearch(BaseModel):
308
308
  @classmethod
309
309
  def parse_sortby(
310
310
  cls,
311
- sortby_post_params: List[Dict[str, str]],
312
- ) -> List[Tuple[str, str]]:
311
+ sortby_post_params: list[dict[str, str]],
312
+ ) -> list[tuple[str, str]]:
313
313
  """
314
314
  Convert STAC POST sortby to EODAG sort_by
315
315
  """
@@ -363,10 +363,16 @@ class EODAGSearch(BaseModel):
363
363
  def to_stac(
364
364
  cls,
365
365
  field_name: str,
366
- stac_item_properties: Optional[List[str]] = None,
366
+ stac_item_properties: Optional[list[str]] = None,
367
367
  provider: Optional[str] = None,
368
368
  ) -> str:
369
369
  """Get the alias of a field in a Pydantic model"""
370
+ # quick fix. TODO: refactor of EODAGSearch.
371
+ if field_name in ("productType", "id", "start_datetime", "end_datetime"):
372
+ return field_name
373
+ # another quick fix to handle different names of geometry
374
+ if field_name == "geometry":
375
+ field_name = "geom"
370
376
  field = cls.model_fields.get(field_name)
371
377
  if field is not None and field.alias is not None:
372
378
  return field.alias
@@ -17,7 +17,7 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Dict, List, Optional, Union
20
+ from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Optional, Union
21
21
 
22
22
  from pydantic import (
23
23
  BaseModel,
@@ -25,8 +25,9 @@ from pydantic import (
25
25
  Field,
26
26
  SerializationInfo,
27
27
  SerializerFunctionWrapHandler,
28
- computed_field,
28
+ field_validator,
29
29
  model_serializer,
30
+ model_validator,
30
31
  )
31
32
 
32
33
  from eodag.rest.types.eodag_search import EODAGSearch
@@ -35,6 +36,7 @@ from eodag.types import python_field_definition_to_json
35
36
 
36
37
  if TYPE_CHECKING:
37
38
  from pydantic.fields import FieldInfo
39
+ from typing_extensions import Self
38
40
 
39
41
 
40
42
  class QueryablesGetParams(BaseModel):
@@ -42,29 +44,39 @@ class QueryablesGetParams(BaseModel):
42
44
 
43
45
  collection: Optional[str] = Field(default=None, serialization_alias="productType")
44
46
  datetime: Optional[str] = Field(default=None)
47
+ start_datetime: Optional[str] = Field(default=None)
48
+ end_datetime: Optional[str] = Field(default=None)
45
49
 
46
50
  model_config = ConfigDict(extra="allow", frozen=True)
47
51
 
48
52
  @model_serializer(mode="wrap")
49
- def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
50
- dumped: Dict[str, Any] = handler(self)
53
+ def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
54
+ dumped: dict[str, Any] = handler(self)
51
55
  return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
52
56
 
53
- # use [prop-decorator] mypy error code when mypy==1.12 is released
54
- @computed_field # type: ignore[misc]
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
57
+ @field_validator("datetime", "start_datetime", "end_datetime", mode="before")
58
+ def validate_datetime(cls, value: Any) -> Optional[str]:
59
+ """datetime, start_datetime and end_datetime must be a string"""
60
+ if isinstance(value, list):
61
+ return value[0]
60
62
 
61
- # use [prop-decorator] mypy error code when mypy==1.12 is released
62
- @computed_field # type: ignore[misc]
63
- @property
64
- def end_datetime(self) -> Optional[str]:
65
- """Extract end_datetime property from datetime"""
66
- end = str_to_interval(self.datetime)[1]
67
- return end.strftime("%Y-%m-%dT%H:%M:%SZ") if end else None
63
+ return value
64
+
65
+ @model_validator(mode="after")
66
+ def compute_datetimes(self: Self) -> Self:
67
+ """Start datetime must be a string"""
68
+ if not self.datetime:
69
+ return self
70
+
71
+ start, end = str_to_interval(self.datetime)
72
+
73
+ if not self.start_datetime and start:
74
+ self.start_datetime = start.strftime("%Y-%m-%dT%H:%M:%SZ")
75
+
76
+ if not self.end_datetime and end:
77
+ self.end_datetime = end.strftime("%Y-%m-%dT%H:%M:%SZ")
78
+
79
+ return self
68
80
 
69
81
 
70
82
  class StacQueryableProperty(BaseModel):
@@ -77,12 +89,12 @@ class StacQueryableProperty(BaseModel):
77
89
 
78
90
  description: str
79
91
  ref: Optional[str] = Field(default=None, serialization_alias="$ref")
80
- type: Optional[Union[str, List[str]]] = None
81
- enum: Optional[List[Any]] = None
92
+ type: Optional[Union[str, list[str]]] = None
93
+ enum: Optional[list[Any]] = None
82
94
  value: Optional[Any] = None
83
- min: Optional[Union[int, List[Union[int, None]]]] = None
84
- max: Optional[Union[int, List[Union[int, None]]]] = None
85
- oneOf: Optional[List[Any]] = None
95
+ min: Optional[Union[int, list[Union[int, None]]]] = None
96
+ max: Optional[Union[int, list[Union[int, None]]]] = None
97
+ oneOf: Optional[list[Any]] = None
86
98
  items: Optional[Any] = None
87
99
 
88
100
  @classmethod
@@ -104,7 +116,7 @@ class StacQueryableProperty(BaseModel):
104
116
  _: SerializationInfo,
105
117
  ):
106
118
  """Remove none value property fields during serialization"""
107
- props: Dict[str, Any] = handler(self)
119
+ props: dict[str, Any] = handler(self)
108
120
  return {k: v for k, v in props.items() if v is not None}
109
121
 
110
122
 
@@ -130,15 +142,13 @@ class StacQueryables(BaseModel):
130
142
  description: str = Field(
131
143
  default="Queryable names for the EODAG STAC API Item Search filter."
132
144
  )
133
- default_properties: ClassVar[Dict[str, StacQueryableProperty]] = {
134
- "id": StacQueryableProperty(
135
- description="ID",
136
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
137
- ),
145
+ default_properties: ClassVar[dict[str, StacQueryableProperty]] = {
138
146
  "collection": StacQueryableProperty(
139
147
  description="Collection",
140
148
  ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
141
- ),
149
+ )
150
+ }
151
+ possible_properties: ClassVar[dict[str, StacQueryableProperty]] = {
142
152
  "geometry": StacQueryableProperty(
143
153
  description="Geometry",
144
154
  ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
@@ -154,7 +164,8 @@ class StacQueryables(BaseModel):
154
164
  items={"type": "number"},
155
165
  ),
156
166
  }
157
- properties: Dict[str, StacQueryableProperty] = Field()
167
+ properties: dict[str, Any] = Field()
168
+ required: Optional[list[str]] = Field(None)
158
169
  additional_properties: bool = Field(
159
170
  default=True, serialization_alias="additionalProperties"
160
171
  )