Repository: googleapis/google-cloud-python

Issue:
[Firestore] higher precision timestamps
The Python SDK timestamps are lossy. The SDK doesn’t expose a way to retrieve timestamps with higher precision.


Discussion:
I'm not sure what this item addresses.  We [expose the `timestamp_pb2.Timestamp` fields](https://github.com/googleapis/google-cloud-python/blob/9033bb72ab4b76f7bf629d25da2fd91d3d486305/firestore/google/cloud/firestore_v1beta1/document.py#L500-L521), which are full nanosecond precision.

For user-set values (versus the `DocumentSnapshot` attributes I linked), we have the `google.api_core.datetime_helpers.DateTimeWithNanos` subclass of the stdlib's `datetime.datetime`, but no conversion utilities between it and `timestamp_pb2.Timestamp`.

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1beta1/_helpers.py b/firestore/google/cloud/firestore_v1beta1/_helpers.py
--- a/firestore/google/cloud/firestore_v1beta1/_helpers.py
+++ b/firestore/google/cloud/firestore_v1beta1/_helpers.py
@@ -23,7 +23,7 @@
 
 from google.cloud import exceptions
 from google.cloud._helpers import _datetime_to_pb_timestamp
-from google.cloud._helpers import _pb_timestamp_to_datetime
+from google.api_core.datetime_helpers import DatetimeWithNanoseconds
 from google.cloud.firestore_v1beta1 import transforms
 from google.cloud.firestore_v1beta1 import types
 from google.cloud.firestore_v1beta1.field_path import FieldPath
@@ -165,6 +165,9 @@ def encode_value(value):
     if isinstance(value, float):
         return document_pb2.Value(double_value=value)
 
+    if isinstance(value, DatetimeWithNanoseconds):
+        return document_pb2.Value(timestamp_value=value.timestamp_pb())
+
     if isinstance(value, datetime.datetime):
         return document_pb2.Value(timestamp_value=_datetime_to_pb_timestamp(value))
 
@@ -275,10 +278,7 @@ def decode_value(value, client):
     elif value_type == "double_value":
         return value.double_value
     elif value_type == "timestamp_value":
-        # NOTE: This conversion is "lossy", Python ``datetime.datetime``
-        #       has microsecond precision but ``timestamp_value`` has
-        #       nanosecond precision.
-        return _pb_timestamp_to_datetime(value.timestamp_value)
+        return DatetimeWithNanoseconds.from_timestamp_pb(value.timestamp_value)
     elif value_type == "string_value":
         return value.string_value
     elif value_type == "bytes_value":


Test patch:
diff --git a/firestore/tests/unit/test__helpers.py b/firestore/tests/unit/test__helpers.py
--- a/firestore/tests/unit/test__helpers.py
+++ b/firestore/tests/unit/test__helpers.py
@@ -163,7 +163,20 @@ def test_float(self):
         expected = _value_pb(double_value=value)
         self.assertEqual(result, expected)
 
-    def test_datetime(self):
+    def test_datetime_with_nanos(self):
+        from google.api_core.datetime_helpers import DatetimeWithNanoseconds
+        from google.protobuf import timestamp_pb2
+
+        dt_seconds = 1488768504
+        dt_nanos = 458816991
+        timestamp_pb = timestamp_pb2.Timestamp(seconds=dt_seconds, nanos=dt_nanos)
+        dt_val = DatetimeWithNanoseconds.from_timestamp_pb(timestamp_pb)
+
+        result = self._call_fut(dt_val)
+        expected = _value_pb(timestamp_value=timestamp_pb)
+        self.assertEqual(result, expected)
+
+    def test_datetime_wo_nanos(self):
         from google.protobuf import timestamp_pb2
 
         dt_seconds = 1488768504
@@ -387,20 +400,16 @@ def test_float(self):
         (3,) <= sys.version_info < (3, 4, 4), "known datetime bug (bpo-23517) in Python"
     )
     def test_datetime(self):
+        from google.api_core.datetime_helpers import DatetimeWithNanoseconds
         from google.protobuf import timestamp_pb2
-        from google.cloud._helpers import UTC
 
         dt_seconds = 552855006
-        dt_nanos = 766961000
-        # Make sure precision is valid in microseconds too.
-        self.assertEqual(dt_nanos % 1000, 0)
+        dt_nanos = 766961828
 
         timestamp_pb = timestamp_pb2.Timestamp(seconds=dt_seconds, nanos=dt_nanos)
         value = _value_pb(timestamp_value=timestamp_pb)
 
-        expected_dt_val = datetime.datetime.utcfromtimestamp(
-            dt_seconds + 1e-9 * dt_nanos
-        ).replace(tzinfo=UTC)
+        expected_dt_val = DatetimeWithNanoseconds.from_timestamp_pb(timestamp_pb)
         self.assertEqual(self._call_fut(value), expected_dt_val)
 
     def test_unicode(self):