diff --git a/google/cloud/spanner_dbapi/types.py b/google/cloud/spanner_dbapi/types.py index 2299f279ba..8c6bd27577 100644 --- a/google/cloud/spanner_dbapi/types.py +++ b/google/cloud/spanner_dbapi/types.py @@ -4,92 +4,77 @@ # license that can be found in the LICENSE file or at # https://developers.google.com/open-source/licenses/bsd -# Implements the types requested by the Python Database API in: -# https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors +"""Implementation of the type objects and constructors according to the + PEP-0249 specification. + + See + https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors +""" import datetime import time from base64 import b64encode -def Date(year, month, day): - return datetime.date(year, month, day) - - -def Time(hour, minute, second): - return datetime.time(hour, minute, second) - - -def Timestamp(year, month, day, hour, minute, second): - return datetime.datetime(year, month, day, hour, minute, second) +def _date_from_ticks(ticks): + """Based on PEP-249 Implementation Hints for Module Authors: - -def DateFromTicks(ticks): + https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors + """ return Date(*time.localtime(ticks)[:3]) -def TimeFromTicks(ticks): - return Time(*time.localtime(ticks)[3:6]) - - -def TimestampFromTicks(ticks): - return Timestamp(*time.localtime(ticks)[:6]) - +def _time_from_ticks(ticks): + """Based on PEP-249 Implementation Hints for Module Authors: -def Binary(string): - """ - Creates an object capable of holding a binary (long) string value. - """ - return b64encode(string) - - -class BINARY: - """ - This object describes (long) binary columns in a database (e.g. LONG, RAW, BLOBS). + https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors """ + return Time(*time.localtime(ticks)[3:6]) - # TODO: Implement me. - pass +def _timestamp_from_ticks(ticks): + """Based on PEP-249 Implementation Hints for Module Authors: -class STRING: - """ - This object describes columns in a database that are string-based (e.g. CHAR). + https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors """ + return Timestamp(*time.localtime(ticks)[:6]) - # TODO: Implement me. - pass +class _DBAPITypeObject(object): + """Implementation of a helper class used for type comparison among similar + but possibly different types. -class NUMBER: - """ - This object describes numeric columns in a database. + See + https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors """ - # TODO: Implement me. - pass + def __init__(self, *values): + self.values = values + def __eq__(self, other): + return other in self.values -class DATETIME: - """ - This object describes date/time columns in a database. - """ - # TODO: Implement me. - pass +Date = datetime.date +Time = datetime.time +Timestamp = datetime.datetime +DateFromTicks = _date_from_ticks +TimeFromTicks = _time_from_ticks +TimestampFromTicks = _timestamp_from_ticks +Binary = b64encode +STRING = "STRING" +BINARY = _DBAPITypeObject("TYPE_CODE_UNSPECIFIED", "BYTES", "ARRAY", "STRUCT") +NUMBER = _DBAPITypeObject("BOOL", "INT64", "FLOAT64", "NUMERIC") +DATETIME = _DBAPITypeObject("TIMESTAMP", "DATE") +ROWID = "STRING" -class ROWID: - """ - This object describes the "Row ID" column in a database. - """ - # TODO: Implement me. - pass +class TimestampStr(str): + """[inherited from the alpha release] + TODO: Decide whether this class is necessary -class TimestampStr(str): - """ TimestampStr exists so that we can purposefully format types as timestamps compatible with Cloud Spanner's TIMESTAMP type, but right before making queries, it'll help differentiate between normal strings and the case of @@ -100,7 +85,10 @@ class TimestampStr(str): class DateStr(str): - """ + """[inherited from the alpha release] + + TODO: Decide whether this class is necessary + DateStr is a sentinel type to help format Django dates as compatible with Cloud Spanner's DATE type, but right before making queries, it'll help differentiate between normal strings and the case of diff --git a/tests/spanner_dbapi/test_types.py b/tests/spanner_dbapi/test_types.py index aa98f6eeee..6c41041628 100644 --- a/tests/spanner_dbapi/test_types.py +++ b/tests/spanner_dbapi/test_types.py @@ -5,74 +5,47 @@ # https://developers.google.com/open-source/licenses/bsd import datetime -import time +from time import timezone from unittest import TestCase -from google.cloud.spanner_dbapi.types import ( - Date, - DateFromTicks, - Time, - TimeFromTicks, - Timestamp, - TimestampFromTicks, -) -from google.cloud.spanner_dbapi.utils import PeekIterator - - -utcOffset = time.timezone # offset for current timezone +from google.cloud.spanner_dbapi import types class TypesTests(TestCase): - def test_Date(self): - actual = Date(2019, 11, 3) - expected = datetime.date(2019, 11, 3) - self.assertEqual(actual, expected, "mismatch between conversion") - - def test_Time(self): - actual = Time(23, 8, 19) - expected = datetime.time(23, 8, 19) - self.assertEqual(actual, expected, "mismatch between conversion") - def test_Timestamp(self): - actual = Timestamp(2019, 11, 3, 23, 8, 19) - expected = datetime.datetime(2019, 11, 3, 23, 8, 19) - self.assertEqual(actual, expected, "mismatch between conversion") + TICKS = 1572822862.9782631 + timezone # Sun 03 Nov 2019 23:14:22 UTC - def test_DateFromTicks(self): - epochTicks = 1572822862 # Sun Nov 03 23:14:22 2019 GMT - - actual = DateFromTicks(epochTicks + utcOffset) + def test__date_from_ticks(self): + actual = types._date_from_ticks(self.TICKS) expected = datetime.date(2019, 11, 3) - self.assertEqual(actual, expected, "mismatch between conversion") - - def test_TimeFromTicks(self): - epochTicks = 1572822862 # Sun Nov 03 23:14:22 2019 GMT + self.assertEqual(actual, expected) - actual = TimeFromTicks(epochTicks + utcOffset) + def test__time_from_ticks(self): + actual = types._time_from_ticks(self.TICKS) expected = datetime.time(23, 14, 22) - self.assertEqual(actual, expected, "mismatch between conversion") + self.assertEqual(actual, expected) - def test_TimestampFromTicks(self): - epochTicks = 1572822862 # Sun Nov 03 23:14:22 2019 GMT - - actual = TimestampFromTicks(epochTicks + utcOffset) + def test__timestamp_from_ticks(self): + actual = types._timestamp_from_ticks(self.TICKS) expected = datetime.datetime(2019, 11, 3, 23, 14, 22) - self.assertEqual(actual, expected, "mismatch between conversion") + self.assertEqual(actual, expected) + + def test_type_equal(self): + self.assertEqual(types.BINARY, "TYPE_CODE_UNSPECIFIED") + self.assertEqual(types.BINARY, "BYTES") + self.assertEqual(types.BINARY, "ARRAY") + self.assertEqual(types.BINARY, "STRUCT") + self.assertNotEqual(types.BINARY, "STRING") - def test_PeekIterator(self): - cases = [ - ("list", [1, 2, 3, 4, 6, 7], [1, 2, 3, 4, 6, 7]), - ("iter_from_list", iter([1, 2, 3, 4, 6, 7]), [1, 2, 3, 4, 6, 7]), - ("tuple", ("a", 12, 0xFF), ["a", 12, 0xFF]), - ("iter_from_tuple", iter(("a", 12, 0xFF)), ["a", 12, 0xFF]), - ("no_args", (), []), - ] + self.assertEqual(types.NUMBER, "BOOL") + self.assertEqual(types.NUMBER, "INT64") + self.assertEqual(types.NUMBER, "FLOAT64") + self.assertEqual(types.NUMBER, "NUMERIC") + self.assertNotEqual(types.NUMBER, "STRING") - for name, data_in, expected in cases: - with self.subTest(name=name): - pitr = PeekIterator(data_in) - actual = list(pitr) - self.assertEqual(actual, expected) + self.assertEqual(types.DATETIME, "TIMESTAMP") + self.assertEqual(types.DATETIME, "DATE") + self.assertNotEqual(types.DATETIME, "STRING") diff --git a/tests/spanner_dbapi/test_utils.py b/tests/spanner_dbapi/test_utils.py index 36f6083ae9..2ec10eefaf 100644 --- a/tests/spanner_dbapi/test_utils.py +++ b/tests/spanner_dbapi/test_utils.py @@ -10,6 +10,21 @@ class UtilsTests(TestCase): + def test_PeekIterator(self): + cases = [ + ("list", [1, 2, 3, 4, 6, 7], [1, 2, 3, 4, 6, 7]), + ("iter_from_list", iter([1, 2, 3, 4, 6, 7]), [1, 2, 3, 4, 6, 7]), + ("tuple", ("a", 12, 0xFF), ["a", 12, 0xFF]), + ("iter_from_tuple", iter(("a", 12, 0xFF)), ["a", 12, 0xFF]), + ("no_args", (), []), + ] + + for name, data_in, expected in cases: + with self.subTest(name=name): + pitr = PeekIterator(data_in) + actual = list(pitr) + self.assertEqual(actual, expected) + def test_peekIterator_list_rows_converted_to_tuples(self): # Cloud Spanner returns results in lists e.g. [result]. # PeekIterator is used by BaseCursor in its fetch* methods.