diff --git a/google/cloud/spanner_v1/_helpers.py b/google/cloud/spanner_v1/_helpers.py index d6b10dba18..5bb8bf656c 100644 --- a/google/cloud/spanner_v1/_helpers.py +++ b/google/cloud/spanner_v1/_helpers.py @@ -24,7 +24,6 @@ from google.api_core import datetime_helpers from google.cloud._helpers import _date_from_iso8601_date -from google.cloud._helpers import _datetime_to_rfc3339 from google.cloud.spanner_v1 import TypeCode from google.cloud.spanner_v1 import ExecuteSqlRequest from google.cloud.spanner_v1 import JsonObject @@ -122,6 +121,40 @@ def _assert_numeric_precision_and_scale(value): raise ValueError(NUMERIC_MAX_PRECISION_ERR_MSG.format(precision + scale)) +def _datetime_to_rfc3339(value): + """Format the provided datatime in the RFC 3339 format. + + :type value: datetime.datetime + :param value: value to format + + :rtype: str + :returns: RFC 3339 formatted datetime string + """ + # Convert to UTC and then drop the timezone so we can append "Z" in lieu of + # allowing isoformat to append the "+00:00" zone offset. + value = value.astimezone(datetime.timezone.utc).replace(tzinfo=None) + return value.isoformat(sep="T", timespec="microseconds") + "Z" + + +def _datetime_to_rfc3339_nanoseconds(value): + """Format the provided datatime in the RFC 3339 format. + + :type value: datetime_helpers.DatetimeWithNanoseconds + :param value: value to format + + :rtype: str + :returns: RFC 3339 formatted datetime string + """ + + if value.nanosecond == 0: + return _datetime_to_rfc3339(value) + nanos = str(value.nanosecond).rjust(9, "0").rstrip("0") + # Convert to UTC and then drop the timezone so we can append "Z" in lieu of + # allowing isoformat to append the "+00:00" zone offset. + value = value.astimezone(datetime.timezone.utc).replace(tzinfo=None) + return "{}.{}Z".format(value.isoformat(sep="T", timespec="seconds"), nanos) + + def _make_value_pb(value): """Helper for :func:`_make_list_value_pbs`. @@ -150,9 +183,9 @@ def _make_value_pb(value): return Value(string_value="-Infinity") return Value(number_value=value) if isinstance(value, datetime_helpers.DatetimeWithNanoseconds): - return Value(string_value=value.rfc3339()) + return Value(string_value=_datetime_to_rfc3339_nanoseconds(value)) if isinstance(value, datetime.datetime): - return Value(string_value=_datetime_to_rfc3339(value, ignore_zone=False)) + return Value(string_value=_datetime_to_rfc3339(value)) if isinstance(value, datetime.date): return Value(string_value=value.isoformat()) if isinstance(value, bytes): diff --git a/tests/unit/test__helpers.py b/tests/unit/test__helpers.py index cb2372406f..5e759baf31 100644 --- a/tests/unit/test__helpers.py +++ b/tests/unit/test__helpers.py @@ -190,6 +190,15 @@ def test_w_date(self): self.assertIsInstance(value_pb, Value) self.assertEqual(value_pb.string_value, today.isoformat()) + def test_w_date_pre1000ad(self): + import datetime + from google.protobuf.struct_pb2 import Value + + when = datetime.date(800, 2, 25) + value_pb = self._callFUT(when) + self.assertIsInstance(value_pb, Value) + self.assertEqual(value_pb.string_value, "0800-02-25") + def test_w_timestamp_w_nanos(self): import datetime from google.protobuf.struct_pb2 import Value @@ -200,7 +209,19 @@ def test_w_timestamp_w_nanos(self): ) value_pb = self._callFUT(when) self.assertIsInstance(value_pb, Value) - self.assertEqual(value_pb.string_value, when.rfc3339()) + self.assertEqual(value_pb.string_value, "2016-12-20T21:13:47.123456789Z") + + def test_w_timestamp_w_nanos_pre1000ad(self): + import datetime + from google.protobuf.struct_pb2 import Value + from google.api_core import datetime_helpers + + when = datetime_helpers.DatetimeWithNanoseconds( + 850, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=datetime.timezone.utc + ) + value_pb = self._callFUT(when) + self.assertIsInstance(value_pb, Value) + self.assertEqual(value_pb.string_value, "0850-12-20T21:13:47.123456789Z") def test_w_listvalue(self): from google.protobuf.struct_pb2 import Value @@ -214,12 +235,20 @@ def test_w_listvalue(self): def test_w_datetime(self): import datetime from google.protobuf.struct_pb2 import Value - from google.api_core import datetime_helpers - now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) - value_pb = self._callFUT(now) + when = datetime.datetime(2021, 2, 8, 0, 0, 0, tzinfo=datetime.timezone.utc) + value_pb = self._callFUT(when) + self.assertIsInstance(value_pb, Value) + self.assertEqual(value_pb.string_value, "2021-02-08T00:00:00.000000Z") + + def test_w_datetime_pre1000ad(self): + import datetime + from google.protobuf.struct_pb2 import Value + + when = datetime.datetime(916, 2, 8, 0, 0, 0, tzinfo=datetime.timezone.utc) + value_pb = self._callFUT(when) self.assertIsInstance(value_pb, Value) - self.assertEqual(value_pb.string_value, datetime_helpers.to_rfc3339(now)) + self.assertEqual(value_pb.string_value, "0916-02-08T00:00:00.000000Z") def test_w_timestamp_w_tz(self): import datetime @@ -231,6 +260,16 @@ def test_w_timestamp_w_tz(self): self.assertIsInstance(value_pb, Value) self.assertEqual(value_pb.string_value, "2021-02-07T23:00:00.000000Z") + def test_w_timestamp_w_tz_pre1000ad(self): + import datetime + from google.protobuf.struct_pb2 import Value + + zone = datetime.timezone(datetime.timedelta(hours=+1), name="CET") + when = datetime.datetime(721, 2, 8, 0, 0, 0, tzinfo=zone) + value_pb = self._callFUT(when) + self.assertIsInstance(value_pb, Value) + self.assertEqual(value_pb.string_value, "0721-02-07T23:00:00.000000Z") + def test_w_unknown_type(self): with self.assertRaises(ValueError): self._callFUT(object())