Repository: googleapis/google-cloud-python

Issue:
Timestamp nanos parsing broken for '2016-05-09T18:49:00.655319Z'
```
ERROR: test_log_text (logging_.TestLogging)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/travis/build/GoogleCloudPlatform/gcloud-python/system_tests/logging_.py", line 79, in test_log_text
    entries, _ = logger.list_entries()
  File "/home/travis/build/GoogleCloudPlatform/gcloud-python/.tox/system-tests3/lib/python3.4/site-packages/gcloud/logging/logger.py", line 317, in list_entries
    page_size=page_size, page_token=page_token)
  File "/home/travis/build/GoogleCloudPlatform/gcloud-python/.tox/system-tests3/lib/python3.4/site-packages/gcloud/logging/client.py", line 141, in list_entries
    for resource in resp.get('entries', ())]
  File "/home/travis/build/GoogleCloudPlatform/gcloud-python/.tox/system-tests3/lib/python3.4/site-packages/gcloud/logging/client.py", line 141, in <listcomp>
    for resource in resp.get('entries', ())]
  File "/home/travis/build/GoogleCloudPlatform/gcloud-python/.tox/system-tests3/lib/python3.4/site-packages/gcloud/logging/client.py", line 79, in _entry_from_resource
    return TextEntry.from_api_repr(resource, self, loggers)
  File "/home/travis/build/GoogleCloudPlatform/gcloud-python/.tox/system-tests3/lib/python3.4/site-packages/gcloud/logging/entries.py", line 92, in from_api_repr
    timestamp = _rfc3339_nanos_to_datetime(timestamp)
  File "/home/travis/build/GoogleCloudPlatform/gcloud-python/.tox/system-tests3/lib/python3.4/site-packages/gcloud/_helpers.py", line 344, in _rfc3339_nanos_to_datetime
    dt_str, _RFC3339_NANOS.pattern))
ValueError: Timestamp: '2016-05-09T18:49:00.655319Z', does not match pattern: '\n    (?P<no_fraction>\n        \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}  # YYYY-MM-DDTHH:MM:SS\n    )\n    \\.                                       # decimal point\n    (?P<nanos>\\d{9})                         # nanoseconds\n    Z                                        # Zulu\n'
```

From https://travis-ci.org/GoogleCloudPlatform/gcloud-python/builds/128927443

I haven't traced down the codepath just yet.



Discussion:
Rotten tomotoes for the back-end team.  "Nanosecond precision" strongly requires that the trailing zeroes be present:  that is the very _definition_ of precision.

It's likely that they are optimizing for payload size; if that's the case they get an A in that category.

WDYT of changing the [nanos check](https://github.com/GoogleCloudPlatform/gcloud-python/blob/22634abd342268f5f77b2637e9c13fb0a9492309/gcloud/_helpers.py#L48) to `(?P<nanos>\d{6-9})` and then using `zfill(9)`?


Resolving patch:
diff --git a/gcloud/_helpers.py b/gcloud/_helpers.py
--- a/gcloud/_helpers.py
+++ b/gcloud/_helpers.py
@@ -45,7 +45,7 @@
         \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}  # YYYY-MM-DDTHH:MM:SS
     )
     \.                                       # decimal point
-    (?P<nanos>\d{9})                         # nanoseconds
+    (?P<nanos>\d{1,9})                       # nanoseconds, maybe truncated
     Z                                        # Zulu
 """, re.VERBOSE)
 
@@ -344,7 +344,9 @@ def _rfc3339_nanos_to_datetime(dt_str):
                 dt_str, _RFC3339_NANOS.pattern))
     bare_seconds = datetime.datetime.strptime(
         with_nanos.group('no_fraction'), _RFC3339_NO_FRACTION)
-    nanos = int(with_nanos.group('nanos'))
+    fraction = with_nanos.group('nanos')
+    scale = 9 - len(fraction)
+    nanos = int(fraction) * (10 ** scale)
     micros = nanos // 1000
     return bare_seconds.replace(microsecond=micros, tzinfo=UTC)
 


Test patch:
diff --git a/gcloud/test__helpers.py b/gcloud/test__helpers.py
--- a/gcloud/test__helpers.py
+++ b/gcloud/test__helpers.py
@@ -483,7 +483,9 @@ def test_w_bogus_zone(self):
         with self.assertRaises(ValueError):
             self._callFUT(dt_str)
 
-    def test_w_microseconds(self):
+    def test_w_truncated_nanos(self):
+        import datetime
+        from gcloud._helpers import UTC
 
         year = 2009
         month = 12
@@ -491,12 +493,24 @@ def test_w_microseconds(self):
         hour = 12
         minute = 44
         seconds = 32
-        micros = 123456
-
-        dt_str = '%d-%02d-%02dT%02d:%02d:%02d.%06dZ' % (
-            year, month, day, hour, minute, seconds, micros)
-        with self.assertRaises(ValueError):
-            self._callFUT(dt_str)
+        truncateds_and_micros = [
+            ('12345678', 123456),
+            ('1234567', 123456),
+            ('123456', 123456),
+            ('12345', 123450),
+            ('1234', 123400),
+            ('123', 123000),
+            ('12', 120000),
+            ('1', 100000),
+        ]
+
+        for truncated, micros in truncateds_and_micros:
+            dt_str = '%d-%02d-%02dT%02d:%02d:%02d.%sZ' % (
+                year, month, day, hour, minute, seconds, truncated)
+            result = self._callFUT(dt_str)
+            expected_result = datetime.datetime(
+                year, month, day, hour, minute, seconds, micros, UTC)
+            self.assertEqual(result, expected_result)
 
     def test_w_naonseconds(self):
         import datetime
@@ -511,7 +525,7 @@ def test_w_naonseconds(self):
         nanos = 123456789
         micros = nanos // 1000
 
-        dt_str = '%d-%02d-%02dT%02d:%02d:%02d.%06dZ' % (
+        dt_str = '%d-%02d-%02dT%02d:%02d:%02d.%09dZ' % (
             year, month, day, hour, minute, seconds, nanos)
         result = self._callFUT(dt_str)
         expected_result = datetime.datetime(