Updates to 'date' and 'time' type handling.

datetime.date --> 'date'
datetime.time --> 'time'

datetime.time seemed to be the best native type for time, but it doesn't
have nanosecond resolution. Clients must use int types for that.
This commit is contained in:
Adam Holmberg
2015-01-15 20:19:14 -06:00
parent 8562fd0e68
commit de8ae577a2
5 changed files with 151 additions and 117 deletions

View File

@@ -36,7 +36,7 @@ import io
import re
import socket
import time
from datetime import datetime, timedelta
import datetime
from uuid import UUID
import warnings
@@ -55,8 +55,8 @@ apache_cassandra_type_prefix = 'org.apache.cassandra.db.marshal.'
if six.PY3:
_number_types = frozenset((int, float))
_time_types = frozenset((int))
_date_types = frozenset((int))
_time_types = frozenset((int,))
_date_types = frozenset((int,))
long = int
else:
_number_types = frozenset((int, long, float))
@@ -74,6 +74,15 @@ def unix_time_from_uuid1(u):
return (u.time - 0x01B21DD213814000) / 10000000.0
def datetime_from_timestamp(timestamp):
if timestamp >= 0:
dt = datetime.datetime.utcfromtimestamp(timestamp)
else:
# PYTHON-119: workaround for Windows
dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=timestamp)
return dt
_casstypes = {}
@@ -543,26 +552,26 @@ class DateType(_CassandraType):
typename = 'timestamp'
@classmethod
def validate(cls, date):
if isinstance(date, six.string_types):
date = cls.interpret_datestring(date)
return date
def validate(cls, val):
if isinstance(val, six.string_types):
val = cls.interpret_datestring(val)
return val
@staticmethod
def interpret_datestring(date):
if date[-5] in ('+', '-'):
offset = (int(date[-4:-2]) * 3600 + int(date[-2:]) * 60) * int(date[-5] + '1')
date = date[:-5]
def interpret_datestring(val):
if val[-5] in ('+', '-'):
offset = (int(val[-4:-2]) * 3600 + int(val[-2:]) * 60) * int(val[-5] + '1')
val = val[:-5]
else:
offset = -time.timezone
for tformat in cql_timestamp_formats:
try:
tval = time.strptime(date, tformat)
tval = time.strptime(val, tformat)
except ValueError:
continue
return calendar.timegm(tval) + offset
else:
raise ValueError("can't interpret %r as a date" % (date,))
raise ValueError("can't interpret %r as a date" % (val,))
def my_timestamp(self):
return self.val
@@ -570,12 +579,7 @@ class DateType(_CassandraType):
@staticmethod
def deserialize(byts, protocol_version):
timestamp = int64_unpack(byts) / 1000.0
if timestamp >= 0:
dt = datetime.utcfromtimestamp(timestamp)
else:
# PYTHON-119: workaround for Windows
dt = datetime(1970, 1, 1) + timedelta(seconds=timestamp)
return dt
return datetime_from_timestamp(timestamp)
@staticmethod
def serialize(v, protocol_version):
@@ -635,32 +639,34 @@ class SimpleDateType(_CassandraType):
date_format = "%Y-%m-%d"
@classmethod
def validate(cls, date):
if isinstance(date, basestring):
date = cls.interpret_simpledate_string(date)
return date
def validate(cls, val):
if isinstance(val, six.string_types):
val = cls.interpret_simpledate_string(val)
elif (not isinstance(val, datetime.date)) and (type(val) not in _date_types):
raise TypeError('SimpleDateType arg must be a datetime.date, unsigned integer, or string in the format YYYY-MM-DD')
return val
@staticmethod
def interpret_simpledate_string(v):
try:
tval = time.strptime(v, SimpleDateType.date_format)
# shift upward w/epoch at 2**31
return (calendar.timegm(tval) / SimpleDateType.seconds_per_day) + 2**31
except TypeError:
# Ints are valid dates too
if type(v) not in _date_types:
raise TypeError('Date arguments must be an unsigned integer or string in the format YYYY-MM-DD')
return v
date_time = datetime.datetime.strptime(v, SimpleDateType.date_format)
return datetime.date(date_time.year, date_time.month, date_time.day)
@staticmethod
def serialize(val, protocol_version):
date_val = SimpleDateType.interpret_simpledate_string(val)
return uint32_pack(date_val)
# Values of the 'date'` type are encoded as 32-bit unsigned integers
# representing a number of days with "the epoch" at the center of the
# range (2^31). Epoch is January 1st, 1970
try:
shifted = (calendar.timegm(val.timetuple()) // SimpleDateType.seconds_per_day) + 2 ** 31
except AttributeError:
shifted = val
return uint32_pack(shifted)
@staticmethod
def deserialize(byts, protocol_version):
Result = namedtuple('SimpleDate', 'value')
return Result(value=uint32_unpack(byts))
timestamp = SimpleDateType.seconds_per_day * (uint32_unpack(byts) - 2 ** 31)
dt = datetime.datetime.utcfromtimestamp(timestamp)
return datetime.date(dt.year, dt.month, dt.day)
class TimeType(_CassandraType):
@@ -673,47 +679,47 @@ class TimeType(_CassandraType):
@classmethod
def validate(cls, val):
if isinstance(val, basestring):
time = cls.interpret_timestring(val)
return time
if isinstance(val, six.string_types):
val = cls.interpret_timestring(val)
elif (not isinstance(val, datetime.time)) and (type(val) not in _time_types):
raise TypeError('TimeType arguments must be a string or whole number')
return val
@staticmethod
def interpret_timestring(val):
try:
nano = 0
try:
base_time_str = val
if '.' in base_time_str:
base_time_str = val[0:val.find('.')]
base_time = time.strptime(base_time_str, "%H:%M:%S")
nano = base_time.tm_hour * TimeType.ONE_HOUR
nano += base_time.tm_min * TimeType.ONE_MINUTE
nano += base_time.tm_sec * TimeType.ONE_SECOND
parts = val.split('.')
base_time = time.strptime(parts[0], "%H:%M:%S")
nano = (base_time.tm_hour * TimeType.ONE_HOUR +
base_time.tm_min * TimeType.ONE_MINUTE +
base_time.tm_sec * TimeType.ONE_SECOND)
if '.' in val:
nano_time_str = val[val.find('.')+1:]
if len(parts) > 1:
# right pad to 9 digits
while len(nano_time_str) < 9:
nano_time_str += "0"
nano_time_str = parts[1] + "0" * (9 - len(parts[1]))
nano += int(nano_time_str)
except AttributeError as e:
if type(val) not in _time_types:
raise TypeError('TimeType arguments must be a string or whole number')
# long / int values passed in are acceptable too
nano = val
return nano
except ValueError as e:
except ValueError:
raise ValueError("can't interpret %r as a time" % (val,))
@staticmethod
def serialize(val, protocol_version):
return int64_pack(TimeType.interpret_timestring(val))
# Values of the @time@ type are encoded as 64-bit signed integers
# representing the number of nanoseconds since midnight.
try:
nano = (val.hour * TimeType.ONE_HOUR +
val.minute * TimeType.ONE_MINUTE +
val.second * TimeType.ONE_SECOND +
val.microsecond * TimeType.ONE_MICRO)
except AttributeError:
nano = val
return int64_pack(nano)
@staticmethod
def deserialize(byts, protocol_version):
Result = namedtuple('Time', 'value')
return Result(value=int64_unpack(byts))
return int64_unpack(byts)
class UTF8Type(_CassandraType):

View File

@@ -74,6 +74,7 @@ class Encoder(object):
UUID: self.cql_encode_object,
datetime.datetime: self.cql_encode_datetime,
datetime.date: self.cql_encode_date,
datetime.time: self.cql_encode_time,
dict: self.cql_encode_map_collection,
OrderedDict: self.cql_encode_map_collection,
list: self.cql_encode_list_collection,
@@ -146,9 +147,16 @@ class Encoder(object):
def cql_encode_date(self, val):
"""
Converts a :class:`datetime.date` object to a string with format
``YYYY-MM-DD-0000``.
``YYYY-MM-DD``.
"""
return "'%s'" % val.strftime('%Y-%m-%d-0000')
return "'%s'" % val.strftime('%Y-%m-%d')
def cql_encode_time(self, val):
"""
Converts a :class:`datetime.date` object to a string with format
``HH:MM:SS.mmmuuunnn``.
"""
return "'%s'" % val
def cql_encode_sequence(self, val):
"""

View File

@@ -22,7 +22,7 @@ import logging
log = logging.getLogger(__name__)
from decimal import Decimal
from datetime import datetime
from datetime import datetime, date, time
import six
from uuid import uuid1, uuid4
@@ -31,7 +31,6 @@ from cassandra.cluster import Cluster
from cassandra.cqltypes import Int32Type, EMPTY
from cassandra.query import dict_factory
from cassandra.util import OrderedDict, sortedset
from collections import namedtuple
from tests.integration import get_server_versions, use_singledc, PROTOCOL_VERSION
@@ -171,6 +170,8 @@ class TypeTests(unittest.TestCase):
v1_uuid = uuid1()
v4_uuid = uuid4()
mydatetime = datetime(2013, 12, 31, 23, 59, 59, 999000)
mydate = date(2015, 1, 15)
mytime = time(16, 47, 25, 7)
params = [
"sometext",
@@ -192,13 +193,10 @@ class TypeTests(unittest.TestCase):
v1_uuid, # timeuuid
u"sometext\u1234", # varchar
123456789123456789123456789, # varint
'2014-01-01', # date
'01:02:03.456789012' # time
mydate, # date
mytime
]
SimpleDate = namedtuple('SimpleDate', 'value')
Time = namedtuple('Time', 'value')
expected_vals = (
"sometext",
"sometext",
@@ -219,8 +217,8 @@ class TypeTests(unittest.TestCase):
v1_uuid, # timeuuid
u"sometext\u1234", # varchar
123456789123456789123456789, # varint
SimpleDate(2147499719), # date
Time(3723456789012) # time
mydate, # date
60445000007000 # time
)
s.execute("""

View File

@@ -19,7 +19,7 @@ except ImportError:
import unittest # noqa
import platform
from datetime import datetime
from datetime import datetime, date
from decimal import Decimal
from uuid import UUID
@@ -79,8 +79,9 @@ marshalled_value_pairs = (
(b'\x00\x00', 'ListType(FloatType)', []),
(b'\x00\x00', 'SetType(IntegerType)', sortedset()),
(b'\x00\x01\x00\x10\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0', 'ListType(TimeUUIDType)', [UUID(bytes=b'\xafYC\xa3\xea<\x11\xe1\xabc\xc4,\x03"y\xf0')]),
(b'\x00\x00>\xc7', 'SimpleDateType', '2014-01-01'),
(b'\x00\x00\x00\x00\x00\x00\x00\x01', 'TimeType', '00:00:00.000000001')
(b'\x80\x00\x00\x01', 'SimpleDateType', date(1970,1,2)),
(b'\x7f\xff\xff\xff', 'SimpleDateType', date(1969,12,31)),
(b'\x00\x00\x00\x00\x00\x00\x00\x01', 'TimeType', 1)
)
ordered_dict_value = OrderedDict()

View File

@@ -128,56 +128,77 @@ class TypeTests(unittest.TestCase):
"""
Test cassandra.cqltypes.SimpleDateType() construction
"""
# from string
expected_date = datetime.date(1492, 10, 12)
sd = SimpleDateType('1492-10-12')
self.assertEqual(sd.val, expected_date)
nd = SimpleDateType.interpret_simpledate_string('2014-01-01')
tval = time.strptime('2014-01-01', SimpleDateType.date_format)
manual = calendar.timegm(tval) / SimpleDateType.seconds_per_day
self.assertEqual(nd, manual)
# date
sd = SimpleDateType(expected_date)
self.assertEqual(sd.val, expected_date)
nd = SimpleDateType.interpret_simpledate_string('1970-01-01')
self.assertEqual(nd, 0)
# int
expected_timestamp = calendar.timegm(expected_date.timetuple())
sd = SimpleDateType(expected_timestamp)
self.assertEqual(sd.val, expected_timestamp)
# no contruct
self.assertRaises(ValueError, SimpleDateType, '1999-10-10-bad-time')
self.assertRaises(TypeError, SimpleDateType, 1.234)
def test_time(self):
"""
Test cassandra.cqltypes.TimeType() construction
"""
one_micro = 1000
one_milli = 1000L*one_micro
one_second = 1000L*one_milli
one_minute = 60L*one_second
one_hour = 60L*one_minute
one_milli = 1000 * one_micro
one_second = 1000 * one_milli
one_minute = 60 * one_second
one_hour = 60 * one_minute
nd = TimeType.interpret_timestring('00:00:00.000000001')
self.assertEqual(nd, 1)
nd = TimeType.interpret_timestring('00:00:00.000001')
self.assertEqual(nd, one_micro)
nd = TimeType.interpret_timestring('00:00:00.001')
self.assertEqual(nd, one_milli)
nd = TimeType.interpret_timestring('00:00:01')
self.assertEqual(nd, one_second)
nd = TimeType.interpret_timestring('00:01:00')
self.assertEqual(nd, one_minute)
nd = TimeType.interpret_timestring('01:00:00')
self.assertEqual(nd, one_hour)
# from strings
tt = TimeType('00:00:00.000000001')
self.assertEqual(tt.val, 1)
tt = TimeType('00:00:00.000001')
self.assertEqual(tt.val, one_micro)
tt = TimeType('00:00:00.001')
self.assertEqual(tt.val, one_milli)
tt = TimeType('00:00:01')
self.assertEqual(tt.val, one_second)
tt = TimeType('00:01:00')
self.assertEqual(tt.val, one_minute)
tt = TimeType('01:00:00')
self.assertEqual(tt.val, one_hour)
tt = TimeType('01:00:00.')
self.assertEqual(tt.val, one_hour)
nd = TimeType('23:59:59.1')
nd = TimeType('23:59:59.12')
nd = TimeType('23:59:59.123')
nd = TimeType('23:59:59.1234')
nd = TimeType('23:59:59.12345')
tt = TimeType('23:59:59.1')
tt = TimeType('23:59:59.12')
tt = TimeType('23:59:59.123')
tt = TimeType('23:59:59.1234')
tt = TimeType('23:59:59.12345')
nd = TimeType.interpret_timestring('23:59:59.123456')
self.assertEquals(nd, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro)
tt = TimeType('23:59:59.123456')
self.assertEqual(tt.val, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro)
nd = TimeType.interpret_timestring('23:59:59.1234567')
self.assertEquals(nd, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro + 700)
tt = TimeType('23:59:59.1234567')
self.assertEqual(tt.val, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro + 700)
nd = TimeType.interpret_timestring('23:59:59.12345678')
self.assertEquals(nd, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro + 780)
tt = TimeType('23:59:59.12345678')
self.assertEqual(tt.val, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro + 780)
tt = TimeType('23:59:59.123456789')
self.assertEqual(tt.val, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro + 789)
# from int
tt = TimeType(12345678)
self.assertEqual(tt.val, 12345678)
# no construct
self.assertRaises(ValueError, TimeType, '1999-10-10 11:11:11.1234')
self.assertRaises(TypeError, TimeType, 1.234)
self.assertRaises(TypeError, TimeType, datetime.datetime(2004, 12, 23, 11, 11, 1))
nd = TimeType.interpret_timestring('23:59:59.123456789')
self.assertEquals(nd, 23*one_hour + 59*one_minute + 59*one_second + 123*one_milli + 456*one_micro + 789)
def test_cql_typename(self):
"""