Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implementing DB-API types according to the PEP-0249 specification #521

Merged
merged 9 commits into from Oct 4, 2020
104 changes: 46 additions & 58 deletions google/cloud/spanner_dbapi/types.py
Expand Up @@ -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
Expand All @@ -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
Expand Down
110 changes: 32 additions & 78 deletions tests/spanner_dbapi/test_types.py
Expand Up @@ -5,93 +5,47 @@
# https://developers.google.com/open-source/licenses/bsd

import datetime
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

tzUTC = 0 # 0 hours offset from UTC
from google.cloud.spanner_dbapi import types


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")
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)

def test__time_from_ticks(self):
actual = types._time_from_ticks(self.TICKS)
expected = datetime.time(23, 14, 22)

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")
self.assertEqual(actual, expected)

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)
)
def test__timestamp_from_ticks(self):
actual = types._timestamp_from_ticks(self.TICKS)
expected = datetime.datetime(2019, 11, 3, 23, 14, 22)

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)
)
self.assertEqual(actual, expected)

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)
)
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, want in cases:
with self.subTest(name=name):
pitr = PeekIterator(data_in)
got = list(pitr)
self.assertEqual(got, want)
self.assertEqual(types.DATETIME, "TIMESTAMP")
self.assertEqual(types.DATETIME, "DATE")
self.assertNotEqual(types.DATETIME, "STRING")
15 changes: 15 additions & 0 deletions tests/spanner_dbapi/test_utils.py
Expand Up @@ -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.
Expand Down