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
@@ -42,6 +42,7 @@ logger = logging.getLogger("eodag.search.creodiass3")
42
42
 
43
43
  def patched_register_downloader(self, downloader, authenticator):
44
44
  """Add the download information to the product.
45
+
45
46
  :param self: product to which information should be added
46
47
  :param downloader: The download method that it can use
47
48
  :class:`~eodag.plugins.download.base.Download` or
@@ -59,7 +60,7 @@ def patched_register_downloader(self, downloader, authenticator):
59
60
 
60
61
 
61
62
  def _update_assets(product: EOProduct, config: PluginConfig, auth: AwsAuth):
62
- product.assets = {}
63
+ product.assets = AssetsDict(product)
63
64
  prefix = (
64
65
  product.properties.get("productIdentifier", None).replace("/eodata/", "") + "/"
65
66
  )
@@ -81,27 +82,32 @@ def _update_assets(product: EOProduct, config: PluginConfig, auth: AwsAuth):
81
82
  )
82
83
  logger.debug("Listing assets in %s", prefix)
83
84
  product.assets = AssetsDict(product)
84
- for asset in auth.s3_client.list_objects(
85
+ s3_res = auth.s3_client.list_objects(
85
86
  Bucket=config.s3_bucket, Prefix=prefix, MaxKeys=300
86
- )["Contents"]:
87
- asset_basename = (
88
- asset["Key"].split("/")[-1] if "/" in asset["Key"] else asset["Key"]
89
- )
90
-
91
- if len(asset_basename) > 0 and asset_basename not in product.assets:
92
- role = (
93
- "data"
94
- if asset_basename.split(".")[-1] in DATA_EXTENSIONS
95
- else "metadata"
87
+ )
88
+ # check if product path has assets or is already a file
89
+ if "Contents" in s3_res:
90
+ for asset in s3_res["Contents"]:
91
+ asset_basename = (
92
+ asset["Key"].split("/")[-1]
93
+ if "/" in asset["Key"]
94
+ else asset["Key"]
96
95
  )
97
96
 
98
- product.assets[asset_basename] = {
99
- "title": asset_basename,
100
- "roles": [role],
101
- "href": f"s3://{config.s3_bucket}/{asset['Key']}",
102
- }
103
- if mime_type := guess_file_type(asset["Key"]):
104
- product.assets[asset_basename]["type"] = mime_type
97
+ if len(asset_basename) > 0 and asset_basename not in product.assets:
98
+ role = (
99
+ "data"
100
+ if asset_basename.split(".")[-1] in DATA_EXTENSIONS
101
+ else "metadata"
102
+ )
103
+
104
+ product.assets[asset_basename] = {
105
+ "title": asset_basename,
106
+ "roles": [role],
107
+ "href": f"s3://{config.s3_bucket}/{asset['Key']}",
108
+ }
109
+ if mime_type := guess_file_type(asset["Key"]):
110
+ product.assets[asset_basename]["type"] = mime_type
105
111
  # update driver
106
112
  product.driver = product.get_driver()
107
113
 
@@ -116,7 +116,7 @@ class DataRequestSearch(Search):
116
116
  (``Dict[str, str]``): mapping for product type metadata (e.g. ``abstract``, ``licence``) which can be parsed
117
117
  from the provider result
118
118
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.generic_product_type_parsable_properties`
119
- (``Dict[str, str]``): mapping for product type properties which can be parsed from the result that are not
119
+ (``Dict[str, str]``): mapping for product type properties which can be parsed from the result and are not
120
120
  product type metadata
121
121
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_collection_fetch_url` (``str``): url to fetch
122
122
  data for a single collection; used if product type metadata is not available from the endpoint given in
@@ -20,7 +20,7 @@ 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
+ from datetime import datetime, timedelta
24
24
  from typing import (
25
25
  TYPE_CHECKING,
26
26
  Annotated,
@@ -75,9 +75,9 @@ from eodag.api.search_result import RawSearchResult
75
75
  from eodag.plugins.search import PreparedSearch
76
76
  from eodag.plugins.search.base import Search
77
77
  from eodag.types import json_field_definition_to_python, model_fields_to_annotated
78
- from eodag.types.queryables import CommonQueryables
79
78
  from eodag.types.search_args import SortByList
80
79
  from eodag.utils import (
80
+ DEFAULT_MISSION_START_DATE,
81
81
  GENERIC_PRODUCT_TYPE,
82
82
  HTTP_REQ_TIMEOUT,
83
83
  REQ_RETRY_BACKOFF_FACTOR,
@@ -94,10 +94,6 @@ from eodag.utils import (
94
94
  update_nested_dict,
95
95
  urlencode,
96
96
  )
97
- from eodag.utils.constraints import (
98
- fetch_constraints,
99
- get_constraint_queryables_with_additional_params,
100
- )
101
97
  from eodag.utils.exceptions import (
102
98
  AuthenticationError,
103
99
  MisconfiguredError,
@@ -190,7 +186,10 @@ class QueryStringSearch(Search):
190
186
  (``Dict[str, str]``): mapping for product type metadata (e.g. ``abstract``, ``licence``) which can be parsed
191
187
  from the provider result
192
188
  * :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
189
+ (``Dict[str, str]``): mapping for product type properties which can be parsed from the result and are not
190
+ product type metadata
191
+ * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.generic_product_type_unparsable_properties`
192
+ (``Dict[str, str]``): mapping for product type properties which cannot be parsed from the result and are not
194
193
  product type metadata
195
194
  * :attr:`~eodag.config.PluginConfig.DiscoverProductTypes.single_collection_fetch_url` (``str``): url to fetch
196
195
  data for a single collection; used if product type metadata is not available from the endpoint given in
@@ -282,13 +281,9 @@ class QueryStringSearch(Search):
282
281
 
283
282
  * :attr:`~eodag.config.PluginConfig.constraints_file_url` (``str``): url to fetch the constraints for a specific
284
283
  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
284
  * :attr:`~eodag.config.PluginConfig.constraints_entry` (``str``): key in the json result where the constraints
288
285
  can be found; if not given, it is assumed that the constraints are on top level of the result, i.e.
289
286
  the result is an array of constraints
290
- * :attr:`~eodag.config.PluginConfig.stop_without_constraints_entry_key` (``bool``): if true only a provider
291
- result containing `constraints_entry` is accepted as valid and used to create constraints; default: ``False``
292
287
  """
293
288
 
294
289
  extract_properties: Dict[str, Callable[..., Dict[str, Any]]] = {
@@ -641,7 +636,11 @@ class QueryStringSearch(Search):
641
636
  ][kf]
642
637
  )
643
638
  for kf in keywords_fields
644
- if conf_update_dict["product_types_config"][
639
+ if kf
640
+ in conf_update_dict["product_types_config"][
641
+ generic_product_type_id
642
+ ]
643
+ and conf_update_dict["product_types_config"][
645
644
  generic_product_type_id
646
645
  ][kf]
647
646
  != NOT_AVAILABLE
@@ -723,102 +722,6 @@ class QueryStringSearch(Search):
723
722
  self.config.discover_product_types["single_product_type_parsable_metadata"],
724
723
  )
725
724
 
726
- def discover_queryables(
727
- self, **kwargs: Any
728
- ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
729
- """Fetch queryables list from provider using its constraints file
730
-
731
- :param kwargs: additional filters for queryables (`productType` and other search
732
- arguments)
733
- :returns: fetched queryable parameters dict
734
- """
735
- product_type = kwargs.pop("productType", None)
736
- if not product_type:
737
- return {}
738
- constraints_file_url = getattr(self.config, "constraints_file_url", "")
739
- if not constraints_file_url:
740
- return {}
741
-
742
- constraints_file_dataset_key = getattr(
743
- self.config, "constraints_file_dataset_key", "dataset"
744
- )
745
- provider_product_type = self.config.products.get(product_type, {}).get(
746
- constraints_file_dataset_key, None
747
- )
748
-
749
- # defaults
750
- default_queryables = self._get_defaults_as_queryables(product_type)
751
- # remove unwanted queryables
752
- for param in getattr(self.config, "remove_from_queryables", []):
753
- default_queryables.pop(param, None)
754
-
755
- non_empty_kwargs = {k: v for k, v in kwargs.items() if v}
756
-
757
- if "{" in constraints_file_url:
758
- constraints_file_url = constraints_file_url.format(
759
- dataset=provider_product_type
760
- )
761
- constraints = fetch_constraints(constraints_file_url, self)
762
- if not constraints:
763
- return default_queryables
764
-
765
- constraint_params: Dict[str, Dict[str, Set[Any]]] = {}
766
- if len(kwargs) == 0:
767
- # get values from constraints without additional filters
768
- for constraint in constraints:
769
- for key in constraint.keys():
770
- if key in constraint_params:
771
- constraint_params[key]["enum"].update(constraint[key])
772
- else:
773
- constraint_params[key] = {"enum": set(constraint[key])}
774
- else:
775
- # get values from constraints with additional filters
776
- constraints_input_params = {k: v for k, v in non_empty_kwargs.items()}
777
- constraint_params = get_constraint_queryables_with_additional_params(
778
- constraints, constraints_input_params, self, product_type
779
- )
780
- # query params that are not in constraints but might be default queryables
781
- if len(constraint_params) == 1 and "not_available" in constraint_params:
782
- not_queryables = set()
783
- for constraint_param in constraint_params["not_available"]["enum"]:
784
- param = CommonQueryables.get_queryable_from_alias(constraint_param)
785
- if param in dict(
786
- CommonQueryables.model_fields, **default_queryables
787
- ):
788
- non_empty_kwargs.pop(constraint_param)
789
- else:
790
- not_queryables.add(constraint_param)
791
- if not_queryables:
792
- raise ValidationError(
793
- f"parameter(s) {str(not_queryables)} not queryable"
794
- )
795
- else:
796
- # get constraints again without common queryables
797
- constraint_params = (
798
- get_constraint_queryables_with_additional_params(
799
- constraints, non_empty_kwargs, self, product_type
800
- )
801
- )
802
-
803
- field_definitions: Dict[str, Any] = dict()
804
- for json_param, json_mtd in constraint_params.items():
805
- param = (
806
- get_queryable_from_provider(
807
- json_param, self.get_metadata_mapping(product_type)
808
- )
809
- or json_param
810
- )
811
- default = kwargs.get(param, None) or self.config.products.get(
812
- product_type, {}
813
- ).get(param, None)
814
- annotated_def = json_field_definition_to_python(
815
- json_mtd, default_value=default, required=True
816
- )
817
- field_definitions[param] = get_args(annotated_def)
818
-
819
- python_queryables = create_model("m", **field_definitions).model_fields
820
- return dict(default_queryables, **model_fields_to_annotated(python_queryables))
821
-
822
725
  def query(
823
726
  self,
824
727
  prep: PreparedSearch = PreparedSearch(),
@@ -1138,9 +1041,13 @@ class QueryStringSearch(Search):
1138
1041
  logger.debug(
1139
1042
  "Could not extract total_items_nb from search results"
1140
1043
  )
1141
- if getattr(self.config, "merge_responses", False):
1044
+ if (
1045
+ getattr(self.config, "merge_responses", False)
1046
+ and self.config.result_type == "json"
1047
+ ):
1048
+ json_result = cast(list[dict[str, Any]], result)
1142
1049
  results = (
1143
- [dict(r, **result[i]) for i, r in enumerate(results)]
1050
+ [dict(r, **json_result[i]) for i, r in enumerate(results)]
1144
1051
  if results
1145
1052
  else result
1146
1053
  )
@@ -1524,54 +1431,53 @@ class PostJsonSearch(QueryStringSearch):
1524
1431
  """
1525
1432
 
1526
1433
  def _get_default_end_date_from_start_date(
1527
- self, start_datetime: str, product_type: str
1434
+ self, start_datetime: str, product_type_conf: Dict[str, Any]
1528
1435
  ) -> 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
1436
  try:
1535
1437
  start_date = datetime.fromisoformat(start_datetime)
1536
1438
  except ValueError:
1537
1439
  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
- ]
1440
+ if "completionTimeFromAscendingNode" in product_type_conf:
1441
+ mapping = product_type_conf["completionTimeFromAscendingNode"]
1442
+ # if date is mapped to year/month/(day), use end_date = start_date else start_date + 1 day
1443
+ # (default dates are only needed for ecmwf products where selected timespans should not be too large)
1546
1444
  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
1445
  end_date = start_date
1549
- return end_date.isoformat()
1446
+ else:
1447
+ end_date = start_date + timedelta(days=1)
1448
+ return end_date.isoformat()
1550
1449
  return self.get_product_type_cfg_value("missionEndDate", today().isoformat())
1551
1450
 
1552
- def _check_date_params(self, keywords: Dict[str, Any], product_type: str) -> None:
1451
+ def _check_date_params(
1452
+ self, keywords: Dict[str, Any], product_type: Optional[str]
1453
+ ) -> None:
1553
1454
  """checks if start and end date are present in the keywords and adds them if not"""
1554
1455
  if (
1555
1456
  "startTimeFromAscendingNode"
1556
1457
  and "completionTimeFromAscendingNode" in keywords
1557
1458
  ):
1558
1459
  return
1460
+
1461
+ product_type_conf = getattr(self.config, "metadata_mapping", {})
1462
+ if (
1463
+ product_type
1464
+ and product_type in self.config.products
1465
+ and "metadata_mapping" in self.config.products[product_type]
1466
+ ):
1467
+ product_type_conf = self.config.products[product_type]["metadata_mapping"]
1559
1468
  # start time given, end time missing
1560
1469
  if "startTimeFromAscendingNode" in keywords:
1561
1470
  keywords[
1562
1471
  "completionTimeFromAscendingNode"
1563
1472
  ] = self._get_default_end_date_from_start_date(
1564
- keywords["startTimeFromAscendingNode"], product_type
1473
+ keywords["startTimeFromAscendingNode"], product_type_conf
1565
1474
  )
1566
1475
  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
- ]
1476
+
1477
+ if "completionTimeFromAscendingNode" in product_type_conf:
1478
+ mapping = product_type_conf["startTimeFromAscendingNode"]
1479
+ if not isinstance(mapping, list):
1480
+ mapping = product_type_conf["completionTimeFromAscendingNode"]
1575
1481
  if isinstance(mapping, list):
1576
1482
  # get time parameters (date, year, month, ...) from metadata mapping
1577
1483
  input_mapping = mapping[0].replace("{{", "").replace("}}", "")
@@ -1587,16 +1493,17 @@ class PostJsonSearch(QueryStringSearch):
1587
1493
  for tp in time_params:
1588
1494
  if tp not in keywords:
1589
1495
  in_keywords = False
1496
+ break
1590
1497
  if not in_keywords:
1591
1498
  keywords[
1592
1499
  "startTimeFromAscendingNode"
1593
1500
  ] = self.get_product_type_cfg_value(
1594
- "missionStartDate", today().isoformat()
1501
+ "missionStartDate", DEFAULT_MISSION_START_DATE
1595
1502
  )
1596
1503
  keywords[
1597
1504
  "completionTimeFromAscendingNode"
1598
1505
  ] = self._get_default_end_date_from_start_date(
1599
- keywords["startTimeFromAscendingNode"], product_type
1506
+ keywords["startTimeFromAscendingNode"], product_type_conf
1600
1507
  )
1601
1508
 
1602
1509
  def query(
@@ -1605,7 +1512,7 @@ class PostJsonSearch(QueryStringSearch):
1605
1512
  **kwargs: Any,
1606
1513
  ) -> Tuple[List[EOProduct], Optional[int]]:
1607
1514
  """Perform a search on an OpenSearch-like interface"""
1608
- product_type = kwargs.get("productType", None)
1515
+ product_type = kwargs.get("productType", "")
1609
1516
  count = prep.count
1610
1517
  # remove "product_type" from search args if exists for compatibility with QueryStringSearch methods
1611
1518
  kwargs.pop("product_type", None)
@@ -1720,6 +1627,7 @@ class PostJsonSearch(QueryStringSearch):
1720
1627
  # do not try to extract total_items from search results if count is False
1721
1628
  del prep.total_items_nb
1722
1629
  del prep.need_count
1630
+
1723
1631
  provider_results = self.do_search(prep, **kwargs)
1724
1632
  if count and total_items is None and hasattr(prep, "total_items_nb"):
1725
1633
  total_items = prep.total_items_nb
@@ -2072,6 +1980,10 @@ class StacSearch(PostJsonSearch):
2072
1980
  field_definitions[param] = get_args(annotated_def)
2073
1981
 
2074
1982
  python_queryables = create_model("m", **field_definitions).model_fields
1983
+ # replace geometry by geom
1984
+ geom_queryable = python_queryables.pop("geometry", None)
1985
+ if geom_queryable:
1986
+ python_queryables["geom"] = geom_queryable
2075
1987
 
2076
1988
  return model_fields_to_annotated(python_queryables)
2077
1989