Repository: conan-io/conan

Issue:
Conan search cannot handle revisions with time that includes an offset
```
$ conan --version
Conan version 1.15.0
$ uname -a
Linux hostname 4.15.0-50-generic #54-Ubuntu SMP Mon May 6 18:46:08 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
```

Our Artifactory instance seems to return timestamps with an offset:
`[{'revision': 'b27c3f77818145a86d8657078987b033', 'time': '2019-05-14T16:52:28.383+0100'}]`

ISO 8601 allows this: https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC

This is passed to `util/dates.py` which cannot handle it:

```bash
$ conan search MyPackage/1.1.1@username/channel --revisions --remote artifactory

Revisions for 'MyPackage/1.1.1@username/channel' at remote 'artifactory':
Traceback (most recent call last):
  File "/home/user/.local/lib/python3.6/site-packages/conans/client/command.py", line 1607, in run
    method(args[0][1:])
  File "/home/user/.local/lib/python3.6/site-packages/conans/client/command.py", line 1090, in search
    self._outputer.print_revisions(ref, info, remote_name=args.remote)
  File "/home/user/.local/lib/python3.6/site-packages/conans/client/conan_command_output.py", line 245, in print_revisions
    for r in revisions]
  File "/home/user/.local/lib/python3.6/site-packages/conans/client/conan_command_output.py", line 245, in <listcomp>
    for r in revisions]
  File "/home/user/.local/lib/python3.6/site-packages/conans/util/dates.py", line 31, in iso8601_to_str
    dt = from_iso8601_to_datetime(iso_str)
  File "/home/user/.local/lib/python3.6/site-packages/conans/util/dates.py", line 27, in from_iso8601_to_datetime
    return datetime.datetime.strptime(iso_str, pattern)
  File "/usr/lib/python3.6/_strptime.py", line 565, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File "/usr/lib/python3.6/_strptime.py", line 362, in _strptime
    (data_string, format))
ValueError: time data '2019-05-14T16:52:28.383+0100Z' does not match format '%Y-%m-%dT%H:%M:%S.%fZ'

ERROR: time data '2019-05-14T16:52:28.383+0100Z' does not match format '%Y-%m-%dT%H:%M:%S.%fZ'
```

Notice that at some point `util/dates.py` has appended a "Z" to the date in the wrong place.

To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.


Discussion:
This seems to be the problem: https://github.com/conan-io/conan/blob/bc8c0cbef266acaae68e9059c3549dc794f549d3/conans/util/dates.py#L12-L15
Thanks for reporting. We will solve it for the coming 1.16

Resolving patch:
diff --git a/conans/server/revision_list.py b/conans/server/revision_list.py
--- a/conans/server/revision_list.py
+++ b/conans/server/revision_list.py
@@ -2,7 +2,7 @@
 import time
 from collections import namedtuple
 
-from conans.util.dates import from_timestamp_to_iso8601, valid_iso8601
+from conans.util.dates import from_timestamp_to_iso8601
 
 _RevisionEntry = namedtuple("RevisionEntry", "revision time")
 
diff --git a/conans/util/dates.py b/conans/util/dates.py
--- a/conans/util/dates.py
+++ b/conans/util/dates.py
@@ -1,5 +1,5 @@
 import datetime
-import re
+from dateutil import parser
 
 
 def from_timestamp_to_iso8601(timestamp):
@@ -7,34 +7,9 @@ def from_timestamp_to_iso8601(timestamp):
 
 
 def from_iso8601_to_datetime(iso_str):
-
-    def transform_to_z_notation(the_str):
-        for p in ("+00:00", "+0000", "+00"):
-            if the_str.endswith(p):
-                the_str = the_str[0:-len(p)]
-                return "{}Z".format(the_str)
-        return the_str
-
-    base_pattern = "%Y-%m-%dT%H:%M:%S"
-    if "." in iso_str:
-        pattern = "{}.%fZ".format(base_pattern)
-    else:
-        pattern = '{}Z'.format(base_pattern)
-
-    iso_str = transform_to_z_notation(iso_str)
-    if not iso_str.endswith("Z"):
-        iso_str += "Z"
-    return datetime.datetime.strptime(iso_str, pattern)
+    return parser.isoparse(iso_str)
 
 
 def iso8601_to_str(iso_str):
     dt = from_iso8601_to_datetime(iso_str)
     return dt.strftime('%Y-%m-%d %H:%M:%S UTC')
-
-
-def valid_iso8601(the_date):
-    regex = r'^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T' \
-            r'(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-]' \
-            r'(?:2[0-3]|[01][0-9]):' \
-            r'[0-5][0-9])?$'
-    return re.match(regex, the_date)


Test patch:
diff --git a/conans/test/unittests/client/util/time_test.py b/conans/test/unittests/client/util/time_test.py
--- a/conans/test/unittests/client/util/time_test.py
+++ b/conans/test/unittests/client/util/time_test.py
@@ -1,7 +1,9 @@
 import datetime
 import unittest
 
-from conans.util.dates import from_timestamp_to_iso8601, from_iso8601_to_datetime, valid_iso8601
+from dateutil.tz import tzutc
+
+from conans.util.dates import from_timestamp_to_iso8601, from_iso8601_to_datetime
 
 
 class TimeTest(unittest.TestCase):
@@ -12,15 +14,18 @@ def time_conversions_test(self):
         self.assertEqual(iso, "2019-01-10T16:34:59Z")
 
         dt = from_iso8601_to_datetime(iso)
-        expected = datetime.datetime(year=2019, month=1, day=10, hour=16, minute=34, second=59)
+        expected = datetime.datetime(year=2019, month=1, day=10, hour=16, minute=34, second=59,
+                                     tzinfo=tzutc())
         self.assertEqual(dt, expected)
 
         artifactory_ret = '2019-02-20T13:54:47.543+0000'
         dt = from_iso8601_to_datetime(artifactory_ret)
         expected = datetime.datetime(year=2019, month=2, day=20, hour=13, minute=54, second=47,
-                                     microsecond=543000)
+                                     microsecond=543000, tzinfo=tzutc())
         self.assertEqual(dt, expected)
 
-    def validation_test(self):
-        self.assertFalse(valid_iso8601("1547138099"))
-        self.assertTrue(valid_iso8601("2019-01-10T16:34:59Z"))
+        artifactory_ret = '2019-05-14T16:52:28.383+0100'
+        dt = from_iso8601_to_datetime(artifactory_ret)  # UTC one hour less
+        expected = datetime.datetime(year=2019, month=5, day=14, hour=15, minute=52, second=28,
+                                     microsecond=383000, tzinfo=tzutc())
+        self.assertEqual(dt, expected)