Skip to content

Commit 0ef6565

Browse files
authored
fix: Dates before 1000AD should use 4-digit years (#1132)
This is required for compliance with RFC3339/ISO8401 and timestamps which do not comply will be rejected by Spanner. Fixes #1131
1 parent 37ac4c1 commit 0ef6565

File tree

2 files changed

+80
-8
lines changed

2 files changed

+80
-8
lines changed

google/cloud/spanner_v1/_helpers.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
from google.api_core import datetime_helpers
2626
from google.cloud._helpers import _date_from_iso8601_date
27-
from google.cloud._helpers import _datetime_to_rfc3339
2827
from google.cloud.spanner_v1 import TypeCode
2928
from google.cloud.spanner_v1 import ExecuteSqlRequest
3029
from google.cloud.spanner_v1 import JsonObject
@@ -122,6 +121,40 @@ def _assert_numeric_precision_and_scale(value):
122121
raise ValueError(NUMERIC_MAX_PRECISION_ERR_MSG.format(precision + scale))
123122

124123

124+
def _datetime_to_rfc3339(value):
125+
"""Format the provided datatime in the RFC 3339 format.
126+
127+
:type value: datetime.datetime
128+
:param value: value to format
129+
130+
:rtype: str
131+
:returns: RFC 3339 formatted datetime string
132+
"""
133+
# Convert to UTC and then drop the timezone so we can append "Z" in lieu of
134+
# allowing isoformat to append the "+00:00" zone offset.
135+
value = value.astimezone(datetime.timezone.utc).replace(tzinfo=None)
136+
return value.isoformat(sep="T", timespec="microseconds") + "Z"
137+
138+
139+
def _datetime_to_rfc3339_nanoseconds(value):
140+
"""Format the provided datatime in the RFC 3339 format.
141+
142+
:type value: datetime_helpers.DatetimeWithNanoseconds
143+
:param value: value to format
144+
145+
:rtype: str
146+
:returns: RFC 3339 formatted datetime string
147+
"""
148+
149+
if value.nanosecond == 0:
150+
return _datetime_to_rfc3339(value)
151+
nanos = str(value.nanosecond).rjust(9, "0").rstrip("0")
152+
# Convert to UTC and then drop the timezone so we can append "Z" in lieu of
153+
# allowing isoformat to append the "+00:00" zone offset.
154+
value = value.astimezone(datetime.timezone.utc).replace(tzinfo=None)
155+
return "{}.{}Z".format(value.isoformat(sep="T", timespec="seconds"), nanos)
156+
157+
125158
def _make_value_pb(value):
126159
"""Helper for :func:`_make_list_value_pbs`.
127160
@@ -150,9 +183,9 @@ def _make_value_pb(value):
150183
return Value(string_value="-Infinity")
151184
return Value(number_value=value)
152185
if isinstance(value, datetime_helpers.DatetimeWithNanoseconds):
153-
return Value(string_value=value.rfc3339())
186+
return Value(string_value=_datetime_to_rfc3339_nanoseconds(value))
154187
if isinstance(value, datetime.datetime):
155-
return Value(string_value=_datetime_to_rfc3339(value, ignore_zone=False))
188+
return Value(string_value=_datetime_to_rfc3339(value))
156189
if isinstance(value, datetime.date):
157190
return Value(string_value=value.isoformat())
158191
if isinstance(value, bytes):

tests/unit/test__helpers.py

+44-5
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,15 @@ def test_w_date(self):
190190
self.assertIsInstance(value_pb, Value)
191191
self.assertEqual(value_pb.string_value, today.isoformat())
192192

193+
def test_w_date_pre1000ad(self):
194+
import datetime
195+
from google.protobuf.struct_pb2 import Value
196+
197+
when = datetime.date(800, 2, 25)
198+
value_pb = self._callFUT(when)
199+
self.assertIsInstance(value_pb, Value)
200+
self.assertEqual(value_pb.string_value, "0800-02-25")
201+
193202
def test_w_timestamp_w_nanos(self):
194203
import datetime
195204
from google.protobuf.struct_pb2 import Value
@@ -200,7 +209,19 @@ def test_w_timestamp_w_nanos(self):
200209
)
201210
value_pb = self._callFUT(when)
202211
self.assertIsInstance(value_pb, Value)
203-
self.assertEqual(value_pb.string_value, when.rfc3339())
212+
self.assertEqual(value_pb.string_value, "2016-12-20T21:13:47.123456789Z")
213+
214+
def test_w_timestamp_w_nanos_pre1000ad(self):
215+
import datetime
216+
from google.protobuf.struct_pb2 import Value
217+
from google.api_core import datetime_helpers
218+
219+
when = datetime_helpers.DatetimeWithNanoseconds(
220+
850, 12, 20, 21, 13, 47, nanosecond=123456789, tzinfo=datetime.timezone.utc
221+
)
222+
value_pb = self._callFUT(when)
223+
self.assertIsInstance(value_pb, Value)
224+
self.assertEqual(value_pb.string_value, "0850-12-20T21:13:47.123456789Z")
204225

205226
def test_w_listvalue(self):
206227
from google.protobuf.struct_pb2 import Value
@@ -214,12 +235,20 @@ def test_w_listvalue(self):
214235
def test_w_datetime(self):
215236
import datetime
216237
from google.protobuf.struct_pb2 import Value
217-
from google.api_core import datetime_helpers
218238

219-
now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
220-
value_pb = self._callFUT(now)
239+
when = datetime.datetime(2021, 2, 8, 0, 0, 0, tzinfo=datetime.timezone.utc)
240+
value_pb = self._callFUT(when)
241+
self.assertIsInstance(value_pb, Value)
242+
self.assertEqual(value_pb.string_value, "2021-02-08T00:00:00.000000Z")
243+
244+
def test_w_datetime_pre1000ad(self):
245+
import datetime
246+
from google.protobuf.struct_pb2 import Value
247+
248+
when = datetime.datetime(916, 2, 8, 0, 0, 0, tzinfo=datetime.timezone.utc)
249+
value_pb = self._callFUT(when)
221250
self.assertIsInstance(value_pb, Value)
222-
self.assertEqual(value_pb.string_value, datetime_helpers.to_rfc3339(now))
251+
self.assertEqual(value_pb.string_value, "0916-02-08T00:00:00.000000Z")
223252

224253
def test_w_timestamp_w_tz(self):
225254
import datetime
@@ -231,6 +260,16 @@ def test_w_timestamp_w_tz(self):
231260
self.assertIsInstance(value_pb, Value)
232261
self.assertEqual(value_pb.string_value, "2021-02-07T23:00:00.000000Z")
233262

263+
def test_w_timestamp_w_tz_pre1000ad(self):
264+
import datetime
265+
from google.protobuf.struct_pb2 import Value
266+
267+
zone = datetime.timezone(datetime.timedelta(hours=+1), name="CET")
268+
when = datetime.datetime(721, 2, 8, 0, 0, 0, tzinfo=zone)
269+
value_pb = self._callFUT(when)
270+
self.assertIsInstance(value_pb, Value)
271+
self.assertEqual(value_pb.string_value, "0721-02-07T23:00:00.000000Z")
272+
234273
def test_w_unknown_type(self):
235274
with self.assertRaises(ValueError):
236275
self._callFUT(object())

0 commit comments

Comments
 (0)