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

942 query parameters validation #1069

Draft
wants to merge 5 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
25 changes: 24 additions & 1 deletion eodag/api/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
RequestError,
UnsupportedProductType,
UnsupportedProvider,
ValidationError,
)
from eodag.utils.stac_reader import fetch_stac_items

Expand Down Expand Up @@ -1613,6 +1614,16 @@ def _prepare_search(
if not provider:
provider = preferred_provider
providers = [plugin.provider for plugin in search_plugins]
to_remove = []
for i, p in enumerate(providers):
try:
self.list_queryables(p, **kwargs)
except ValidationError:
to_remove.append(p)
if i == len(providers) - 1 and len(to_remove) == len(providers):
raise

providers = [p for p in providers if p not in to_remove]
if provider not in providers:
logger.warning(
"Product type '%s' is not available with provider '%s'. "
Expand Down Expand Up @@ -2174,7 +2185,8 @@ def list_queryables(
pt["ID"]
for pt in self.list_product_types(provider=provider, fetch_providers=False)
]
product_type = kwargs.get("productType")
user_kwargs = deepcopy(kwargs)
product_type = kwargs.get("productType", None)

if product_type:
try:
Expand Down Expand Up @@ -2213,6 +2225,17 @@ def list_queryables(
queryable.default = kwargs[key]

queryables.update(model_fields_to_annotated(common_queryables))
for key in user_kwargs:
if key in [
"startTimeFromAscendingNode",
"completionTimeFromAscendingNode",
"productType",
"geometry",
"sortBy",
]:
continue
elif key not in queryables.keys():
raise ValidationError(f"parameter {key} not queryable")

return queryables

Expand Down
1 change: 1 addition & 0 deletions eodag/plugins/apis/cds.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ def discover_queryables(
non_empty_kwargs.pop(constraint_param)
else:
not_queryables.add(constraint_param)

if not_queryables:
raise ValidationError(
f"parameter(s) {str(not_queryables)} not queryable"
Expand Down
35 changes: 35 additions & 0 deletions eodag/plugins/apis/ecmwf.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,21 @@
import geojson
from ecmwfapi import ECMWFDataServer, ECMWFService
from ecmwfapi.api import APIException, Connection, get_apikey_values
from pydantic import create_model
from pydantic.fields import FieldInfo
from typing_extensions import Annotated, get_args

from eodag.plugins.apis.base import Api
from eodag.plugins.search.base import Search
from eodag.plugins.search.build_search_result import BuildPostSearchResult
from eodag.rest.stac import DEFAULT_MISSION_START_DATE
from eodag.types import json_field_definition_to_python, model_fields_to_annotated
from eodag.utils import (
DEFAULT_DOWNLOAD_TIMEOUT,
DEFAULT_DOWNLOAD_WAIT,
DEFAULT_ITEMS_PER_PAGE,
DEFAULT_PAGE,
deepcopy,
get_geometry_from_various,
path_to_uri,
sanitize,
Expand Down Expand Up @@ -274,3 +279,33 @@ def download_all(
def clear(self) -> None:
"""Clear search context"""
pass

def discover_queryables(
self, **kwargs: Any
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
"""Get queryables list for ecmwf using metadata mapping

:param kwargs: additional filters for queryables (`productType` and other search
arguments)
:type kwargs: Any
:returns: fetched queryable parameters dict
:rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
"""
metadata_mapping: Dict[str, Any] = deepcopy(
getattr(self.config, "metadata_mapping", {})
)
queryables = []
for key, mapping in metadata_mapping.items():
if isinstance(mapping, list):
queryables.append(key)

field_definitions = dict()
for queryable in queryables:
default = kwargs.get(queryable, None)
annotated_def = json_field_definition_to_python(
{}, default_value=default, required=True
)
field_definitions[queryable] = get_args(annotated_def)

python_queryables = create_model("m", **field_definitions).model_fields
return dict(**model_fields_to_annotated(python_queryables))
31 changes: 30 additions & 1 deletion eodag/utils/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
# limitations under the License.
import copy
import logging
import re
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Set, Union

import requests
Expand Down Expand Up @@ -76,7 +78,11 @@ def get_constraint_queryables_with_additional_params(
if provider_key and provider_key in constraint:
eodag_provider_key_mapping[provider_key] = param
params_available[param] = True
if value in constraint[provider_key]:
if (
value in constraint[provider_key]
or "date" in provider_key
and _check_date(value, constraint[provider_key][0])
):
params_matched[param] = True
values_available[param].update(constraint[provider_key])
# match with default values of params
Expand Down Expand Up @@ -158,6 +164,29 @@ def get_constraint_queryables_with_additional_params(
return queryables


def _check_date(datetime_value, constraint_value):
try:
date = datetime.strptime(datetime_value, "%Y-%m-%dT%H:%M:%SZ")
except ValueError:
try:
date = datetime.strptime(datetime_value, "%Y-%m-%d")
except ValueError:
return False
constraint_dates_str = re.findall(
"[0-9]{4}-[0-1][0-9]-[0-3][0-9]", constraint_value
)
constraint_dates = []
for c_date in constraint_dates_str:
constraint_dates.append(datetime.strptime(c_date, "%Y-%m-%d"))
if len(constraint_dates) == 0:
return False
if len(constraint_dates) == 1 and date == constraint_dates[0]:
return True
if len(constraint_dates) > 1 and constraint_dates[0] <= date <= constraint_dates[1]:
return True
return False


def fetch_constraints(
constraints_url: str, plugin: Union[Search, Api]
) -> List[Dict[Any, Any]]:
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/test_core_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ def tearDown(self):
self.expanduser_mock.stop()
self.tmp_home_dir.cleanup()

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request", autospec=True
)
@mock.patch(
"eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
)
@mock.patch("eodag.plugins.search.qssearch.PostJsonSearch._request", autospec=True)
def test_core_providers_config_update(
self, mock__request, mock_fetch_product_types_list
self, mock__request, mock_fetch_product_types_list, mock_qssearch_request
):
"""Providers config must be updatable"""
mock__request.return_value = mock.Mock()
Expand Down
50 changes: 36 additions & 14 deletions tests/integration/test_core_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from requests.exceptions import RequestException

from eodag.utils.exceptions import UnsupportedProductType
from tests import TEST_RESOURCES_PATH
from tests.context import (
EODataAccessGateway,
Expand Down Expand Up @@ -80,13 +81,18 @@ def test_core_search_errors_qssearch(
self.dag.set_preferred_provider("peps")
self.assertRaises(ValidationError, self.dag.search, raise_errors=True)
self.assertRaises(
RequestError, self.dag.search, productType="foo", raise_errors=True
UnsupportedProductType,
self.dag.search,
productType="foo",
raise_errors=True,
)
# search iterator
self.assertRaises(
RequestError, next, self.dag.search_iter_page(productType="foo")
)
with self.assertRaises(UnsupportedProductType):
self.dag.search_iter_page(productType="foo")

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request", autospec=True
)
@mock.patch(
"eodag.plugins.search.qssearch.requests.post",
autospec=True,
Expand All @@ -100,7 +106,11 @@ def test_core_search_errors_qssearch(
autospec=True,
)
def test_core_search_errors_stacsearch(
self, mock_query, mock_fetch_product_types_list, mock_post
self,
mock_query,
mock_fetch_product_types_list,
mock_post,
mock_qssearch_request,
):
mock_query.return_value = ([], 0)
# StacSearch / astraea_eod
Expand Down Expand Up @@ -155,13 +165,19 @@ def test_core_search_errors_odata(
self.dag.set_preferred_provider("onda")
self.assertRaises(ValidationError, self.dag.search, raise_errors=True)
self.assertRaises(
RequestError, self.dag.search, productType="foo", raise_errors=True
UnsupportedProductType,
self.dag.search,
productType="foo",
raise_errors=True,
)
# search iterator
self.assertRaises(
RequestError, next, self.dag.search_iter_page(productType="foo")
)
with self.assertRaises(UnsupportedProductType):
self.dag.search_iter_page(productType="foo")

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
autospec=True,
)
@mock.patch(
"eodag.plugins.apis.usgs.api.scene_search", autospec=True, side_effect=USGSError
)
Expand All @@ -170,18 +186,24 @@ def test_core_search_errors_odata(
"eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
)
def test_core_search_errors_usgs(
self, mock_fetch_product_types_list, mock_login, mock_scene_search
self,
mock_fetch_product_types_list,
mock_login,
mock_scene_search,
mock_qssearch_get,
):
# UsgsApi / usgs
self.dag.set_preferred_provider("usgs")
self.assertRaises(NoMatchingProductType, self.dag.search, raise_errors=True)
self.assertRaises(
RequestError, self.dag.search, raise_errors=True, productType="foo"
UnsupportedProductType,
self.dag.search,
raise_errors=True,
productType="foo",
)
# search iterator
self.assertRaises(
RequestError, next, self.dag.search_iter_page(productType="foo")
)
with self.assertRaises(UnsupportedProductType):
self.dag.search_iter_page(productType="foo")

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
Expand Down
46 changes: 40 additions & 6 deletions tests/integration/test_search_stac_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,16 @@ def test_search_stac_static_load_root_recursive(self):
self.assertEqual(item.provider, self.stac_provider)
self.assertEqual(item.product_type, self.product_type)

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
autospec=True,
)
@mock.patch(
"eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
)
def test_search_stac_static(self, mock_fetch_product_types_list):
def test_search_stac_static(
self, mock_fetch_product_types_list, mock_qssearch_request
):
"""Use StaticStacSearch plugin to search all items"""
items, nb = self.dag.search()
self.assertEqual(len(items), self.root_cat_len)
Expand Down Expand Up @@ -257,10 +263,16 @@ def test_search_stac_static_crunch_filter_date(self):
for item in filtered_items:
self.assertIn("2018", item.properties["startTimeFromAscendingNode"])

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
autospec=True,
)
@mock.patch(
"eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
)
def test_search_stac_static_by_date(self, mock_fetch_product_types_list):
def test_search_stac_static_by_date(
self, mock_fetch_product_types_list, mock_qssearch_request
):
"""Use StaticStacSearch plugin to search by date"""
filtered_items, nb = self.dag.search(start="2018-01-01", end="2019-01-01")
self.assertEqual(len(filtered_items), self.child_cat_len)
Expand Down Expand Up @@ -328,18 +340,28 @@ def test_search_stac_static_crunch_filter_overlap(self):
)
self.assertEqual(len(filtered_items), 1)

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
autospec=True,
)
@mock.patch(
"eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
)
def test_search_stac_static_by_geom(self, mock_fetch_product_types_list):
def test_search_stac_static_by_geom(
self, mock_fetch_product_types_list, mock_qssearch_request
):
"""Use StaticStacSearch plugin to search by geometry"""
items, nb = self.dag.search(
geom=self.extent_big,
)
self.assertEqual(len(items), 3)
self.assertEqual(nb, 3)

def test_search_stac_static_crunch_filter_property(self):
@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
autospec=True,
)
def test_search_stac_static_crunch_filter_property(self, mock_qssearch_request):
"""load_stac_items from root and filter by property"""
with pytest.warns(
DeprecationWarning,
Expand All @@ -366,19 +388,31 @@ def test_search_stac_static_crunch_filter_property(self):
)
self.assertEqual(len(filtered_items), 1)

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
autospec=True,
)
@mock.patch(
"eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
)
def test_search_stac_static_by_property(self, mock_fetch_product_types_list):
def test_search_stac_static_by_property(
self, mock_fetch_product_types_list, mock_qssearch_request
):
"""Use StaticStacSearch plugin to search by property"""
items, nb = self.dag.search(orbitNumber=110)
self.assertEqual(len(items), 3)
self.assertEqual(nb, 3)

@mock.patch(
"eodag.plugins.search.qssearch.QueryStringSearch._request",
autospec=True,
)
@mock.patch(
"eodag.api.core.EODataAccessGateway.fetch_product_types_list", autospec=True
)
def test_search_stac_static_by_cloudcover(self, mock_fetch_product_types_list):
def test_search_stac_static_by_cloudcover(
self, mock_fetch_product_types_list, mock_qssearch_request
):
"""Use StaticStacSearch plugin to search by cloud cover"""
items, nb = self.dag.search(cloudCover=10)
self.assertEqual(len(items), 1)
Expand Down
Loading
Loading