Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Dates before 1000AD should use 4-digit years #1132

Merged
merged 1 commit into from Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 36 additions & 3 deletions google/cloud/spanner_v1/_helpers.py
Expand Up @@ -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
Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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):
Expand Down
49 changes: 44 additions & 5 deletions tests/unit/test__helpers.py
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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())
Expand Down