eodag 3.0.1__py3-none-any.whl → 3.1.0b1__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 (44) hide show
  1. eodag/api/core.py +116 -86
  2. eodag/api/product/_assets.py +6 -6
  3. eodag/api/product/_product.py +18 -18
  4. eodag/api/product/metadata_mapping.py +39 -11
  5. eodag/cli.py +22 -1
  6. eodag/config.py +14 -14
  7. eodag/plugins/apis/ecmwf.py +37 -14
  8. eodag/plugins/apis/usgs.py +5 -5
  9. eodag/plugins/authentication/openid_connect.py +2 -2
  10. eodag/plugins/authentication/token.py +37 -6
  11. eodag/plugins/crunch/filter_property.py +2 -3
  12. eodag/plugins/download/aws.py +11 -12
  13. eodag/plugins/download/base.py +30 -39
  14. eodag/plugins/download/creodias_s3.py +29 -0
  15. eodag/plugins/download/http.py +144 -152
  16. eodag/plugins/download/s3rest.py +5 -7
  17. eodag/plugins/search/base.py +73 -25
  18. eodag/plugins/search/build_search_result.py +1047 -310
  19. eodag/plugins/search/creodias_s3.py +25 -19
  20. eodag/plugins/search/data_request_search.py +1 -1
  21. eodag/plugins/search/qssearch.py +51 -139
  22. eodag/resources/ext_product_types.json +1 -1
  23. eodag/resources/product_types.yml +391 -32
  24. eodag/resources/providers.yml +678 -1744
  25. eodag/rest/core.py +92 -62
  26. eodag/rest/server.py +31 -4
  27. eodag/rest/types/eodag_search.py +6 -0
  28. eodag/rest/types/queryables.py +5 -6
  29. eodag/rest/utils/__init__.py +3 -0
  30. eodag/types/__init__.py +56 -15
  31. eodag/types/download_args.py +2 -2
  32. eodag/types/queryables.py +180 -72
  33. eodag/types/whoosh.py +126 -0
  34. eodag/utils/__init__.py +71 -10
  35. eodag/utils/exceptions.py +27 -20
  36. eodag/utils/repr.py +65 -6
  37. eodag/utils/requests.py +11 -11
  38. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/METADATA +76 -76
  39. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/RECORD +43 -44
  40. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
  41. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +3 -2
  42. eodag/utils/constraints.py +0 -244
  43. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
  44. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
@@ -27,6 +27,7 @@ from datetime import datetime
27
27
  from email.message import Message
28
28
  from itertools import chain
29
29
  from json import JSONDecodeError
30
+ from pathlib import Path
30
31
  from typing import (
31
32
  TYPE_CHECKING,
32
33
  Any,
@@ -80,8 +81,8 @@ from eodag.utils.exceptions import (
80
81
  DownloadError,
81
82
  MisconfiguredError,
82
83
  NotAvailableError,
83
- RequestError,
84
84
  TimeOutError,
85
+ ValidationError,
85
86
  )
86
87
 
87
88
  if TYPE_CHECKING:
@@ -122,8 +123,6 @@ class HTTPDownload(Download):
122
123
  default: ``5``
123
124
  * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be verified in
124
125
  requests; default: ``True``
125
- * :attr:`~eodag.config.PluginConfig.output_extension` (``str``): which extension should be used for the
126
- downloaded file
127
126
  * :attr:`~eodag.config.PluginConfig.no_auth_download` (``bool``): if the download should be done without
128
127
  authentication; default: ``True``
129
128
  * :attr:`~eodag.config.PluginConfig.order_enabled` (``bool``): if the product has to be ordered to download it;
@@ -140,16 +139,15 @@ class HTTPDownload(Download):
140
139
  configuration to handle the order status; contains information which method to use, how the response data is
141
140
  interpreted, which status corresponds to success, ordered and error and what should be done on success.
142
141
  * :attr:`~eodag.config.PluginConfig.products` (``Dict[str, Dict[str, Any]``): product type specific config; the
143
- keys are the product types, the values are dictionaries which can contain the keys
144
- :attr:`~eodag.config.PluginConfig.output_extension` and :attr:`~eodag.config.PluginConfig.extract` to
145
- overwrite the provider config for a specific product type
142
+ keys are the product types, the values are dictionaries which can contain the key
143
+ :attr:`~eodag.config.PluginConfig.extract` to overwrite the provider config for a specific product type
146
144
 
147
145
  """
148
146
 
149
147
  def __init__(self, provider: str, config: PluginConfig) -> None:
150
148
  super(HTTPDownload, self).__init__(provider, config)
151
149
 
152
- def order_download(
150
+ def _order(
153
151
  self,
154
152
  product: EOProduct,
155
153
  auth: Optional[AuthBase] = None,
@@ -226,9 +224,11 @@ class HTTPDownload(Download):
226
224
  product.properties["storageStatus"] = STAGING_STATUS
227
225
  except RequestException as e:
228
226
  self._check_auth_exception(e)
229
- title = product.properties["title"]
230
- message = f"{title} could not be ordered"
231
- raise RequestError.from_error(e, message) from e
227
+ msg = f'{product.properties["title"]} could not be ordered'
228
+ if e.response is not None and e.response.status_code == 400:
229
+ raise ValidationError.from_error(e, msg) from e
230
+ else:
231
+ raise DownloadError.from_error(e, msg) from e
232
232
 
233
233
  return self.order_response_process(response, product)
234
234
  except requests.exceptions.Timeout as exc:
@@ -271,7 +271,7 @@ class HTTPDownload(Download):
271
271
 
272
272
  return json_response
273
273
 
274
- def order_download_status(
274
+ def _order_status(
275
275
  self,
276
276
  product: EOProduct,
277
277
  auth: Optional[AuthBase] = None,
@@ -398,13 +398,11 @@ class HTTPDownload(Download):
398
398
  # success and no need to get status response content
399
399
  skip_parsing_status_response = True
400
400
  except RequestException as e:
401
- raise DownloadError(
402
- "%s order status could not be checked, request returned %s"
403
- % (
404
- product.properties["title"],
405
- e,
406
- )
407
- ) from e
401
+ msg = f'{product.properties["title"]} order status could not be checked'
402
+ if e.response is not None and e.response.status_code == 400:
403
+ raise ValidationError.from_error(e, msg) from e
404
+ else:
405
+ raise DownloadError.from_error(e, msg) from e
408
406
 
409
407
  if not skip_parsing_status_response:
410
408
  # status request
@@ -566,8 +564,8 @@ class HTTPDownload(Download):
566
564
  product: EOProduct,
567
565
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
568
566
  progress_callback: Optional[ProgressCallback] = None,
569
- wait: int = DEFAULT_DOWNLOAD_WAIT,
570
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
567
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
568
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
571
569
  **kwargs: Unpack[DownloadConf],
572
570
  ) -> Optional[str]:
573
571
  """Download a product using HTTP protocol.
@@ -585,13 +583,6 @@ class HTTPDownload(Download):
585
583
  )
586
584
  progress_callback = ProgressCallback(disable=True)
587
585
 
588
- output_extension = getattr(self.config, "products", {}).get(
589
- product.product_type, {}
590
- ).get("output_extension", None) or getattr(
591
- self.config, "output_extension", ".zip"
592
- )
593
- kwargs["output_extension"] = kwargs.get("output_extension", output_extension)
594
-
595
586
  fs_path, record_filename = self._prepare_download(
596
587
  product,
597
588
  progress_callback=progress_callback,
@@ -610,7 +601,7 @@ class HTTPDownload(Download):
610
601
  try:
611
602
  fs_path = self._download_assets(
612
603
  product,
613
- fs_path.replace(".zip", ""),
604
+ fs_path,
614
605
  record_filename,
615
606
  auth,
616
607
  progress_callback,
@@ -627,82 +618,62 @@ class HTTPDownload(Download):
627
618
 
628
619
  url = product.remote_location
629
620
 
630
- @self._download_retry(product, wait, timeout)
621
+ @self._order_download_retry(product, wait, timeout)
631
622
  def download_request(
632
623
  product: EOProduct,
633
624
  auth: AuthBase,
634
625
  progress_callback: ProgressCallback,
635
- wait: int,
636
- timeout: int,
626
+ wait: float,
627
+ timeout: float,
637
628
  **kwargs: Unpack[DownloadConf],
638
- ) -> None:
639
- chunks = self._stream_download(product, auth, progress_callback, **kwargs)
629
+ ) -> os.PathLike:
640
630
  is_empty = True
631
+ chunk_iterator = self._stream_download(
632
+ product, auth, progress_callback, **kwargs
633
+ )
634
+ if fs_path is not None:
635
+ ext = Path(product.filename).suffix
636
+ path = Path(fs_path).with_suffix(ext)
641
637
 
642
- with open(fs_path, "wb") as fhandle:
643
- for chunk in chunks:
644
- is_empty = False
645
- fhandle.write(chunk)
638
+ with open(path, "wb") as fhandle:
639
+ for chunk in chunk_iterator:
640
+ is_empty = False
641
+ progress_callback(len(chunk))
642
+ fhandle.write(chunk)
643
+ self.stream.close() # Closing response stream
646
644
 
647
- if is_empty:
648
- raise DownloadError(f"product {product.properties['id']} is empty")
645
+ if is_empty:
646
+ raise DownloadError(f"product {product.properties['id']} is empty")
649
647
 
650
- download_request(product, auth, progress_callback, wait, timeout, **kwargs)
648
+ return path
649
+ else:
650
+ raise DownloadError(
651
+ f"download of product {product.properties['id']} failed"
652
+ )
653
+
654
+ path = download_request(
655
+ product, auth, progress_callback, wait, timeout, **kwargs
656
+ )
651
657
 
652
658
  with open(record_filename, "w") as fh:
653
659
  fh.write(url)
654
660
  logger.debug("Download recorded in %s", record_filename)
655
661
 
656
- # Check that the downloaded file is really a zip file
657
- if not zipfile.is_zipfile(fs_path) and output_extension == ".zip":
658
- logger.warning(
659
- "Downloaded product is not a Zip File. Please check its file type before using it"
660
- )
661
- new_fs_path = os.path.join(
662
- os.path.dirname(fs_path),
663
- sanitize(product.properties["title"]),
664
- )
665
- if os.path.isfile(fs_path) and not tarfile.is_tarfile(fs_path):
666
- if not os.path.isdir(new_fs_path):
667
- os.makedirs(new_fs_path)
668
- shutil.move(fs_path, new_fs_path)
669
- file_path = os.path.join(new_fs_path, os.path.basename(fs_path))
670
- new_file_path = file_path[: file_path.index(".zip")]
671
- shutil.move(file_path, new_file_path)
672
- # in the case where the outputs extension has not been set
673
- # to ".tar" in the product type nor provider configuration
674
- elif tarfile.is_tarfile(fs_path):
675
- if not new_fs_path.endswith(".tar"):
676
- new_fs_path += ".tar"
677
- shutil.move(fs_path, new_fs_path)
678
- kwargs["output_extension"] = ".tar"
679
- product_path = self._finalize(
680
- new_fs_path,
681
- progress_callback=progress_callback,
682
- **kwargs,
683
- )
684
- product.location = path_to_uri(product_path)
685
- return product_path
686
- else:
687
- # not a file (dir with zip extension)
688
- shutil.move(fs_path, new_fs_path)
689
- product.location = path_to_uri(new_fs_path)
690
- return new_fs_path
691
-
692
- if os.path.isfile(fs_path) and not (
693
- zipfile.is_zipfile(fs_path) or tarfile.is_tarfile(fs_path)
662
+ if os.path.isfile(path) and not (
663
+ zipfile.is_zipfile(path) or tarfile.is_tarfile(path)
694
664
  ):
695
665
  new_fs_path = os.path.join(
696
- os.path.dirname(fs_path),
666
+ os.path.dirname(path),
697
667
  sanitize(product.properties["title"]),
698
668
  )
699
669
  if not os.path.isdir(new_fs_path):
700
670
  os.makedirs(new_fs_path)
701
- shutil.move(fs_path, new_fs_path)
671
+ shutil.move(path, new_fs_path)
702
672
  product.location = path_to_uri(new_fs_path)
703
673
  return new_fs_path
674
+
704
675
  product_path = self._finalize(
705
- fs_path,
676
+ str(path),
706
677
  progress_callback=progress_callback,
707
678
  **kwargs,
708
679
  )
@@ -743,15 +714,6 @@ class HTTPDownload(Download):
743
714
  ext = guess_extension(content_type)
744
715
  if ext:
745
716
  filename += ext
746
- else:
747
- output_extension: Optional[str] = (
748
- getattr(self.config, "products", {})
749
- .get(product.product_type, {})
750
- .get("output_extension")
751
- )
752
- if output_extension:
753
- filename += output_extension
754
-
755
717
  return filename
756
718
 
757
719
  def _stream_download_dict(
@@ -759,8 +721,8 @@ class HTTPDownload(Download):
759
721
  product: EOProduct,
760
722
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
761
723
  progress_callback: Optional[ProgressCallback] = None,
762
- wait: int = DEFAULT_DOWNLOAD_WAIT,
763
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
724
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
725
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
764
726
  **kwargs: Unpack[DownloadConf],
765
727
  ) -> StreamResponse:
766
728
  r"""
@@ -834,17 +796,20 @@ class HTTPDownload(Download):
834
796
  else:
835
797
  pass
836
798
 
837
- chunks = self._stream_download(product, auth, progress_callback, **kwargs)
799
+ chunk_iterator = self._stream_download(
800
+ product, auth, progress_callback, **kwargs
801
+ )
802
+
838
803
  # start reading chunks to set product.headers
839
804
  try:
840
- first_chunk = next(chunks)
805
+ first_chunk = next(chunk_iterator)
841
806
  except StopIteration:
842
807
  # product is empty file
843
808
  logger.error("product %s is empty", product.properties["id"])
844
809
  raise NotAvailableError(f"product {product.properties['id']} is empty")
845
810
 
846
811
  return StreamResponse(
847
- content=chain(iter([first_chunk]), chunks),
812
+ content=chain(iter([first_chunk]), chunk_iterator),
848
813
  headers=product.headers,
849
814
  )
850
815
 
@@ -902,6 +867,44 @@ class HTTPDownload(Download):
902
867
  else:
903
868
  logger.error("Error while getting resource :\n%s", tb.format_exc())
904
869
 
870
+ def _order_request(
871
+ self,
872
+ product: EOProduct,
873
+ auth: Optional[AuthBase],
874
+ ) -> None:
875
+ if (
876
+ "orderLink" in product.properties
877
+ and product.properties.get("storageStatus") == OFFLINE_STATUS
878
+ and not product.properties.get("orderStatus")
879
+ ):
880
+ self._order(product=product, auth=auth)
881
+
882
+ if (
883
+ product.properties.get("orderStatusLink", None)
884
+ and product.properties.get("storageStatus") != ONLINE_STATUS
885
+ ):
886
+ self._order_status(product=product, auth=auth)
887
+
888
+ def order(
889
+ self,
890
+ product: EOProduct,
891
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
892
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
893
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
894
+ ) -> None:
895
+ """
896
+ Order product and poll to check its status
897
+
898
+ :param product: The EO product to download
899
+ :param auth: (optional) authenticated object
900
+ :param wait: (optional) Wait time in minutes between two order status check
901
+ :param timeout: (optional) Maximum time in minutes before stop checking
902
+ order status
903
+ """
904
+ self._order_download_retry(product, wait, timeout)(self._order_request)(
905
+ product, auth
906
+ )
907
+
905
908
  def _stream_download(
906
909
  self,
907
910
  product: EOProduct,
@@ -910,8 +913,9 @@ class HTTPDownload(Download):
910
913
  **kwargs: Unpack[DownloadConf],
911
914
  ) -> Iterator[Any]:
912
915
  """
913
- fetches a zip file containing the assets of a given product as a stream
916
+ Fetches a zip file containing the assets of a given product as a stream
914
917
  and returns a generator yielding the chunks of the file
918
+
915
919
  :param product: product for which the assets should be downloaded
916
920
  :param auth: The configuration of a plugin of type Authentication
917
921
  :param progress_callback: A method or a callable object
@@ -928,18 +932,8 @@ class HTTPDownload(Download):
928
932
  ssl_verify = getattr(self.config, "ssl_verify", True)
929
933
 
930
934
  ordered_message = ""
931
- if (
932
- "orderLink" in product.properties
933
- and product.properties.get("storageStatus") == OFFLINE_STATUS
934
- and not product.properties.get("orderStatus")
935
- ):
936
- self.order_download(product=product, auth=auth)
937
-
938
- if (
939
- product.properties.get("orderStatusLink", None)
940
- and product.properties.get("storageStatus") != ONLINE_STATUS
941
- ):
942
- self.order_download_status(product=product, auth=auth)
935
+ # retry handled at download level
936
+ self._order_request(product, auth)
943
937
 
944
938
  params = kwargs.pop("dl_url_params", None) or getattr(
945
939
  self.config, "dl_url_params", {}
@@ -969,7 +963,7 @@ class HTTPDownload(Download):
969
963
  auth = None
970
964
 
971
965
  s = requests.Session()
972
- with s.request(
966
+ self.stream = s.request(
973
967
  req_method,
974
968
  req_url,
975
969
  stream=True,
@@ -979,48 +973,46 @@ class HTTPDownload(Download):
979
973
  timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
980
974
  verify=ssl_verify,
981
975
  **req_kwargs,
982
- ) as self.stream:
983
- try:
984
- self.stream.raise_for_status()
985
- except requests.exceptions.Timeout as exc:
986
- raise TimeOutError(
987
- exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT
988
- ) from exc
989
- except RequestException as e:
990
- self._process_exception(e, product, ordered_message)
991
- else:
992
- # check if product was ordered
993
-
994
- if getattr(
995
- self.stream, "status_code", None
996
- ) is not None and self.stream.status_code == getattr(
997
- self.config, "order_status", {}
998
- ).get(
999
- "ordered", {}
1000
- ).get(
1001
- "http_code"
1002
- ):
1003
- product.properties["storageStatus"] = "ORDERED"
1004
- self._process_exception(None, product, ordered_message)
1005
- stream_size = self._check_stream_size(product) or None
1006
-
1007
- product.headers = self.stream.headers
1008
- filename = self._check_product_filename(product) or None
1009
- product.headers[
1010
- "content-disposition"
1011
- ] = f"attachment; filename={filename}"
1012
- content_type = product.headers.get("Content-Type")
1013
- guessed_content_type = (
1014
- guess_file_type(filename) if filename and not content_type else None
1015
- )
1016
- if guessed_content_type is not None:
1017
- product.headers["Content-Type"] = guessed_content_type
976
+ )
977
+ try:
978
+ self.stream.raise_for_status()
979
+ except requests.exceptions.Timeout as exc:
980
+ raise TimeOutError(exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT) from exc
981
+ except RequestException as e:
982
+ self._process_exception(e, product, ordered_message)
983
+ raise DownloadError(
984
+ f"download of {product.properties['id']} is empty"
985
+ ) from e
986
+ else:
987
+ # check if product was ordered
988
+
989
+ if getattr(
990
+ self.stream, "status_code", None
991
+ ) is not None and self.stream.status_code == getattr(
992
+ self.config, "order_status", {}
993
+ ).get(
994
+ "ordered", {}
995
+ ).get(
996
+ "http_code"
997
+ ):
998
+ product.properties["storageStatus"] = "ORDERED"
999
+ self._process_exception(None, product, ordered_message)
1000
+ stream_size = self._check_stream_size(product) or None
1001
+
1002
+ product.headers = self.stream.headers
1003
+ filename = self._check_product_filename(product)
1004
+ product.headers["content-disposition"] = f"attachment; filename={filename}"
1005
+ content_type = product.headers.get("Content-Type")
1006
+ guessed_content_type = (
1007
+ guess_file_type(filename) if filename and not content_type else None
1008
+ )
1009
+ if guessed_content_type is not None:
1010
+ product.headers["Content-Type"] = guessed_content_type
1018
1011
 
1019
- progress_callback.reset(total=stream_size)
1020
- for chunk in self.stream.iter_content(chunk_size=64 * 1024):
1021
- if chunk:
1022
- progress_callback(len(chunk))
1023
- yield chunk
1012
+ progress_callback.reset(total=stream_size)
1013
+
1014
+ product.filename = filename
1015
+ return self.stream.iter_content(chunk_size=64 * 1024)
1024
1016
 
1025
1017
  def _stream_download_assets(
1026
1018
  self,
@@ -1375,8 +1367,8 @@ class HTTPDownload(Download):
1375
1367
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
1376
1368
  downloaded_callback: Optional[DownloadedCallback] = None,
1377
1369
  progress_callback: Optional[ProgressCallback] = None,
1378
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1379
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1370
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
1371
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1380
1372
  **kwargs: Unpack[DownloadConf],
1381
1373
  ):
1382
1374
  """
@@ -95,8 +95,8 @@ class S3RestDownload(Download):
95
95
  product: EOProduct,
96
96
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
97
97
  progress_callback: Optional[ProgressCallback] = None,
98
- wait: int = DEFAULT_DOWNLOAD_WAIT,
99
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
98
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
99
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
100
100
  **kwargs: Unpack[DownloadConf],
101
101
  ) -> Optional[str]:
102
102
  """Download method for S3 REST API.
@@ -130,9 +130,9 @@ class S3RestDownload(Download):
130
130
  and "storageStatus" in product.properties
131
131
  and product.properties["storageStatus"] != ONLINE_STATUS
132
132
  ):
133
- self.http_download_plugin.order_download(product=product, auth=auth)
133
+ self.http_download_plugin._order(product=product, auth=auth)
134
134
 
135
- @self._download_retry(product, wait, timeout)
135
+ @self._order_download_retry(product, wait, timeout)
136
136
  def download_request(
137
137
  product: EOProduct,
138
138
  auth: AuthBase,
@@ -142,9 +142,7 @@ class S3RestDownload(Download):
142
142
  ):
143
143
  # check order status
144
144
  if product.properties.get("orderStatusLink", None):
145
- self.http_download_plugin.order_download_status(
146
- product=product, auth=auth
147
- )
145
+ self.http_download_plugin._order_status(product=product, auth=auth)
148
146
 
149
147
  # get bucket urls
150
148
  bucket_name, prefix = get_bucket_name_and_prefix(
@@ -31,7 +31,7 @@ from eodag.api.product.metadata_mapping import (
31
31
  from eodag.plugins.base import PluginTopic
32
32
  from eodag.plugins.search import PreparedSearch
33
33
  from eodag.types import model_fields_to_annotated
34
- from eodag.types.queryables import Queryables
34
+ from eodag.types.queryables import Queryables, QueryablesDict
35
35
  from eodag.types.search_args import SortByList
36
36
  from eodag.utils import (
37
37
  GENERIC_PRODUCT_TYPE,
@@ -325,35 +325,93 @@ class Search(PluginTopic):
325
325
  sort_by_qs += parsed_sort_by_tpl
326
326
  return (sort_by_qs, sort_by_qp)
327
327
 
328
+ def _get_product_type_queryables(
329
+ self, product_type: Optional[str], alias: Optional[str], filters: Dict[str, Any]
330
+ ) -> Dict[str, Annotated[Any, FieldInfo]]:
331
+ default_values: Dict[str, Any] = deepcopy(
332
+ getattr(self.config, "products", {}).get(product_type, {})
333
+ )
334
+ default_values.pop("metadata_mapping", None)
335
+ try:
336
+ filters["productType"] = product_type
337
+ return self.discover_queryables(**{**default_values, **filters}) or {}
338
+ except NotImplementedError:
339
+ return self.queryables_from_metadata_mapping(product_type, alias)
340
+
328
341
  def list_queryables(
329
342
  self,
330
343
  filters: Dict[str, Any],
344
+ available_product_types: List[Any],
345
+ product_type_configs: Dict[str, Dict[str, Any]],
331
346
  product_type: Optional[str] = None,
332
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
347
+ alias: Optional[str] = None,
348
+ ) -> QueryablesDict:
333
349
  """
334
350
  Get queryables
335
351
 
336
352
  :param filters: Additional filters for queryables.
353
+ :param available_product_types: list of available product types
354
+ :param product_type_configs: dict containing the product type information for all used product types
337
355
  :param product_type: (optional) The product type.
356
+ :param alias: (optional) alias of the product type
338
357
 
339
358
  :return: A dictionary containing the queryable properties, associating parameters to their
340
359
  annotated type.
341
360
  """
342
- default_values: Dict[str, Any] = deepcopy(
343
- getattr(self.config, "products", {}).get(product_type, {})
361
+ additional_info = (
362
+ "Please select a product type to get the possible values of the parameters!"
363
+ if not product_type
364
+ else ""
344
365
  )
345
- default_values.pop("metadata_mapping", None)
346
-
347
- queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
348
- try:
349
- queryables = self.discover_queryables(**{**default_values, **filters}) or {}
350
- except NotImplementedError:
351
- pass
366
+ if product_type or getattr(self.config, "discover_queryables", {}).get(
367
+ "fetch_url", ""
368
+ ):
369
+ if product_type:
370
+ self.config.product_type_config = product_type_configs[product_type]
371
+ queryables = self._get_product_type_queryables(product_type, alias, filters)
372
+ if getattr(self.config, "discover_queryables", {}).get(
373
+ "constraints_url", ""
374
+ ):
375
+ additional_properties = False
376
+ else:
377
+ additional_properties = True
378
+ return QueryablesDict(
379
+ additional_properties=additional_properties,
380
+ additional_information=additional_info,
381
+ **queryables,
382
+ )
383
+ else:
384
+ all_queryables: Dict[str, Any] = {}
385
+ for pt in available_product_types:
386
+ self.config.product_type_config = product_type_configs[pt]
387
+ pt_queryables = self._get_product_type_queryables(pt, None, filters)
388
+ # only use key and type because values and defaults will vary between product types
389
+ pt_queryables_neutral = {
390
+ k: Annotated[v.__args__[0], Field(default=None)]
391
+ for k, v in pt_queryables.items()
392
+ }
393
+ all_queryables.update(pt_queryables_neutral)
394
+ return QueryablesDict(
395
+ additional_properties=True,
396
+ additional_information=additional_info,
397
+ **all_queryables,
398
+ )
352
399
 
400
+ def queryables_from_metadata_mapping(
401
+ self, product_type: Optional[str] = None, alias: Optional[str] = None
402
+ ) -> Dict[str, Annotated[Any, FieldInfo]]:
403
+ """
404
+ Extract queryable parameters from product type metadata mapping.
405
+ :param product_type: product type id (optional)
406
+ :param alias: (optional) alias of the product type
407
+ :returns: dict of annotated queryables
408
+ """
353
409
  metadata_mapping: Dict[str, Any] = deepcopy(
354
410
  self.get_metadata_mapping(product_type)
355
411
  )
356
412
 
413
+ queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
414
+
357
415
  for param in list(metadata_mapping.keys()):
358
416
  if NOT_MAPPED in metadata_mapping[param] or not isinstance(
359
417
  metadata_mapping[param], list
@@ -363,28 +421,18 @@ class Search(PluginTopic):
363
421
  eodag_queryables = copy_deepcopy(
364
422
  model_fields_to_annotated(Queryables.model_fields)
365
423
  )
424
+ # add default value for product type
425
+ if alias:
426
+ eodag_queryables.pop("productType")
427
+ eodag_queryables["productType"] = Annotated[str, Field(default=alias)]
366
428
  for k, v in eodag_queryables.items():
367
429
  eodag_queryable_field_info = (
368
430
  get_args(v)[1] if len(get_args(v)) > 1 else None
369
431
  )
370
432
  if not isinstance(eodag_queryable_field_info, FieldInfo):
371
433
  continue
372
- # keep default field info of eodag queryables
373
- if k in filters and k in queryables:
374
- queryable_field_info = (
375
- get_args(queryables[k])[1]
376
- if len(get_args(queryables[k])) > 1
377
- else None
378
- )
379
- if not isinstance(queryable_field_info, FieldInfo):
380
- continue
381
- queryable_field_info.default = filters[k]
382
- continue
383
- if k in queryables:
384
- continue
385
434
  if eodag_queryable_field_info.is_required() or (
386
435
  (eodag_queryable_field_info.alias or k) in metadata_mapping
387
436
  ):
388
437
  queryables[k] = v
389
-
390
438
  return queryables