eodag 2.12.0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -165
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.0.dist-info/RECORD +0 -94
  92. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
@@ -19,17 +19,48 @@ from __future__ import annotations
19
19
 
20
20
  import hashlib
21
21
  import logging
22
- from typing import Any, Dict, List, Optional, Tuple
22
+ from datetime import datetime, timedelta, timezone
23
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, cast
23
24
  from urllib.parse import quote_plus, unquote_plus
24
25
 
25
26
  import geojson
26
27
  import orjson
27
- from jsonpath_ng import Fields
28
+ from dateutil.parser import isoparse
29
+ from dateutil.tz import tzutc
30
+ from jsonpath_ng import Child, Fields, Root
31
+ from pydantic import create_model
32
+ from pydantic.fields import FieldInfo
33
+ from typing_extensions import get_args
28
34
 
29
35
  from eodag.api.product import EOProduct
30
- from eodag.api.product.metadata_mapping import properties_from_json
36
+ from eodag.api.product.metadata_mapping import (
37
+ NOT_AVAILABLE,
38
+ NOT_MAPPED,
39
+ get_queryable_from_provider,
40
+ mtd_cfg_as_conversion_and_querypath,
41
+ properties_from_json,
42
+ )
43
+ from eodag.api.search_result import RawSearchResult
44
+ from eodag.plugins.search import PreparedSearch
45
+ from eodag.plugins.search.base import Search
31
46
  from eodag.plugins.search.qssearch import PostJsonSearch
32
- from eodag.utils import dict_items_recursive_sort
47
+ from eodag.types import json_field_definition_to_python, model_fields_to_annotated
48
+ from eodag.types.queryables import CommonQueryables
49
+ from eodag.utils import (
50
+ DEFAULT_MISSION_START_DATE,
51
+ Annotated,
52
+ deepcopy,
53
+ dict_items_recursive_sort,
54
+ get_geometry_from_various,
55
+ )
56
+ from eodag.utils.constraints import (
57
+ fetch_constraints,
58
+ get_constraint_queryables_with_additional_params,
59
+ )
60
+ from eodag.utils.exceptions import ValidationError
61
+
62
+ if TYPE_CHECKING:
63
+ from eodag.config import PluginConfig
33
64
 
34
65
  logger = logging.getLogger("eodag.search.build_search_result")
35
66
 
@@ -55,9 +86,7 @@ class BuildPostSearchResult(PostJsonSearch):
55
86
  method before being loaded as json.
56
87
 
57
88
  :param provider: An eodag providers configuration dictionary
58
- :type provider: dict
59
89
  :param config: Path to the user configuration file
60
- :type config: str
61
90
  """
62
91
 
63
92
  def count_hits(
@@ -68,39 +97,35 @@ class BuildPostSearchResult(PostJsonSearch):
68
97
 
69
98
  def collect_search_urls(
70
99
  self,
71
- page: Optional[int] = None,
72
- items_per_page: Optional[int] = None,
73
- count: bool = True,
100
+ prep: PreparedSearch = PreparedSearch(),
74
101
  **kwargs: Any,
75
102
  ) -> Tuple[List[str], int]:
76
103
  """Wraps PostJsonSearch.collect_search_urls to force product count to 1"""
77
- urls, _ = super(BuildPostSearchResult, self).collect_search_urls(
78
- page=page, items_per_page=items_per_page, count=count, **kwargs
79
- )
104
+ urls, _ = super(BuildPostSearchResult, self).collect_search_urls(prep, **kwargs)
80
105
  return urls, 1
81
106
 
82
- def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
107
+ def do_search(
108
+ self, prep: PreparedSearch = PreparedSearch(items_per_page=None), **kwargs: Any
109
+ ) -> List[Dict[str, Any]]:
83
110
  """Perform the actual search request, and return result in a single element."""
84
- search_url = self.search_urls[0]
85
- response = self._request(
86
- search_url,
87
- info_message=f"Sending search request: {search_url}",
88
- exception_message=f"Skipping error while searching for {self.provider} "
89
- f"{self.__class__.__name__} instance:",
111
+ prep.url = prep.search_urls[0]
112
+ prep.info_message = f"Sending search request: {prep.url}"
113
+ prep.exception_message = (
114
+ f"Skipping error while searching for {self.provider}"
115
+ f" {self.__class__.__name__} instance"
90
116
  )
117
+ response = self._request(prep)
118
+
91
119
  return [response.json()]
92
120
 
93
121
  def normalize_results(
94
- self, results: List[Dict[str, Any]], **kwargs: Any
122
+ self, results: RawSearchResult, **kwargs: Any
95
123
  ) -> List[EOProduct]:
96
124
  """Build :class:`~eodag.api.product._product.EOProduct` from provider result
97
125
 
98
126
  :param results: Raw provider result as single dict in list
99
- :type results: list
100
127
  :param kwargs: Search arguments
101
- :type kwargs: Union[int, str, bool, dict, list]
102
128
  :returns: list of single :class:`~eodag.api.product._product.EOProduct`
103
- :rtype: list
104
129
  """
105
130
  product_type = kwargs.get("productType")
106
131
 
@@ -110,22 +135,20 @@ class BuildPostSearchResult(PostJsonSearch):
110
135
  _dc_qs = kwargs.pop("_dc_qs", None)
111
136
  if _dc_qs is not None:
112
137
  qs = unquote_plus(unquote_plus(_dc_qs))
113
- unpaginated_query_params = sorted_unpaginated_query_params = geojson.loads(
114
- qs
115
- )
138
+ sorted_unpaginated_query_params = geojson.loads(qs)
116
139
  else:
117
140
  # update result with query parameters without pagination (or search-only params)
118
141
  if isinstance(
119
142
  self.config.pagination["next_page_query_obj"], str
120
- ) and hasattr(self, "query_params_unpaginated"):
121
- unpaginated_query_params = self.query_params_unpaginated
143
+ ) and hasattr(results, "query_params_unpaginated"):
144
+ unpaginated_query_params = results.query_params_unpaginated
122
145
  elif isinstance(self.config.pagination["next_page_query_obj"], str):
123
146
  next_page_query_obj = orjson.loads(
124
147
  self.config.pagination["next_page_query_obj"].format()
125
148
  )
126
149
  unpaginated_query_params = {
127
150
  k: v[0] if (isinstance(v, list) and len(v) == 1) else v
128
- for k, v in self.query_params.items()
151
+ for k, v in results.query_params.items()
129
152
  if (k, v) not in next_page_query_obj.items()
130
153
  }
131
154
  else:
@@ -135,14 +158,25 @@ class BuildPostSearchResult(PostJsonSearch):
135
158
  sorted_unpaginated_query_params = dict_items_recursive_sort(
136
159
  unpaginated_query_params
137
160
  )
138
- qs = geojson.dumps(sorted_unpaginated_query_params)
139
161
 
140
- query_hash = hashlib.sha1(str(qs).encode("UTF-8")).hexdigest()
162
+ # use all available query_params to parse properties
163
+ result = dict(
164
+ result,
165
+ **sorted_unpaginated_query_params,
166
+ qs=sorted_unpaginated_query_params,
167
+ )
168
+
169
+ # remove unwanted query params
170
+ for param in getattr(self.config, "remove_from_query", []):
171
+ sorted_unpaginated_query_params.pop(param, None)
141
172
 
142
- result = dict(result, **unpaginated_query_params)
173
+ qs = geojson.dumps(sorted_unpaginated_query_params)
174
+
175
+ query_hash = hashlib.sha1(str(qs).encode("UTF-8")).hexdigest()
143
176
 
144
- # update result with search args if not None (and not auth)
177
+ # update result with product_type_def_params and search args if not None (and not auth)
145
178
  kwargs.pop("auth", None)
179
+ result.update(results.product_type_def_params)
146
180
  result = dict(result, **{k: v for k, v in kwargs.items() if v is not None})
147
181
 
148
182
  # parse porperties
@@ -157,22 +191,28 @@ class BuildPostSearchResult(PostJsonSearch):
157
191
 
158
192
  # build product id
159
193
  id_prefix = (product_type or self.provider).upper()
160
- product_id = "%s_%s_%s" % (
194
+ product_id = "%s_%s_%s_%s" % (
161
195
  id_prefix,
162
196
  parsed_properties["startTimeFromAscendingNode"]
163
197
  .split("T")[0]
164
198
  .replace("-", ""),
199
+ parsed_properties["completionTimeFromAscendingNode"]
200
+ .split("T")[0]
201
+ .replace("-", ""),
165
202
  query_hash,
166
203
  )
167
204
  parsed_properties["id"] = parsed_properties["title"] = product_id
168
205
 
169
- # update downloadLink
170
- parsed_properties["downloadLink"] += f"?{qs}"
206
+ # update downloadLink and orderLink
171
207
  parsed_properties["_dc_qs"] = quote_plus(qs)
208
+ if parsed_properties["downloadLink"] != "Not Available":
209
+ parsed_properties["downloadLink"] += f"?{qs}"
172
210
 
173
211
  # parse metadata needing downloadLink
212
+ dl_path = Fields("downloadLink")
213
+ dl_path_from_root = Child(Root(), dl_path)
174
214
  for param, mapping in self.config.metadata_mapping.items():
175
- if Fields("downloadLink") in mapping:
215
+ if dl_path in mapping or dl_path_from_root in mapping:
176
216
  parsed_properties.update(
177
217
  properties_from_json(parsed_properties, {param: mapping})
178
218
  )
@@ -192,3 +232,308 @@ class BuildPostSearchResult(PostJsonSearch):
192
232
  return [
193
233
  product,
194
234
  ]
235
+
236
+
237
+ class BuildSearchResult(BuildPostSearchResult):
238
+ """BuildSearchResult search plugin.
239
+
240
+ This plugin builds a single :class:`~eodag.api.search_result.SearchResult` object
241
+ using given query parameters as product properties.
242
+
243
+ The available configuration parameters inherits from parent classes, with particularly
244
+ for this plugin:
245
+
246
+ - **end_date_excluded**: Set to `False` if provider does not include end date to
247
+ search
248
+
249
+ - **remove_from_query**: List of parameters used to parse metadata but that must
250
+ not be included to the query
251
+
252
+ - **constraints_file_url**: url of the constraint file used to build queryables
253
+
254
+ :param provider: An eodag providers configuration dictionary
255
+ :param config: Path to the user configuration file
256
+ """
257
+
258
+ def __init__(self, provider: str, config: PluginConfig) -> None:
259
+ # init self.config.metadata_mapping using Search Base plugin
260
+ Search.__init__(self, provider, config)
261
+
262
+ self.config.__dict__.setdefault("api_endpoint", "")
263
+
264
+ # needed by QueryStringSearch.build_query_string / format_free_text_search
265
+ self.config.__dict__.setdefault("free_text_search_operations", {})
266
+ # needed for compatibility
267
+ self.config.__dict__.setdefault("pagination", {"next_page_query_obj": "{{}}"})
268
+
269
+ # parse jsonpath on init: product type specific metadata-mapping
270
+ for product_type in self.config.products.keys():
271
+ if "metadata_mapping" in self.config.products[product_type].keys():
272
+ self.config.products[product_type][
273
+ "metadata_mapping"
274
+ ] = mtd_cfg_as_conversion_and_querypath(
275
+ self.config.products[product_type]["metadata_mapping"]
276
+ )
277
+ # Complete and ready to use product type specific metadata-mapping
278
+ product_type_metadata_mapping = deepcopy(self.config.metadata_mapping)
279
+
280
+ # update config using provider product type definition metadata_mapping
281
+ # from another product
282
+ other_product_for_mapping = cast(
283
+ str,
284
+ self.config.products[product_type].get(
285
+ "metadata_mapping_from_product", ""
286
+ ),
287
+ )
288
+ if other_product_for_mapping:
289
+ other_product_type_def_params = self.get_product_type_def_params(
290
+ other_product_for_mapping,
291
+ )
292
+ product_type_metadata_mapping.update(
293
+ other_product_type_def_params.get("metadata_mapping", {})
294
+ )
295
+ # from current product
296
+ product_type_metadata_mapping.update(
297
+ self.config.products[product_type]["metadata_mapping"]
298
+ )
299
+
300
+ self.config.products[product_type][
301
+ "metadata_mapping"
302
+ ] = product_type_metadata_mapping
303
+
304
+ def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
305
+ """Should perform the actual search request."""
306
+ return [{}]
307
+
308
+ def query(
309
+ self,
310
+ prep: PreparedSearch = PreparedSearch(),
311
+ **kwargs: Any,
312
+ ) -> Tuple[List[EOProduct], Optional[int]]:
313
+ """Build ready-to-download SearchResult"""
314
+
315
+ self._preprocess_search_params(kwargs)
316
+
317
+ return BuildPostSearchResult.query(self, prep, **kwargs)
318
+
319
+ def clear(self) -> None:
320
+ """Clear search context"""
321
+ pass
322
+
323
+ def build_query_string(
324
+ self, product_type: str, **kwargs: Any
325
+ ) -> Tuple[Dict[str, Any], str]:
326
+ """Build The query string using the search parameters"""
327
+ # parse kwargs as properties as they might be needed to build the query
328
+ parsed_properties = properties_from_json(
329
+ kwargs,
330
+ self.config.metadata_mapping,
331
+ )
332
+ available_properties = {
333
+ k: v
334
+ for k, v in parsed_properties.items()
335
+ if v not in [NOT_AVAILABLE, NOT_MAPPED]
336
+ }
337
+
338
+ # build and return the query
339
+ return BuildPostSearchResult.build_query_string(
340
+ self, product_type=product_type, **available_properties
341
+ )
342
+
343
+ def _preprocess_search_params(self, params: Dict[str, Any]) -> None:
344
+ """Preprocess search parameters before making a request to the CDS API.
345
+
346
+ This method is responsible for checking and updating the provided search parameters
347
+ to ensure that required parameters like 'productType', 'startTimeFromAscendingNode',
348
+ 'completionTimeFromAscendingNode', and 'geometry' are properly set. If not specified
349
+ in the input parameters, default values or values from the configuration are used.
350
+
351
+ :param params: Search parameters to be preprocessed.
352
+ """
353
+ _dc_qs = params.get("_dc_qs", None)
354
+ if _dc_qs is not None:
355
+ # if available, update search params using datacube query-string
356
+ _dc_qp = geojson.loads(unquote_plus(unquote_plus(_dc_qs)))
357
+ if "/to/" in _dc_qp.get("date", ""):
358
+ (
359
+ params["startTimeFromAscendingNode"],
360
+ params["completionTimeFromAscendingNode"],
361
+ ) = _dc_qp["date"].split("/to/")
362
+ elif "/" in _dc_qp.get("date", ""):
363
+ (
364
+ params["startTimeFromAscendingNode"],
365
+ params["completionTimeFromAscendingNode"],
366
+ ) = _dc_qp["date"].split("/")
367
+ elif _dc_qp.get("date", None):
368
+ params["startTimeFromAscendingNode"] = params[
369
+ "completionTimeFromAscendingNode"
370
+ ] = _dc_qp["date"]
371
+
372
+ if "/" in _dc_qp.get("area", ""):
373
+ params["geometry"] = _dc_qp["area"].split("/")
374
+
375
+ non_none_params = {k: v for k, v in params.items() if v}
376
+
377
+ # productType
378
+ dataset = params.get("dataset", None)
379
+ params["productType"] = non_none_params.get("productType", dataset)
380
+
381
+ # dates
382
+ mission_start_dt = datetime.fromisoformat(
383
+ self.get_product_type_cfg_value(
384
+ "missionStartDate", DEFAULT_MISSION_START_DATE
385
+ ).replace(
386
+ "Z", "+00:00"
387
+ ) # before 3.11
388
+ )
389
+
390
+ default_end_from_cfg = self.config.products.get(params["productType"], {}).get(
391
+ "_default_end_date", None
392
+ )
393
+ default_end_str = (
394
+ default_end_from_cfg
395
+ or (
396
+ datetime.now(timezone.utc)
397
+ if params.get("startTimeFromAscendingNode")
398
+ else mission_start_dt + timedelta(days=1)
399
+ ).isoformat()
400
+ )
401
+
402
+ params["startTimeFromAscendingNode"] = non_none_params.get(
403
+ "startTimeFromAscendingNode", mission_start_dt.isoformat()
404
+ )
405
+ params["completionTimeFromAscendingNode"] = non_none_params.get(
406
+ "completionTimeFromAscendingNode", default_end_str
407
+ )
408
+
409
+ # adapt end date if it is midnight
410
+ end_date_excluded = getattr(self.config, "end_date_excluded", True)
411
+ is_datetime = True
412
+ try:
413
+ end_date = datetime.strptime(
414
+ params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%SZ"
415
+ )
416
+ end_date = end_date.replace(tzinfo=tzutc())
417
+ except ValueError:
418
+ try:
419
+ end_date = datetime.strptime(
420
+ params["completionTimeFromAscendingNode"], "%Y-%m-%dT%H:%M:%S.%fZ"
421
+ )
422
+ end_date = end_date.replace(tzinfo=tzutc())
423
+ except ValueError:
424
+ end_date = isoparse(params["completionTimeFromAscendingNode"])
425
+ is_datetime = False
426
+ start_date = isoparse(params["startTimeFromAscendingNode"])
427
+ if (
428
+ not end_date_excluded
429
+ and is_datetime
430
+ and end_date > start_date
431
+ and end_date == end_date.replace(hour=0, minute=0, second=0, microsecond=0)
432
+ ):
433
+ end_date += timedelta(days=-1)
434
+ params["completionTimeFromAscendingNode"] = end_date.isoformat()
435
+
436
+ # geometry
437
+ if "geometry" in params:
438
+ params["geometry"] = get_geometry_from_various(geometry=params["geometry"])
439
+
440
+ def discover_queryables(
441
+ self, **kwargs: Any
442
+ ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
443
+ """Fetch queryables list from provider using its constraints file
444
+
445
+ :param kwargs: additional filters for queryables (`productType` and other search
446
+ arguments)
447
+ :returns: fetched queryable parameters dict
448
+ """
449
+ constraints_file_url = getattr(self.config, "constraints_file_url", "")
450
+ if not constraints_file_url:
451
+ return {}
452
+ product_type = kwargs.pop("productType", None)
453
+ if not product_type:
454
+ return {}
455
+
456
+ provider_product_type = self.config.products.get(product_type, {}).get(
457
+ "dataset", None
458
+ )
459
+ user_provider_product_type = kwargs.pop("dataset", None)
460
+ if (
461
+ user_provider_product_type
462
+ and user_provider_product_type != provider_product_type
463
+ ):
464
+ raise ValidationError(
465
+ f"Cannot change dataset from {provider_product_type} to {user_provider_product_type}"
466
+ )
467
+
468
+ # defaults
469
+ default_queryables = self._get_defaults_as_queryables(product_type)
470
+ # remove dataset from queryables
471
+ default_queryables.pop("dataset", None)
472
+
473
+ non_empty_kwargs = {k: v for k, v in kwargs.items() if v}
474
+
475
+ if "{" in constraints_file_url:
476
+ constraints_file_url = constraints_file_url.format(
477
+ dataset=provider_product_type
478
+ )
479
+ constraints = fetch_constraints(constraints_file_url, self)
480
+ if not constraints:
481
+ return default_queryables
482
+
483
+ constraint_params: Dict[str, Dict[str, Set[Any]]] = {}
484
+ if len(kwargs) == 0:
485
+ # get values from constraints without additional filters
486
+ for constraint in constraints:
487
+ for key in constraint.keys():
488
+ if key in constraint_params:
489
+ constraint_params[key]["enum"].update(constraint[key])
490
+ else:
491
+ constraint_params[key] = {}
492
+ constraint_params[key]["enum"] = set(constraint[key])
493
+ else:
494
+ # get values from constraints with additional filters
495
+ constraints_input_params = {k: v for k, v in non_empty_kwargs.items()}
496
+ constraint_params = get_constraint_queryables_with_additional_params(
497
+ constraints, constraints_input_params, self, product_type
498
+ )
499
+ # query params that are not in constraints but might be default queryables
500
+ if len(constraint_params) == 1 and "not_available" in constraint_params:
501
+ not_queryables: Set[str] = set()
502
+ for constraint_param in constraint_params["not_available"]["enum"]:
503
+ param = CommonQueryables.get_queryable_from_alias(constraint_param)
504
+ if param in dict(
505
+ CommonQueryables.model_fields, **default_queryables
506
+ ):
507
+ non_empty_kwargs.pop(constraint_param)
508
+ else:
509
+ not_queryables.add(constraint_param)
510
+ if not_queryables:
511
+ raise ValidationError(
512
+ f"parameter(s) {not_queryables} not queryable"
513
+ )
514
+ else:
515
+ # get constraints again without common queryables
516
+ constraint_params = (
517
+ get_constraint_queryables_with_additional_params(
518
+ constraints, non_empty_kwargs, self, product_type
519
+ )
520
+ )
521
+
522
+ field_definitions: Dict[str, Any] = {}
523
+ for json_param, json_mtd in constraint_params.items():
524
+ param = (
525
+ get_queryable_from_provider(
526
+ json_param, self.get_metadata_mapping(product_type)
527
+ )
528
+ or json_param
529
+ )
530
+ default = kwargs.get(param, None) or self.config.products.get(
531
+ product_type, {}
532
+ ).get(param, None)
533
+ annotated_def = json_field_definition_to_python(
534
+ json_mtd, default_value=default, required=True
535
+ )
536
+ field_definitions[param] = get_args(annotated_def)
537
+
538
+ python_queryables = create_model("m", **field_definitions).model_fields
539
+ return {**default_queryables, **model_fields_to_annotated(python_queryables)}