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
@@ -19,19 +19,15 @@ from __future__ import annotations
19
19
 
20
20
  import logging
21
21
  import re
22
+ import socket
22
23
  from copy import copy as copy_copy
23
- from datetime import datetime
24
24
  from typing import (
25
25
  TYPE_CHECKING,
26
26
  Annotated,
27
27
  Any,
28
28
  Callable,
29
- Dict,
30
- List,
31
29
  Optional,
32
30
  Sequence,
33
- Set,
34
- Tuple,
35
31
  TypedDict,
36
32
  cast,
37
33
  get_args,
@@ -52,7 +48,6 @@ import geojson
52
48
  import orjson
53
49
  import requests
54
50
  import yaml
55
- from dateutil.utils import today
56
51
  from jsonpath_ng import JSONPath
57
52
  from lxml import etree
58
53
  from pydantic import create_model
@@ -75,9 +70,9 @@ from eodag.api.search_result import RawSearchResult
75
70
  from eodag.plugins.search import PreparedSearch
76
71
  from eodag.plugins.search.base import Search
77
72
  from eodag.types import json_field_definition_to_python, model_fields_to_annotated
78
- from eodag.types.queryables import CommonQueryables
79
73
  from eodag.types.search_args import SortByList
80
74
  from eodag.utils import (
75
+ DEFAULT_SEARCH_TIMEOUT,
81
76
  GENERIC_PRODUCT_TYPE,
82
77
  HTTP_REQ_TIMEOUT,
83
78
  REQ_RETRY_BACKOFF_FACTOR,
@@ -94,10 +89,6 @@ from eodag.utils import (
94
89
  update_nested_dict,
95
90
  urlencode,
96
91
  )
97
- from eodag.utils.constraints import (
98
- fetch_constraints,
99
- get_constraint_queryables_with_additional_params,
100
- )
101
92
  from eodag.utils.exceptions import (
102
93
  AuthenticationError,
103
94
  MisconfiguredError,
@@ -132,7 +123,7 @@ class QueryStringSearch(Search):
132
123
  authentication error; only used if ``need_auth=true``
133
124
  * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be verified in
134
125
  requests; default: ``True``
135
- * :attr:`~eodag.config.PluginConfig.dont_quote` (``List[str]``): characters that should not be quoted in the
126
+ * :attr:`~eodag.config.PluginConfig.dont_quote` (``list[str]``): characters that should not be quoted in the
136
127
  url params
137
128
  * :attr:`~eodag.config.PluginConfig.timeout` (``int``): time to wait until request timeout in seconds;
138
129
  default: ``5``
@@ -140,10 +131,10 @@ class QueryStringSearch(Search):
140
131
  total number of retries to allow; default: ``3``
141
132
  * :attr:`~eodag.config.PluginConfig.retry_backoff_factor` (``int``): :class:`urllib3.util.Retry`
142
133
  ``backoff_factor`` parameter, backoff factor to apply between attempts after the second try; default: ``2``
143
- * :attr:`~eodag.config.PluginConfig.retry_status_forcelist` (``List[int]``): :class:`urllib3.util.Retry`
134
+ * :attr:`~eodag.config.PluginConfig.retry_status_forcelist` (``list[int]``): :class:`urllib3.util.Retry`
144
135
  ``status_forcelist`` parameter, list of integer HTTP status codes that we should force a retry on; default:
145
136
  ``[401, 429, 500, 502, 503, 504]``
146
- * :attr:`~eodag.config.PluginConfig.literal_search_params` (``Dict[str, str]``): A mapping of (search_param =>
137
+ * :attr:`~eodag.config.PluginConfig.literal_search_params` (``dict[str, str]``): A mapping of (search_param =>
147
138
  search_value) pairs giving search parameters to be passed as is in the search url query string. This is useful
148
139
  for example in situations where the user wants to add a fixed search query parameter exactly
149
140
  as it is done on the provider interface.
@@ -187,10 +178,13 @@ class QueryStringSearch(Search):
187
178
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.generic_product_type_id` (``str``): mapping for the
188
179
  product type id
189
180
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.generic_product_type_parsable_metadata`
190
- (``Dict[str, str]``): mapping for product type metadata (e.g. ``abstract``, ``licence``) which can be parsed
181
+ (``dict[str, str]``): mapping for product type metadata (e.g. ``abstract``, ``licence``) which can be parsed
191
182
  from the provider result
192
183
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.generic_product_type_parsable_properties`
193
- (``Dict[str, str]``): mapping for product type properties which can be parsed from the result that are not
184
+ (``dict[str, str]``): mapping for product type properties which can be parsed from the result and are not
185
+ product type metadata
186
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.generic_product_type_unparsable_properties`
187
+ (``dict[str, str]``): mapping for product type properties which cannot be parsed from the result and are not
194
188
  product type metadata
195
189
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_collection_fetch_url` (``str``): url to fetch
196
190
  data for a single collection; used if product type metadata is not available from the endpoint given in
@@ -199,13 +193,13 @@ class QueryStringSearch(Search):
199
193
  to be added to the :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.fetch_url` to filter for a
200
194
  collection
201
195
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_product_type_parsable_metadata`
202
- (``Dict[str, str]``): mapping for product type metadata returned by the endpoint given in
196
+ (``dict[str, str]``): mapping for product type metadata returned by the endpoint given in
203
197
  :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_collection_fetch_url`.
204
198
 
205
199
  * :attr:`~eodag.config.PluginConfig.sort` (:class:`~eodag.config.PluginConfig.Sort`): configuration for sorting
206
200
  the results. It contains the keys:
207
201
 
208
- * :attr:`~eodag.config.PluginConfig.Sort.sort_by_default` (``List[Tuple(str, Literal["ASC", "DESC"])]``):
202
+ * :attr:`~eodag.config.PluginConfig.Sort.sort_by_default` (``list[Tuple(str, Literal["ASC", "DESC"])]``):
209
203
  parameter and sort order by which the result will be sorted by default (if the user does not enter a
210
204
  ``sort_by`` parameter); if not given the result will use the default sorting of the provider; Attention:
211
205
  for some providers sorting might cause a timeout if no filters are used. In that case no default
@@ -221,12 +215,12 @@ class QueryStringSearch(Search):
221
215
  * :attr:`~eodag.config.PluginConfig.Sort.sort_param_mapping` (``Dict [str, str]``): mapping for the parameters
222
216
  available for sorting
223
217
  * :attr:`~eodag.config.PluginConfig.Sort.sort_order_mapping`
224
- (``Dict[Literal["ascending", "descending"], str]``): mapping for the sort order
218
+ (``dict[Literal["ascending", "descending"], str]``): mapping for the sort order
225
219
  * :attr:`~eodag.config.PluginConfig.Sort.max_sort_params` (``int``): maximum number of sort parameters
226
220
  supported by the provider; used to validate the user input to avoid failed requests or unexpected behaviour
227
221
  (not all parameters are used in the request)
228
222
 
229
- * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``Dict[str, Any]``): The search plugins of this kind can
223
+ * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``dict[str, Any]``): The search plugins of this kind can
230
224
  detect when a metadata mapping is "query-able", and get the semantics of how to format the query string
231
225
  parameter that enables to make a query on the corresponding metadata. To make a metadata query-able,
232
226
  just configure it in the metadata mapping to be a list of 2 items, the first one being the
@@ -259,7 +253,7 @@ class QueryStringSearch(Search):
259
253
  metadata is activated; default: ``False``; if false, the other parameters are not used;
260
254
  * :attr:`~eodag.config.PluginConfig.DiscoverMetadata.metadata_pattern` (``str``): regex string a parameter in
261
255
  the result should match so that is used
262
- * :attr:`~eodag.config.PluginConfig.DiscoverMetadata.search_param` (``Union [str, Dict[str, Any]]``): format
256
+ * :attr:`~eodag.config.PluginConfig.DiscoverMetadata.search_param` (``Union [str, dict[str, Any]]``): format
263
257
  to add a query param given by the user and not in the metadata mapping to the requests, 'metadata' will be
264
258
  replaced by the search param; can be a string or a dict containing
265
259
  :attr:`~eodag.config.PluginConfig.free_text_search_operations`
@@ -282,16 +276,12 @@ class QueryStringSearch(Search):
282
276
 
283
277
  * :attr:`~eodag.config.PluginConfig.constraints_file_url` (``str``): url to fetch the constraints for a specific
284
278
  product type, can be an http url or a path to a file; the constraints are used to build queryables
285
- * :attr:`~eodag.config.PluginConfig.constraints_file_dataset_key` (``str``): key which is used in the eodag
286
- configuration to map the eodag product type to the provider product type; default: ``dataset``
287
279
  * :attr:`~eodag.config.PluginConfig.constraints_entry` (``str``): key in the json result where the constraints
288
280
  can be found; if not given, it is assumed that the constraints are on top level of the result, i.e.
289
281
  the result is an array of constraints
290
- * :attr:`~eodag.config.PluginConfig.stop_without_constraints_entry_key` (``bool``): if true only a provider
291
- result containing `constraints_entry` is accepted as valid and used to create constraints; default: ``False``
292
282
  """
293
283
 
294
- extract_properties: Dict[str, Callable[..., Dict[str, Any]]] = {
284
+ extract_properties: dict[str, Callable[..., dict[str, Any]]] = {
295
285
  "xml": properties_from_xml,
296
286
  "json": properties_from_json,
297
287
  }
@@ -302,8 +292,8 @@ class QueryStringSearch(Search):
302
292
  self.config.__dict__.setdefault("results_entry", "features")
303
293
  self.config.__dict__.setdefault("pagination", {})
304
294
  self.config.__dict__.setdefault("free_text_search_operations", {})
305
- self.search_urls: List[str] = []
306
- self.query_params: Dict[str, str] = dict()
295
+ self.search_urls: list[str] = []
296
+ self.query_params: dict[str, str] = dict()
307
297
  self.query_string = ""
308
298
  self.next_page_url = None
309
299
  self.next_page_query_obj = None
@@ -448,7 +438,7 @@ class QueryStringSearch(Search):
448
438
  self.next_page_query_obj = None
449
439
  self.next_page_merge = None
450
440
 
451
- def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
441
+ def discover_product_types(self, **kwargs: Any) -> Optional[dict[str, Any]]:
452
442
  """Fetch product types list from provider using `discover_product_types` conf
453
443
 
454
444
  :returns: configuration dict containing fetched product types information
@@ -465,7 +455,7 @@ class QueryStringSearch(Search):
465
455
  # no pagination
466
456
  return self.discover_product_types_per_page(**kwargs)
467
457
 
468
- conf_update_dict: Dict[str, Any] = {
458
+ conf_update_dict: dict[str, Any] = {
469
459
  "providers_config": {},
470
460
  "product_types_config": {},
471
461
  }
@@ -498,7 +488,7 @@ class QueryStringSearch(Search):
498
488
 
499
489
  def discover_product_types_per_page(
500
490
  self, **kwargs: Any
501
- ) -> Optional[Dict[str, Any]]:
491
+ ) -> Optional[dict[str, Any]]:
502
492
  """Fetch product types list from provider using `discover_product_types` conf
503
493
  using paginated ``kwargs["fetch_url"]``
504
494
 
@@ -541,7 +531,7 @@ class QueryStringSearch(Search):
541
531
 
542
532
  prep.info_message = "Fetching product types: {}".format(prep.url)
543
533
  prep.exception_message = (
544
- "Skipping error while fetching product types for " "{} {} instance:"
534
+ "Skipping error while fetching product types for {} {} instance:"
545
535
  ).format(self.provider, self.__class__.__name__)
546
536
 
547
537
  # Query using appropriate method
@@ -556,7 +546,7 @@ class QueryStringSearch(Search):
556
546
  return None
557
547
  else:
558
548
  try:
559
- conf_update_dict: Dict[str, Any] = {
549
+ conf_update_dict: dict[str, Any] = {
560
550
  "providers_config": {},
561
551
  "product_types_config": {},
562
552
  }
@@ -575,7 +565,7 @@ class QueryStringSearch(Search):
575
565
  result = result[0]
576
566
 
577
567
  def conf_update_from_product_type_result(
578
- product_type_result: Dict[str, Any]
568
+ product_type_result: dict[str, Any],
579
569
  ) -> None:
580
570
  """Update ``conf_update_dict`` using given product type json response"""
581
571
  # providers_config extraction
@@ -641,7 +631,11 @@ class QueryStringSearch(Search):
641
631
  ][kf]
642
632
  )
643
633
  for kf in keywords_fields
644
- if conf_update_dict["product_types_config"][
634
+ if kf
635
+ in conf_update_dict["product_types_config"][
636
+ generic_product_type_id
637
+ ]
638
+ and conf_update_dict["product_types_config"][
645
639
  generic_product_type_id
646
640
  ][kf]
647
641
  != NOT_AVAILABLE
@@ -699,7 +693,7 @@ class QueryStringSearch(Search):
699
693
 
700
694
  def _get_product_type_metadata_from_single_collection_endpoint(
701
695
  self, product_type: str
702
- ) -> Dict[str, Any]:
696
+ ) -> dict[str, Any]:
703
697
  """
704
698
  retrieves additional product type information from an endpoint returning data for a single collection
705
699
  :param product_type: product type
@@ -723,107 +717,11 @@ class QueryStringSearch(Search):
723
717
  self.config.discover_product_types["single_product_type_parsable_metadata"],
724
718
  )
725
719
 
726
- def discover_queryables(
727
- self, **kwargs: Any
728
- ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
729
- """Fetch queryables list from provider using its constraints file
730
-
731
- :param kwargs: additional filters for queryables (`productType` and other search
732
- arguments)
733
- :returns: fetched queryable parameters dict
734
- """
735
- product_type = kwargs.pop("productType", None)
736
- if not product_type:
737
- return {}
738
- constraints_file_url = getattr(self.config, "constraints_file_url", "")
739
- if not constraints_file_url:
740
- return {}
741
-
742
- constraints_file_dataset_key = getattr(
743
- self.config, "constraints_file_dataset_key", "dataset"
744
- )
745
- provider_product_type = self.config.products.get(product_type, {}).get(
746
- constraints_file_dataset_key, None
747
- )
748
-
749
- # defaults
750
- default_queryables = self._get_defaults_as_queryables(product_type)
751
- # remove unwanted queryables
752
- for param in getattr(self.config, "remove_from_queryables", []):
753
- default_queryables.pop(param, None)
754
-
755
- non_empty_kwargs = {k: v for k, v in kwargs.items() if v}
756
-
757
- if "{" in constraints_file_url:
758
- constraints_file_url = constraints_file_url.format(
759
- dataset=provider_product_type
760
- )
761
- constraints = fetch_constraints(constraints_file_url, self)
762
- if not constraints:
763
- return default_queryables
764
-
765
- constraint_params: Dict[str, Dict[str, Set[Any]]] = {}
766
- if len(kwargs) == 0:
767
- # get values from constraints without additional filters
768
- for constraint in constraints:
769
- for key in constraint.keys():
770
- if key in constraint_params:
771
- constraint_params[key]["enum"].update(constraint[key])
772
- else:
773
- constraint_params[key] = {"enum": set(constraint[key])}
774
- else:
775
- # get values from constraints with additional filters
776
- constraints_input_params = {k: v for k, v in non_empty_kwargs.items()}
777
- constraint_params = get_constraint_queryables_with_additional_params(
778
- constraints, constraints_input_params, self, product_type
779
- )
780
- # query params that are not in constraints but might be default queryables
781
- if len(constraint_params) == 1 and "not_available" in constraint_params:
782
- not_queryables = set()
783
- for constraint_param in constraint_params["not_available"]["enum"]:
784
- param = CommonQueryables.get_queryable_from_alias(constraint_param)
785
- if param in dict(
786
- CommonQueryables.model_fields, **default_queryables
787
- ):
788
- non_empty_kwargs.pop(constraint_param)
789
- else:
790
- not_queryables.add(constraint_param)
791
- if not_queryables:
792
- raise ValidationError(
793
- f"parameter(s) {str(not_queryables)} not queryable"
794
- )
795
- else:
796
- # get constraints again without common queryables
797
- constraint_params = (
798
- get_constraint_queryables_with_additional_params(
799
- constraints, non_empty_kwargs, self, product_type
800
- )
801
- )
802
-
803
- field_definitions: Dict[str, Any] = dict()
804
- for json_param, json_mtd in constraint_params.items():
805
- param = (
806
- get_queryable_from_provider(
807
- json_param, self.get_metadata_mapping(product_type)
808
- )
809
- or json_param
810
- )
811
- default = kwargs.get(param, None) or self.config.products.get(
812
- product_type, {}
813
- ).get(param, None)
814
- annotated_def = json_field_definition_to_python(
815
- json_mtd, default_value=default, required=True
816
- )
817
- field_definitions[param] = get_args(annotated_def)
818
-
819
- python_queryables = create_model("m", **field_definitions).model_fields
820
- return dict(default_queryables, **model_fields_to_annotated(python_queryables))
821
-
822
720
  def query(
823
721
  self,
824
722
  prep: PreparedSearch = PreparedSearch(),
825
723
  **kwargs: Any,
826
- ) -> Tuple[List[EOProduct], Optional[int]]:
724
+ ) -> tuple[list[EOProduct], Optional[int]]:
827
725
  """Perform a search on an OpenSearch-like interface
828
726
 
829
727
  :param prep: Object collecting needed information for search.
@@ -851,7 +749,7 @@ class QueryStringSearch(Search):
851
749
 
852
750
  # provider product type specific conf
853
751
  prep.product_type_def_params = (
854
- self.get_product_type_def_params(product_type, **kwargs)
752
+ self.get_product_type_def_params(product_type, format_variables=kwargs)
855
753
  if product_type is not None
856
754
  else {}
857
755
  )
@@ -875,7 +773,7 @@ class QueryStringSearch(Search):
875
773
  }
876
774
  )
877
775
 
878
- qp, qs = self.build_query_string(product_type, **keywords)
776
+ qp, qs = self.build_query_string(product_type, keywords)
879
777
 
880
778
  prep.query_params = qp
881
779
  prep.query_string = qs
@@ -903,21 +801,21 @@ class QueryStringSearch(Search):
903
801
  reason="Simply run `self.config.metadata_mapping.update(metadata_mapping)` instead",
904
802
  version="2.10.0",
905
803
  )
906
- def update_metadata_mapping(self, metadata_mapping: Dict[str, Any]) -> None:
804
+ def update_metadata_mapping(self, metadata_mapping: dict[str, Any]) -> None:
907
805
  """Update plugin metadata_mapping with input metadata_mapping configuration"""
908
806
  if self.config.metadata_mapping:
909
807
  self.config.metadata_mapping.update(metadata_mapping)
910
808
 
911
809
  def build_query_string(
912
- self, product_type: str, **kwargs: Any
913
- ) -> Tuple[Dict[str, Any], str]:
810
+ self, product_type: str, query_dict: dict[str, Any]
811
+ ) -> tuple[dict[str, Any], str]:
914
812
  """Build The query string using the search parameters"""
915
813
  logger.debug("Building the query string that will be used for search")
916
- query_params = format_query_params(product_type, self.config, kwargs)
814
+ query_params = format_query_params(product_type, self.config, query_dict)
917
815
 
918
816
  # Build the final query string, in one go without quoting it
919
817
  # (some providers do not operate well with urlencoded and quoted query strings)
920
- def quote_via(x: Any, *_args, **_kwargs) -> str:
818
+ def quote_via(x: Any, *_args: Any, **_kwargs: Any) -> str:
921
819
  return x
922
820
 
923
821
  return (
@@ -929,7 +827,7 @@ class QueryStringSearch(Search):
929
827
  self,
930
828
  prep: PreparedSearch = PreparedSearch(page=None, items_per_page=None),
931
829
  **kwargs: Any,
932
- ) -> Tuple[List[str], Optional[int]]:
830
+ ) -> tuple[list[str], Optional[int]]:
933
831
  """Build paginated urls"""
934
832
  page = prep.page
935
833
  items_per_page = prep.items_per_page
@@ -998,7 +896,7 @@ class QueryStringSearch(Search):
998
896
 
999
897
  def do_search(
1000
898
  self, prep: PreparedSearch = PreparedSearch(items_per_page=None), **kwargs: Any
1001
- ) -> List[Any]:
899
+ ) -> list[Any]:
1002
900
  """Perform the actual search request.
1003
901
 
1004
902
  If there is a specified number of items per page, return the results as soon
@@ -1015,7 +913,7 @@ class QueryStringSearch(Search):
1015
913
  "total_items_nb_key_path"
1016
914
  ]
1017
915
 
1018
- results: List[Any] = []
916
+ results: list[Any] = []
1019
917
  for search_url in prep.search_urls:
1020
918
  single_search_prep = copy_copy(prep)
1021
919
  single_search_prep.url = search_url
@@ -1138,9 +1036,13 @@ class QueryStringSearch(Search):
1138
1036
  logger.debug(
1139
1037
  "Could not extract total_items_nb from search results"
1140
1038
  )
1141
- if getattr(self.config, "merge_responses", False):
1039
+ if (
1040
+ getattr(self.config, "merge_responses", False)
1041
+ and self.config.result_type == "json"
1042
+ ):
1043
+ json_result = cast(list[dict[str, Any]], result)
1142
1044
  results = (
1143
- [dict(r, **result[i]) for i, r in enumerate(results)]
1045
+ [dict(r, **json_result[i]) for i, r in enumerate(results)]
1144
1046
  if results
1145
1047
  else result
1146
1048
  )
@@ -1162,14 +1064,14 @@ class QueryStringSearch(Search):
1162
1064
 
1163
1065
  def normalize_results(
1164
1066
  self, results: RawSearchResult, **kwargs: Any
1165
- ) -> List[EOProduct]:
1067
+ ) -> list[EOProduct]:
1166
1068
  """Build EOProducts from provider results"""
1167
1069
  normalize_remaining_count = len(results)
1168
1070
  logger.debug(
1169
1071
  "Adapting %s plugin results to eodag product representation"
1170
1072
  % normalize_remaining_count
1171
1073
  )
1172
- products: List[EOProduct] = []
1074
+ products: list[EOProduct] = []
1173
1075
  for result in results:
1174
1076
  product = EOProduct(
1175
1077
  self.provider,
@@ -1184,8 +1086,15 @@ class QueryStringSearch(Search):
1184
1086
  product.properties = dict(
1185
1087
  getattr(self.config, "product_type_config", {}), **product.properties
1186
1088
  )
1187
- # move assets from properties to product's attr
1188
- product.assets.update(product.properties.pop("assets", {}))
1089
+ # move assets from properties to product's attr, normalize keys & roles
1090
+ for key, asset in product.properties.pop("assets", {}).items():
1091
+ norm_key, asset["roles"] = product.driver.guess_asset_key_and_roles(
1092
+ asset.get("href", ""), product
1093
+ )
1094
+ if norm_key:
1095
+ product.assets[norm_key] = asset
1096
+ # sort assets
1097
+ product.assets.data = dict(sorted(product.assets.data.items()))
1189
1098
  products.append(product)
1190
1099
  return products
1191
1100
 
@@ -1227,7 +1136,7 @@ class QueryStringSearch(Search):
1227
1136
  total_results = int(count_results)
1228
1137
  return total_results
1229
1138
 
1230
- def get_collections(self, prep: PreparedSearch, **kwargs: Any) -> Tuple[str, ...]:
1139
+ def get_collections(self, prep: PreparedSearch, **kwargs: Any) -> tuple[str, ...]:
1231
1140
  """Get the collection to which the product belongs"""
1232
1141
  # See https://earth.esa.int/web/sentinel/missions/sentinel-2/news/-
1233
1142
  # /asset_publisher/Ac0d/content/change-of
@@ -1238,7 +1147,7 @@ class QueryStringSearch(Search):
1238
1147
  not hasattr(prep, "product_type_def_params")
1239
1148
  or not prep.product_type_def_params
1240
1149
  ):
1241
- collections: Set[str] = set()
1150
+ collections: set[str] = set()
1242
1151
  collection = getattr(self.config, "collection", None)
1243
1152
  if collection is None:
1244
1153
  try:
@@ -1280,7 +1189,7 @@ class QueryStringSearch(Search):
1280
1189
  info_message = prep.info_message
1281
1190
  exception_message = prep.exception_message
1282
1191
  try:
1283
- timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
1192
+ timeout = getattr(self.config, "timeout", DEFAULT_SEARCH_TIMEOUT)
1284
1193
  ssl_verify = getattr(self.config, "ssl_verify", True)
1285
1194
 
1286
1195
  retry_total = getattr(self.config, "retry_total", REQ_RETRY_TOTAL)
@@ -1293,7 +1202,7 @@ class QueryStringSearch(Search):
1293
1202
 
1294
1203
  ssl_ctx = get_ssl_context(ssl_verify)
1295
1204
  # auth if needed
1296
- kwargs: Dict[str, Any] = {}
1205
+ kwargs: dict[str, Any] = {}
1297
1206
  if (
1298
1207
  getattr(self.config, "need_auth", False)
1299
1208
  and hasattr(prep, "auth")
@@ -1348,6 +1257,9 @@ class QueryStringSearch(Search):
1348
1257
  response.raise_for_status()
1349
1258
  except requests.exceptions.Timeout as exc:
1350
1259
  raise TimeOutError(exc, timeout=timeout) from exc
1260
+ except socket.timeout:
1261
+ err = requests.exceptions.Timeout(request=requests.Request(url=url))
1262
+ raise TimeOutError(err, timeout=timeout)
1351
1263
  except (requests.RequestException, URLError) as err:
1352
1264
  err_msg = err.readlines() if hasattr(err, "readlines") else ""
1353
1265
  if exception_message:
@@ -1424,7 +1336,7 @@ class ODataV4Search(QueryStringSearch):
1424
1336
 
1425
1337
  def do_search(
1426
1338
  self, prep: PreparedSearch = PreparedSearch(), **kwargs: Any
1427
- ) -> List[Any]:
1339
+ ) -> list[Any]:
1428
1340
  """A two step search can be performed if the metadata are not given into the search result"""
1429
1341
 
1430
1342
  if getattr(self.config, "per_product_metadata_query", False):
@@ -1459,7 +1371,7 @@ class ODataV4Search(QueryStringSearch):
1459
1371
  else:
1460
1372
  return super(ODataV4Search, self).do_search(prep, **kwargs)
1461
1373
 
1462
- def get_metadata_search_url(self, entity: Dict[str, Any]) -> str:
1374
+ def get_metadata_search_url(self, entity: dict[str, Any]) -> str:
1463
1375
  """Build the metadata link for the given entity"""
1464
1376
  return "{}({})/Metadata".format(
1465
1377
  self.config.api_endpoint.rstrip("/"), entity["id"]
@@ -1467,7 +1379,7 @@ class ODataV4Search(QueryStringSearch):
1467
1379
 
1468
1380
  def normalize_results(
1469
1381
  self, results: RawSearchResult, **kwargs: Any
1470
- ) -> List[EOProduct]:
1382
+ ) -> list[EOProduct]:
1471
1383
  """Build EOProducts from provider results
1472
1384
 
1473
1385
  If configured, a metadata pre-mapping can be applied to simplify further metadata extraction.
@@ -1523,89 +1435,13 @@ class PostJsonSearch(QueryStringSearch):
1523
1435
 
1524
1436
  """
1525
1437
 
1526
- def _get_default_end_date_from_start_date(
1527
- self, start_datetime: str, product_type: str
1528
- ) -> str:
1529
- default_end_date = self.config.products.get(product_type, {}).get(
1530
- "_default_end_date", None
1531
- )
1532
- if default_end_date:
1533
- return default_end_date
1534
- try:
1535
- start_date = datetime.fromisoformat(start_datetime)
1536
- except ValueError:
1537
- start_date = datetime.strptime(start_datetime, "%Y-%m-%dT%H:%M:%SZ")
1538
- product_type_conf = self.config.products[product_type]
1539
- if (
1540
- "metadata_mapping" in product_type_conf
1541
- and "startTimeFromAscendingNode" in product_type_conf["metadata_mapping"]
1542
- ):
1543
- mapping = product_type_conf["metadata_mapping"][
1544
- "startTimeFromAscendingNode"
1545
- ]
1546
- if isinstance(mapping, list) and "year" in mapping[0]:
1547
- # if date is mapped to year/month/(day), use end_date = start_date to avoid large requests
1548
- end_date = start_date
1549
- return end_date.isoformat()
1550
- return self.get_product_type_cfg_value("missionEndDate", today().isoformat())
1551
-
1552
- def _check_date_params(self, keywords: Dict[str, Any], product_type: str) -> None:
1553
- """checks if start and end date are present in the keywords and adds them if not"""
1554
- if (
1555
- "startTimeFromAscendingNode"
1556
- and "completionTimeFromAscendingNode" in keywords
1557
- ):
1558
- return
1559
- # start time given, end time missing
1560
- if "startTimeFromAscendingNode" in keywords:
1561
- keywords[
1562
- "completionTimeFromAscendingNode"
1563
- ] = self._get_default_end_date_from_start_date(
1564
- keywords["startTimeFromAscendingNode"], product_type
1565
- )
1566
- return
1567
- product_type_conf = self.config.products[product_type]
1568
- if (
1569
- "metadata_mapping" in product_type_conf
1570
- and "startTimeFromAscendingNode" in product_type_conf["metadata_mapping"]
1571
- ):
1572
- mapping = product_type_conf["metadata_mapping"][
1573
- "startTimeFromAscendingNode"
1574
- ]
1575
- if isinstance(mapping, list):
1576
- # get time parameters (date, year, month, ...) from metadata mapping
1577
- input_mapping = mapping[0].replace("{{", "").replace("}}", "")
1578
- time_params = [
1579
- values.split(":")[0].strip() for values in input_mapping.split(",")
1580
- ]
1581
- time_params = [
1582
- tp.replace('"', "").replace("'", "") for tp in time_params
1583
- ]
1584
- # if startTime is not given but other time params (e.g. year/month/(day)) are given,
1585
- # no default date is required
1586
- in_keywords = True
1587
- for tp in time_params:
1588
- if tp not in keywords:
1589
- in_keywords = False
1590
- if not in_keywords:
1591
- keywords[
1592
- "startTimeFromAscendingNode"
1593
- ] = self.get_product_type_cfg_value(
1594
- "missionStartDate", today().isoformat()
1595
- )
1596
- keywords[
1597
- "completionTimeFromAscendingNode"
1598
- ] = self._get_default_end_date_from_start_date(
1599
- keywords["startTimeFromAscendingNode"], product_type
1600
- )
1601
-
1602
1438
  def query(
1603
1439
  self,
1604
1440
  prep: PreparedSearch = PreparedSearch(),
1605
1441
  **kwargs: Any,
1606
- ) -> Tuple[List[EOProduct], Optional[int]]:
1442
+ ) -> tuple[list[EOProduct], Optional[int]]:
1607
1443
  """Perform a search on an OpenSearch-like interface"""
1608
- product_type = kwargs.get("productType", None)
1444
+ product_type = kwargs.get("productType", "")
1609
1445
  count = prep.count
1610
1446
  # remove "product_type" from search args if exists for compatibility with QueryStringSearch methods
1611
1447
  kwargs.pop("product_type", None)
@@ -1621,7 +1457,7 @@ class PostJsonSearch(QueryStringSearch):
1621
1457
 
1622
1458
  # provider product type specific conf
1623
1459
  prep.product_type_def_params = self.get_product_type_def_params(
1624
- product_type, **kwargs
1460
+ product_type, format_variables=kwargs
1625
1461
  )
1626
1462
  else:
1627
1463
  keywords = {
@@ -1635,7 +1471,7 @@ class PostJsonSearch(QueryStringSearch):
1635
1471
 
1636
1472
  # provider product type specific conf
1637
1473
  prep.product_type_def_params = self.get_product_type_def_params(
1638
- product_type, **kwargs
1474
+ product_type, format_variables=kwargs
1639
1475
  )
1640
1476
 
1641
1477
  # Add to the query, the queryable parameters set in the provider product type definition
@@ -1648,10 +1484,8 @@ class PostJsonSearch(QueryStringSearch):
1648
1484
  and isinstance(self.config.metadata_mapping[k], list)
1649
1485
  }
1650
1486
  )
1651
- if getattr(self.config, "dates_required", False):
1652
- self._check_date_params(keywords, product_type)
1653
1487
 
1654
- qp, _ = self.build_query_string(product_type, **keywords)
1488
+ qp, _ = self.build_query_string(product_type, keywords)
1655
1489
 
1656
1490
  for query_param, query_value in qp.items():
1657
1491
  if (
@@ -1720,6 +1554,7 @@ class PostJsonSearch(QueryStringSearch):
1720
1554
  # do not try to extract total_items from search results if count is False
1721
1555
  del prep.total_items_nb
1722
1556
  del prep.need_count
1557
+
1723
1558
  provider_results = self.do_search(prep, **kwargs)
1724
1559
  if count and total_items is None and hasattr(prep, "total_items_nb"):
1725
1560
  total_items = prep.total_items_nb
@@ -1733,7 +1568,7 @@ class PostJsonSearch(QueryStringSearch):
1733
1568
 
1734
1569
  def normalize_results(
1735
1570
  self, results: RawSearchResult, **kwargs: Any
1736
- ) -> List[EOProduct]:
1571
+ ) -> list[EOProduct]:
1737
1572
  """Build EOProducts from provider results"""
1738
1573
  normalized = super().normalize_results(results, **kwargs)
1739
1574
  for product in normalized:
@@ -1768,12 +1603,12 @@ class PostJsonSearch(QueryStringSearch):
1768
1603
  self,
1769
1604
  prep: PreparedSearch = PreparedSearch(),
1770
1605
  **kwargs: Any,
1771
- ) -> Tuple[List[str], Optional[int]]:
1606
+ ) -> tuple[list[str], Optional[int]]:
1772
1607
  """Adds pagination to query parameters, and auth to url"""
1773
1608
  page = prep.page
1774
1609
  items_per_page = prep.items_per_page
1775
1610
  count = prep.count
1776
- urls: List[str] = []
1611
+ urls: list[str] = []
1777
1612
  total_results = 0 if count else None
1778
1613
 
1779
1614
  if "count_endpoint" not in self.config.pagination:
@@ -1842,7 +1677,7 @@ class PostJsonSearch(QueryStringSearch):
1842
1677
  raise ValidationError("Cannot request empty URL")
1843
1678
  info_message = prep.info_message
1844
1679
  exception_message = prep.exception_message
1845
- timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
1680
+ timeout = getattr(self.config, "timeout", DEFAULT_SEARCH_TIMEOUT)
1846
1681
  ssl_verify = getattr(self.config, "ssl_verify", True)
1847
1682
  try:
1848
1683
  # auth if needed
@@ -1933,24 +1768,24 @@ class StacSearch(PostJsonSearch):
1933
1768
  self.config.results_entry = results_entry
1934
1769
 
1935
1770
  def build_query_string(
1936
- self, product_type: str, **kwargs: Any
1937
- ) -> Tuple[Dict[str, Any], str]:
1771
+ self, product_type: str, query_dict: dict[str, Any]
1772
+ ) -> tuple[dict[str, Any], str]:
1938
1773
  """Build The query string using the search parameters"""
1939
1774
  logger.debug("Building the query string that will be used for search")
1940
1775
 
1941
1776
  # handle opened time intervals
1942
1777
  if any(
1943
- k in kwargs
1944
- for k in ("startTimeFromAscendingNode", "completionTimeFromAscendingNode")
1778
+ q in query_dict
1779
+ for q in ("startTimeFromAscendingNode", "completionTimeFromAscendingNode")
1945
1780
  ):
1946
- kwargs.setdefault("startTimeFromAscendingNode", "..")
1947
- kwargs.setdefault("completionTimeFromAscendingNode", "..")
1781
+ query_dict.setdefault("startTimeFromAscendingNode", "..")
1782
+ query_dict.setdefault("completionTimeFromAscendingNode", "..")
1948
1783
 
1949
- query_params = format_query_params(product_type, self.config, kwargs)
1784
+ query_params = format_query_params(product_type, self.config, query_dict)
1950
1785
 
1951
1786
  # Build the final query string, in one go without quoting it
1952
1787
  # (some providers do not operate well with urlencoded and quoted query strings)
1953
- def quote_via(x: Any, *_args, **_kwargs) -> str:
1788
+ def quote_via(x: Any, *_args: Any, **_kwargs: Any) -> str:
1954
1789
  return x
1955
1790
 
1956
1791
  return (
@@ -1960,7 +1795,7 @@ class StacSearch(PostJsonSearch):
1960
1795
 
1961
1796
  def discover_queryables(
1962
1797
  self, **kwargs: Any
1963
- ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
1798
+ ) -> Optional[dict[str, Annotated[Any, FieldInfo]]]:
1964
1799
  """Fetch queryables list from provider using `discover_queryables` conf
1965
1800
 
1966
1801
  :param kwargs: additional filters for queryables (`productType` and other search
@@ -2007,7 +1842,8 @@ class StacSearch(PostJsonSearch):
2007
1842
  return None
2008
1843
 
2009
1844
  fetch_url = unparsed_fetch_url.format(
2010
- provider_product_type=provider_product_type, **self.config.__dict__
1845
+ provider_product_type=provider_product_type,
1846
+ **self.config.__dict__,
2011
1847
  )
2012
1848
  auth = (
2013
1849
  self.auth
@@ -2024,7 +1860,8 @@ class StacSearch(PostJsonSearch):
2024
1860
  "{} {} instance:".format(self.provider, self.__class__.__name__),
2025
1861
  ),
2026
1862
  )
2027
- except (RequestError, KeyError, AttributeError):
1863
+ except (RequestError, KeyError, AttributeError) as e:
1864
+ logger.warning("failure in queryables discovery: %s", e)
2028
1865
  return None
2029
1866
  else:
2030
1867
  json_queryables = dict()
@@ -2056,7 +1893,7 @@ class StacSearch(PostJsonSearch):
2056
1893
  return None
2057
1894
 
2058
1895
  # convert json results to pydantic model fields
2059
- field_definitions: Dict[str, Any] = dict()
1896
+ field_definitions: dict[str, Any] = dict()
2060
1897
  for json_param, json_mtd in json_queryables.items():
2061
1898
  param = (
2062
1899
  get_queryable_from_provider(
@@ -2072,6 +1909,10 @@ class StacSearch(PostJsonSearch):
2072
1909
  field_definitions[param] = get_args(annotated_def)
2073
1910
 
2074
1911
  python_queryables = create_model("m", **field_definitions).model_fields
1912
+ # replace geometry by geom
1913
+ geom_queryable = python_queryables.pop("geometry", None)
1914
+ if geom_queryable:
1915
+ python_queryables["geom"] = geom_queryable
2075
1916
 
2076
1917
  return model_fields_to_annotated(python_queryables)
2077
1918
 
@@ -2085,7 +1926,7 @@ class PostJsonSearchWithStacQueryables(StacSearch, PostJsonSearch):
2085
1926
  PostJsonSearch.__init__(self, provider, config)
2086
1927
 
2087
1928
  def build_query_string(
2088
- self, product_type: str, **kwargs: Any
2089
- ) -> Tuple[Dict[str, Any], str]:
1929
+ self, product_type: str, query_dict: dict[str, Any]
1930
+ ) -> tuple[dict[str, Any], str]:
2090
1931
  """Build The query string using the search parameters"""
2091
- return PostJsonSearch.build_query_string(self, product_type, **kwargs)
1932
+ return PostJsonSearch.build_query_string(self, product_type, query_dict)