eodag 3.0.0b1__py3-none-any.whl → 3.0.0b3__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 (66) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +119 -171
  3. eodag/api/product/__init__.py +10 -4
  4. eodag/api/product/_assets.py +52 -14
  5. eodag/api/product/_product.py +59 -30
  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 +0 -28
  9. eodag/api/search_result.py +31 -9
  10. eodag/config.py +45 -41
  11. eodag/plugins/apis/base.py +3 -3
  12. eodag/plugins/apis/ecmwf.py +2 -3
  13. eodag/plugins/apis/usgs.py +43 -14
  14. eodag/plugins/authentication/aws_auth.py +11 -2
  15. eodag/plugins/authentication/openid_connect.py +5 -4
  16. eodag/plugins/authentication/token.py +2 -1
  17. eodag/plugins/crunch/base.py +3 -1
  18. eodag/plugins/crunch/filter_date.py +3 -9
  19. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  20. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  21. eodag/plugins/crunch/filter_overlap.py +4 -8
  22. eodag/plugins/crunch/filter_property.py +5 -11
  23. eodag/plugins/download/aws.py +46 -78
  24. eodag/plugins/download/base.py +27 -68
  25. eodag/plugins/download/http.py +48 -57
  26. eodag/plugins/download/s3rest.py +17 -25
  27. eodag/plugins/manager.py +6 -18
  28. eodag/plugins/search/__init__.py +9 -9
  29. eodag/plugins/search/base.py +7 -26
  30. eodag/plugins/search/build_search_result.py +0 -13
  31. eodag/plugins/search/cop_marine.py +1 -3
  32. eodag/plugins/search/creodias_s3.py +0 -3
  33. eodag/plugins/search/data_request_search.py +10 -5
  34. eodag/plugins/search/qssearch.py +95 -53
  35. eodag/plugins/search/static_stac_search.py +6 -3
  36. eodag/resources/ext_product_types.json +1 -1
  37. eodag/resources/product_types.yml +24 -0
  38. eodag/resources/providers.yml +198 -154
  39. eodag/resources/user_conf_template.yml +27 -27
  40. eodag/rest/core.py +11 -43
  41. eodag/rest/server.py +1 -6
  42. eodag/rest/stac.py +13 -87
  43. eodag/rest/types/eodag_search.py +4 -7
  44. eodag/rest/types/queryables.py +4 -12
  45. eodag/rest/types/stac_search.py +7 -11
  46. eodag/rest/utils/rfc3339.py +0 -1
  47. eodag/types/__init__.py +9 -3
  48. eodag/types/download_args.py +14 -5
  49. eodag/types/search_args.py +7 -8
  50. eodag/types/whoosh.py +0 -2
  51. eodag/utils/__init__.py +20 -79
  52. eodag/utils/constraints.py +0 -8
  53. eodag/utils/import_system.py +0 -4
  54. eodag/utils/logging.py +0 -3
  55. eodag/utils/notebook.py +4 -4
  56. eodag/utils/repr.py +113 -0
  57. eodag/utils/requests.py +12 -20
  58. eodag/utils/rest.py +0 -4
  59. eodag/utils/stac_reader.py +2 -14
  60. {eodag-3.0.0b1.dist-info → eodag-3.0.0b3.dist-info}/METADATA +33 -14
  61. eodag-3.0.0b3.dist-info/RECORD +110 -0
  62. {eodag-3.0.0b1.dist-info → eodag-3.0.0b3.dist-info}/WHEEL +1 -1
  63. eodag-3.0.0b1.dist-info/RECORD +0 -109
  64. {eodag-3.0.0b1.dist-info → eodag-3.0.0b3.dist-info}/LICENSE +0 -0
  65. {eodag-3.0.0b1.dist-info → eodag-3.0.0b3.dist-info}/entry_points.txt +0 -0
  66. {eodag-3.0.0b1.dist-info → eodag-3.0.0b3.dist-info}/top_level.txt +0 -0
@@ -85,9 +85,7 @@ class BuildPostSearchResult(PostJsonSearch):
85
85
  method before being loaded as json.
86
86
 
87
87
  :param provider: An eodag providers configuration dictionary
88
- :type provider: dict
89
88
  :param config: Path to the user configuration file
90
- :type config: str
91
89
  """
92
90
 
93
91
  def count_hits(
@@ -125,11 +123,8 @@ class BuildPostSearchResult(PostJsonSearch):
125
123
  """Build :class:`~eodag.api.product._product.EOProduct` from provider result
126
124
 
127
125
  :param results: Raw provider result as single dict in list
128
- :type results: list
129
126
  :param kwargs: Search arguments
130
- :type kwargs: Union[int, str, bool, dict, list]
131
127
  :returns: list of single :class:`~eodag.api.product._product.EOProduct`
132
- :rtype: list
133
128
  """
134
129
  product_type = kwargs.get("productType")
135
130
 
@@ -256,9 +251,7 @@ class BuildSearchResult(BuildPostSearchResult):
256
251
  - **constraints_file_url**: url of the constraint file used to build queryables
257
252
 
258
253
  :param provider: An eodag providers configuration dictionary
259
- :type provider: dict
260
254
  :param config: Path to the user configuration file
261
- :type config: str
262
255
  """
263
256
 
264
257
  def __init__(self, provider: str, config: PluginConfig) -> None:
@@ -355,12 +348,9 @@ class BuildSearchResult(BuildPostSearchResult):
355
348
  default value is returned.
356
349
 
357
350
  :param key: The configuration option key.
358
- :type key: str
359
351
  :param default: The default value to be returned if the option is not found (default is None).
360
- :type default: Any
361
352
 
362
353
  :return: The value of the specified configuration option or the default value.
363
- :rtype: Any
364
354
  """
365
355
  product_type_cfg = getattr(self.config, "product_type_config", {})
366
356
  non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
@@ -376,7 +366,6 @@ class BuildSearchResult(BuildPostSearchResult):
376
366
  in the input parameters, default values or values from the configuration are used.
377
367
 
378
368
  :param params: Search parameters to be preprocessed.
379
- :type params: dict
380
369
  """
381
370
  _dc_qs = params.get("_dc_qs", None)
382
371
  if _dc_qs is not None:
@@ -454,9 +443,7 @@ class BuildSearchResult(BuildPostSearchResult):
454
443
 
455
444
  :param kwargs: additional filters for queryables (`productType` and other search
456
445
  arguments)
457
- :type kwargs: Any
458
446
  :returns: fetched queryable parameters dict
459
- :rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
460
447
  """
461
448
  constraints_file_url = getattr(self.config, "constraints_file_url", "")
462
449
  if not constraints_file_url:
@@ -238,16 +238,14 @@ class CopMarineSearch(StaticStacSearch):
238
238
  """
239
239
  Implementation of search for the Copernicus Marine provider
240
240
  :param prep: object containing search parameterds
241
- :type prep: PreparedSearch
242
241
  :param kwargs: additional search arguments
243
242
  :returns: list of products and total number of products
244
- :rtype: Tuple[List[EOProduct], Optional[int]]
245
243
  """
246
244
  page = prep.page
247
245
  items_per_page = prep.items_per_page
248
246
 
249
247
  # only return 1 page if pagination is disabled
250
- if page > 1 and items_per_page <= 0:
248
+ if page is None or items_per_page is None or page > 1 and items_per_page <= 0:
251
249
  return ([], 0) if prep.count else ([], None)
252
250
 
253
251
  product_type = kwargs.get("productType", prep.product_type)
@@ -38,13 +38,10 @@ logger = logging.getLogger("eodag.search.creodiass3")
38
38
  def patched_register_downloader(self, downloader, authenticator):
39
39
  """Add the download information to the product.
40
40
  :param self: product to which information should be added
41
- :type self: EoProduct
42
41
  :param downloader: The download method that it can use
43
- :type downloader: Concrete subclass of
44
42
  :class:`~eodag.plugins.download.base.Download` or
45
43
  :class:`~eodag.plugins.api.base.Api`
46
44
  :param authenticator: The authentication method needed to perform the download
47
- :type authenticator: Concrete subclass of
48
45
  :class:`~eodag.plugins.authentication.base.Authentication`
49
46
  """
50
47
  # register downloader
@@ -116,7 +116,6 @@ class DataRequestSearch(Search):
116
116
  """Fetch product types is disabled for `DataRequestSearch`
117
117
 
118
118
  :returns: empty dict
119
- :rtype: (optional) dict
120
119
  """
121
120
  return None
122
121
 
@@ -133,7 +132,7 @@ class DataRequestSearch(Search):
133
132
  """
134
133
  performs the search for a provider where several steps are required to fetch the data
135
134
  """
136
- if kwargs.get("sortBy"):
135
+ if kwargs.get("sort_by"):
137
136
  raise ValidationError(f"{self.provider} does not support sorting feature")
138
137
 
139
138
  product_type = kwargs.get("productType", None)
@@ -386,8 +385,11 @@ class DataRequestSearch(Search):
386
385
  total_items_nb_key_path = string_to_jsonpath(
387
386
  self.config.pagination["total_items_nb_key_path"]
388
387
  )
389
- if len(total_items_nb_key_path.find(results)) > 0:
390
- total_items_nb = total_items_nb_key_path.find(results)[0].value
388
+ found_total_items_nb_paths = total_items_nb_key_path.find(results)
389
+ if found_total_items_nb_paths and not isinstance(
390
+ found_total_items_nb_paths, int
391
+ ):
392
+ total_items_nb = found_total_items_nb_paths[0].value
391
393
  else:
392
394
  total_items_nb = 0
393
395
  for p in products:
@@ -423,7 +425,10 @@ class DataRequestSearch(Search):
423
425
  path = string_to_jsonpath(custom_filters["filter_attribute"])
424
426
  indexes = custom_filters["indexes"].split("-")
425
427
  for record in results:
426
- filter_param = path.find(record)[0].value
428
+ found_paths = path.find(record)
429
+ if not found_paths or isinstance(found_paths, int):
430
+ continue
431
+ filter_param = found_paths[0].value
427
432
  filter_value = filter_param[int(indexes[0]) : int(indexes[1])]
428
433
  filter_clause = "'" + filter_value + "' " + custom_filters["filter_clause"]
429
434
  if eval(filter_clause):
@@ -19,7 +19,6 @@ from __future__ import annotations
19
19
 
20
20
  import logging
21
21
  import re
22
- from collections.abc import Iterable
23
22
  from copy import copy as copy_copy
24
23
  from typing import (
25
24
  TYPE_CHECKING,
@@ -28,6 +27,7 @@ from typing import (
28
27
  Dict,
29
28
  List,
30
29
  Optional,
30
+ Sequence,
31
31
  Set,
32
32
  Tuple,
33
33
  TypedDict,
@@ -48,6 +48,7 @@ import geojson
48
48
  import orjson
49
49
  import requests
50
50
  import yaml
51
+ from jsonpath_ng import JSONPath
51
52
  from lxml import etree
52
53
  from pydantic import create_model
53
54
  from pydantic.fields import FieldInfo
@@ -93,6 +94,7 @@ from eodag.utils.constraints import (
93
94
  from eodag.utils.exceptions import (
94
95
  AuthenticationError,
95
96
  MisconfiguredError,
97
+ PluginImplementationError,
96
98
  RequestError,
97
99
  TimeOutError,
98
100
  ValidationError,
@@ -197,12 +199,13 @@ class QueryStringSearch(Search):
197
199
  ``free_text_search_operations`` configuration parameter follow the same rule.
198
200
 
199
201
  :param provider: An eodag providers configuration dictionary
200
- :type provider: dict
201
202
  :param config: Path to the user configuration file
202
- :type config: str
203
203
  """
204
204
 
205
- extract_properties = {"xml": properties_from_xml, "json": properties_from_json}
205
+ extract_properties: Dict[str, Callable[..., Dict[str, Any]]] = {
206
+ "xml": properties_from_xml,
207
+ "json": properties_from_json,
208
+ }
206
209
 
207
210
  def __init__(self, provider: str, config: PluginConfig) -> None:
208
211
  super(QueryStringSearch, self).__init__(provider, config)
@@ -360,7 +363,6 @@ class QueryStringSearch(Search):
360
363
  """Fetch product types list from provider using `discover_product_types` conf
361
364
 
362
365
  :returns: configuration dict containing fetched product types information
363
- :rtype: (optional) dict
364
366
  """
365
367
  try:
366
368
  prep = PreparedSearch()
@@ -527,9 +529,7 @@ class QueryStringSearch(Search):
527
529
  """
528
530
  retrieves additional product type information from an endpoint returning data for a single collection
529
531
  :param product_type: product type
530
- :type product_type: str
531
532
  :return: product types and their metadata
532
- :rtype: Dict[str, Any]
533
533
  """
534
534
  single_collection_url = self.config.discover_product_types[
535
535
  "single_collection_fetch_url"
@@ -558,9 +558,7 @@ class QueryStringSearch(Search):
558
558
 
559
559
  :param kwargs: additional filters for queryables (`productType` and other search
560
560
  arguments)
561
- :type kwargs: Any
562
561
  :returns: fetched queryable parameters dict
563
- :rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
564
562
  """
565
563
  product_type = kwargs.pop("productType", None)
566
564
  if not product_type:
@@ -630,7 +628,7 @@ class QueryStringSearch(Search):
630
628
  )
631
629
  )
632
630
 
633
- field_definitions = dict()
631
+ field_definitions: Dict[str, Any] = dict()
634
632
  for json_param, json_mtd in constraint_params.items():
635
633
  param = (
636
634
  get_queryable_from_provider(
@@ -657,7 +655,6 @@ class QueryStringSearch(Search):
657
655
  """Perform a search on an OpenSearch-like interface
658
656
 
659
657
  :param prep: Object collecting needed information for search.
660
- :type prep: :class:`~eodag.plugins.search.PreparedSearch`
661
658
  """
662
659
  count = prep.count
663
660
  product_type = kwargs.get("productType", prep.product_type)
@@ -751,7 +748,9 @@ class QueryStringSearch(Search):
751
748
 
752
749
  # Build the final query string, in one go without quoting it
753
750
  # (some providers do not operate well with urlencoded and quoted query strings)
754
- quote_via: Callable[[Any, str, str, str], str] = lambda x, *_args, **_kwargs: x
751
+ def quote_via(x: Any, *_args, **_kwargs) -> str:
752
+ return x
753
+
755
754
  return (
756
755
  query_params,
757
756
  urlencode(query_params, doseq=True, quote_via=quote_via),
@@ -783,7 +782,7 @@ class QueryStringSearch(Search):
783
782
  prep.need_count = True
784
783
  prep.total_items_nb = None
785
784
 
786
- for collection in self.get_collections(prep, **kwargs):
785
+ for collection in self.get_collections(prep, **kwargs) or (None,):
787
786
  # skip empty collection if one is required in api_endpoint
788
787
  if "{collection}" in self.config.api_endpoint and not collection:
789
788
  continue
@@ -811,6 +810,10 @@ class QueryStringSearch(Search):
811
810
  0 if total_results is None else total_results
812
811
  )
813
812
  total_results += _total_results or 0
813
+ if "next_page_url_tpl" not in self.config.pagination:
814
+ raise MisconfiguredError(
815
+ f"next_page_url_tpl is missing in {self.provider} search.pagination configuration"
816
+ )
814
817
  next_url = self.config.pagination["next_page_url_tpl"].format(
815
818
  url=search_endpoint,
816
819
  search=qs_with_sort,
@@ -833,7 +836,6 @@ class QueryStringSearch(Search):
833
836
  as this number is reached
834
837
 
835
838
  :param prep: Object collecting needed information for search.
836
- :type prep: :class:`~eodag.plugins.search.PreparedSearch`
837
839
  """
838
840
  items_per_page = prep.items_per_page
839
841
  total_items_nb = 0
@@ -873,7 +875,7 @@ class QueryStringSearch(Search):
873
875
  )
874
876
  result = (
875
877
  [etree.tostring(element_or_tree=entry) for entry in results_xpath]
876
- if isinstance(results_xpath, Iterable)
878
+ if isinstance(results_xpath, Sequence)
877
879
  else []
878
880
  )
879
881
 
@@ -893,7 +895,7 @@ class QueryStringSearch(Search):
893
895
  )
894
896
  total_nb_results = (
895
897
  total_nb_results_xpath
896
- if isinstance(total_nb_results_xpath, Iterable)
898
+ if isinstance(total_nb_results_xpath, Sequence)
897
899
  else []
898
900
  )[0]
899
901
  _total_items_nb = int(total_nb_results)
@@ -910,55 +912,60 @@ class QueryStringSearch(Search):
910
912
  resp_as_json = response.json()
911
913
  if next_page_url_key_path:
912
914
  path_parsed = next_page_url_key_path
913
- try:
914
- self.next_page_url = path_parsed.find(resp_as_json)[0].value
915
+ found_paths = path_parsed.find(resp_as_json)
916
+ if found_paths and not isinstance(found_paths, int):
917
+ self.next_page_url = found_paths[0].value
915
918
  logger.debug(
916
919
  "Next page URL collected and set for the next search",
917
920
  )
918
- except IndexError:
921
+ else:
919
922
  logger.debug("Next page URL could not be collected")
920
923
  if next_page_query_obj_key_path:
921
924
  path_parsed = next_page_query_obj_key_path
922
- try:
923
- self.next_page_query_obj = path_parsed.find(resp_as_json)[
924
- 0
925
- ].value
925
+ found_paths = path_parsed.find(resp_as_json)
926
+ if found_paths and not isinstance(found_paths, int):
927
+ self.next_page_query_obj = found_paths[0].value
926
928
  logger.debug(
927
929
  "Next page Query-object collected and set for the next search",
928
930
  )
929
- except IndexError:
931
+ else:
930
932
  logger.debug("Next page Query-object could not be collected")
931
933
  if next_page_merge_key_path:
932
934
  path_parsed = next_page_merge_key_path
933
- try:
934
- self.next_page_merge = path_parsed.find(resp_as_json)[0].value
935
+ found_paths = path_parsed.find(resp_as_json)
936
+ if found_paths and not isinstance(found_paths, int):
937
+ self.next_page_merge = found_paths[0].value
935
938
  logger.debug(
936
939
  "Next page merge collected and set for the next search",
937
940
  )
938
- except IndexError:
941
+ else:
939
942
  logger.debug("Next page merge could not be collected")
940
943
 
941
944
  results_entry = string_to_jsonpath(
942
945
  self.config.results_entry, force=True
943
946
  )
944
- try:
945
- result = results_entry.find(resp_as_json)[0].value
946
- except Exception:
947
+ found_entry_paths = results_entry.find(resp_as_json)
948
+ if found_entry_paths and not isinstance(found_entry_paths, int):
949
+ result = found_entry_paths[0].value
950
+ else:
947
951
  result = []
948
952
  if not isinstance(result, list):
949
953
  result = [result]
950
954
 
951
955
  if getattr(prep, "need_count", False):
952
956
  # extract total_items_nb from search results
953
- try:
954
- _total_items_nb = total_items_nb_key_path_parsed.find(
955
- resp_as_json
956
- )[0].value
957
+ found_total_items_nb_paths = total_items_nb_key_path_parsed.find(
958
+ resp_as_json
959
+ )
960
+ if found_total_items_nb_paths and not isinstance(
961
+ found_total_items_nb_paths, int
962
+ ):
963
+ _total_items_nb = found_total_items_nb_paths[0].value
957
964
  if getattr(self.config, "merge_responses", False):
958
965
  total_items_nb = _total_items_nb or 0
959
966
  else:
960
967
  total_items_nb += _total_items_nb or 0
961
- except IndexError:
968
+ else:
962
969
  logger.debug(
963
970
  "Could not extract total_items_nb from search results"
964
971
  )
@@ -1036,25 +1043,34 @@ class QueryStringSearch(Search):
1036
1043
  count_results = response.json()
1037
1044
  if isinstance(count_results, dict):
1038
1045
  path_parsed = self.config.pagination["total_items_nb_key_path"]
1039
- total_results = path_parsed.find(count_results)[0].value
1046
+ if not isinstance(path_parsed, JSONPath):
1047
+ raise PluginImplementationError(
1048
+ "total_items_nb_key_path must be parsed to JSONPath on plugin init"
1049
+ )
1050
+ found_paths = path_parsed.find(count_results)
1051
+ if found_paths and not isinstance(found_paths, int):
1052
+ total_results = found_paths[0].value
1053
+ else:
1054
+ raise MisconfiguredError(
1055
+ "Could not get results count from response using total_items_nb_key_path"
1056
+ )
1040
1057
  else: # interpret the result as a raw int
1041
1058
  total_results = int(count_results)
1042
1059
  return total_results
1043
1060
 
1044
- def get_collections(
1045
- self, prep: PreparedSearch, **kwargs: Any
1046
- ) -> Tuple[Set[Dict[str, Any]], ...]:
1061
+ def get_collections(self, prep: PreparedSearch, **kwargs: Any) -> Tuple[str, ...]:
1047
1062
  """Get the collection to which the product belongs"""
1048
1063
  # See https://earth.esa.int/web/sentinel/missions/sentinel-2/news/-
1049
1064
  # /asset_publisher/Ac0d/content/change-of
1050
1065
  # -format-for-new-sentinel-2-level-1c-products-starting-on-6-december
1051
1066
  product_type: Optional[str] = kwargs.get("productType")
1067
+ collection: Optional[str] = None
1052
1068
  if product_type is None and (
1053
1069
  not hasattr(prep, "product_type_def_params")
1054
1070
  or not prep.product_type_def_params
1055
1071
  ):
1056
- collections: Set[Dict[str, Any]] = set()
1057
- collection: Optional[str] = getattr(self.config, "collection", None)
1072
+ collections: Set[str] = set()
1073
+ collection = getattr(self.config, "collection", None)
1058
1074
  if collection is None:
1059
1075
  try:
1060
1076
  for product_type, product_config in self.config.products.items():
@@ -1072,18 +1088,26 @@ class QueryStringSearch(Search):
1072
1088
  collections.add(collection)
1073
1089
  return tuple(collections)
1074
1090
 
1075
- collection: Optional[str] = getattr(self.config, "collection", None)
1091
+ collection = getattr(self.config, "collection", None)
1076
1092
  if collection is None:
1077
1093
  collection = (
1078
1094
  prep.product_type_def_params.get("collection", None) or product_type
1079
1095
  )
1080
- return (collection,) if not isinstance(collection, list) else tuple(collection)
1096
+
1097
+ if collection is None:
1098
+ return ()
1099
+ elif not isinstance(collection, list):
1100
+ return (collection,)
1101
+ else:
1102
+ return tuple(collection)
1081
1103
 
1082
1104
  def _request(
1083
1105
  self,
1084
1106
  prep: PreparedSearch,
1085
1107
  ) -> Response:
1086
1108
  url = prep.url
1109
+ if url is None:
1110
+ raise ValidationError("Cannot request empty URL")
1087
1111
  info_message = prep.info_message
1088
1112
  exception_message = prep.exception_message
1089
1113
  try:
@@ -1329,8 +1353,11 @@ class PostJsonSearch(QueryStringSearch):
1329
1353
  "specific_qssearch"
1330
1354
  ].get("merge_responses", None)
1331
1355
 
1332
- self.count_hits = lambda *x, **y: 1
1333
- self._request = super(PostJsonSearch, self)._request
1356
+ def count_hits(self, *x, **y):
1357
+ return 1
1358
+
1359
+ def _request(self, *x, **y):
1360
+ return super(PostJsonSearch, self)._request(*x, **y)
1334
1361
 
1335
1362
  try:
1336
1363
  eo_products, total_items = super(PostJsonSearch, self).query(
@@ -1431,7 +1458,7 @@ class PostJsonSearch(QueryStringSearch):
1431
1458
  auth_conf_dict = getattr(prep.auth_plugin.config, "credentials", {})
1432
1459
  else:
1433
1460
  auth_conf_dict = {}
1434
- for collection in self.get_collections(prep, **kwargs):
1461
+ for collection in self.get_collections(prep, **kwargs) or (None,):
1435
1462
  try:
1436
1463
  search_endpoint: str = self.config.api_endpoint.rstrip("/").format(
1437
1464
  **dict(collection=collection, **auth_conf_dict)
@@ -1454,7 +1481,11 @@ class PostJsonSearch(QueryStringSearch):
1454
1481
  if getattr(self.config, "merge_responses", False):
1455
1482
  total_results = _total_results or 0
1456
1483
  else:
1457
- total_results += _total_results or 0
1484
+ total_results = (
1485
+ (_total_results or 0)
1486
+ if total_results is None
1487
+ else total_results + (_total_results or 0)
1488
+ )
1458
1489
  if "next_page_query_obj" in self.config.pagination and isinstance(
1459
1490
  self.config.pagination["next_page_query_obj"], str
1460
1491
  ):
@@ -1479,6 +1510,8 @@ class PostJsonSearch(QueryStringSearch):
1479
1510
  prep: PreparedSearch,
1480
1511
  ) -> Response:
1481
1512
  url = prep.url
1513
+ if url is None:
1514
+ raise ValidationError("Cannot request empty URL")
1482
1515
  info_message = prep.info_message
1483
1516
  exception_message = prep.exception_message
1484
1517
  timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
@@ -1497,7 +1530,10 @@ class PostJsonSearch(QueryStringSearch):
1497
1530
  kwargs["auth"] = prep.auth
1498
1531
 
1499
1532
  # perform the request using the next page arguments if they are defined
1500
- if getattr(self, "next_page_query_obj", None):
1533
+ if (
1534
+ hasattr(self, "next_page_query_obj")
1535
+ and self.next_page_query_obj is not None
1536
+ ):
1501
1537
  prep.query_params = self.next_page_query_obj
1502
1538
  if info_message:
1503
1539
  logger.info(info_message)
@@ -1520,7 +1556,9 @@ class PostJsonSearch(QueryStringSearch):
1520
1556
  if not isinstance(auth_errors, list):
1521
1557
  auth_errors = [auth_errors]
1522
1558
  if (
1523
- hasattr(err.response, "status_code")
1559
+ hasattr(err, "response")
1560
+ and err.response is not None
1561
+ and getattr(err.response, "status_code", None)
1524
1562
  and err.response.status_code in auth_errors
1525
1563
  ):
1526
1564
  raise AuthenticationError(
@@ -1542,7 +1580,11 @@ class PostJsonSearch(QueryStringSearch):
1542
1580
  if "response" in locals():
1543
1581
  logger.debug(response.content)
1544
1582
  error_text = str(err)
1545
- if getattr(err, "response", None) is not None:
1583
+ if (
1584
+ hasattr(err, "response")
1585
+ and err.response is not None
1586
+ and getattr(err.response, "text", None)
1587
+ ):
1546
1588
  error_text = err.response.text
1547
1589
  raise RequestError(error_text) from err
1548
1590
  return response
@@ -1578,7 +1620,9 @@ class StacSearch(PostJsonSearch):
1578
1620
 
1579
1621
  # Build the final query string, in one go without quoting it
1580
1622
  # (some providers do not operate well with urlencoded and quoted query strings)
1581
- quote_via: Callable[[Any, str, str, str], str] = lambda x, *_args, **_kwargs: x
1623
+ def quote_via(x: Any, *_args, **_kwargs) -> str:
1624
+ return x
1625
+
1582
1626
  return (
1583
1627
  query_params,
1584
1628
  urlencode(query_params, doseq=True, quote_via=quote_via),
@@ -1591,9 +1635,7 @@ class StacSearch(PostJsonSearch):
1591
1635
 
1592
1636
  :param kwargs: additional filters for queryables (`productType` and other search
1593
1637
  arguments)
1594
- :type kwargs: Any
1595
1638
  :returns: fetched queryable parameters dict
1596
- :rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
1597
1639
  """
1598
1640
  product_type = kwargs.get("productType", None)
1599
1641
  provider_product_type = (
@@ -60,9 +60,7 @@ class StaticStacSearch(StacSearch):
60
60
  Then it uses crunchers to only keep products matching query parameters.
61
61
 
62
62
  :param provider: An eodag providers configuration dictionary
63
- :type provider: dict
64
63
  :param config: Path to the user configuration file
65
- :type config: str
66
64
  """
67
65
 
68
66
  def __init__(self, provider: str, config: PluginConfig) -> None:
@@ -85,12 +83,17 @@ class StaticStacSearch(StacSearch):
85
83
  "total_items_nb_key_path", "$.null"
86
84
  )
87
85
  self.config.__dict__["pagination"].setdefault("max_items_per_page", -1)
86
+ # disable product types discovery by default (if endpoints equals to STAC API default)
87
+ if (
88
+ getattr(self.config, "discover_product_types", {}).get("fetch_url")
89
+ == "{api_endpoint}/../collections"
90
+ ):
91
+ self.config.discover_product_types = {"fetch_url": None}
88
92
 
89
93
  def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
90
94
  """Fetch product types list from a static STAC Catalog provider using `discover_product_types` conf
91
95
 
92
96
  :returns: configuration dict containing fetched product types information
93
- :rtype: Optional[Dict[str, Any]]
94
97
  """
95
98
  fetch_url = cast(
96
99
  str,