From bf152c8bec3d5c95a1c685a4232825dc712ae972 Mon Sep 17 00:00:00 2001 From: "STATION\\MF" Date: Tue, 29 Sep 2020 16:29:54 -0400 Subject: [PATCH 1/5] feat: BASELINE for DB API standard data types --- google/cloud/spanner_dbapi/connection.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index 869586e363..af3afa7da1 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -38,9 +38,8 @@ def _raise_if_closed(self): raise InterfaceError("connection is already closed") def __handle_update_ddl(self, ddl_statements): - """ - Run the list of Data Definition Language (DDL) statements on the underlying - database. Each DDL statement MUST NOT contain a semicolon. + """Run the list of Data Definition Language (DDL) statements on the + underlying database. Each DDL statement MUST NOT contain a semicolon. Args: ddl_statements: a list of DDL statements, each without a semicolon. Returns: From 9300e6a0d13b975a4c4bc0c85a789b5871e0a2b2 Mon Sep 17 00:00:00 2001 From: "STATION\\MF" Date: Tue, 29 Sep 2020 20:43:40 -0400 Subject: [PATCH 2/5] feat: implementation of PEP-0249 types --- google/cloud/spanner_dbapi/types.py | 112 +++++++++++++--------------- tests/spanner_dbapi/test_types.py | 110 ++++++++------------------- tests/spanner_dbapi/test_utils.py | 15 ++++ 3 files changed, 97 insertions(+), 140 deletions(-) diff --git a/google/cloud/spanner_dbapi/types.py b/google/cloud/spanner_dbapi/types.py index 2299f279ba..fa95bd4742 100644 --- a/google/cloud/spanner_dbapi/types.py +++ b/google/cloud/spanner_dbapi/types.py @@ -4,92 +4,79 @@ # 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 - -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) +"""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 +""" -def DateFromTicks(ticks): - return Date(*time.localtime(ticks)[:3]) - +import datetime -def TimeFromTicks(ticks): - return Time(*time.localtime(ticks)[3:6]) +def _time_from_ticks(ticks, tz=None): + """A helper method used to construct a DB-API time value. -def TimestampFromTicks(ticks): - return Timestamp(*time.localtime(ticks)[:6]) + :type ticks: float + :param ticks: The number of seconds passed since the epoch. + :type tz: :class:`datetime.tzinfo` + :param tz: (Optional) The timezone information to use for conversion. -def Binary(string): + :rtype: :class:`datetime.time` + :returns: The corresponding time value. """ - Creates an object capable of holding a binary (long) string value. - """ - return b64encode(string) - + return datetime.datetime.fromtimestamp(ticks, tz=tz).timetz() -class BINARY: - """ - This object describes (long) binary columns in a database (e.g. LONG, RAW, BLOBS). - """ - # TODO: Implement me. - pass +def _binary(string): + """A helper method used to construct an object capable of + holding a binary (long) string value. + :type string: str + :param string: A string to encode as binary bytes. -class STRING: - """ - This object describes columns in a database that are string-based (e.g. CHAR). + :rtype: bytes + :returns: The UTF-8 encoded representation of the string. """ + return string.encode("utf-8") - # 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 = datetime.date.fromtimestamp +TimeFromTicks = _time_from_ticks +TimestampFromTicks = datetime.datetime.fromtimestamp +Binary = _binary +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 +87,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 642f43f340..32e6ede4a9 100644 --- a/tests/spanner_dbapi/test_types.py +++ b/tests/spanner_dbapi/test_types.py @@ -5,93 +5,45 @@ # https://developers.google.com/open-source/licenses/bsd import datetime +import time 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 +from google.cloud._helpers import UTC +from google.cloud.spanner_dbapi import types -tzUTC = 0 # 0 hours offset from UTC +utcOffset = time.timezone # offset for current timezone -class TypesTests(TestCase): - def test_Date(self): - got = Date(2019, 11, 3) - want = datetime.date(2019, 11, 3) - self.assertEqual(got, want, "mismatch between conversion") - - def test_Time(self): - got = Time(23, 8, 19) - want = datetime.time(23, 8, 19) - self.assertEqual(got, want, "mismatch between conversion") - - def test_Timestamp(self): - got = Timestamp(2019, 11, 3, 23, 8, 19) - want = datetime.datetime(2019, 11, 3, 23, 8, 19) - self.assertEqual(got, want, "mismatch between conversion") - def test_DateFromTicks(self): - epochTicks = 1572851662.9782631 # Sun Nov 03 23:14:22 2019 - got = DateFromTicks(epochTicks) - # Since continuous integration infrastructure such as Travis CI - # uses clocks on UTC, it is useful to be able to compare against - # either of UTC or the known standard time. - want = ( - datetime.date(2019, 11, 3), - datetime.datetime(2019, 11, 4, tzUTC).date(), - ) - matches = got in want - self.assertTrue( - matches, "`%s` not present in any of\n`%s`" % (got, want) - ) +class TypesTests(TestCase): + def test__time_from_ticks(self): + ticks = 1572822862.9782631 # Sun 03 Nov 2019 23:14:22 UTC + timezone = UTC - def test_TimeFromTicks(self): - epochTicks = 1572851662.9782631 # Sun Nov 03 23:14:22 2019 - got = TimeFromTicks(epochTicks) - # Since continuous integration infrastructure such as Travis CI - # uses clocks on UTC, it is useful to be able to compare against - # either of UTC or the known standard time. - want = ( - datetime.time(23, 14, 22), - datetime.datetime(2019, 11, 4, 7, 14, 22, tzUTC).time(), - ) - matches = got in want - self.assertTrue( - matches, "`%s` not present in any of\n`%s`" % (got, want) - ) + actual = types.TimeFromTicks(ticks, tz=timezone) + expected = datetime.datetime.fromtimestamp(ticks, tz=timezone).timetz() - def test_TimestampFromTicks(self): - epochTicks = 1572851662.9782631 # Sun Nov 03 23:14:22 2019 - got = TimestampFromTicks(epochTicks) - # Since continuous integration infrastructure such as Travis CI - # uses clocks on UTC, it is useful to be able to compare against - # either of UTC or the known standard time. - want = ( - datetime.datetime(2019, 11, 3, 23, 14, 22), - datetime.datetime(2019, 11, 4, 7, 14, 22, tzUTC), - ) - matches = got in want self.assertTrue( - matches, "`%s` not present in any of\n`%s`" % (got, want) + actual == expected, "`%s` doesn't match\n`%s`" % (actual, expected) ) - 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, want in cases: - with self.subTest(name=name): - pitr = PeekIterator(data_in) - got = list(pitr) - self.assertEqual(got, want) + 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") + + 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") + + self.assertEqual(types.DATETIME, "TIMESTAMP") + self.assertEqual(types.DATETIME, "DATE") + self.assertNotEqual(types.DATETIME, "STRING") + + self.assertNotEqual("STRING", types.BINARY) + self.assertEqual(types.Binary(u"hello"), b"hello") + self.assertEqual(types.Binary(u"\u1f60"), u"\u1f60".encode("utf-8")) 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. From 44acdec9058d4cfe61d602a86b1292610947d3ef Mon Sep 17 00:00:00 2001 From: "STATION\\MF" Date: Tue, 29 Sep 2020 23:39:28 -0400 Subject: [PATCH 3/5] chore: reverting "dummy" changes --- google/cloud/spanner_dbapi/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index af3afa7da1..25d40c8d6e 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -38,7 +38,8 @@ def _raise_if_closed(self): raise InterfaceError("connection is already closed") def __handle_update_ddl(self, ddl_statements): - """Run the list of Data Definition Language (DDL) statements on the + """ + Run the list of Data Definition Language (DDL) statements on the underlying database. Each DDL statement MUST NOT contain a semicolon. Args: ddl_statements: a list of DDL statements, each without a semicolon. From 4c6ce519e45bcf979245edc5cbfb6f7e5ce24f00 Mon Sep 17 00:00:00 2001 From: "STATION\\MF" Date: Wed, 30 Sep 2020 02:01:21 -0400 Subject: [PATCH 4/5] fix: cleanup --- google/cloud/spanner_dbapi/connection.py | 4 ++-- google/cloud/spanner_dbapi/types.py | 16 ++-------------- tests/spanner_dbapi/test_types.py | 4 ---- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/google/cloud/spanner_dbapi/connection.py b/google/cloud/spanner_dbapi/connection.py index 25d40c8d6e..869586e363 100644 --- a/google/cloud/spanner_dbapi/connection.py +++ b/google/cloud/spanner_dbapi/connection.py @@ -39,8 +39,8 @@ def _raise_if_closed(self): def __handle_update_ddl(self, ddl_statements): """ - Run the list of Data Definition Language (DDL) statements on the - underlying database. Each DDL statement MUST NOT contain a semicolon. + Run the list of Data Definition Language (DDL) statements on the underlying + database. Each DDL statement MUST NOT contain a semicolon. Args: ddl_statements: a list of DDL statements, each without a semicolon. Returns: diff --git a/google/cloud/spanner_dbapi/types.py b/google/cloud/spanner_dbapi/types.py index fa95bd4742..13b3898361 100644 --- a/google/cloud/spanner_dbapi/types.py +++ b/google/cloud/spanner_dbapi/types.py @@ -12,6 +12,7 @@ """ import datetime +from base64 import b64encode def _time_from_ticks(ticks, tz=None): @@ -29,19 +30,6 @@ def _time_from_ticks(ticks, tz=None): return datetime.datetime.fromtimestamp(ticks, tz=tz).timetz() -def _binary(string): - """A helper method used to construct an object capable of - holding a binary (long) string value. - - :type string: str - :param string: A string to encode as binary bytes. - - :rtype: bytes - :returns: The UTF-8 encoded representation of the string. - """ - return string.encode("utf-8") - - class _DBAPITypeObject(object): """Implementation of a helper class used for type comparison among similar but possibly different types. @@ -63,7 +51,7 @@ def __eq__(self, other): DateFromTicks = datetime.date.fromtimestamp TimeFromTicks = _time_from_ticks TimestampFromTicks = datetime.datetime.fromtimestamp -Binary = _binary +Binary = b64encode STRING = "STRING" BINARY = _DBAPITypeObject("TYPE_CODE_UNSPECIFIED", "BYTES", "ARRAY", "STRUCT") diff --git a/tests/spanner_dbapi/test_types.py b/tests/spanner_dbapi/test_types.py index 32e6ede4a9..f80679b78c 100644 --- a/tests/spanner_dbapi/test_types.py +++ b/tests/spanner_dbapi/test_types.py @@ -43,7 +43,3 @@ def test_type_equal(self): self.assertEqual(types.DATETIME, "TIMESTAMP") self.assertEqual(types.DATETIME, "DATE") self.assertNotEqual(types.DATETIME, "STRING") - - self.assertNotEqual("STRING", types.BINARY) - self.assertEqual(types.Binary(u"hello"), b"hello") - self.assertEqual(types.Binary(u"\u1f60"), u"\u1f60".encode("utf-8")) From ee93d4fc1038944da93d75aba75450826ee646b9 Mon Sep 17 00:00:00 2001 From: "STATION\\MF" Date: Thu, 1 Oct 2020 12:32:24 -0400 Subject: [PATCH 5/5] chore: refactor --- google/cloud/spanner_dbapi/types.py | 32 +++++++++++++++++++---------- tests/spanner_dbapi/test_types.py | 28 +++++++++++++++---------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/google/cloud/spanner_dbapi/types.py b/google/cloud/spanner_dbapi/types.py index 13b3898361..8c6bd27577 100644 --- a/google/cloud/spanner_dbapi/types.py +++ b/google/cloud/spanner_dbapi/types.py @@ -12,22 +12,32 @@ """ import datetime +import time from base64 import b64encode -def _time_from_ticks(ticks, tz=None): - """A helper method used to construct a DB-API time value. +def _date_from_ticks(ticks): + """Based on PEP-249 Implementation Hints for Module Authors: + + https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors + """ + return Date(*time.localtime(ticks)[:3]) - :type ticks: float - :param ticks: The number of seconds passed since the epoch. - :type tz: :class:`datetime.tzinfo` - :param tz: (Optional) The timezone information to use for conversion. +def _time_from_ticks(ticks): + """Based on PEP-249 Implementation Hints for Module Authors: - :rtype: :class:`datetime.time` - :returns: The corresponding time value. + https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors + """ + return Time(*time.localtime(ticks)[3:6]) + + +def _timestamp_from_ticks(ticks): + """Based on PEP-249 Implementation Hints for Module Authors: + + https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors """ - return datetime.datetime.fromtimestamp(ticks, tz=tz).timetz() + return Timestamp(*time.localtime(ticks)[:6]) class _DBAPITypeObject(object): @@ -48,9 +58,9 @@ def __eq__(self, other): Date = datetime.date Time = datetime.time Timestamp = datetime.datetime -DateFromTicks = datetime.date.fromtimestamp +DateFromTicks = _date_from_ticks TimeFromTicks = _time_from_ticks -TimestampFromTicks = datetime.datetime.fromtimestamp +TimestampFromTicks = _timestamp_from_ticks Binary = b64encode STRING = "STRING" diff --git a/tests/spanner_dbapi/test_types.py b/tests/spanner_dbapi/test_types.py index f80679b78c..6c41041628 100644 --- a/tests/spanner_dbapi/test_types.py +++ b/tests/spanner_dbapi/test_types.py @@ -5,27 +5,33 @@ # https://developers.google.com/open-source/licenses/bsd import datetime -import time +from time import timezone from unittest import TestCase -from google.cloud._helpers import UTC from google.cloud.spanner_dbapi import types -utcOffset = time.timezone # offset for current timezone +class TypesTests(TestCase): + TICKS = 1572822862.9782631 + timezone # Sun 03 Nov 2019 23:14:22 UTC + + def test__date_from_ticks(self): + actual = types._date_from_ticks(self.TICKS) + expected = datetime.date(2019, 11, 3) + + self.assertEqual(actual, expected) -class TypesTests(TestCase): def test__time_from_ticks(self): - ticks = 1572822862.9782631 # Sun 03 Nov 2019 23:14:22 UTC - timezone = UTC + actual = types._time_from_ticks(self.TICKS) + expected = datetime.time(23, 14, 22) + + self.assertEqual(actual, expected) - actual = types.TimeFromTicks(ticks, tz=timezone) - expected = datetime.datetime.fromtimestamp(ticks, tz=timezone).timetz() + def test__timestamp_from_ticks(self): + actual = types._timestamp_from_ticks(self.TICKS) + expected = datetime.datetime(2019, 11, 3, 23, 14, 22) - self.assertTrue( - actual == expected, "`%s` doesn't match\n`%s`" % (actual, expected) - ) + self.assertEqual(actual, expected) def test_type_equal(self): self.assertEqual(types.BINARY, "TYPE_CODE_UNSPECIFIED")