Repository: googleapis/google-cloud-python

Issue:
Runtimeconfig: Update time fails to parse
OS: Linux (Debian 4.9.65)
Python: 3.5.3 / 2.7.13
Version: google-cloud-runtimeconfig==0.28.0, google-cloud==0.32.0

Steps to reproduce:
1. Using gcloud, set a runtimeconfig variable.
2. In Python, load that variable
3. Try to access its update_time property.

```pycon
>>> from google.cloud import runtimeconfig
>>> client = runtimeconfig.Client()
>>> config = client.config('testconfig')
>>> var = config.get_variable('foo')
>>> var.update_time
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/redacted/lib/python3.5/site-packages/google/cloud/runtimeconfig/variable.py", line 160, in update_time
    value = _rfc3339_to_datetime(value)
  File "/redacted/lib/python3.5/site-packages/google/cloud/_helpers.py", line 274, in _rfc3339_to_datetime
    dt_str, _RFC3339_MICROS).replace(tzinfo=UTC)
  File "/usr/lib/python3.5/_strptime.py", line 510, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File "/usr/lib/python3.5/_strptime.py", line 343, in _strptime
    (data_string, format))
ValueError: time data '2018-01-22T21:39:44.095040522Z' does not match format '%Y-%m-%dT%H:%M:%S.%fZ'
>>> var._properties
{'text': '43', 'updateTime': '2018-01-22T21:39:44.095040522Z'}
```

Observation: The `%f` format accepts microseconds, not fractions of arbitrary precision. That means six digits at most, but I see nine in the data.


Discussion:
Interesting. RuntimeConfig must have started recording nanoseconds somewhat recently.

@jonparrott It seems [Python only supports microsecond precision, not nanoseconds](https://stackoverflow.com/a/10612166/101923).

I'm guessing we should modify the parsing to discard nanoseconds when present.

Resolving patch:
diff --git a/runtimeconfig/google/cloud/runtimeconfig/variable.py b/runtimeconfig/google/cloud/runtimeconfig/variable.py
--- a/runtimeconfig/google/cloud/runtimeconfig/variable.py
+++ b/runtimeconfig/google/cloud/runtimeconfig/variable.py
@@ -36,8 +36,11 @@
 """
 
 import base64
+import datetime
 
-from google.cloud._helpers import _rfc3339_to_datetime
+import pytz
+
+from google.api_core import datetime_helpers
 from google.cloud.exceptions import NotFound
 from google.cloud.runtimeconfig._helpers import variable_name_from_full_name
 
@@ -151,13 +154,28 @@ def update_time(self):
         See
         https://cloud.google.com/deployment-manager/runtime-configurator/reference/rest/v1beta1/projects.configs.variables
 
-        :rtype: :class:`datetime.datetime` or ``NoneType``
-        :returns: Datetime object parsed from RFC3339 valid timestamp, or
-                  ``None`` if the property is not set locally.
+        Returns:
+            :class:`~api_core.datetime_helpers.DatetimeWithNanoseconds`,
+            :class:`datetime.datetime` or ``NoneType``:
+            Datetime object parsed from RFC3339 valid timestamp, or
+            ``None`` if the property is not set locally.
+
+        Raises:
+            ValueError: if value is not a valid RFC3339 timestamp
         """
         value = self._properties.get('updateTime')
         if value is not None:
-            value = _rfc3339_to_datetime(value)
+            try:
+                value = datetime.datetime.strptime(
+                    value, datetime_helpers._RFC3339_MICROS)
+            except ValueError:
+                DatetimeNS = datetime_helpers.DatetimeWithNanoseconds
+                value = DatetimeNS.from_rfc3339(value)
+            naive = (
+                value.tzinfo is None
+                or value.tzinfo.utcoffset(value) is None)
+            if naive:
+                value = pytz.utc.localize(value)
         return value
 
     def _require_client(self, client):


Test patch:
diff --git a/runtimeconfig/tests/unit/test_config.py b/runtimeconfig/tests/unit/test_config.py
--- a/runtimeconfig/tests/unit/test_config.py
+++ b/runtimeconfig/tests/unit/test_config.py
@@ -145,14 +145,14 @@ def test_variable(self):
         self.assertEqual(len(conn._requested), 0)
 
     def test_get_variable_w_bound_client(self):
-        from google.cloud._helpers import _rfc3339_to_datetime
+        from google.api_core import datetime_helpers
 
         VARIABLE_NAME = 'my-variable/abcd'
         VARIABLE_PATH = '%s/variables/%s' % (self.CONFIG_PATH, VARIABLE_NAME)
         RESOURCE = {
             'name': VARIABLE_PATH,
             'value': 'bXktdmFyaWFibGUtdmFsdWU=',  # base64 my-variable-value
-            'updateTime': '2016-04-14T21:21:54.5000Z',
+            'updateTime': '2016-04-14T21:21:54.123456789Z',
             'state': 'VARIABLE_STATE_UNSPECIFIED',
         }
         conn = _Connection(RESOURCE)
@@ -163,9 +163,10 @@ def test_get_variable_w_bound_client(self):
 
         self.assertEqual(variable.name, VARIABLE_NAME)
         self.assertEqual(variable.full_name, VARIABLE_PATH)
+        DatetimeWithNanoseconds = datetime_helpers.DatetimeWithNanoseconds
         self.assertEqual(
             variable.update_time,
-            _rfc3339_to_datetime(RESOURCE['updateTime']))
+            DatetimeWithNanoseconds.from_rfc3339(RESOURCE['updateTime']))
         self.assertEqual(variable.state, RESOURCE['state'])
 
         self.assertEqual(len(conn._requested), 1)