Repository: marshmallow-code/marshmallow

Issue:
ISO8601 DateTimes ending with Z considered not valid in 2.19.4
Probably related to #1247 and #1234 - in marshmallow `2.19.4`, with `python-dateutil` _not_ installed, it seems that loading a datetime in ISO8601 that ends in `Z` (UTC time) results in an error:

```python
class Foo(Schema):
    date = DateTime(required=True)


foo_schema = Foo(strict=True)

a_date_with_z = '2019-06-17T00:57:41.000Z'
foo_schema.load({'date': a_date_with_z})
```

```
marshmallow.exceptions.ValidationError: {'date': ['Not a valid datetime.']}
```

Digging a bit deeper, it seems [`from_iso_datetime`](https://github.com/marshmallow-code/marshmallow/blob/dev/src/marshmallow/utils.py#L213-L215) is failing with a `unconverted data remains: Z` - my understanding of the spec is rather limited, but it seems that they are indeed valid ISO8601 dates (and in `marshmallow==2.19.3` and earlier, the previous snippet seems to work without raising validation errors).



Discussion:
@lafrech Would you mind looking into this?
Thanks for reporting.

This is definitely a side effect of https://github.com/marshmallow-code/marshmallow/pull/1249/files. Sorry about that.

I don't own a copy of the spec, so the work on this is based on examples... I assumed that microseconds always came as a six-pack. It seems only three digits (your example) is acceptable. From what I understand in the regex we copied from Django, we could even expect any number of digits in [1; 6].

I see two solutions to this:

- Split around `"."`, then in the right part, get all numbers and ignore letters/symbols.
- Split around `"."`, then split the right part around anything that delimitates a timezone (`"Z"`, `"+"`, `"-"`, what else?).


Thanks both for the prompt reply! I don't have a copy of the spec myself either - for the timezone suffix, I have based my previous comment on [the Wikipedia entry](https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators), which seems to hint at the following designators being allowed:
```
<time>Z
<time>±hh:mm
<time>±hhmm
<time>±hh
```
I also use this WP page, but it doesn't show much about milli/microseconds.

Patch:
diff --git a/src/marshmallow/utils.py b/src/marshmallow/utils.py
--- a/src/marshmallow/utils.py
+++ b/src/marshmallow/utils.py
@@ -285,6 +285,9 @@ def from_iso(datestring, use_dateutil=True):
         # Strip off timezone info.
         if '.' in datestring:
             # datestring contains microseconds
+            (dt_nomstz, mstz) = datestring.split('.')
+            ms_notz = mstz[:len(mstz) - len(mstz.lstrip('0123456789'))]
+            datestring = '.'.join((dt_nomstz, ms_notz))
             return datetime.datetime.strptime(datestring[:26], '%Y-%m-%dT%H:%M:%S.%f')
         return datetime.datetime.strptime(datestring[:19], '%Y-%m-%dT%H:%M:%S')
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -200,6 +200,15 @@ def test_from_iso_datetime(use_dateutil, timezone):
     assert type(result) == dt.datetime
     assert_datetime_equal(result, d)
 
+    # Test with 3-digit only microseconds
+    # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1251
+    d = dt.datetime.now(tz=timezone).replace(microsecond=123000)
+    formatted = d.isoformat()
+    formatted = formatted[:23] + formatted[26:]
+    result = utils.from_iso(formatted, use_dateutil=use_dateutil)
+    assert type(result) == dt.datetime
+    assert_datetime_equal(result, d)
+
 def test_from_iso_with_tz():
     d = central.localize(dt.datetime.now())
     formatted = d.isoformat()