Skip to content

Commit 62c22b1

Browse files
authored
feat: Implementing DB-API types according to the PEP-0249 specification (#521)
* feat: BASELINE for DB API standard data types * feat: implementation of PEP-0249 types * chore: reverting "dummy" changes * fix: cleanup * chore: refactor
1 parent 3f5db62 commit 62c22b1

File tree

3 files changed

+88
-112
lines changed

3 files changed

+88
-112
lines changed

google/cloud/spanner_dbapi/types.py

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,92 +4,77 @@
44
# license that can be found in the LICENSE file or at
55
# https://developers.google.com/open-source/licenses/bsd
66

7-
# Implements the types requested by the Python Database API in:
8-
# https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors
7+
"""Implementation of the type objects and constructors according to the
8+
PEP-0249 specification.
9+
10+
See
11+
https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors
12+
"""
913

1014
import datetime
1115
import time
1216
from base64 import b64encode
1317

1418

15-
def Date(year, month, day):
16-
return datetime.date(year, month, day)
17-
18-
19-
def Time(hour, minute, second):
20-
return datetime.time(hour, minute, second)
21-
22-
23-
def Timestamp(year, month, day, hour, minute, second):
24-
return datetime.datetime(year, month, day, hour, minute, second)
19+
def _date_from_ticks(ticks):
20+
"""Based on PEP-249 Implementation Hints for Module Authors:
2521
26-
27-
def DateFromTicks(ticks):
22+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
23+
"""
2824
return Date(*time.localtime(ticks)[:3])
2925

3026

31-
def TimeFromTicks(ticks):
32-
return Time(*time.localtime(ticks)[3:6])
33-
34-
35-
def TimestampFromTicks(ticks):
36-
return Timestamp(*time.localtime(ticks)[:6])
37-
27+
def _time_from_ticks(ticks):
28+
"""Based on PEP-249 Implementation Hints for Module Authors:
3829
39-
def Binary(string):
40-
"""
41-
Creates an object capable of holding a binary (long) string value.
42-
"""
43-
return b64encode(string)
44-
45-
46-
class BINARY:
47-
"""
48-
This object describes (long) binary columns in a database (e.g. LONG, RAW, BLOBS).
30+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
4931
"""
32+
return Time(*time.localtime(ticks)[3:6])
5033

51-
# TODO: Implement me.
52-
pass
5334

35+
def _timestamp_from_ticks(ticks):
36+
"""Based on PEP-249 Implementation Hints for Module Authors:
5437
55-
class STRING:
56-
"""
57-
This object describes columns in a database that are string-based (e.g. CHAR).
38+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
5839
"""
40+
return Timestamp(*time.localtime(ticks)[:6])
5941

60-
# TODO: Implement me.
61-
pass
6242

43+
class _DBAPITypeObject(object):
44+
"""Implementation of a helper class used for type comparison among similar
45+
but possibly different types.
6346
64-
class NUMBER:
65-
"""
66-
This object describes numeric columns in a database.
47+
See
48+
https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
6749
"""
6850

69-
# TODO: Implement me.
70-
pass
51+
def __init__(self, *values):
52+
self.values = values
7153

54+
def __eq__(self, other):
55+
return other in self.values
7256

73-
class DATETIME:
74-
"""
75-
This object describes date/time columns in a database.
76-
"""
7757

78-
# TODO: Implement me.
79-
pass
58+
Date = datetime.date
59+
Time = datetime.time
60+
Timestamp = datetime.datetime
61+
DateFromTicks = _date_from_ticks
62+
TimeFromTicks = _time_from_ticks
63+
TimestampFromTicks = _timestamp_from_ticks
64+
Binary = b64encode
8065

66+
STRING = "STRING"
67+
BINARY = _DBAPITypeObject("TYPE_CODE_UNSPECIFIED", "BYTES", "ARRAY", "STRUCT")
68+
NUMBER = _DBAPITypeObject("BOOL", "INT64", "FLOAT64", "NUMERIC")
69+
DATETIME = _DBAPITypeObject("TIMESTAMP", "DATE")
70+
ROWID = "STRING"
8171

82-
class ROWID:
83-
"""
84-
This object describes the "Row ID" column in a database.
85-
"""
8672

87-
# TODO: Implement me.
88-
pass
73+
class TimestampStr(str):
74+
"""[inherited from the alpha release]
8975
76+
TODO: Decide whether this class is necessary
9077
91-
class TimestampStr(str):
92-
"""
9378
TimestampStr exists so that we can purposefully format types as timestamps
9479
compatible with Cloud Spanner's TIMESTAMP type, but right before making
9580
queries, it'll help differentiate between normal strings and the case of
@@ -100,7 +85,10 @@ class TimestampStr(str):
10085

10186

10287
class DateStr(str):
103-
"""
88+
"""[inherited from the alpha release]
89+
90+
TODO: Decide whether this class is necessary
91+
10492
DateStr is a sentinel type to help format Django dates as
10593
compatible with Cloud Spanner's DATE type, but right before making
10694
queries, it'll help differentiate between normal strings and the case of

tests/spanner_dbapi/test_types.py

Lines changed: 27 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,74 +5,47 @@
55
# https://developers.google.com/open-source/licenses/bsd
66

77
import datetime
8-
import time
8+
from time import timezone
99
from unittest import TestCase
1010

11-
from google.cloud.spanner_dbapi.types import (
12-
Date,
13-
DateFromTicks,
14-
Time,
15-
TimeFromTicks,
16-
Timestamp,
17-
TimestampFromTicks,
18-
)
19-
from google.cloud.spanner_dbapi.utils import PeekIterator
20-
21-
22-
utcOffset = time.timezone # offset for current timezone
11+
from google.cloud.spanner_dbapi import types
2312

2413

2514
class TypesTests(TestCase):
26-
def test_Date(self):
27-
actual = Date(2019, 11, 3)
28-
expected = datetime.date(2019, 11, 3)
29-
self.assertEqual(actual, expected, "mismatch between conversion")
30-
31-
def test_Time(self):
32-
actual = Time(23, 8, 19)
33-
expected = datetime.time(23, 8, 19)
34-
self.assertEqual(actual, expected, "mismatch between conversion")
3515

36-
def test_Timestamp(self):
37-
actual = Timestamp(2019, 11, 3, 23, 8, 19)
38-
expected = datetime.datetime(2019, 11, 3, 23, 8, 19)
39-
self.assertEqual(actual, expected, "mismatch between conversion")
16+
TICKS = 1572822862.9782631 + timezone # Sun 03 Nov 2019 23:14:22 UTC
4017

41-
def test_DateFromTicks(self):
42-
epochTicks = 1572822862 # Sun Nov 03 23:14:22 2019 GMT
43-
44-
actual = DateFromTicks(epochTicks + utcOffset)
18+
def test__date_from_ticks(self):
19+
actual = types._date_from_ticks(self.TICKS)
4520
expected = datetime.date(2019, 11, 3)
4621

47-
self.assertEqual(actual, expected, "mismatch between conversion")
48-
49-
def test_TimeFromTicks(self):
50-
epochTicks = 1572822862 # Sun Nov 03 23:14:22 2019 GMT
22+
self.assertEqual(actual, expected)
5123

52-
actual = TimeFromTicks(epochTicks + utcOffset)
24+
def test__time_from_ticks(self):
25+
actual = types._time_from_ticks(self.TICKS)
5326
expected = datetime.time(23, 14, 22)
5427

55-
self.assertEqual(actual, expected, "mismatch between conversion")
28+
self.assertEqual(actual, expected)
5629

57-
def test_TimestampFromTicks(self):
58-
epochTicks = 1572822862 # Sun Nov 03 23:14:22 2019 GMT
59-
60-
actual = TimestampFromTicks(epochTicks + utcOffset)
30+
def test__timestamp_from_ticks(self):
31+
actual = types._timestamp_from_ticks(self.TICKS)
6132
expected = datetime.datetime(2019, 11, 3, 23, 14, 22)
6233

63-
self.assertEqual(actual, expected, "mismatch between conversion")
34+
self.assertEqual(actual, expected)
35+
36+
def test_type_equal(self):
37+
self.assertEqual(types.BINARY, "TYPE_CODE_UNSPECIFIED")
38+
self.assertEqual(types.BINARY, "BYTES")
39+
self.assertEqual(types.BINARY, "ARRAY")
40+
self.assertEqual(types.BINARY, "STRUCT")
41+
self.assertNotEqual(types.BINARY, "STRING")
6442

65-
def test_PeekIterator(self):
66-
cases = [
67-
("list", [1, 2, 3, 4, 6, 7], [1, 2, 3, 4, 6, 7]),
68-
("iter_from_list", iter([1, 2, 3, 4, 6, 7]), [1, 2, 3, 4, 6, 7]),
69-
("tuple", ("a", 12, 0xFF), ["a", 12, 0xFF]),
70-
("iter_from_tuple", iter(("a", 12, 0xFF)), ["a", 12, 0xFF]),
71-
("no_args", (), []),
72-
]
43+
self.assertEqual(types.NUMBER, "BOOL")
44+
self.assertEqual(types.NUMBER, "INT64")
45+
self.assertEqual(types.NUMBER, "FLOAT64")
46+
self.assertEqual(types.NUMBER, "NUMERIC")
47+
self.assertNotEqual(types.NUMBER, "STRING")
7348

74-
for name, data_in, expected in cases:
75-
with self.subTest(name=name):
76-
pitr = PeekIterator(data_in)
77-
actual = list(pitr)
78-
self.assertEqual(actual, expected)
49+
self.assertEqual(types.DATETIME, "TIMESTAMP")
50+
self.assertEqual(types.DATETIME, "DATE")
51+
self.assertNotEqual(types.DATETIME, "STRING")

tests/spanner_dbapi/test_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@
1010

1111

1212
class UtilsTests(TestCase):
13+
def test_PeekIterator(self):
14+
cases = [
15+
("list", [1, 2, 3, 4, 6, 7], [1, 2, 3, 4, 6, 7]),
16+
("iter_from_list", iter([1, 2, 3, 4, 6, 7]), [1, 2, 3, 4, 6, 7]),
17+
("tuple", ("a", 12, 0xFF), ["a", 12, 0xFF]),
18+
("iter_from_tuple", iter(("a", 12, 0xFF)), ["a", 12, 0xFF]),
19+
("no_args", (), []),
20+
]
21+
22+
for name, data_in, expected in cases:
23+
with self.subTest(name=name):
24+
pitr = PeekIterator(data_in)
25+
actual = list(pitr)
26+
self.assertEqual(actual, expected)
27+
1328
def test_peekIterator_list_rows_converted_to_tuples(self):
1429
# Cloud Spanner returns results in lists e.g. [result].
1530
# PeekIterator is used by BaseCursor in its fetch* methods.

0 commit comments

Comments
 (0)