Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: Implementing DB-API types according to the PEP-0249 specificati…
…on (#521)

* feat: BASELINE for DB API standard data types

* feat: implementation of PEP-0249 types

* chore: reverting "dummy" changes

* fix: cleanup

* chore: refactor
  • Loading branch information
mf2199 committed Oct 4, 2020
1 parent 3f5db62 commit 62c22b1
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 112 deletions.
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
81 changes: 27 additions & 54 deletions tests/spanner_dbapi/test_types.py
Expand Up @@ -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")
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

0 comments on commit 62c22b1

Please sign in to comment.