Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge queryables by provider priority #1431

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions eodag/api/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2268,7 +2268,7 @@ def list_queryables(
properties, associating parameters to their annotated type, and a additional_properties attribute
"""
# only fetch providers if product type is not found
available_product_types = [
available_product_types: List[str] = [
pt["ID"]
for pt in self.list_product_types(provider=provider, fetch_providers=False)
]
Expand Down Expand Up @@ -2300,12 +2300,12 @@ def list_queryables(
)

queryables: QueryablesDict = QueryablesDict(
additional_properties=True, additional_information="", **{}
additional_properties=False, additional_information="", **{}
)

for plugin in self._plugins_manager.get_search_plugins(product_type, provider):
# attach product type config
product_type_configs = {}
product_type_configs: Dict[str, Any] = {}
if product_type:
self._attach_product_type_config(plugin, product_type)
product_type_configs[product_type] = plugin.config.product_type_config
Expand All @@ -2325,20 +2325,25 @@ def list_queryables(
"queryables from provider %s could not be fetched due to an authentication error",
plugin.provider,
)

plugin_queryables = plugin.list_queryables(
kwargs,
available_product_types,
product_type_configs,
product_type,
pt_alias,
)
queryables.update(plugin_queryables)
if not plugin_queryables.additional_properties:
queryables.additional_properties = False

information = queryables.additional_information
if plugin_queryables.additional_information:
queryables.additional_information += (
f"{provider}: {plugin_queryables.additional_information}"
)
information += f"{provider}: {plugin_queryables.additional_information}"

queryables = QueryablesDict(
queryables.additional_properties
or plugin_queryables.additional_properties,
information,
**{**plugin_queryables, **queryables},
)

return queryables

Expand Down
19 changes: 1 addition & 18 deletions eodag/api/product/metadata_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ def convert_get_hydrological_year(date: str):
date_object = datetime.strptime(utc_date, "%Y-%m-%dT%H:%M:%S.%fZ")
date_object_second_year = date_object + relativedelta(years=1)
return [
f'{date_object.strftime("%Y")}_{date_object_second_year.strftime("%y")}'
f"{date_object.strftime('%Y')}_{date_object_second_year.strftime('%y')}"
]

@staticmethod
Expand Down Expand Up @@ -1546,23 +1546,6 @@ def get_provider_queryable_key(
return eodag_key


def eodag_key_from_provider_key(
provider_key: str,
metadata_mapping: Dict[str, Union[List[Any], str]],
) -> str:
"""Get eodag key for provider key based on the metadata mapping if the provider key
appears in the metadata mapping, otherwise the provider key is returned

:param provider_key: name of the variable received from the provider
:param metadata_mapping: metadata mapping of the provider
:returns: eodag key
"""
for mm, mv in metadata_mapping.items():
if isinstance(mv, list) and len(mv) > 1 and provider_key == mv[0]:
return mm
return provider_key


# Keys taken from OpenSearch extension for Earth Observation http://docs.opengeospatial.org/is/13-026r9/13-026r9.html
# For a metadata to be queryable, The way to query it must be specified in the
# provider metadata_mapping configuration parameter. It will be automatically
Expand Down
22 changes: 8 additions & 14 deletions eodag/plugins/search/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,16 +327,18 @@ def build_sort_by(

def _get_product_type_queryables(
self, product_type: Optional[str], alias: Optional[str], filters: Dict[str, Any]
) -> Dict[str, Annotated[Any, FieldInfo]]:
) -> QueryablesDict:
default_values: Dict[str, Any] = deepcopy(
getattr(self.config, "products", {}).get(product_type, {})
)
default_values.pop("metadata_mapping", None)
try:
filters["productType"] = product_type
return self.discover_queryables(**{**default_values, **filters}) or {}
queryables = self.discover_queryables(**{**default_values, **filters}) or {}
except NotImplementedError:
return self.queryables_from_metadata_mapping(product_type, alias)
queryables = self.queryables_from_metadata_mapping(product_type, alias)

return QueryablesDict(**queryables)

def list_queryables(
self,
Expand Down Expand Up @@ -369,17 +371,9 @@ def list_queryables(
if product_type:
self.config.product_type_config = product_type_configs[product_type]
queryables = self._get_product_type_queryables(product_type, alias, filters)
if getattr(self.config, "discover_queryables", {}).get(
"constraints_url", ""
):
additional_properties = False
else:
additional_properties = True
return QueryablesDict(
additional_properties=additional_properties,
additional_information=additional_info,
**queryables,
)
queryables.additional_information = additional_info

return queryables
else:
all_queryables: Dict[str, Any] = {}
for pt in available_product_types:
Expand Down
66 changes: 48 additions & 18 deletions eodag/plugins/search/build_search_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
from eodag.api.product.metadata_mapping import (
NOT_AVAILABLE,
NOT_MAPPED,
eodag_key_from_provider_key,
format_metadata,
format_query_params,
mtd_cfg_as_conversion_and_querypath,
Expand All @@ -62,7 +61,7 @@
from eodag.plugins.search import PreparedSearch
from eodag.plugins.search.qssearch import PostJsonSearch, QueryStringSearch
from eodag.types import json_field_definition_to_python
from eodag.types.queryables import Queryables
from eodag.types.queryables import Queryables, QueryablesDict
from eodag.utils import (
HTTP_REQ_TIMEOUT,
deepcopy,
Expand Down Expand Up @@ -317,6 +316,11 @@ def _update_properties_from_element(
prop["description"] = description


def ecmwf_format(v: str) -> str:
"""Add ECMWF prefix to value v if v is a ECMWF keyword."""
return "ecmwf:" + v if v in ECMWF_KEYWORDS + COP_DS_KEYWORDS else v


class ECMWFSearch(PostJsonSearch):
"""ECMWF search plugin.

Expand Down Expand Up @@ -539,6 +543,20 @@ def _preprocess_search_params(
if "geometry" in params:
params["geometry"] = get_geometry_from_various(geometry=params["geometry"])

def _get_product_type_queryables(
self, product_type: Optional[str], alias: Optional[str], filters: Dict[str, Any]
) -> QueryablesDict:
"""Override to set additional_properties to false."""
default_values: Dict[str, Any] = deepcopy(
getattr(self.config, "products", {}).get(product_type, {})
)
default_values.pop("metadata_mapping", None)

filters["productType"] = product_type
queryables = self.discover_queryables(**{**default_values, **filters}) or {}

return QueryablesDict(additional_properties=False, **queryables)

def discover_queryables(
self, **kwargs: Any
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
Expand Down Expand Up @@ -589,7 +607,7 @@ def discover_queryables(
):
formated_kwargs[key] = kwargs[key]
else:
raise ValidationError(f"{key} in not a queryable parameter")
raise ValidationError(f"{key} is not a queryable parameter")

# we use non empty kwargs as default to integrate user inputs
# it is needed because pydantic json schema does not represent "value"
Expand Down Expand Up @@ -877,15 +895,11 @@ def queryables_by_form(
if default and prop["type"] == "string" and isinstance(default, list):
default = ",".join(default)

# rename keywords from form with metadata mapping.
# needed to map constraints like "xxxx" to eodag parameter "cop_cds:xxxx"
key = eodag_key_from_provider_key(name, self.config.metadata_mapping)

is_required = bool(element.get("required"))
if is_required:
required_list.append(name)

queryables[key] = Annotated[
queryables[ecmwf_format(name)] = Annotated[
get_args(
json_field_definition_to_python(
prop,
Expand Down Expand Up @@ -914,16 +928,13 @@ def queryables_by_values(
"""
# Rename keywords from form with metadata mapping.
# Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
required = [
eodag_key_from_provider_key(k, self.config.metadata_mapping)
for k in required_keywords
]
required = [ecmwf_format(k) for k in required_keywords]

queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
for name, values in available_values.items():
# Rename keywords from form with metadata mapping.
# Needed to map constraints like "xxxx" to eodag parameter "ecmwf:xxxx"
key = eodag_key_from_provider_key(name, self.config.metadata_mapping)
key = ecmwf_format(name)

default = defaults.get(key)

Expand Down Expand Up @@ -1229,12 +1240,22 @@ def normalize_results(
:param kwargs: Search arguments
:returns: list of single :class:`~eodag.api.product._product.EOProduct`
"""

# formating of orderLink requires access to the productType value.
results.data = [
{**result, **results.product_type_def_params} for result in results
]

normalized = QueryStringSearch.normalize_results(self, results, **kwargs)

if len(normalized) > 0:
normalized[0].properties["_dc_qs"] = quote_plus(
orjson.dumps(results.query_params)
)
if not normalized:
return normalized

query_params_encoded = quote_plus(orjson.dumps(results.query_params))
for product in normalized:
properties = {**product.properties, **results.query_params}
properties["_dc_qs"] = query_params_encoded
product.properties = {ecmwf_format(k): v for k, v in properties.items()}

return normalized

Expand All @@ -1256,6 +1277,15 @@ def build_query_string(
:param kwargs: keyword arguments to be used in the query string
:return: formatted query params and encode query string
"""
# Reorder kwargs to make sure year/month/day/time if set overwrite default datetime.
# strip_quotes to remove duplicated quotes like "'1_1'" produced by convertors like to_geojson.
priority_keys = [
"startTimeFromAscendingNode",
"completionTimeFromAscendingNode",
]
ordered_kwargs = {k: kwargs[k] for k in priority_keys if k in kwargs}
ordered_kwargs.update({k: strip_quotes(v) for k, v in kwargs.items()})

return QueryStringSearch.build_query_string(
self, product_type=product_type, **kwargs
self, product_type=product_type, **ordered_kwargs
)
2 changes: 1 addition & 1 deletion eodag/resources/product_types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2396,7 +2396,7 @@ CAMS_EU_AIR_QUALITY_FORECAST:
sensorType: ATMOSPHERIC
license: proprietary
title: CAMS European air quality forecasts
missionStartDate: "2021-11-22T00:00:00Z"
missionStartDate: "2021-11-27T00:00:00Z"

CAMS_GFE_GFAS:
abstract: |
Expand Down
Loading
Loading