Repository: dateutil/dateutil (Python)

Issue:
Parser only partially accounts for case-insensitive nature of ISO8601 datestamps
```bash
$ python -c "from dateutil.parser import parse
> print parse('1970-01-01T00:00:00Z')
> print parse('1970-01-01t00:00:00Z')
> print parse('1970-01-01t00:00:00')
> print parse('1970-01-01t00:00:00z')"
1970-01-01 00:00:00+00:00
1970-01-01 00:00:00+00:00
1970-01-01 00:00:00
Traceback (most recent call last):
  File "<string>", line 5, in <module>
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 1182, in parse
    return DEFAULTPARSER.parse(timestr, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/dateutil/parser.py", line 559, in parse
    raise ValueError("Unknown string format")
ValueError: Unknown string format
```

It looks like this library properly supports both the use of `t` and `T` as legal time separators for ISO8601 datestamps, but only supports the uppercased shorthand for UTC (`Z`), and not `z`. From what I can find online (namely, this RFC: https://tools.ietf.org/html/rfc3339#section-5.6), it seems like the latter is also legal syntax.


Hints:
If true that the ISO 8601 format allows for `z`, then we should at the very least update `dateutil.parser.isoparse`.

I don't know if we need to or want to add support for this in `dateutil.parser.parse`. The obvious way to do so would be to change `parserinfo.UTCZONE` to include `'z'`, but I suspect a lower-case z is more often there for some reason *other* than because the datetime is supposed to be in UTC. Anyone who knows they have a `z` can always create a modified `parserinfo` if they run into this problem.

That said, one thing I was considering for the long term is refactoring `dateutil.parser.parse` to use `dateutil.parser.isoparse` *first*, and fall back to the regular parser if the parse fails. In that case, `YYYY-MM-DDtHH:MM:SSz` would be accepted, but `Dec 19, 2018 10:30z` would not be, which I'm kinda OK with.

@jbrockmendel Thoughts? Am I overthinking this? Should we just add `'z'` to the default UTCZONE?
> Should we just add 'z' to the default UTCZONE?

It seems pretty unambiguous in the example `Dec 19, 2018 10:30z`.  Off the top of my head I can't think of a counter-example where recognizing it as UTC would be incorrect.
@jbrockmendel OK, I agree.

Take-away for anyone who wants to make a PR:

1. Change the `Z` detection to detect `z` as well in `dateutil.parser.isoparse`
2. Add `z` to `UTCZONE`.

The only remaining uncertainty I have is whether we should make `UTCZONE` case-insensitive. I'm thinking maybe in a future version we can have that, but with a switch to turn it off.

Patch:
diff --git a/dateutil/parser/_parser.py b/dateutil/parser/_parser.py
--- a/dateutil/parser/_parser.py
+++ b/dateutil/parser/_parser.py
@@ -291,7 +291,7 @@ class parserinfo(object):
            ("s", "second", "seconds")]
     AMPM = [("am", "a"),
             ("pm", "p")]
-    UTCZONE = ["UTC", "GMT", "Z"]
+    UTCZONE = ["UTC", "GMT", "Z", "z"]
     PERTAIN = ["of"]
     TZOFFSET = {}
     # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
@@ -388,7 +388,8 @@ def validate(self, res):
         if res.year is not None:
             res.year = self.convertyear(res.year, res.century_specified)
 
-        if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z':
+        if ((res.tzoffset == 0 and not res.tzname) or
+             (res.tzname == 'Z' or res.tzname == 'z')):
             res.tzname = "UTC"
             res.tzoffset = 0
         elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
@@ -1060,7 +1061,8 @@ def _could_be_tzname(self, hour, tzname, tzoffset, token):
                 tzname is None and
                 tzoffset is None and
                 len(token) <= 5 and
-                all(x in string.ascii_uppercase for x in token))
+                (all(x in string.ascii_uppercase for x in token)
+                 or token in self.info.UTCZONE))
 
     def _ampm_valid(self, hour, ampm, fuzzy):
         """
diff --git a/dateutil/parser/isoparser.py b/dateutil/parser/isoparser.py
--- a/dateutil/parser/isoparser.py
+++ b/dateutil/parser/isoparser.py
@@ -341,7 +341,7 @@ def _parse_isotime(self, timestr):
         while pos < len_str and comp < 5:
             comp += 1
 
-            if timestr[pos:pos + 1] in b'-+Z':
+            if timestr[pos:pos + 1] in b'-+Zz':
                 # Detect time zone boundary
                 components[-1] = self._parse_tzstr(timestr[pos:])
                 pos = len_str
@@ -376,7 +376,7 @@ def _parse_isotime(self, timestr):
         return components
 
     def _parse_tzstr(self, tzstr, zero_as_utc=True):
-        if tzstr == b'Z':
+        if tzstr == b'Z' or tzstr == b'z':
             return tz.tzutc()
 
         if len(tzstr) not in {3, 5, 6}:


Test patch:
diff --git a/dateutil/test/test_isoparser.py b/dateutil/test/test_isoparser.py
--- a/dateutil/test/test_isoparser.py
+++ b/dateutil/test/test_isoparser.py
@@ -228,6 +228,8 @@ def test_iso_ordinal(isoord, dt_expected):
     (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
     (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
                                            tz.tzutc())),
+    (b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000,
+                                           tz.tzutc())),
     (b'2014-02-04T12:30:15.224+05:00',
         datetime(2014, 2, 4, 12, 30, 15, 224000,
                  tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
diff --git a/dateutil/test/test_parser.py b/dateutil/test/test_parser.py
--- a/dateutil/test/test_parser.py
+++ b/dateutil/test/test_parser.py
@@ -472,6 +472,11 @@ def testRandomFormat17(self):
         self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True),
                          datetime(1976, 7, 4, 0, 1, 2))
 
+    def testRandomFormat18(self):
+        self.assertEqual(parse("1986-07-05T08:15:30z",
+                               ignoretz=True),
+                         datetime(1986, 7, 5, 8, 15, 30))
+
     def testRandomFormat20(self):
         self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True),
                          datetime(1995, 4, 4, 0, 22, 12))