diff --git a/eodag/api/core.py b/eodag/api/core.py index 1c02a37b97..18c66d9de9 100644 --- a/eodag/api/core.py +++ b/eodag/api/core.py @@ -1879,10 +1879,10 @@ def _do_search( search_result = search_plugin.query(prep, **search_params) - if not isinstance(search_result.data, list): + if not isinstance(search_result, SearchResult): raise PluginImplementationError( - "The query function of a Search plugin must return a list of " - "results, got {} instead".format(type(search_result.data)) + "The query function of a Search plugin must return a SearchResult " + "results, got {} instead".format(type(search_result)) ) # Filter and attach to each eoproduct in the result the plugin capable of # downloading it (this is done to enable the eo_product to download itself @@ -1893,7 +1893,7 @@ def _do_search( # WARNING: this means an eo_product that has an invalid geometry can still # be returned as a search result if there was no search extent (because we # will not try to do an intersection) - for eo_product in search_result.data: + for eo_product in search_result: # if collection is not defined, try to guess using properties if eo_product.collection is None: pattern = re.compile(r"[^\w,]+") diff --git a/eodag/api/product/metadata_mapping.py b/eodag/api/product/metadata_mapping.py index e466a903ae..45afcf320c 100644 --- a/eodag/api/product/metadata_mapping.py +++ b/eodag/api/product/metadata_mapping.py @@ -242,10 +242,10 @@ def get_value( """ if isinstance(key, str): original_key = key.replace("__", ":") - key_with_COLON = key.replace("__", "_COLON_") result = kwargs.get(original_key) if result is not None: return result + key_with_COLON = key.replace("__", "_COLON_") return kwargs.get(key_with_COLON) return super().get_value(key, args, kwargs) @@ -963,6 +963,8 @@ def convert_interval_to_datetime_dict( start_date_object = datetime.strptime( start_utc_date, "%Y-%m-%dT%H:%M:%S.%fZ" ) + if end_utc_date == "None": + end_utc_date = start_utc_date end_date_object = datetime.strptime(end_utc_date, "%Y-%m-%dT%H:%M:%S.%fZ") delta_utc_date = end_date_object - start_date_object diff --git a/eodag/config.py b/eodag/config.py index 049c0940e1..eec9783969 100644 --- a/eodag/config.py +++ b/eodag/config.py @@ -396,9 +396,6 @@ class MetadataPreMapping(TypedDict, total=False): #: Maximum number of connections for concurrent HTTP requests max_connections: int #: :class:`~eodag.plugins.search.build_search_result.ECMWFSearch` - #: if date parameters are mandatory in the request - dates_required: bool - #: :class:`~eodag.plugins.search.build_search_result.ECMWFSearch` #: Whether end date should be excluded from search request or not end_date_excluded: bool #: :class:`~eodag.plugins.search.build_search_result.ECMWFSearch` diff --git a/eodag/plugins/apis/ecmwf.py b/eodag/plugins/apis/ecmwf.py index 1a8014e4e9..8a41903dbd 100644 --- a/eodag/plugins/apis/ecmwf.py +++ b/eodag/plugins/apis/ecmwf.py @@ -19,7 +19,6 @@ import logging import os -from datetime import datetime, timezone from typing import TYPE_CHECKING, Annotated import geojson @@ -35,8 +34,6 @@ from eodag.utils import ( DEFAULT_DOWNLOAD_TIMEOUT, DEFAULT_DOWNLOAD_WAIT, - DEFAULT_MISSION_START_DATE, - get_collection_dates, get_geometry_from_various, path_to_uri, sanitize, @@ -135,18 +132,6 @@ def query( kwargs.get("ecmwf:levtype", ""), ) - col_start, col_end = get_collection_dates( - getattr(self.config, "collection_config", {}) - ) - # start date - if "start_datetime" not in kwargs: - kwargs["start_datetime"] = col_start or DEFAULT_MISSION_START_DATE - # end date - if "end_datetime" not in kwargs: - kwargs["end_datetime"] = col_end or datetime.now(timezone.utc).isoformat( - timespec="seconds" - ) - # geometry if "geometry" in kwargs: kwargs["geometry"] = get_geometry_from_various(geometry=kwargs["geometry"]) diff --git a/eodag/plugins/search/build_search_result.py b/eodag/plugins/search/build_search_result.py index 26860400cd..fc8d40a6b0 100644 --- a/eodag/plugins/search/build_search_result.py +++ b/eodag/plugins/search/build_search_result.py @@ -30,7 +30,6 @@ import orjson from dateutil.parser import isoparse from dateutil.tz import tzutc -from dateutil.utils import today from pydantic import AliasChoices from pydantic.fields import FieldInfo from requests.auth import AuthBase @@ -52,7 +51,6 @@ from eodag.types import json_field_definition_to_python # noqa: F401 from eodag.types.queryables import Queryables, QueryablesDict from eodag.utils import ( - DEFAULT_MISSION_START_DATE, DEFAULT_SEARCH_TIMEOUT, deepcopy, dict_items_recursive_sort, @@ -66,10 +64,13 @@ from eodag.utils.dates import ( COMPACT_DATE_RANGE_PATTERN, DATE_RANGE_PATTERN, + compute_date_range_from_params, format_date, is_range_in_range, parse_date, parse_year_month_day, + time_values_to_hhmm, + validate_datetime_param, ) from eodag.utils.exceptions import DownloadError, NotAvailableError, ValidationError from eodag.utils.requests import fetch_json @@ -395,7 +396,6 @@ class ECMWFSearch(PostJsonSearch): used to parse metadata but that must not be included to the query * :attr:`~eodag.config.PluginConfig.end_date_excluded` (``bool``): Set to `False` if provider does not include end date to search - * :attr:`~eodag.config.PluginConfig.dates_required` (``bool``): if date parameters are mandatory in the request * :attr:`~eodag.config.PluginConfig.discover_queryables` (:class:`~eodag.config.PluginConfig.DiscoverQueryables`): configuration to fetch the queryables from a provider queryables endpoint; It has the following keys: @@ -566,17 +566,23 @@ def _preprocess_search_params( if v is not None } - # dates - # check if default dates have to be added - if getattr(self.config, "dates_required", False): - self._check_date_params(params, collection) - # read 'start_datetime' and 'end_datetime' from 'date' range if "date" in params: start_date, end_date = parse_date(params["date"]) params[START] = format_date(start_date) params[END] = format_date(end_date) + # start_datetime / computed from "date", "time", "year", "month", "day" + indirects = self._preprocess_indirect_date_parameters(params) + if params.get(START) is None: + value = indirects.get("start_datetime", None) + if value is not None: + params[START] = value + if params.get(END) is None: + value = indirects.get("end_datetime", None) + if value is not None: + params[END] = value + # adapt end date if it is midnight if END in params: end_date_excluded = getattr(self.config, "end_date_excluded", True) @@ -623,51 +629,55 @@ def _preprocess_search_params( return params - def _check_date_params( - self, keywords: dict[str, Any], collection: Optional[str] - ) -> None: - """checks if start and end date are present in the keywords and adds them if not""" - - if START in keywords and END in keywords: - return + def _preprocess_indirect_date_parameters(self, params: dict[str, Any]) -> dict: + """ + Compute start_datetime / end_datetime from "date", "time", "year", "month", "day" + """ + indirects: dict[str, Any] = {} - collection_conf = getattr(self.config, "metadata_mapping", {}) - if ( - collection - and collection in self.config.products - and "metadata_mapping" in self.config.products[collection] - ): - collection_conf = self.config.products[collection]["metadata_mapping"] - - # start time given, end time missing - if START in keywords: - keywords[END] = ( - keywords[START] - if END in collection_conf - # else self.get_collection_cfg_value( - else self.get_collection_cfg_dates(None, today().isoformat())[1] - ) - return - - if END in collection_conf: - mapping = collection_conf[START] - if not isinstance(mapping, list): - mapping = collection_conf[END] - if isinstance(mapping, list): - # if startTime is not given but other time params (e.g. year/month/(day)) are given, - # no default date is required - start, end = ecmwf_temporal_to_eodag(keywords) - if start is None: - col_start, col_end = self.get_collection_cfg_dates( - DEFAULT_MISSION_START_DATE, today().isoformat() - ) - keywords[START] = col_start - keywords[END] = ( - keywords[START] if END in collection_conf else col_end + # Validate and collect indirect date parameters + time = validate_datetime_param( + params.get("time"), "time", ["%H%M", "%H:%M", "%H%M%S", "%H:%M:%S"] + ) + year = validate_datetime_param(params.get("year"), "year", ["%Y"]) + month = validate_datetime_param(params.get("month"), "month", ["%m"]) + day = validate_datetime_param(params.get("day"), "day", ["%d"]) + + if time is not None: + time = time_values_to_hhmm(time) + indirects["time"] = time + if year is not None: + indirects["year"] = year + if month is not None: + indirects["month"] = month + if day is not None: + indirects["day"] = day + + # Compute date range from "date" param (takes precedence) + date = params.get("date", None) + if date is not None: + try: + start, end = compute_date_range_from_params(date=date, time=time) + indirects["start_datetime"] = start + indirects["end_datetime"] = end + return indirects + except Exception as e: + raise ValidationError( + 'Malformed parameter "date" (date given "{}", time given: "{}"): {}'.format( + params.get("date"), params.get("time"), str(e) ) - else: - keywords[START] = start - keywords[END] = end + ) + + # Compute date range from year/month/day/time params + start, end = compute_date_range_from_params( + year=year, month=month, day=day, time=time + ) + if start is not None: + indirects["start_datetime"] = start + if end is not None: + indirects["end_datetime"] = end + + return indirects def _get_collection_queryables( self, collection: Optional[str], alias: Optional[str], filters: dict[str, Any] diff --git a/eodag/plugins/search/qssearch.py b/eodag/plugins/search/qssearch.py index b6c38ae881..723739ad25 100644 --- a/eodag/plugins/search/qssearch.py +++ b/eodag/plugins/search/qssearch.py @@ -1738,6 +1738,11 @@ def query( qp, _ = self.build_query_string(collection, keywords) + # Force sort qp list parameters + for key in qp: + if isinstance(qp[key], list): + qp[key].sort() + for query_param, query_value in qp.items(): if ( query_param diff --git a/eodag/resources/providers.yml b/eodag/resources/providers.yml index 51e01d2c7f..e7a0856bad 100644 --- a/eodag/resources/providers.yml +++ b/eodag/resources/providers.yml @@ -4424,7 +4424,6 @@ type: ECMWFSearch need_auth: true ssl_verify: true - dates_required: True discover_queryables: fetch_url: null collection_fetch_url: null @@ -4622,7 +4621,6 @@ type: ECMWFSearch need_auth: true ssl_verify: true - dates_required: True discover_queryables: fetch_url: null collection_fetch_url: null diff --git a/eodag/utils/dates.py b/eodag/utils/dates.py index 6e8f9e29c1..2db546bc08 100644 --- a/eodag/utils/dates.py +++ b/eodag/utils/dates.py @@ -17,6 +17,7 @@ # limitations under the License. """eodag.rest.dates methods that must be importable without eodag[server] installeds""" +import calendar import datetime import re from datetime import date @@ -402,3 +403,194 @@ def format_date_range(start: dt, end: dt) -> str: '2020-12-02/2020-12-31' """ return f"{format_date(start)}/{format_date(end)}" + + +def validate_datetime_param( + value: Optional[Union[str, list[str]]], + param_name: str, + formatters: list[str], +) -> Optional[list[str]]: + """Validate and collect parameter values matching any of the given datetime formats. + + Ensures each value can be parsed by at least one of the ``formatters`` + (``datetime.strptime`` patterns), and returns the sorted list of valid values. + + :param value: Raw value(s) from search parameters (string or list of strings) + :param param_name: Parameter name (used in error messages) + :param formatters: ``datetime.strptime`` format strings used for validation + :returns: Sorted list of valid values, or ``None`` if ``value`` is ``None`` + :raises ValidationError: If none of the values match any formatter + + Examples: + >>> validate_datetime_param(["2023", "2024"], "year", ["%Y"]) + ['2023', '2024'] + >>> validate_datetime_param("12:00", "time", ["%H:%M", "%H%M"]) + ['12:00'] + >>> validate_datetime_param(None, "year", ["%Y"]) is None + True + >>> validate_datetime_param("bad", "year", ["%Y"]) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + eodag.utils.exceptions.ValidationError: Malformed parameter "year": ... + """ + if value is None: + return None + + if not isinstance(value, list): + value = [value] + + buffer = [] + has_error = None + for item in value: + for formatter in formatters: + try: + # Prepend a dummy year when format contains %d without %Y + # to avoid ambiguity (deprecated in 3.14, error in 3.15+) + if "%d" in formatter and "%Y" not in formatter: + dt.strptime(f"2000 {item}", f"%Y {formatter}") + else: + dt.strptime(item, formatter) + buffer.append(item) + except Exception as e: + has_error = e + + if has_error is not None and len(buffer) == 0: + raise ValidationError( + 'Malformed parameter "{}": {}'.format(param_name, str(has_error)) + ) + + buffer.sort() + return buffer + + +def time_values_to_hhmm(time_values: list[str]) -> list[str]: + """Convert time values to 4-digit HHMM format. + + Strips non-digit characters (e.g. ``"12:00"`` -> ``"1200"``, ``"06:00"`` -> ``"0600"``), + then right-pads with zeros to handle 2-digit hour-only values (e.g. ``"06"`` -> ``"0600"``). + Deduplicates while preserving order. + + :param time_values: List of time strings in various formats + :returns: List of unique time strings in HHMM format + + Examples: + >>> time_values_to_hhmm(["12:00", "06:00"]) + ['1200', '0600'] + >>> time_values_to_hhmm(["12:00", "12:00"]) + ['1200'] + >>> time_values_to_hhmm(["06"]) + ['0600'] + """ + buffer: list[str] = [] + for time_str in time_values: + time_str = re.sub("[^0-9]+", "", time_str) + time_str = time_str.ljust(4, "0") + if time_str not in buffer: + buffer.append(time_str) + return buffer + + +def to_iso_utc_string( + raw: Optional[Union[dt, str]], +) -> Optional[str]: + """Convert a datetime or date string to an ISO 8601 UTC string. + + :param raw: A datetime object or date string to convert + :returns: ISO 8601 formatted UTC string (``YYYY-MM-DDTHH:MM:SSZ``), or ``None`` + + Examples: + >>> from datetime import datetime + >>> to_iso_utc_string(datetime(2020, 1, 1, 12, 0)) + '2020-01-01T12:00:00Z' + >>> to_iso_utc_string("2020-01-01") + '2020-01-01T00:00:00Z' + >>> to_iso_utc_string(None) is None + True + """ + if raw is None: + return None + if isinstance(raw, dt): + raw = raw.replace(tzinfo=tz.UTC) + return raw.strftime("%Y-%m-%dT%H:%M:%SZ") + if not isinstance(raw, str): + return None + + try: + parsed_datetime = isoparse(raw) + if parsed_datetime.tzinfo is None: + parsed_datetime = parsed_datetime.replace(tzinfo=tz.UTC) + return parsed_datetime.strftime("%Y-%m-%dT%H:%M:%SZ") + except (ValueError, OverflowError): + return None + + +def compute_date_range_from_params( + date: Optional[str] = None, + time: Optional[list[str]] = None, + year: Optional[list[str]] = None, + month: Optional[list[str]] = None, + day: Optional[list[str]] = None, +) -> tuple[Optional[str], Optional[str]]: + """Compute start/end ISO UTC datetime strings from date parameters. + + Handles two modes: + + - **date** + optional **time**: parse the date string and apply time bounds + - **year** + optional **month**/**day**/**time**: compute bounds from year/month/day/time ranges + + Time values are expected in HHMM format (see :func:`time_values_to_hhmm`). + + Returns ``(None, None)`` if neither ``date`` nor ``year`` is provided. + + :param date: Date string (single date, or interval with ``/`` or ``/to/``) + :param time: List of normalized time strings in HHMM format + :param year: List of year strings + :param month: List of month strings (zero-padded) + :param day: List of day strings (zero-padded) + :returns: Tuple of (start_datetime, end_datetime) as ISO UTC strings + + Examples: + >>> compute_date_range_from_params(date="2020-12-15") + ('2020-12-15T00:00:00Z', '2020-12-15T00:00:00Z') + >>> compute_date_range_from_params(date="2020-12-15", time=["0600", "1800"]) + ('2020-12-15T06:00:00Z', '2020-12-15T18:00:00Z') + >>> compute_date_range_from_params(year=["2020", "2021"]) + ('2020-01-01T00:00:00Z', '2021-12-31T23:59:59Z') + >>> compute_date_range_from_params(year=["2020"], month=["03"], day=["15"]) + ('2020-03-15T00:00:00Z', '2020-03-15T23:59:59Z') + >>> compute_date_range_from_params() + (None, None) + """ + if date is not None: + start, end = parse_date(date, time) + return to_iso_utc_string(start), to_iso_utc_string(end) + + if year is not None: + min_year, max_year = year[0], year[-1] + + if month: + min_month, max_month = month[0], month[-1] + else: + min_month, max_month = "01", "12" + + _, last_day = calendar.monthrange(int(max_year), int(max_month)) + min_day = "01" + max_day = str(last_day).zfill(2) + + if day: + if min_day <= day[0] <= max_day: + min_day = day[0] + if min_day <= day[-1] <= max_day: + max_day = day[-1] + + if time: + min_time = f"{time[0][0:2]}:{time[0][2:4]}:00" + max_time = f"{time[-1][0:2]}:{time[-1][2:4]}:00" + else: + min_time, max_time = "00:00:00", "23:59:59" + + start_str = f"{min_year}-{min_month}-{min_day}T{min_time}Z" + end_str = f"{max_year}-{max_month}-{max_day}T{max_time}Z" + return start_str, end_str + + return None, None diff --git a/tests/units/test_apis_plugins.py b/tests/units/test_apis_plugins.py index 0fb898ab80..c07e798170 100644 --- a/tests/units/test_apis_plugins.py +++ b/tests/units/test_apis_plugins.py @@ -18,13 +18,11 @@ import ast import os import unittest -from datetime import datetime, timedelta, timezone from tempfile import TemporaryDirectory from unittest import mock import geojson import responses -from dateutil.parser import isoparse from ecmwfapi.api import ANONYMOUS_APIKEY_VALUES from shapely.geometry import shape @@ -32,7 +30,6 @@ from eodag.utils import deepcopy from tests.context import ( DEFAULT_DOWNLOAD_WAIT, - DEFAULT_MISSION_START_DATE, ONLINE_STATUS, USER_AGENT, USGS_TMPFILE, @@ -134,16 +131,8 @@ def test_plugins_apis_ecmwf_query_dates_missing(self): collection=self.collection, ) eoproduct = results.data[0] - self.assertIn( - eoproduct.properties["start_datetime"], - DEFAULT_MISSION_START_DATE, - ) - # less than 10 seconds should have passed since the product was created - self.assertLess( - datetime.now(timezone.utc) - isoparse(eoproduct.properties["end_datetime"]), - timedelta(seconds=10), - "stop date should have been created from datetime.now", - ) + self.assertNotIn("start_datetime", eoproduct.properties) + self.assertNotIn("end_datetime", eoproduct.properties) # missing start & stop and plugin.collection_config set (set in core._prepare_search) self.api_plugin.config.collection_config = { @@ -157,14 +146,8 @@ def test_plugins_apis_ecmwf_query_dates_missing(self): collection=self.collection, ) eoproduct = results[0] - self.assertEqual( - eoproduct.properties["start_datetime"], - "1985-10-26T00:00:00.000Z", - ) - self.assertEqual( - eoproduct.properties["end_datetime"], - "2015-10-21T00:00:00.000Z", - ) + self.assertNotIn("start_datetime", eoproduct.properties) + self.assertNotIn("end_datetime", eoproduct.properties) def test_plugins_apis_ecmwf_query_without_collection(self): """ diff --git a/tests/units/test_search_plugins.py b/tests/units/test_search_plugins.py index 6d48fbb095..20f643ea0e 100644 --- a/tests/units/test_search_plugins.py +++ b/tests/units/test_search_plugins.py @@ -22,7 +22,6 @@ import ssl import unittest from copy import deepcopy as copy_deepcopy -from datetime import datetime from pathlib import Path from typing import Literal, Union, get_origin from unittest import mock @@ -50,7 +49,6 @@ ValidationError, ) from tests.context import ( - DEFAULT_MISSION_START_DATE, DEFAULT_SEARCH_TIMEOUT, HTTP_REQ_TIMEOUT, NOT_AVAILABLE, @@ -1298,7 +1296,6 @@ def test_plugins_search_postjsonsearch_default_dates( provider = "wekeo_ecmwf" search_plugins = self.plugins_manager.get_search_plugins(provider=provider) search_plugin = next(search_plugins) - search_plugin.config.dates_required = True mock_request.return_value = MockResponse({"features": []}, 200) # year, month, day, time given -> don't use default dates search_plugin.query( @@ -1385,10 +1382,6 @@ def test_plugins_search_postjsonsearch_default_dates( mock_request.assert_called_with( "https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/search", json={ - "year": ["1940"], - "month": ["01"], - "day": ["01"], - "time": ["00:00"], "dataset_id": "EO:ECMWF:DAT:REANALYSIS_ERA5_SINGLE_LEVELS", "itemsPerPage": 20, "startIndex": 0, @@ -1428,13 +1421,9 @@ def test_plugins_search_postjsonsearch_default_dates( **{"_collection": "CAMS_EAC4"}, ) search_plugin.query(collection="CAMS_EAC4", prep=PreparedSearch()) - # `date` mapping to pass validation of ECMWF parameters mock_request.assert_called_with( "https://gateway.prod.wekeo2.eu/hda-broker/api/v1/dataaccess/search", json={ - "startdate": "2003-01-01T00:00:00.000Z", - "enddate": "2003-01-01T00:00:00.000Z", - "date": "2003-01-01/2003-01-01", "dataset_id": "EO:ECMWF:DAT:CAMS_GLOBAL_REANALYSIS_EAC4", "itemsPerPage": 20, "startIndex": 0, @@ -1443,8 +1432,6 @@ def test_plugins_search_postjsonsearch_default_dates( timeout=60, verify=True, ) - # restore previous config - delattr(search_plugin.config, "dates_required") @mock.patch("eodag.plugins.search.qssearch.PostJsonSearch._request", autospec=True) def test_plugins_search_postjsonsearch_query_params_wekeo(self, mock__request): @@ -2717,9 +2704,8 @@ def test_plugins_search_ecmwfsearch_exclude_end_date(self): eoproduct.properties["end_datetime"], ) - def test_plugins_search_ecmwfsearch_dates_missing(self): + def test_plugins_search_ecmwfsearch_dates(self): """ECMWFSearch.query must use default dates if missing""" - self.search_plugin.config.dates_required = True # given start & stop results = self.search_plugin.query( @@ -2742,46 +2728,11 @@ def test_plugins_search_ecmwfsearch_dates_missing(self): collection=self.collection, ) eoproduct = results.data[0] - self.assertIn( - eoproduct.properties["start_datetime"], - DEFAULT_MISSION_START_DATE, - ) - exp_end_date = datetime.strptime( - DEFAULT_MISSION_START_DATE, "%Y-%m-%dT%H:%M:%S.%fZ" - ) - self.assertIn( - eoproduct.properties["end_datetime"], - exp_end_date.strftime("%Y-%m-%dT%H:%M:%S.%fZ")[:-4] + "Z", - ) - - # missing start & stop and plugin.collection_config set (set in core._prepare_search) - self.search_plugin.config.collection_config = { - "_collection": self.collection, - "extent": { - "spatial": {"bbox": [[-180.0, -90.0, 180.0, 90.0]]}, - "temporal": {"interval": [["1985-10-26", "2015-10-21"]]}, - }, - } - results = self.search_plugin.query( - collection=self.collection, - ) - eoproduct = results.data[0] - self.assertEqual( - eoproduct.properties["start_datetime"], - "1985-10-26T00:00:00.000Z", - ) - self.assertEqual( - eoproduct.properties["end_datetime"], - "1985-10-26T00:00:00.000Z", - ) - - # restore previous config - delattr(self.search_plugin.config, "dates_required") + self.assertNotIn("start_datetime", eoproduct.properties) + self.assertNotIn("end_datetime", eoproduct.properties) def test_plugins_search_ecmwfsearch_with_year_month_day_filter(self): """ECMWFSearch.query must use have datetime in response if year, month, day used in filters""" - self.search_plugin.config.dates_required = True - results = self.search_plugin.query( prep=PreparedSearch(), collection="ERA5_SL", @@ -2815,9 +2766,6 @@ def test_plugins_search_ecmwfsearch_with_year_month_day_filter(self): ["20", "21"], ) - # restore previous config - delattr(self.search_plugin.config, "dates_required") - def test_plugins_search_ecmwfsearch_collection_with_alias(self): """alias of collection must be used in search result""" self.search_plugin.config.collection_config = {