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
108 changes: 43 additions & 65 deletions google/cloud/spanner_dbapi/types.py
Expand Up @@ -4,92 +4,67 @@
# 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 DateFromTicks(ticks):
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 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).
"""
def _time_from_ticks(ticks, tz=None):
"""A helper method used to construct a DB-API time value.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should first add system tests for these types, before merging. As they are gonna be used to convert data from user types to the Spanner types, we better check how it's working in the case close to the real.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IlyaFaer Reverted to the previous convention, PTAL.


# TODO: Implement me.
pass
: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.

class STRING:
"""
This object describes columns in a database that are string-based (e.g. CHAR).
:rtype: :class:`datetime.time`
:returns: The corresponding time value.
"""
return datetime.datetime.fromtimestamp(ticks, tz=tz).timetz()

# 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 = 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 +75,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
106 changes: 27 additions & 79 deletions tests/spanner_dbapi/test_types.py
Expand Up @@ -5,93 +5,41 @@
# 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")
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