Skip to content

Commit de8ae57

Browse files
committed
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.
1 parent 8562fd0 commit de8ae57

5 files changed

Lines changed: 159 additions & 125 deletions

File tree

cassandra/cqltypes.py

Lines changed: 75 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import re
3737
import socket
3838
import time
39-
from datetime import datetime, timedelta
39+
import datetime
4040
from uuid import UUID
4141
import warnings
4242

@@ -55,8 +55,8 @@
5555

5656
if six.PY3:
5757
_number_types = frozenset((int, float))
58-
_time_types = frozenset((int))
59-
_date_types = frozenset((int))
58+
_time_types = frozenset((int,))
59+
_date_types = frozenset((int,))
6060
long = int
6161
else:
6262
_number_types = frozenset((int, long, float))
@@ -74,6 +74,15 @@ def unix_time_from_uuid1(u):
7474
return (u.time - 0x01B21DD213814000) / 10000000.0
7575

7676

77+
def datetime_from_timestamp(timestamp):
78+
if timestamp >= 0:
79+
dt = datetime.datetime.utcfromtimestamp(timestamp)
80+
else:
81+
# PYTHON-119: workaround for Windows
82+
dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=timestamp)
83+
return dt
84+
85+
7786
_casstypes = {}
7887

7988

@@ -543,39 +552,34 @@ class DateType(_CassandraType):
543552
typename = 'timestamp'
544553

545554
@classmethod
546-
def validate(cls, date):
547-
if isinstance(date, six.string_types):
548-
date = cls.interpret_datestring(date)
549-
return date
555+
def validate(cls, val):
556+
if isinstance(val, six.string_types):
557+
val = cls.interpret_datestring(val)
558+
return val
550559

551560
@staticmethod
552-
def interpret_datestring(date):
553-
if date[-5] in ('+', '-'):
554-
offset = (int(date[-4:-2]) * 3600 + int(date[-2:]) * 60) * int(date[-5] + '1')
555-
date = date[:-5]
561+
def interpret_datestring(val):
562+
if val[-5] in ('+', '-'):
563+
offset = (int(val[-4:-2]) * 3600 + int(val[-2:]) * 60) * int(val[-5] + '1')
564+
val = val[:-5]
556565
else:
557566
offset = -time.timezone
558567
for tformat in cql_timestamp_formats:
559568
try:
560-
tval = time.strptime(date, tformat)
569+
tval = time.strptime(val, tformat)
561570
except ValueError:
562571
continue
563572
return calendar.timegm(tval) + offset
564573
else:
565-
raise ValueError("can't interpret %r as a date" % (date,))
574+
raise ValueError("can't interpret %r as a date" % (val,))
566575

567576
def my_timestamp(self):
568577
return self.val
569578

570579
@staticmethod
571580
def deserialize(byts, protocol_version):
572581
timestamp = int64_unpack(byts) / 1000.0
573-
if timestamp >= 0:
574-
dt = datetime.utcfromtimestamp(timestamp)
575-
else:
576-
# PYTHON-119: workaround for Windows
577-
dt = datetime(1970, 1, 1) + timedelta(seconds=timestamp)
578-
return dt
582+
return datetime_from_timestamp(timestamp)
579583

580584
@staticmethod
581585
def serialize(v, protocol_version):
@@ -635,85 +639,87 @@ class SimpleDateType(_CassandraType):
635639
date_format = "%Y-%m-%d"
636640

637641
@classmethod
638-
def validate(cls, date):
639-
if isinstance(date, basestring):
640-
date = cls.interpret_simpledate_string(date)
641-
return date
642+
def validate(cls, val):
643+
if isinstance(val, six.string_types):
644+
val = cls.interpret_simpledate_string(val)
645+
elif (not isinstance(val, datetime.date)) and (type(val) not in _date_types):
646+
raise TypeError('SimpleDateType arg must be a datetime.date, unsigned integer, or string in the format YYYY-MM-DD')
647+
return val
642648

643649
@staticmethod
644650
def interpret_simpledate_string(v):
645-
try:
646-
tval = time.strptime(v, SimpleDateType.date_format)
647-
# shift upward w/epoch at 2**31
648-
return (calendar.timegm(tval) / SimpleDateType.seconds_per_day) + 2**31
649-
except TypeError:
650-
# Ints are valid dates too
651-
if type(v) not in _date_types:
652-
raise TypeError('Date arguments must be an unsigned integer or string in the format YYYY-MM-DD')
653-
return v
651+
date_time = datetime.datetime.strptime(v, SimpleDateType.date_format)
652+
return datetime.date(date_time.year, date_time.month, date_time.day)
654653

655654
@staticmethod
656655
def serialize(val, protocol_version):
657-
date_val = SimpleDateType.interpret_simpledate_string(val)
658-
return uint32_pack(date_val)
656+
# Values of the 'date'` type are encoded as 32-bit unsigned integers
657+
# representing a number of days with "the epoch" at the center of the
658+
# range (2^31). Epoch is January 1st, 1970
659+
try:
660+
shifted = (calendar.timegm(val.timetuple()) // SimpleDateType.seconds_per_day) + 2 ** 31
661+
except AttributeError:
662+
shifted = val
663+
return uint32_pack(shifted)
659664

660665
@staticmethod
661666
def deserialize(byts, protocol_version):
662-
Result = namedtuple('SimpleDate', 'value')
663-
return Result(value=uint32_unpack(byts))
667+
timestamp = SimpleDateType.seconds_per_day * (uint32_unpack(byts) - 2 ** 31)
668+
dt = datetime.datetime.utcfromtimestamp(timestamp)
669+
return datetime.date(dt.year, dt.month, dt.day)
664670

665671

666672
class TimeType(_CassandraType):
667673
typename = 'time'
668-
ONE_MICRO=1000
669-
ONE_MILLI=1000*ONE_MICRO
670-
ONE_SECOND=1000*ONE_MILLI
671-
ONE_MINUTE=60*ONE_SECOND
672-
ONE_HOUR=60*ONE_MINUTE
674+
ONE_MICRO = 1000
675+
ONE_MILLI = 1000 * ONE_MICRO
676+
ONE_SECOND = 1000 * ONE_MILLI
677+
ONE_MINUTE = 60 * ONE_SECOND
678+
ONE_HOUR = 60 * ONE_MINUTE
673679

674680
@classmethod
675681
def validate(cls, val):
676-
if isinstance(val, basestring):
677-
time = cls.interpret_timestring(val)
678-
return time
682+
if isinstance(val, six.string_types):
683+
val = cls.interpret_timestring(val)
684+
elif (not isinstance(val, datetime.time)) and (type(val) not in _time_types):
685+
raise TypeError('TimeType arguments must be a string or whole number')
686+
return val
679687

680688
@staticmethod
681689
def interpret_timestring(val):
682690
try:
683691
nano = 0
684-
try:
685-
base_time_str = val
686-
if '.' in base_time_str:
687-
base_time_str = val[0:val.find('.')]
688-
base_time = time.strptime(base_time_str, "%H:%M:%S")
689-
nano = base_time.tm_hour * TimeType.ONE_HOUR
690-
nano += base_time.tm_min * TimeType.ONE_MINUTE
691-
nano += base_time.tm_sec * TimeType.ONE_SECOND
692-
693-
if '.' in val:
694-
nano_time_str = val[val.find('.')+1:]
695-
# right pad to 9 digits
696-
while len(nano_time_str) < 9:
697-
nano_time_str += "0"
698-
nano += int(nano_time_str)
699-
700-
except AttributeError as e:
701-
if type(val) not in _time_types:
702-
raise TypeError('TimeType arguments must be a string or whole number')
703-
# long / int values passed in are acceptable too
704-
nano = val
692+
parts = val.split('.')
693+
base_time = time.strptime(parts[0], "%H:%M:%S")
694+
nano = (base_time.tm_hour * TimeType.ONE_HOUR +
695+
base_time.tm_min * TimeType.ONE_MINUTE +
696+
base_time.tm_sec * TimeType.ONE_SECOND)
697+
698+
if len(parts) > 1:
699+
# right pad to 9 digits
700+
nano_time_str = parts[1] + "0" * (9 - len(parts[1]))
701+
nano += int(nano_time_str)
702+
705703
return nano
706-
except ValueError as e:
704+
except ValueError:
707705
raise ValueError("can't interpret %r as a time" % (val,))
708706

709707
@staticmethod
710708
def serialize(val, protocol_version):
711-
return int64_pack(TimeType.interpret_timestring(val))
709+
# Values of the @time@ type are encoded as 64-bit signed integers
710+
# representing the number of nanoseconds since midnight.
711+
try:
712+
nano = (val.hour * TimeType.ONE_HOUR +
713+
val.minute * TimeType.ONE_MINUTE +
714+
val.second * TimeType.ONE_SECOND +
715+
val.microsecond * TimeType.ONE_MICRO)
716+
except AttributeError:
717+
nano = val
718+
return int64_pack(nano)
712719

713720
@staticmethod
714721
def deserialize(byts, protocol_version):
715-
Result = namedtuple('Time', 'value')
716-
return Result(value=int64_unpack(byts))
722+
return int64_unpack(byts)
717723

718724

719725
class UTF8Type(_CassandraType):

cassandra/encoder.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def __init__(self):
7474
UUID: self.cql_encode_object,
7575
datetime.datetime: self.cql_encode_datetime,
7676
datetime.date: self.cql_encode_date,
77+
datetime.time: self.cql_encode_time,
7778
dict: self.cql_encode_map_collection,
7879
OrderedDict: self.cql_encode_map_collection,
7980
list: self.cql_encode_list_collection,
@@ -146,9 +147,16 @@ def cql_encode_datetime(self, val):
146147
def cql_encode_date(self, val):
147148
"""
148149
Converts a :class:`datetime.date` object to a string with format
149-
``YYYY-MM-DD-0000``.
150+
``YYYY-MM-DD``.
150151
"""
151-
return "'%s'" % val.strftime('%Y-%m-%d-0000')
152+
return "'%s'" % val.strftime('%Y-%m-%d')
153+
154+
def cql_encode_time(self, val):
155+
"""
156+
Converts a :class:`datetime.date` object to a string with format
157+
``HH:MM:SS.mmmuuunnn``.
158+
"""
159+
return "'%s'" % val
152160

153161
def cql_encode_sequence(self, val):
154162
"""

tests/integration/standard/test_types.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
log = logging.getLogger(__name__)
2323

2424
from decimal import Decimal
25-
from datetime import datetime
25+
from datetime import datetime, date, time
2626
import six
2727
from uuid import uuid1, uuid4
2828

@@ -31,7 +31,6 @@
3131
from cassandra.cqltypes import Int32Type, EMPTY
3232
from cassandra.query import dict_factory
3333
from cassandra.util import OrderedDict, sortedset
34-
from collections import namedtuple
3534

3635
from tests.integration import get_server_versions, use_singledc, PROTOCOL_VERSION
3736

@@ -171,6 +170,8 @@ def test_basic_types(self):
171170
v1_uuid = uuid1()
172171
v4_uuid = uuid4()
173172
mydatetime = datetime(2013, 12, 31, 23, 59, 59, 999000)
173+
mydate = date(2015, 1, 15)
174+
mytime = time(16, 47, 25, 7)
174175

175176
params = [
176177
"sometext",
@@ -192,13 +193,10 @@ def test_basic_types(self):
192193
v1_uuid, # timeuuid
193194
u"sometext\u1234", # varchar
194195
123456789123456789123456789, # varint
195-
'2014-01-01', # date
196-
'01:02:03.456789012' # time
196+
mydate, # date
197+
mytime
197198
]
198199

199-
SimpleDate = namedtuple('SimpleDate', 'value')
200-
Time = namedtuple('Time', 'value')
201-
202200
expected_vals = (
203201
"sometext",
204202
"sometext",
@@ -219,8 +217,8 @@ def test_basic_types(self):
219217
v1_uuid, # timeuuid
220218
u"sometext\u1234", # varchar
221219
123456789123456789123456789, # varint
222-
SimpleDate(2147499719), # date
223-
Time(3723456789012) # time
220+
mydate, # date
221+
60445000007000 # time
224222
)
225223

226224
s.execute("""

tests/unit/test_marshalling.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import unittest # noqa
2020

2121
import platform
22-
from datetime import datetime
22+
from datetime import datetime, date
2323
from decimal import Decimal
2424
from uuid import UUID
2525

@@ -79,8 +79,9 @@
7979
(b'\x00\x00', 'ListType(FloatType)', []),
8080
(b'\x00\x00', 'SetType(IntegerType)', sortedset()),
8181
(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')]),
82-
(b'\x00\x00>\xc7', 'SimpleDateType', '2014-01-01'),
83-
(b'\x00\x00\x00\x00\x00\x00\x00\x01', 'TimeType', '00:00:00.000000001')
82+
(b'\x80\x00\x00\x01', 'SimpleDateType', date(1970,1,2)),
83+
(b'\x7f\xff\xff\xff', 'SimpleDateType', date(1969,12,31)),
84+
(b'\x00\x00\x00\x00\x00\x00\x00\x01', 'TimeType', 1)
8485
)
8586

8687
ordered_dict_value = OrderedDict()

0 commit comments

Comments
 (0)