eodag 3.0.0b3__py3-none-any.whl → 3.0.1__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 (71) hide show
  1. eodag/api/core.py +189 -125
  2. eodag/api/product/metadata_mapping.py +12 -3
  3. eodag/api/search_result.py +29 -3
  4. eodag/cli.py +35 -19
  5. eodag/config.py +412 -116
  6. eodag/plugins/apis/base.py +10 -4
  7. eodag/plugins/apis/ecmwf.py +14 -4
  8. eodag/plugins/apis/usgs.py +25 -2
  9. eodag/plugins/authentication/aws_auth.py +14 -5
  10. eodag/plugins/authentication/base.py +10 -1
  11. eodag/plugins/authentication/generic.py +14 -3
  12. eodag/plugins/authentication/header.py +12 -4
  13. eodag/plugins/authentication/keycloak.py +41 -22
  14. eodag/plugins/authentication/oauth.py +11 -1
  15. eodag/plugins/authentication/openid_connect.py +178 -163
  16. eodag/plugins/authentication/qsauth.py +12 -4
  17. eodag/plugins/authentication/sas_auth.py +19 -2
  18. eodag/plugins/authentication/token.py +57 -10
  19. eodag/plugins/authentication/token_exchange.py +19 -19
  20. eodag/plugins/crunch/base.py +4 -1
  21. eodag/plugins/crunch/filter_date.py +5 -2
  22. eodag/plugins/crunch/filter_latest_intersect.py +5 -4
  23. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  24. eodag/plugins/crunch/filter_overlap.py +5 -7
  25. eodag/plugins/crunch/filter_property.py +4 -3
  26. eodag/plugins/download/aws.py +39 -22
  27. eodag/plugins/download/base.py +11 -11
  28. eodag/plugins/download/creodias_s3.py +11 -2
  29. eodag/plugins/download/http.py +86 -52
  30. eodag/plugins/download/s3rest.py +20 -18
  31. eodag/plugins/manager.py +168 -23
  32. eodag/plugins/search/base.py +33 -14
  33. eodag/plugins/search/build_search_result.py +55 -51
  34. eodag/plugins/search/cop_marine.py +112 -29
  35. eodag/plugins/search/creodias_s3.py +20 -5
  36. eodag/plugins/search/csw.py +41 -1
  37. eodag/plugins/search/data_request_search.py +109 -9
  38. eodag/plugins/search/qssearch.py +532 -152
  39. eodag/plugins/search/static_stac_search.py +20 -21
  40. eodag/resources/ext_product_types.json +1 -1
  41. eodag/resources/product_types.yml +187 -56
  42. eodag/resources/providers.yml +1610 -1701
  43. eodag/resources/stac.yml +3 -163
  44. eodag/resources/user_conf_template.yml +112 -97
  45. eodag/rest/config.py +1 -2
  46. eodag/rest/constants.py +0 -1
  47. eodag/rest/core.py +61 -51
  48. eodag/rest/errors.py +181 -0
  49. eodag/rest/server.py +24 -325
  50. eodag/rest/stac.py +93 -544
  51. eodag/rest/types/eodag_search.py +13 -8
  52. eodag/rest/types/queryables.py +1 -2
  53. eodag/rest/types/stac_search.py +11 -2
  54. eodag/types/__init__.py +15 -3
  55. eodag/types/download_args.py +1 -1
  56. eodag/types/queryables.py +1 -2
  57. eodag/types/search_args.py +3 -3
  58. eodag/utils/__init__.py +77 -57
  59. eodag/utils/exceptions.py +23 -9
  60. eodag/utils/logging.py +37 -77
  61. eodag/utils/requests.py +1 -3
  62. eodag/utils/stac_reader.py +1 -1
  63. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/METADATA +11 -12
  64. eodag-3.0.1.dist-info/RECORD +109 -0
  65. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/WHEEL +1 -1
  66. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/entry_points.txt +1 -0
  67. eodag/resources/constraints/climate-dt.json +0 -13
  68. eodag/resources/constraints/extremes-dt.json +0 -8
  69. eodag-3.0.0b3.dist-info/RECORD +0 -110
  70. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/LICENSE +0 -0
  71. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/top_level.txt +0 -0
@@ -20,8 +20,10 @@ from __future__ import annotations
20
20
  import logging
21
21
  import re
22
22
  from copy import copy as copy_copy
23
+ from datetime import datetime
23
24
  from typing import (
24
25
  TYPE_CHECKING,
26
+ Annotated,
25
27
  Any,
26
28
  Callable,
27
29
  Dict,
@@ -32,6 +34,7 @@ from typing import (
32
34
  Tuple,
33
35
  TypedDict,
34
36
  cast,
37
+ get_args,
35
38
  )
36
39
  from urllib.error import URLError
37
40
  from urllib.parse import (
@@ -44,10 +47,12 @@ from urllib.parse import (
44
47
  )
45
48
  from urllib.request import Request, urlopen
46
49
 
50
+ import concurrent.futures
47
51
  import geojson
48
52
  import orjson
49
53
  import requests
50
54
  import yaml
55
+ from dateutil.utils import today
51
56
  from jsonpath_ng import JSONPath
52
57
  from lxml import etree
53
58
  from pydantic import create_model
@@ -55,6 +60,7 @@ from pydantic.fields import FieldInfo
55
60
  from requests import Response
56
61
  from requests.adapters import HTTPAdapter
57
62
  from requests.auth import AuthBase
63
+ from urllib3 import Retry
58
64
 
59
65
  from eodag.api.product import EOProduct
60
66
  from eodag.api.product.metadata_mapping import (
@@ -74,13 +80,14 @@ from eodag.types.search_args import SortByList
74
80
  from eodag.utils import (
75
81
  GENERIC_PRODUCT_TYPE,
76
82
  HTTP_REQ_TIMEOUT,
83
+ REQ_RETRY_BACKOFF_FACTOR,
84
+ REQ_RETRY_STATUS_FORCELIST,
85
+ REQ_RETRY_TOTAL,
77
86
  USER_AGENT,
78
- Annotated,
79
87
  _deprecated,
80
88
  deepcopy,
81
89
  dict_items_recursive_apply,
82
90
  format_dict_items,
83
- get_args,
84
91
  get_ssl_context,
85
92
  quote,
86
93
  string_to_jsonpath,
@@ -108,98 +115,180 @@ logger = logging.getLogger("eodag.search.qssearch")
108
115
 
109
116
  class QueryStringSearch(Search):
110
117
  """A plugin that helps implementing any kind of search protocol that relies on
111
- query strings (e.g: opensearch).
112
-
113
- The available configuration parameters for this kind of plugin are:
114
-
115
- - **result_type**: (optional) One of "json" or "xml", depending on the
116
- representation of the provider's search results. The default is "json"
117
-
118
- - **results_entry**: (mandatory) The name of the key in the provider search
119
- result that gives access to the result entries
120
-
121
- - **api_endpoint**: (mandatory) The endpoint of the provider's search interface
122
-
123
- - **literal_search_params**: (optional) A mapping of (search_param =>
124
- search_value) pairs giving search parameters to be passed as is in the search
125
- url query string. This is useful for example in situations where the user wants
126
- to pass-in a search query as it is done on the provider interface. In such a case,
127
- the user can put in his configuration file the query he needs to pass to the provider.
128
-
129
- - **pagination**: (mandatory) The configuration of how the pagination is done
130
- on the provider. It is a tree with the following nodes:
131
-
132
- - *next_page_url_tpl*: The template for pagination requests. This is a simple
133
- Python format string which will be resolved using the following keywords:
134
- ``url`` (the base url of the search endpoint), ``search`` (the query string
135
- corresponding to the search request), ``items_per_page`` (the number of
136
- items to return per page), ``skip`` (the number of items to skip) or
137
- ``skip_base_1`` (the number of items to skip, starting from 1) and
138
- ``page`` (which page to return).
139
-
140
- - *total_items_nb_key_path*: (optional) An XPath or JsonPath leading to the
141
- total number of results satisfying a request. This is used for providers
142
- which provides the total results metadata along with the result of the
143
- query and don't have an endpoint for querying the number of items
144
- satisfying a request, or for providers for which the count endpoint returns
145
- a json or xml document
146
-
147
- - *count_endpoint*: (optional) The endpoint for counting the number of items
148
- satisfying a request
149
-
150
- - *next_page_url_key_path*: (optional) A JSONPATH expression used to retrieve
151
- the URL of the next page in the response of the current page.
152
-
153
- - **free_text_search_operations**: (optional) A tree structure of the form::
154
-
155
- # noqa: E800
156
- <search-param>: # e.g: $search
157
- union: # how to join the operations below (e.g: ' AND ' -->
158
- # '(op1 AND op2) AND (op3 OR op4)')
159
- wrapper: # a pattern for how each operation will be wrapped
160
- # (e.g: '({})' --> '(op1 AND op2)')
161
- operations: # The operations to build
162
- <opname>: # e.g: AND
163
- - <op1> # e.g:
164
- # 'sensingStartDate:[{startTimeFromAscendingNode}Z TO *]'
165
- - <op2> # e.g:
166
- # 'sensingStopDate:[* TO {completionTimeFromAscendingNode}Z]'
167
- ...
168
- ...
169
- ...
170
-
171
- With the structure above, each operation will become a string of the form:
172
- '(<op1> <opname> <op2>)', then the operations will be joined together using
173
- the union string and finally if the number of operations is greater than 1,
174
- they will be wrapped as specified by the wrapper config key.
175
-
176
- The search plugins of this kind can detect when a metadata mapping is "query-able",
177
- and get the semantics of how to format the query string parameter that enables to
178
- make a query on the corresponding metadata. To make a metadata query-able, just
179
- configure it in the metadata mapping to be a list of 2 items, the first one being
180
- the specification of the query string search formatting. The later is a string
181
- following the specification of Python string formatting, with a special behaviour
182
- added to it. For example, an entry in the metadata mapping of this kind::
183
-
184
- completionTimeFromAscendingNode:
185
- - 'f=acquisition.endViewingDate:lte:{completionTimeFromAscendingNode#timestamp}'
186
- - '$.properties.acquisition.endViewingDate'
187
-
188
- means that the search url will have a query string parameter named *"f"* with a
189
- value of *"acquisition.endViewingDate:lte:1543922280.0"* if the search was done
190
- with the value of ``completionTimeFromAscendingNode`` being
191
- ``2018-12-04T12:18:00``. What happened is that
192
- ``{completionTimeFromAscendingNode#timestamp}`` was replaced with the timestamp
193
- of the value of ``completionTimeFromAscendingNode``. This example shows all there
194
- is to know about the semantics of the query string formatting introduced by this
195
- plugin: any eodag search parameter can be referenced in the query string
196
- with an additional optional conversion function that is separated from it by a
197
- ``#`` (see :func:`~eodag.utils.format_metadata` for further details on the
198
- available converters). Note that for the values in the
199
- ``free_text_search_operations`` configuration parameter follow the same rule.
200
-
201
- :param provider: An eodag providers configuration dictionary
202
- :param config: Path to the user configuration file
118
+ query strings (e.g: opensearch). Most of the other search plugins inherit from this plugin.
119
+
120
+ :param provider: provider name
121
+ :param config: Search plugin configuration:
122
+
123
+ * :attr:`~eodag.config.PluginConfig.result_type` (``str``): One of ``json`` or ``xml``, depending on the
124
+ representation of the provider's search results. The default is ``json``.
125
+ * :attr:`~eodag.config.PluginConfig.results_entry` (``str``) (**mandatory**): The name of the key in the
126
+ provider search result that gives access to the result entries
127
+ * :attr:`~eodag.config.PluginConfig.api_endpoint` (``str``) (**mandatory**): The endpoint of the provider's
128
+ search interface
129
+ * :attr:`~eodag.config.PluginConfig.need_auth` (``bool``): if authentication is needed for the search request;
130
+ default: ``False``
131
+ * :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is returned in case of an
132
+ authentication error; only used if ``need_auth=true``
133
+ * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be verified in
134
+ requests; default: ``True``
135
+ * :attr:`~eodag.config.PluginConfig.dont_quote` (``List[str]``): characters that should not be quoted in the
136
+ url params
137
+ * :attr:`~eodag.config.PluginConfig.timeout` (``int``): time to wait until request timeout in seconds;
138
+ default: ``5``
139
+ * :attr:`~eodag.config.PluginConfig.retry_total` (``int``): :class:`urllib3.util.Retry` ``total`` parameter,
140
+ total number of retries to allow; default: ``3``
141
+ * :attr:`~eodag.config.PluginConfig.retry_backoff_factor` (``int``): :class:`urllib3.util.Retry`
142
+ ``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`
144
+ ``status_forcelist`` parameter, list of integer HTTP status codes that we should force a retry on; default:
145
+ ``[401, 429, 500, 502, 503, 504]``
146
+ * :attr:`~eodag.config.PluginConfig.literal_search_params` (``Dict[str, str]``): A mapping of (search_param =>
147
+ search_value) pairs giving search parameters to be passed as is in the search url query string. This is useful
148
+ for example in situations where the user wants to add a fixed search query parameter exactly
149
+ as it is done on the provider interface.
150
+ * :attr:`~eodag.config.PluginConfig.pagination` (:class:`~eodag.config.PluginConfig.Pagination`)
151
+ (**mandatory**): The configuration of how the pagination is done on the provider. It is a tree with the
152
+ following nodes:
153
+
154
+ * :attr:`~eodag.config.PluginConfig.Pagination.next_page_url_tpl` (``str``) (**mandatory**): The template for
155
+ pagination requests. This is a simple Python format string which will be resolved using the following
156
+ keywords: ``url`` (the base url of the search endpoint), ``search`` (the query string corresponding
157
+ to the search request), ``items_per_page`` (the number of items to return per page),
158
+ ``skip`` (the number of items to skip) or ``skip_base_1`` (the number of items to skip,
159
+ starting from 1) and ``page`` (which page to return).
160
+ * :attr:`~eodag.config.PluginConfig.Pagination.total_items_nb_key_path` (``str``): An XPath or JsonPath
161
+ leading to the total number of results satisfying a request. This is used for providers which provides the
162
+ total results metadata along with the result of the query and don't have an endpoint for querying
163
+ the number of items satisfying a request, or for providers for which the count endpoint
164
+ returns a json or xml document
165
+ * :attr:`~eodag.config.PluginConfig.Pagination.count_endpoint` (``str``): The endpoint for counting the number
166
+ of items satisfying a request
167
+ * :attr:`~eodag.config.PluginConfig.Pagination.count_tpl` (``str``): template for the count parameter that
168
+ should be added to the search request
169
+ * :attr:`~eodag.config.PluginConfig.Pagination.next_page_url_key_path` (``str``): A JsonPath expression used
170
+ to retrieve the URL of the next page in the response of the current page.
171
+ * :attr:`~eodag.config.PluginConfig.Pagination.max_items_per_page` (``int``): The maximum number of items per
172
+ page that the provider can handle; default: ``50``
173
+ * :attr:`~eodag.config.PluginConfig.Pagination.start_page` (``int``): number of the first page; default: ``1``
174
+
175
+ * :attr:`~eodag.config.PluginConfig.discover_product_types`
176
+ (:class:`~eodag.config.PluginConfig.DiscoverProductTypes`): configuration for product type discovery based on
177
+ information from the provider; It contains the keys:
178
+
179
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.fetch_url` (``str``) (**mandatory**): url from which
180
+ the product types can be fetched
181
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.max_connections` (``int``): Maximum number of
182
+ connections for concurrent HTTP requests
183
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.result_type` (``str``): type of the provider result;
184
+ currently only ``json`` is supported (other types could be used in an extension of this plugin)
185
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.results_entry` (``str``) (**mandatory**): json path
186
+ to the list of product types
187
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.generic_product_type_id` (``str``): mapping for the
188
+ product type id
189
+ * :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
191
+ from the provider result
192
+ * :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
194
+ product type metadata
195
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_collection_fetch_url` (``str``): url to fetch
196
+ data for a single collection; used if product type metadata is not available from the endpoint given in
197
+ :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.fetch_url`
198
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_collection_fetch_qs` (``str``): query string
199
+ to be added to the :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.fetch_url` to filter for a
200
+ collection
201
+ * :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
203
+ :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_collection_fetch_url`.
204
+
205
+ * :attr:`~eodag.config.PluginConfig.sort` (:class:`~eodag.config.PluginConfig.Sort`): configuration for sorting
206
+ the results. It contains the keys:
207
+
208
+ * :attr:`~eodag.config.PluginConfig.Sort.sort_by_default` (``List[Tuple(str, Literal["ASC", "DESC"])]``):
209
+ parameter and sort order by which the result will be sorted by default (if the user does not enter a
210
+ ``sort_by`` parameter); if not given the result will use the default sorting of the provider; Attention:
211
+ for some providers sorting might cause a timeout if no filters are used. In that case no default
212
+ sort parameters should be given. The format is::
213
+
214
+ sort_by_default:
215
+ - !!python/tuple [<param>, <sort order> (ASC or DESC)]
216
+
217
+ * :attr:`~eodag.config.PluginConfig.Sort.sort_by_tpl` (``str``): template for the sort parameter that is added
218
+ to the request; It contains the parameters `sort_param` and `sort_order` which will be replaced by user
219
+ input or default value. If the parameters are added as query params to a GET request, the string
220
+ should start with ``&``, otherwise it should be a valid json string surrounded by ``{{ }}``.
221
+ * :attr:`~eodag.config.PluginConfig.Sort.sort_param_mapping` (``Dict [str, str]``): mapping for the parameters
222
+ available for sorting
223
+ * :attr:`~eodag.config.PluginConfig.Sort.sort_order_mapping`
224
+ (``Dict[Literal["ascending", "descending"], str]``): mapping for the sort order
225
+ * :attr:`~eodag.config.PluginConfig.Sort.max_sort_params` (``int``): maximum number of sort parameters
226
+ supported by the provider; used to validate the user input to avoid failed requests or unexpected behaviour
227
+ (not all parameters are used in the request)
228
+
229
+ * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``Dict[str, Any]``): The search plugins of this kind can
230
+ detect when a metadata mapping is "query-able", and get the semantics of how to format the query string
231
+ parameter that enables to make a query on the corresponding metadata. To make a metadata query-able,
232
+ just configure it in the metadata mapping to be a list of 2 items, the first one being the
233
+ specification of the query string search formatting. The later is a string following the
234
+ specification of Python string formatting, with a special behaviour added to it. For example,
235
+ an entry in the metadata mapping of this kind::
236
+
237
+ completionTimeFromAscendingNode:
238
+ - 'f=acquisition.endViewingDate:lte:{completionTimeFromAscendingNode#timestamp}'
239
+ - '$.properties.acquisition.endViewingDate'
240
+
241
+ means that the search url will have a query string parameter named ``f`` with a value of
242
+ ``acquisition.endViewingDate:lte:1543922280.0`` if the search was done with the value
243
+ of ``completionTimeFromAscendingNode`` being ``2018-12-04T12:18:00``. What happened is that
244
+ ``{completionTimeFromAscendingNode#timestamp}`` was replaced with the timestamp of the value
245
+ of ``completionTimeFromAscendingNode``. This example shows all there is to know about the
246
+ semantics of the query string formatting introduced by this plugin: any eodag search parameter
247
+ can be referenced in the query string with an additional optional conversion function that
248
+ is separated from it by a ``#`` (see :func:`~eodag.api.product.metadata_mapping.format_metadata` for further
249
+ details on the available converters). Note that for the values in the
250
+ :attr:`~eodag.config.PluginConfig.free_text_search_operations` configuration parameter follow the same rule.
251
+ If the metadata_mapping is not a list but only a string, this means that the parameters is not queryable but
252
+ it is included in the result obtained from the provider. The string indicates how the provider result should
253
+ be mapped to the eodag parameter.
254
+ * :attr:`~eodag.config.PluginConfig.discover_metadata` (:class:`~eodag.config.PluginConfig.DiscoverMetadata`):
255
+ configuration for the auto-discovery of queryable parameters as well as parameters returned by the provider
256
+ which are not in the metadata mapping. It has the attributes:
257
+
258
+ * :attr:`~eodag.config.PluginConfig.DiscoverMetadata.auto_discovery` (``bool``): if the automatic discovery of
259
+ metadata is activated; default: ``False``; if false, the other parameters are not used;
260
+ * :attr:`~eodag.config.PluginConfig.DiscoverMetadata.metadata_pattern` (``str``): regex string a parameter in
261
+ the result should match so that is used
262
+ * :attr:`~eodag.config.PluginConfig.DiscoverMetadata.search_param` (``Union [str, Dict[str, Any]]``): format
263
+ to add a query param given by the user and not in the metadata mapping to the requests, 'metadata' will be
264
+ replaced by the search param; can be a string or a dict containing
265
+ :attr:`~eodag.config.PluginConfig.free_text_search_operations`
266
+ (see :class:`~eodag.plugins.search.qssearch.ODataV4Search`)
267
+ * :attr:`~eodag.config.PluginConfig.DiscoverMetadata.metadata_path` (``str``): path where the queryable
268
+ properties can be found in the provider result
269
+
270
+ * :attr:`~eodag.config.PluginConfig.discover_queryables`
271
+ (:class:`~eodag.config.PluginConfig.DiscoverQueryables`): configuration to fetch the queryables from a
272
+ provider queryables endpoint; It has the following keys:
273
+
274
+ * :attr:`~eodag.config.PluginConfig.DiscoverQueryables.fetch_url` (``str``): url to fetch the queryables valid
275
+ for all product types
276
+ * :attr:`~eodag.config.PluginConfig.DiscoverQueryables.product_type_fetch_url` (``str``): url to fetch the
277
+ queryables for a specific product type
278
+ * :attr:`~eodag.config.PluginConfig.DiscoverQueryables.result_type` (``str``): type of the result (currently
279
+ only ``json`` is used)
280
+ * :attr:`~eodag.config.PluginConfig.DiscoverQueryables.results_entry` (``str``): json path to retrieve the
281
+ queryables from the provider result
282
+
283
+ * :attr:`~eodag.config.PluginConfig.constraints_file_url` (``str``): url to fetch the constraints for a specific
284
+ 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
+ * :attr:`~eodag.config.PluginConfig.constraints_entry` (``str``): key in the json result where the constraints
288
+ can be found; if not given, it is assumed that the constraints are on top level of the result, i.e.
289
+ 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``
203
292
  """
204
293
 
205
294
  extract_properties: Dict[str, Callable[..., Dict[str, Any]]] = {
@@ -362,17 +451,70 @@ class QueryStringSearch(Search):
362
451
  def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
363
452
  """Fetch product types list from provider using `discover_product_types` conf
364
453
 
454
+ :returns: configuration dict containing fetched product types information
455
+ """
456
+ unpaginated_fetch_url = self.config.discover_product_types.get("fetch_url")
457
+ if not unpaginated_fetch_url:
458
+ return None
459
+
460
+ # product types pagination
461
+ next_page_url_tpl = self.config.discover_product_types.get("next_page_url_tpl")
462
+ page = self.config.discover_product_types.get("start_page", 1)
463
+
464
+ if not next_page_url_tpl:
465
+ # no pagination
466
+ return self.discover_product_types_per_page(**kwargs)
467
+
468
+ conf_update_dict: Dict[str, Any] = {
469
+ "providers_config": {},
470
+ "product_types_config": {},
471
+ }
472
+
473
+ while True:
474
+ fetch_url = next_page_url_tpl.format(url=unpaginated_fetch_url, page=page)
475
+
476
+ conf_update_dict_per_page = self.discover_product_types_per_page(
477
+ fetch_url=fetch_url, **kwargs
478
+ )
479
+
480
+ if (
481
+ not conf_update_dict_per_page
482
+ or not conf_update_dict_per_page.get("providers_config")
483
+ or conf_update_dict_per_page.items() <= conf_update_dict.items()
484
+ ):
485
+ # conf_update_dict_per_page is empty or a subset on existing conf
486
+ break
487
+ else:
488
+ conf_update_dict["providers_config"].update(
489
+ conf_update_dict_per_page["providers_config"]
490
+ )
491
+ conf_update_dict["product_types_config"].update(
492
+ conf_update_dict_per_page["product_types_config"]
493
+ )
494
+
495
+ page += 1
496
+
497
+ return conf_update_dict
498
+
499
+ def discover_product_types_per_page(
500
+ self, **kwargs: Any
501
+ ) -> Optional[Dict[str, Any]]:
502
+ """Fetch product types list from provider using `discover_product_types` conf
503
+ using paginated ``kwargs["fetch_url"]``
504
+
365
505
  :returns: configuration dict containing fetched product types information
366
506
  """
367
507
  try:
368
508
  prep = PreparedSearch()
369
509
 
370
- prep.url = cast(
371
- str,
372
- self.config.discover_product_types["fetch_url"].format(
373
- **self.config.__dict__
374
- ),
375
- )
510
+ # url from discover_product_types() or conf
511
+ fetch_url: Optional[str] = kwargs.get("fetch_url")
512
+ if fetch_url is None:
513
+ if fetch_url := self.config.discover_product_types.get("fetch_url"):
514
+ fetch_url = fetch_url.format(**self.config.__dict__)
515
+ else:
516
+ return None
517
+ prep.url = fetch_url
376
518
 
377
519
  # get auth if available
378
520
  if "auth" in kwargs:
@@ -402,7 +544,14 @@ class QueryStringSearch(Search):
402
544
  "Skipping error while fetching product types for " "{} {} instance:"
403
545
  ).format(self.provider, self.__class__.__name__)
404
546
 
405
- response = QueryStringSearch._request(self, prep)
547
+ # Query using appropriate method
548
+ fetch_method = self.config.discover_product_types.get("fetch_method", "GET")
549
+ fetch_body = self.config.discover_product_types.get("fetch_body", {})
550
+ if fetch_method == "POST" and isinstance(self, PostJsonSearch):
551
+ prep.query_params = fetch_body
552
+ response = self._request(prep)
553
+ else:
554
+ response = QueryStringSearch._request(self, prep)
406
555
  except (RequestError, KeyError, AttributeError):
407
556
  return None
408
557
  else:
@@ -414,16 +563,21 @@ class QueryStringSearch(Search):
414
563
  if self.config.discover_product_types["result_type"] == "json":
415
564
  resp_as_json = response.json()
416
565
  # extract results from response json
417
- result = [
418
- match.value
419
- for match in self.config.discover_product_types[
420
- "results_entry"
421
- ].find(resp_as_json)
422
- ]
566
+ results_entry = self.config.discover_product_types["results_entry"]
567
+ if not isinstance(results_entry, JSONPath):
568
+ logger.warning(
569
+ f"Could not parse {self.provider} discover_product_types.results_entry"
570
+ f" as JSONPath: {results_entry}"
571
+ )
572
+ return None
573
+ result = [match.value for match in results_entry.find(resp_as_json)]
423
574
  if result and isinstance(result[0], list):
424
575
  result = result[0]
425
576
 
426
- for product_type_result in result:
577
+ def conf_update_from_product_type_result(
578
+ product_type_result: Dict[str, Any]
579
+ ) -> None:
580
+ """Update ``conf_update_dict`` using given product type json response"""
427
581
  # providers_config extraction
428
582
  extracted_mapping = properties_from_json(
429
583
  product_type_result,
@@ -510,6 +664,20 @@ class QueryStringSearch(Search):
510
664
  conf_update_dict["product_types_config"][
511
665
  generic_product_type_id
512
666
  ]["keywords"] = keywords_values_str
667
+
668
+ # runs concurrent requests and aggregate results in conf_update_dict
669
+ max_connections = self.config.discover_product_types.get(
670
+ "max_connections"
671
+ )
672
+ with concurrent.futures.ThreadPoolExecutor(
673
+ max_workers=max_connections
674
+ ) as executor:
675
+ futures = (
676
+ executor.submit(conf_update_from_product_type_result, r)
677
+ for r in result
678
+ )
679
+ [f.result() for f in concurrent.futures.as_completed(futures)]
680
+
513
681
  except KeyError as e:
514
682
  logger.warning(
515
683
  "Incomplete %s discover_product_types configuration: %s",
@@ -517,6 +685,12 @@ class QueryStringSearch(Search):
517
685
  e,
518
686
  )
519
687
  return None
688
+ except requests.RequestException as e:
689
+ logger.debug(
690
+ "Could not parse discovered product types response from "
691
+ f"{self.provider}, {type(e).__name__}: {e.args}"
692
+ )
693
+ return None
520
694
  conf_update_dict["product_types_config"] = dict_items_recursive_apply(
521
695
  conf_update_dict["product_types_config"],
522
696
  lambda k, v: v if v != NOT_AVAILABLE else None,
@@ -538,9 +712,7 @@ class QueryStringSearch(Search):
538
712
  self,
539
713
  PreparedSearch(
540
714
  url=single_collection_url,
541
- info_message="Fetching data for product type product type: {}".format(
542
- product_type
543
- ),
715
+ info_message=f"Fetching data for product type: {product_type}",
544
716
  exception_message="Skipping error while fetching product types for "
545
717
  "{} {} instance:".format(self.provider, self.__class__.__name__),
546
718
  ),
@@ -703,9 +875,6 @@ class QueryStringSearch(Search):
703
875
  }
704
876
  )
705
877
 
706
- if product_type is None:
707
- raise ValidationError("Required productType is missing")
708
-
709
878
  qp, qs = self.build_query_string(product_type, **keywords)
710
879
 
711
880
  prep.query_params = qp
@@ -825,7 +994,7 @@ class QueryStringSearch(Search):
825
994
  else:
826
995
  next_url = "{}?{}".format(search_endpoint, qs_with_sort)
827
996
  urls.append(next_url)
828
- return urls, total_results
997
+ return list(dict.fromkeys(urls)), total_results
829
998
 
830
999
  def do_search(
831
1000
  self, prep: PreparedSearch = PreparedSearch(items_per_page=None), **kwargs: Any
@@ -854,8 +1023,8 @@ class QueryStringSearch(Search):
854
1023
  search_url
855
1024
  )
856
1025
  single_search_prep.exception_message = (
857
- "Skipping error while searching for {} {} "
858
- "instance:".format(self.provider, self.__class__.__name__)
1026
+ f"Skipping error while searching for {self.provider}"
1027
+ f" {self.__class__.__name__} instance"
859
1028
  )
860
1029
  response = self._request(single_search_prep)
861
1030
  next_page_url_key_path = self.config.pagination.get(
@@ -1114,6 +1283,14 @@ class QueryStringSearch(Search):
1114
1283
  timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
1115
1284
  ssl_verify = getattr(self.config, "ssl_verify", True)
1116
1285
 
1286
+ retry_total = getattr(self.config, "retry_total", REQ_RETRY_TOTAL)
1287
+ retry_backoff_factor = getattr(
1288
+ self.config, "retry_backoff_factor", REQ_RETRY_BACKOFF_FACTOR
1289
+ )
1290
+ retry_status_forcelist = getattr(
1291
+ self.config, "retry_status_forcelist", REQ_RETRY_STATUS_FORCELIST
1292
+ )
1293
+
1117
1294
  ssl_ctx = get_ssl_context(ssl_verify)
1118
1295
  # auth if needed
1119
1296
  kwargs: Dict[str, Any] = {}
@@ -1152,7 +1329,16 @@ class QueryStringSearch(Search):
1152
1329
  else:
1153
1330
  if info_message:
1154
1331
  logger.info(info_message)
1155
- response = requests.get(
1332
+
1333
+ session = requests.Session()
1334
+ retries = Retry(
1335
+ total=retry_total,
1336
+ backoff_factor=retry_backoff_factor,
1337
+ status_forcelist=retry_status_forcelist,
1338
+ )
1339
+ session.mount(url, HTTPAdapter(max_retries=retries))
1340
+
1341
+ response = session.get(
1156
1342
  url,
1157
1343
  timeout=timeout,
1158
1344
  headers=USER_AGENT,
@@ -1174,13 +1360,54 @@ class QueryStringSearch(Search):
1174
1360
  self.__class__.__name__,
1175
1361
  err_msg,
1176
1362
  )
1177
- raise RequestError(str(err))
1363
+ raise RequestError.from_error(err, exception_message) from err
1178
1364
  return response
1179
1365
 
1180
1366
 
1181
1367
  class ODataV4Search(QueryStringSearch):
1182
- """A specialisation of a QueryStringSearch that does a two step search to retrieve
1183
- all products metadata"""
1368
+ """A specialisation of a :class:`~eodag.plugins.search.qssearch.QueryStringSearch` that does a two step search to
1369
+ retrieve all products metadata. All configuration parameters of
1370
+ :class:`~eodag.plugins.search.qssearch.QueryStringSearch` are also available for this plugin. In addition, the
1371
+ following parameters can be configured:
1372
+
1373
+ :param provider: provider name
1374
+ :param config: Search plugin configuration:
1375
+
1376
+ * :attr:`~eodag.config.PluginConfig.per_product_metadata_query` (``bool``): should be set to true if the metadata
1377
+ is not given in the search result and a two step search has to be performed; default: false
1378
+ * :attr:`~eodag.config.PluginConfig.metadata_pre_mapping` (:class:`~eodag.config.PluginConfig.MetadataPreMapping`)
1379
+ : a dictionary which can be used to simplify further metadata extraction. For example, going from
1380
+ ``$.Metadata[?(@.id="foo")].value`` to ``$.Metadata.foo.value``. It has the keys:
1381
+
1382
+ * :attr:`~eodag.config.PluginConfig.MetadataPreMapping.metadata_path` (``str``): json path of the metadata entry
1383
+ * :attr:`~eodag.config.PluginConfig.MetadataPreMapping.metadata_path_id` (``str``): key to get the metadata id
1384
+ * :attr:`~eodag.config.PluginConfig.MetadataPreMapping.metadata_path_value` (``str``): key to get the metadata
1385
+ value
1386
+
1387
+ * :attr:`~eodag.config.PluginConfig.free_text_search_operations`: (optional) A tree structure of the form::
1388
+
1389
+ # noqa: E800
1390
+ <search-param>: # e.g: $search
1391
+ union: # how to join the operations below (e.g: ' AND ' -->
1392
+ # '(op1 AND op2) AND (op3 OR op4)')
1393
+ wrapper: # a pattern for how each operation will be wrapped
1394
+ # (e.g: '({})' --> '(op1 AND op2)')
1395
+ operations: # The operations to build
1396
+ <opname>: # e.g: AND
1397
+ - <op1> # e.g:
1398
+ # 'sensingStartDate:[{startTimeFromAscendingNode}Z TO *]'
1399
+ - <op2> # e.g:
1400
+ # 'sensingStopDate:[* TO {completionTimeFromAscendingNode}Z]'
1401
+ ...
1402
+ ...
1403
+ ...
1404
+
1405
+ With the structure above, each operation will become a string of the form:
1406
+ ``(<op1> <opname> <op2>)``, then the operations will be joined together using
1407
+ the union string and finally if the number of operations is greater than 1,
1408
+ they will be wrapped as specified by the wrapper config key.
1409
+
1410
+ """
1184
1411
 
1185
1412
  def __init__(self, provider: str, config: PluginConfig) -> None:
1186
1413
  super(ODataV4Search, self).__init__(provider, config)
@@ -1219,7 +1446,7 @@ class ODataV4Search(QueryStringSearch):
1219
1446
  raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
1220
1447
  except requests.RequestException:
1221
1448
  logger.exception(
1222
- "Skipping error while searching for %s %s instance:",
1449
+ "Skipping error while searching for %s %s instance",
1223
1450
  self.provider,
1224
1451
  self.__class__.__name__,
1225
1452
  )
@@ -1271,7 +1498,106 @@ class ODataV4Search(QueryStringSearch):
1271
1498
 
1272
1499
 
1273
1500
  class PostJsonSearch(QueryStringSearch):
1274
- """A specialisation of a QueryStringSearch that uses POST method"""
1501
+ """A specialisation of a :class:`~eodag.plugins.search.qssearch.QueryStringSearch` that uses POST method
1502
+
1503
+ All configuration parameters available for :class:`~eodag.plugins.search.qssearch.QueryStringSearch`
1504
+ are also available for PostJsonSearch. The mappings given in metadata_mapping are used to construct
1505
+ a (json) body for the POST request that is sent to the provider. Due to the fact that we sent a POST request and
1506
+ not a get request, the pagination configuration will look slightly different. It has the
1507
+ following parameters:
1508
+
1509
+ :param provider: provider name
1510
+ :param config: Search plugin configuration:
1511
+
1512
+ * :attr:`~eodag.config.PluginConfig.Pagination.next_page_query_obj` (``str``): The additional parameters
1513
+ needed to add pagination information to the search request. These parameters won't be
1514
+ included in result. This must be a json dict formatted like ``{{"foo":"bar"}}`` because
1515
+ it will be passed to a :meth:`str.format` method before being loaded as json.
1516
+ * :attr:`~eodag.config.PluginConfig.Pagination.total_items_nb_key_path` (``str``): An XPath or JsonPath
1517
+ leading to the total number of results satisfying a request. This is used for providers
1518
+ which provides the total results metadata along with the result of the query and don't
1519
+ have an endpoint for querying the number of items satisfying a request, or for providers
1520
+ for which the count endpoint returns a json or xml document
1521
+ * :attr:`~eodag.config.PluginConfig.Pagination.max_items_per_page` (``int``): The maximum number of items
1522
+ per page that the provider can handle; default: ``50``
1523
+
1524
+ """
1525
+
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
+ )
1275
1601
 
1276
1602
  def query(
1277
1603
  self,
@@ -1322,6 +1648,8 @@ class PostJsonSearch(QueryStringSearch):
1322
1648
  and isinstance(self.config.metadata_mapping[k], list)
1323
1649
  }
1324
1650
  )
1651
+ if getattr(self.config, "dates_required", False):
1652
+ self._check_date_params(keywords, product_type)
1325
1653
 
1326
1654
  qp, _ = self.build_query_string(product_type, **keywords)
1327
1655
 
@@ -1503,7 +1831,7 @@ class PostJsonSearch(QueryStringSearch):
1503
1831
  )
1504
1832
 
1505
1833
  urls.append(search_endpoint)
1506
- return urls, total_results
1834
+ return list(dict.fromkeys(urls)), total_results
1507
1835
 
1508
1836
  def _request(
1509
1837
  self,
@@ -1537,8 +1865,14 @@ class PostJsonSearch(QueryStringSearch):
1537
1865
  prep.query_params = self.next_page_query_obj
1538
1866
  if info_message:
1539
1867
  logger.info(info_message)
1540
- logger.debug("Query parameters: %s" % prep.query_params)
1541
- logger.debug("Query kwargs: %s" % kwargs)
1868
+ try:
1869
+ logger.debug("Query parameters: %s" % geojson.dumps(prep.query_params))
1870
+ except TypeError:
1871
+ logger.debug("Query parameters: %s" % prep.query_params)
1872
+ try:
1873
+ logger.debug("Query kwargs: %s" % geojson.dumps(kwargs))
1874
+ except TypeError:
1875
+ logger.debug("Query kwargs: %s" % kwargs)
1542
1876
  response = requests.post(
1543
1877
  url,
1544
1878
  json=prep.query_params,
@@ -1551,22 +1885,16 @@ class PostJsonSearch(QueryStringSearch):
1551
1885
  except requests.exceptions.Timeout as exc:
1552
1886
  raise TimeOutError(exc, timeout=timeout) from exc
1553
1887
  except (requests.RequestException, URLError) as err:
1888
+ response = locals().get("response", Response())
1554
1889
  # check if error is identified as auth_error in provider conf
1555
1890
  auth_errors = getattr(self.config, "auth_error_code", [None])
1556
1891
  if not isinstance(auth_errors, list):
1557
1892
  auth_errors = [auth_errors]
1558
- if (
1559
- hasattr(err, "response")
1560
- and err.response is not None
1561
- and getattr(err.response, "status_code", None)
1562
- and err.response.status_code in auth_errors
1563
- ):
1893
+ if response.status_code and response.status_code in auth_errors:
1564
1894
  raise AuthenticationError(
1565
- "HTTP Error {} returned:\n{}\nPlease check your credentials for {}".format(
1566
- err.response.status_code,
1567
- err.response.text.strip(),
1568
- self.provider,
1569
- )
1895
+ f"Please check your credentials for {self.provider}.",
1896
+ f"HTTP Error {response.status_code} returned.",
1897
+ response.text.strip(),
1570
1898
  )
1571
1899
  if exception_message:
1572
1900
  logger.exception(exception_message)
@@ -1577,21 +1905,23 @@ class PostJsonSearch(QueryStringSearch):
1577
1905
  self.provider,
1578
1906
  self.__class__.__name__,
1579
1907
  )
1580
- if "response" in locals():
1581
- logger.debug(response.content)
1582
- error_text = str(err)
1583
- if (
1584
- hasattr(err, "response")
1585
- and err.response is not None
1586
- and getattr(err.response, "text", None)
1587
- ):
1588
- error_text = err.response.text
1589
- raise RequestError(error_text) from err
1908
+ logger.debug(response.content or str(err))
1909
+ raise RequestError.from_error(err, exception_message) from err
1590
1910
  return response
1591
1911
 
1592
1912
 
1593
1913
  class StacSearch(PostJsonSearch):
1594
- """A specialisation of a QueryStringSearch that uses generic STAC configuration"""
1914
+ """A specialisation of :class:`~eodag.plugins.search.qssearch.PostJsonSearch` that uses generic
1915
+ STAC configuration, it therefore has the same configuration parameters (those inherited
1916
+ from :class:`~eodag.plugins.search.qssearch.QueryStringSearch`).
1917
+ For providers using ``StacSearch`` default values are defined for most of the parameters
1918
+ (see ``stac_provider.yml``). If some parameters are different for a specific provider, they
1919
+ have to be overwritten. If certain functionalities are not available, their configuration
1920
+ parameters have to be overwritten with ``null``. E.g. if there is no queryables endpoint,
1921
+ the :attr:`~eodag.config.PluginConfig.DiscoverQueryables.fetch_url` and
1922
+ :attr:`~eodag.config.PluginConfig.DiscoverQueryables.product_type_fetch_url` in the
1923
+ :attr:`~eodag.config.PluginConfig.discover_queryables` config have to be set to ``null``.
1924
+ """
1595
1925
 
1596
1926
  def __init__(self, provider: str, config: PluginConfig) -> None:
1597
1927
  # backup results_entry overwritten by init
@@ -1637,12 +1967,35 @@ class StacSearch(PostJsonSearch):
1637
1967
  arguments)
1638
1968
  :returns: fetched queryable parameters dict
1639
1969
  """
1970
+ if (
1971
+ not self.config.discover_queryables["fetch_url"]
1972
+ and not self.config.discover_queryables["product_type_fetch_url"]
1973
+ ):
1974
+ logger.info(f"Cannot fetch queryables with {self.provider}")
1975
+ return None
1976
+
1640
1977
  product_type = kwargs.get("productType", None)
1641
1978
  provider_product_type = (
1642
1979
  self.config.products.get(product_type, {}).get("productType", product_type)
1643
1980
  if product_type
1644
1981
  else None
1645
1982
  )
1983
+ if (
1984
+ provider_product_type
1985
+ and not self.config.discover_queryables["product_type_fetch_url"]
1986
+ ):
1987
+ logger.info(
1988
+ f"Cannot fetch queryables for a specific product type with {self.provider}"
1989
+ )
1990
+ return None
1991
+ if (
1992
+ not provider_product_type
1993
+ and not self.config.discover_queryables["fetch_url"]
1994
+ ):
1995
+ logger.info(
1996
+ f"Cannot fetch global queryables with {self.provider}. A product type must be specified"
1997
+ )
1998
+ return None
1646
1999
 
1647
2000
  try:
1648
2001
  unparsed_fetch_url = (
@@ -1650,14 +2003,22 @@ class StacSearch(PostJsonSearch):
1650
2003
  if provider_product_type
1651
2004
  else self.config.discover_queryables["fetch_url"]
1652
2005
  )
2006
+ if unparsed_fetch_url is None:
2007
+ return None
1653
2008
 
1654
2009
  fetch_url = unparsed_fetch_url.format(
1655
2010
  provider_product_type=provider_product_type, **self.config.__dict__
1656
2011
  )
2012
+ auth = (
2013
+ self.auth
2014
+ if hasattr(self, "auth") and isinstance(self.auth, AuthBase)
2015
+ else None
2016
+ )
1657
2017
  response = QueryStringSearch._request(
1658
2018
  self,
1659
2019
  PreparedSearch(
1660
2020
  url=fetch_url,
2021
+ auth=auth,
1661
2022
  info_message="Fetching queryables: {}".format(fetch_url),
1662
2023
  exception_message="Skipping error while fetching queryables for "
1663
2024
  "{} {} instance:".format(self.provider, self.__class__.__name__),
@@ -1671,11 +2032,15 @@ class StacSearch(PostJsonSearch):
1671
2032
  resp_as_json = response.json()
1672
2033
 
1673
2034
  # extract results from response json
1674
- json_queryables = [
1675
- match.value
1676
- for match in self.config.discover_queryables["results_entry"].find(
1677
- resp_as_json
2035
+ results_entry = self.config.discover_queryables["results_entry"]
2036
+ if not isinstance(results_entry, JSONPath):
2037
+ logger.warning(
2038
+ f"Could not parse {self.provider} discover_queryables.results_entry"
2039
+ f" as JSONPath: {results_entry}"
1678
2040
  )
2041
+ return None
2042
+ json_queryables = [
2043
+ match.value for match in results_entry.find(resp_as_json)
1679
2044
  ][0]
1680
2045
 
1681
2046
  except KeyError as e:
@@ -1709,3 +2074,18 @@ class StacSearch(PostJsonSearch):
1709
2074
  python_queryables = create_model("m", **field_definitions).model_fields
1710
2075
 
1711
2076
  return model_fields_to_annotated(python_queryables)
2077
+
2078
+
2079
+ class PostJsonSearchWithStacQueryables(StacSearch, PostJsonSearch):
2080
+ """A specialisation of a :class:`~eodag.plugins.search.qssearch.PostJsonSearch` that uses
2081
+ generic STAC configuration for queryables (inherited from :class:`~eodag.plugins.search.qssearch.StacSearch`).
2082
+ """
2083
+
2084
+ def __init__(self, provider: str, config: PluginConfig) -> None:
2085
+ PostJsonSearch.__init__(self, provider, config)
2086
+
2087
+ def build_query_string(
2088
+ self, product_type: str, **kwargs: Any
2089
+ ) -> Tuple[Dict[str, Any], str]:
2090
+ """Build The query string using the search parameters"""
2091
+ return PostJsonSearch.build_query_string(self, product_type, **kwargs)