Repository: ecmwf/earthkit-data (python)

Issue:
normalize to date-list does not accept requests which are already lists of dates
### Is your feature request related to a problem? Please describe.

(I'm not sure if this qualifies as a bug or an enhancement, but to be positive I'll put it in the enhancement bucket!)

The normalize decorator for date-list successfully turns a MARS format list-string (i.e. a string with `/` used as a separator to make a list). However, if I provide a list of dates the decorator returns the following error:
```
AttributeError: 'StrWrapper' object has no attribute 'to_datetime'
```

I experienced this primarily with cds requests, where valid cdsapi requests now fail, for example:
```
s = from_source(
    "cds",
    "reanalysis-era5-single-levels",
    variable=["2t"],  #, "msl"],
    product_type="reanalysis",
    area=[50, -50, 20, 50],
    date=["2007-01-01","2008-02-04"],
    time="12:00",
)
```

And for completeness if I pass the date in the MARS format it works fine:
```
s = from_source(
    "cds",
    "reanalysis-era5-single-levels",
    variable=["2t"],  #, "msl"],
    product_type="reanalysis",
    area=[50, -50, 20, 50],
    date="2007-01-01/2008-02-04",
    time="12:00",
)
```


### Describe the solution you'd like

- I would like the decorators not to fail if the argument is already in the expected format
- I would not like to lose functionality we have implemented in the cdsapi

### Describe alternatives you've considered

_No response_

### Additional context

For a super simple minimal reproducible example, this code might be useful:
```
from earthkit.data.decorators import normalize

@normalize("date", "date-list(%Y-%m-%d)")
def f_norm(*args, **kwargs):
    return args, kwargs

# understands:
f_norm(date="2002-01-01/2003-03-01/2003-02-01")
# does not understand:
f_norm(date=["2002-01-01","2003-03-01","2003-02-01"])
```

### Organisation

ECMWF

Pull request:
Feature/fix datelist in request
fixes #237 

Patch:
diff --git a/earthkit/data/wrappers/string.py b/earthkit/data/wrappers/string.py
index 6439050..41949fc 100644
--- a/earthkit/data/wrappers/string.py
+++ b/earthkit/data/wrappers/string.py
@@ -45,6 +45,9 @@ class StrWrapper(Wrapper):
     def datetime(self):
         return parse_date(self.data)
 
+    def to_datetime(self):
+        return self.datetime()
+
     def to_datetime_list(self):
         from earthkit.data.utils.dates import mars_like_date_list
 


Test patch:
diff --git a/tests/normalize/test_normalize_date.py b/tests/normalize/test_normalize_date.py
index dd26dd8..198202a 100644
--- a/tests/normalize/test_normalize_date.py
+++ b/tests/normalize/test_normalize_date.py
@@ -39,14 +39,14 @@ def test_normalize_dates_from_source():
         assert dates_list_3(source[0]) == [datetime.datetime(2020, 5, 13, 12, 0)]
 
 
-def test_dates_formatted_1():
+def test_normalize_dates_formatted_1():
     date_formatted = normalize("d", "date", format="%Y.%m.%d")(f)
 
     assert date_formatted("20200513") == "2020.05.13"
 
 
 @pytest.mark.skip(reason="Not implemented yet.")
-def test_enum_dates_formatted():
+def test_normalize_enum_dates_formatted():
     date_formatted = normalize(
         "d", values=["20010512", "20020512"], type="date", format="%Y.%m.%d"
     )(f)
@@ -54,8 +54,7 @@ def test_enum_dates_formatted():
     assert date_formatted("20200513") == "2020.05.13"
 
 
-@pytest.mark.xfail
-def test_dates_formatted():
+def test_normalize_dates_formatted():
     date_formatted = normalize("d", "date-list(%Y.%m.%d)")(f)
 
     assert date_formatted(["20200513", "20200514"]) == ["2020.05.13", "2020.05.14"]
@@ -64,7 +63,7 @@ def test_dates_formatted():
     assert date_formatted([datetime.datetime(2020, 5, 13, 23, 59)]) == ["2020.05.13"]
 
 
-def test_dates_multiple():
+def test_normalize_dates_multiple():
     date_1 = normalize("d", "date-list(%Y.%m.%d)")(f)
     date_2 = normalize("d", "date(%Y.%m.%d)", multiple=True)(f)
     date_3 = normalize("d", "date(%Y.%m.%d)", multiple=False)(f)
@@ -78,8 +77,60 @@ def test_dates_multiple():
         date_4("20200514")
 
 
+@pytest.mark.parametrize(
+    "date,expected",
+    [
+        (
+            "20200511/TO/20200512",
+            [
+                "2020.05.11",
+                "2020.05.12",
+            ],
+        ),
+        (
+            "20200511/to/20200512",
+            [
+                "2020.05.11",
+                "2020.05.12",
+            ],
+        ),
+        (
+            "20200511/TO/20200513/BY/2",
+            [
+                "2020.05.11",
+                "2020.05.13",
+            ],
+        ),
+        (
+            "20200511/to/20200513/by/2",
+            [
+                "2020.05.11",
+                "2020.05.13",
+            ],
+        ),
+        (
+            ["20200511", "20200513"],
+            [
+                "2020.05.11",
+                "2020.05.13",
+            ],
+        ),
+        (
+            ["2020-05-11", "2020-05-13"],
+            [
+                "2020.05.11",
+                "2020.05.13",
+            ],
+        ),
+    ],
+)
+def test_normalize_date_list(date, expected):
+    n = normalize("d", "date-list(%Y.%m.%d)")(f)
+    assert n(date) == expected
+
+
 @pytest.mark.xfail
-def test_dates_formatted_from_pandas():
+def test_normalize_dates_formatted_from_pandas():
     import pandas as pd
 
     df1 = pd.DataFrame(
@@ -113,7 +164,7 @@ def test_dates_formatted_from_pandas():
 
 
 @pytest.mark.skip("Not implemented (yet?).")
-def test_dates_formatted_from_object():
+def test_normalize_dates_formatted_from_object():
     date_formatted = normalize("d", "date", format="%Y.%m.%d")(f)
 
     class CustomDateObject:
@@ -133,7 +184,7 @@ def test_dates_formatted_from_object():
     assert date_formatted(obj) == "2020.05.13"
 
 
-def test_date_none_1():
+def test_normalize_date_none_1():
     @normalize(
         "name",
         "date(%Y%m%d)",
@@ -145,7 +196,7 @@ def test_date_none_1():
     assert date_default_none() is None
 
 
-def test_date_list_none_1():
+def test_normalize_date_list_none_1():
     @normalize(
         "name",
         "date-list(%Y%m%d)",
@@ -157,7 +208,7 @@ def test_date_list_none_1():
     assert date_default_none() is None
 
 
-def test_date_default_1():
+def test_normalize_date_default_1():
     @normalize(
         "name",
         "date",
diff --git a/tests/sources/test_cds.py b/tests/sources/test_cds.py
index 15198aa..a2a359c 100644
--- a/tests/sources/test_cds.py
+++ b/tests/sources/test_cds.py
@@ -89,31 +89,35 @@ def test_cds_grib_split_on_var():
     assert len(s.indexes) == 2
 
 
+@pytest.mark.parametrize(
+    "date,expected_date",
+    [
+        (
+            "2012-12-12/to/2012-12-14",
+            [20121212, 20121212, 20121213, 20121213, 20121214, 20121214],
+        ),
+        (
+            ["2014-05-12", "2014-05-13", "2014-05-14"],
+            [20140512, 20140512, 20140513, 20140513, 20140514, 20140514],
+        ),
+    ],
+)
 @pytest.mark.long_test
 @pytest.mark.download
 @pytest.mark.skipif(NO_CDS, reason="No access to CDS")
-def test_cds_grib_multi_var_date():
+def test_cds_grib_multi_var_date(date, expected_date):
     s = from_source(
         "cds",
         "reanalysis-era5-single-levels",
         variable=["2t", "msl"],
         product_type="reanalysis",
         area=[50, -50, 20, 50],
-        date="2012-12-12/to/2012-12-15",
+        date=date,
         time="12:00",
     )
-    assert len(s) == 8
-    assert s.metadata("param") == ["2t", "msl"] * 4
-    assert s.metadata("date") == [
-        20121212,
-        20121212,
-        20121213,
-        20121213,
-        20121214,
-        20121214,
-        20121215,
-        20121215,
-    ]
+    assert len(s) == 6
+    assert s.metadata("param") == ["2t", "msl"] * 3
+    assert s.metadata("date") == expected_date
 
 
 @pytest.mark.long_test
diff --git a/tests/sources/test_polytope.py b/tests/sources/test_polytope.py
index fe3b27e..039a953 100644
--- a/tests/sources/test_polytope.py
+++ b/tests/sources/test_polytope.py
@@ -25,6 +25,7 @@ def test_no_polytope_client(monkeypatch):
         assert "pip install polytope-client" in str(excinfo.value)
 
 
+@pytest.mark.skip("Data not available?")
 @pytest.mark.long_test
 @pytest.mark.download
 @pytest.mark.skipif(NO_POLYTOPE, reason="No access to Polytope Web API")
@@ -49,6 +50,7 @@ def test_polytope_odb():
     assert len(df) == 52
 
 
+# @pytest.mark.skip("Data not available?")
 @pytest.mark.long_test
 @pytest.mark.download
 @pytest.mark.skipif(NO_POLYTOPE, reason="No access to Polytope Web API")