Skip to content

Commit 61b4385

Browse files
authored
fix: invalid conversion of timezone-aware datetime values to JSON (#480)
* fix: correctly convert timezone-aware datetimes * blacken * Remove python-dateutil test dependency * Remove unused dst() methods
1 parent 530e1e8 commit 61b4385

File tree

2 files changed

+51
-34
lines changed

2 files changed

+51
-34
lines changed

google/cloud/bigquery/_helpers.py

+8
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,21 @@ def _timestamp_to_json_parameter(value):
315315
def _timestamp_to_json_row(value):
316316
"""Coerce 'value' to an JSON-compatible representation."""
317317
if isinstance(value, datetime.datetime):
318+
# For naive datetime objects UTC timezone is assumed, thus we format
319+
# those to string directly without conversion.
320+
if value.tzinfo is not None:
321+
value = value.astimezone(UTC)
318322
value = value.strftime(_RFC3339_MICROS)
319323
return value
320324

321325

322326
def _datetime_to_json(value):
323327
"""Coerce 'value' to an JSON-compatible representation."""
324328
if isinstance(value, datetime.datetime):
329+
# For naive datetime objects UTC timezone is assumed, thus we format
330+
# those to string directly without conversion.
331+
if value.tzinfo is not None:
332+
value = value.astimezone(UTC)
325333
value = value.strftime(_RFC3339_MICROS_NO_ZULU)
326334
return value
327335

tests/unit/test__helpers.py

+43-34
Original file line numberDiff line numberDiff line change
@@ -420,27 +420,27 @@ def _call_fut(self, row, schema):
420420
def test_w_single_scalar_column(self):
421421
# SELECT 1 AS col
422422
col = _Field("REQUIRED", "col", "INTEGER")
423-
row = {u"f": [{u"v": u"1"}]}
423+
row = {"f": [{"v": "1"}]}
424424
self.assertEqual(self._call_fut(row, schema=[col]), (1,))
425425

426426
def test_w_single_scalar_geography_column(self):
427427
# SELECT 1 AS col
428428
col = _Field("REQUIRED", "geo", "GEOGRAPHY")
429-
row = {u"f": [{u"v": u"POINT(1, 2)"}]}
429+
row = {"f": [{"v": "POINT(1, 2)"}]}
430430
self.assertEqual(self._call_fut(row, schema=[col]), ("POINT(1, 2)",))
431431

432432
def test_w_single_struct_column(self):
433433
# SELECT (1, 2) AS col
434434
sub_1 = _Field("REQUIRED", "sub_1", "INTEGER")
435435
sub_2 = _Field("REQUIRED", "sub_2", "INTEGER")
436436
col = _Field("REQUIRED", "col", "RECORD", fields=[sub_1, sub_2])
437-
row = {u"f": [{u"v": {u"f": [{u"v": u"1"}, {u"v": u"2"}]}}]}
437+
row = {"f": [{"v": {"f": [{"v": "1"}, {"v": "2"}]}}]}
438438
self.assertEqual(self._call_fut(row, schema=[col]), ({"sub_1": 1, "sub_2": 2},))
439439

440440
def test_w_single_array_column(self):
441441
# SELECT [1, 2, 3] as col
442442
col = _Field("REPEATED", "col", "INTEGER")
443-
row = {u"f": [{u"v": [{u"v": u"1"}, {u"v": u"2"}, {u"v": u"3"}]}]}
443+
row = {"f": [{"v": [{"v": "1"}, {"v": "2"}, {"v": "3"}]}]}
444444
self.assertEqual(self._call_fut(row, schema=[col]), ([1, 2, 3],))
445445

446446
def test_w_struct_w_nested_array_column(self):
@@ -450,21 +450,21 @@ def test_w_struct_w_nested_array_column(self):
450450
third = _Field("REPEATED", "third", "INTEGER")
451451
col = _Field("REQUIRED", "col", "RECORD", fields=[first, second, third])
452452
row = {
453-
u"f": [
453+
"f": [
454454
{
455-
u"v": {
456-
u"f": [
457-
{u"v": [{u"v": u"1"}, {u"v": u"2"}]},
458-
{u"v": u"3"},
459-
{u"v": [{u"v": u"4"}, {u"v": u"5"}]},
455+
"v": {
456+
"f": [
457+
{"v": [{"v": "1"}, {"v": "2"}]},
458+
{"v": "3"},
459+
{"v": [{"v": "4"}, {"v": "5"}]},
460460
]
461461
}
462462
}
463463
]
464464
}
465465
self.assertEqual(
466466
self._call_fut(row, schema=[col]),
467-
({u"first": [1, 2], u"second": 3, u"third": [4, 5]},),
467+
({"first": [1, 2], "second": 3, "third": [4, 5]},),
468468
)
469469

470470
def test_w_array_of_struct(self):
@@ -474,11 +474,11 @@ def test_w_array_of_struct(self):
474474
third = _Field("REQUIRED", "third", "INTEGER")
475475
col = _Field("REPEATED", "col", "RECORD", fields=[first, second, third])
476476
row = {
477-
u"f": [
477+
"f": [
478478
{
479-
u"v": [
480-
{u"v": {u"f": [{u"v": u"1"}, {u"v": u"2"}, {u"v": u"3"}]}},
481-
{u"v": {u"f": [{u"v": u"4"}, {u"v": u"5"}, {u"v": u"6"}]}},
479+
"v": [
480+
{"v": {"f": [{"v": "1"}, {"v": "2"}, {"v": "3"}]}},
481+
{"v": {"f": [{"v": "4"}, {"v": "5"}, {"v": "6"}]}},
482482
]
483483
}
484484
]
@@ -487,8 +487,8 @@ def test_w_array_of_struct(self):
487487
self._call_fut(row, schema=[col]),
488488
(
489489
[
490-
{u"first": 1, u"second": 2, u"third": 3},
491-
{u"first": 4, u"second": 5, u"third": 6},
490+
{"first": 1, "second": 2, "third": 3},
491+
{"first": 4, "second": 5, "third": 6},
492492
],
493493
),
494494
)
@@ -499,32 +499,25 @@ def test_w_array_of_struct_w_array(self):
499499
second = _Field("REQUIRED", "second", "INTEGER")
500500
col = _Field("REPEATED", "col", "RECORD", fields=[first, second])
501501
row = {
502-
u"f": [
502+
"f": [
503503
{
504-
u"v": [
505-
{
506-
u"v": {
507-
u"f": [
508-
{u"v": [{u"v": u"1"}, {u"v": u"2"}, {u"v": u"3"}]},
509-
{u"v": u"4"},
510-
]
511-
}
512-
},
504+
"v": [
513505
{
514-
u"v": {
515-
u"f": [
516-
{u"v": [{u"v": u"5"}, {u"v": u"6"}]},
517-
{u"v": u"7"},
506+
"v": {
507+
"f": [
508+
{"v": [{"v": "1"}, {"v": "2"}, {"v": "3"}]},
509+
{"v": "4"},
518510
]
519511
}
520512
},
513+
{"v": {"f": [{"v": [{"v": "5"}, {"v": "6"}]}, {"v": "7"}]}},
521514
]
522515
}
523516
]
524517
}
525518
self.assertEqual(
526519
self._call_fut(row, schema=[col]),
527-
([{u"first": [1, 2, 3], u"second": 4}, {u"first": [5, 6], u"second": 7}],),
520+
([{"first": [1, 2, 3], "second": 4}, {"first": [5, 6], "second": 7}],),
528521
)
529522

530523

@@ -673,7 +666,7 @@ def test_w_non_bytes(self):
673666

674667
def test_w_bytes(self):
675668
source = b"source"
676-
expected = u"c291cmNl"
669+
expected = "c291cmNl"
677670
converted = self._call_fut(source)
678671
self.assertEqual(converted, expected)
679672

@@ -726,7 +719,7 @@ def test_w_string(self):
726719
ZULU = "2016-12-20 15:58:27.339328+00:00"
727720
self.assertEqual(self._call_fut(ZULU), ZULU)
728721

729-
def test_w_datetime(self):
722+
def test_w_datetime_no_zone(self):
730723
when = datetime.datetime(2016, 12, 20, 15, 58, 27, 339328)
731724
self.assertEqual(self._call_fut(when), "2016-12-20T15:58:27.339328Z")
732725

@@ -736,6 +729,14 @@ def test_w_datetime_w_utc_zone(self):
736729
when = datetime.datetime(2020, 11, 17, 1, 6, 52, 353795, tzinfo=UTC)
737730
self.assertEqual(self._call_fut(when), "2020-11-17T01:06:52.353795Z")
738731

732+
def test_w_datetime_w_non_utc_zone(self):
733+
class EstZone(datetime.tzinfo):
734+
def utcoffset(self, _):
735+
return datetime.timedelta(minutes=-300)
736+
737+
when = datetime.datetime(2020, 11, 17, 1, 6, 52, 353795, tzinfo=EstZone())
738+
self.assertEqual(self._call_fut(when), "2020-11-17T06:06:52.353795Z")
739+
739740

740741
class Test_datetime_to_json(unittest.TestCase):
741742
def _call_fut(self, value):
@@ -753,6 +754,14 @@ def test_w_datetime(self):
753754
when = datetime.datetime(2016, 12, 3, 14, 11, 27, 123456, tzinfo=UTC)
754755
self.assertEqual(self._call_fut(when), "2016-12-03T14:11:27.123456")
755756

757+
def test_w_datetime_w_non_utc_zone(self):
758+
class EstZone(datetime.tzinfo):
759+
def utcoffset(self, _):
760+
return datetime.timedelta(minutes=-300)
761+
762+
when = datetime.datetime(2016, 12, 3, 14, 11, 27, 123456, tzinfo=EstZone())
763+
self.assertEqual(self._call_fut(when), "2016-12-03T19:11:27.123456")
764+
756765

757766
class Test_date_to_json(unittest.TestCase):
758767
def _call_fut(self, value):

0 commit comments

Comments
 (0)