8000 Introduce Date type to accomodate ranges outside datetime.[MIN|MAX}YEAR · scylladb/python-driver@4f3c77c · GitHub
[go: up one dir, main page]

Skip to content

Commit 4f3c77c

Browse files
committed
Introduce Date type to accomodate ranges outside datetime.[MIN|MAX}YEAR
1 parent 33f3077 commit 4f3c77c

File tree

4 files changed

+85
-24
lines changed

4 files changed

+85
-24
lines changed

cassandra/cqltypes.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from binascii import unhexlify
3232
import calendar
3333
from collections import namedtuple
34-
import datetime
3534
from decimal import Decimal
3635
import io
3736
import logging
@@ -626,38 +625,29 @@ def serialize(timeuuid, protocol_version):
626625

627626
class SimpleDateType(_CassandraType):
628627
typename = 'date'
629-
seconds_per_day = 60 * 60 * 24
630628
date_format = "%Y-%m-%d"
631629

632630
@classmethod
633631
def validate(cls, val):
634-
if isinstance(val, six.string_types):
635-
val = cls.interpret_simpledate_string(val)
636-
elif (not isinstance(val, datetime.date)) and not isinstance(val, six.integer_types):
637-
raise TypeError('SimpleDateType arg must be a datetime.date, unsigned integer, or string in the format YYYY-MM-DD')
632+
if not isinstance(val, util.Date):
633+
val = util.Date(val)
638634
return val
639635

640-
@staticmethod
641-
def interpret_simpledate_string(v):
642-
date_time = datetime.datetime.strptime(v, SimpleDateType.date_format)
643-
return datetime.date(date_time.year, date_time.month, date_time.day)
644-
645636
@staticmethod
646637
def serialize(val, protocol_version):
647638
# Values of the 'date'` type are encoded as 32-bit unsigned integers
648-
# representing a number of days with "the epoch" at the center of the
649-
# range (2^31). Epoch is January 1st, 1970
639+
# representing a number of days with epoch (January 1st, 1970) at the center of the
640+
# range (2^31).
650641
try:
651-
shifted = (calendar.timegm(val.timetuple()) //< 8000 /span> SimpleDateType.seconds_per_day) + 2 ** 31
642+
days = val.days_from_epoch
652643
except AttributeError:
653-
shifted = val
654-
return uint32_pack(shifted)
644+
days = util.Date(val).days_from_epoch
645+
return uint32_pack(days + 2 ** 31)
655646

656647
@staticmethod
657648
def deserialize(byts, protocol_version):
658-
timestamp = SimpleDateType.seconds_per_day * (uint32_unpack(byts) - 2 ** 31)
659-
dt = util.datetime_from_timestamp(timestamp)
660-
return datetime.date(dt.year, dt.month, dt.day)
649+
days = uint32_unpack(byts) - 2 ** 31
650+
return util.Date(days)
661651

662652

663653
class TimeType(_CassandraType):

cassandra/util.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -892,3 +892,74 @@ def __repr__(self):
892892
def __str__(self):
893893
return "%02d:%02d:%02d.%09d" % (self.hour, self.minute,
894894
self.second, self.nanosecond)
895+
896+
897+
class Date(object):
898+
'''
899+
Idealized naive date: year, month, day
900+
901+
Offers wider year range than datetime.date. Dates that cannot be represented
902+
as a date (because datetime.MINYEAR, datetime.MAXYEAR), this type falls back
903+
to printing days_from_epoch offset.
904+
'''
905+
906+
MINUTE = 60
907+
HOUR = 60 * MINUTE
908+
DAY = 24 * HOUR
909+
910+
date_format = "%Y-%m-%d"
911+
912+
days_from_epoch = 0
913+
914+
def __init__(self, value):
915+
if isinstance(value, six.integer_types):
916+
self.days_from_epoch = value
917+
elif isinstance(value, (datetime.date, datetime.datetime)):
918+
self._from_timetuple(value.timetuple())
919+
elif isinstance(value, six.string_types):
920+
self._from_datestring(value)
921+
else:
922+
raise TypeError('Date arguments must be a whole number, datetime.date, or string')
923+
924+
@property
925+
def seconds(self):
926+
return self.days_from_epoch * Date.DAY
927+
928+
def date(self):
929+
try:
930+
dt = datetime_from_timestamp(self.seconds)
931+
return datetime.date(dt.year, dt.month, 9E88 dt.day)
932+
except Exception:
933+
raise ValueError("%r exceeds ranges for built-in datetime.date" % self)
934+
935+
def _from_timetuple(self, t):
936+
self.days_from_epoch = calendar.timegm(t) // Date.DAY
937+
938+
def _from_datestring(self, s):
939+
if s[0] == '+':
940+
s = s[1:]
941+
dt = datetime.datetime.strptime(s, self.date_format)
942+
self._from_timetuple(dt.timetuple())
943+
944+
def __eq__(self, other):
945+
if isinstance(other, Date):
946+
return self.days_from_epoch == other.days_from_epoch
947+
948+
if isinstance(other, six.integer_types):
949+
return self.days_from_epoch == other
950+
951+
try:
952+
return self.date == other
953+
except Exception:
954+
return False
955+
956+
def __repr__(self):
957+
return "Date(%s)" % self.days_from_epoch
958+
959+
def __str__(self):
960+
try:
961+
dt = datetime_from_timestamp(self.seconds)
962+
return "%04d-%02d-%02d" % (dt.year, dt.month, dt.day)
963+
except:
964+
# If we overflow datetime.[MIN|M
965+
return str(self.days_from_epoch)

tests/unit/test_marshalling.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from uuid import UUID
2525

2626
from cassandra.cqltypes import lookup_casstype, DecimalType, UTF8Type
27-
from cassandra.util import OrderedMap, OrderedMapSerializedKey, sortedset, Time
27+
from cassandra.util import OrderedMap, OrderedMapSerializedKey, sortedset, Time, Date
2828

2929
marshalled_value_pairs = (
3030
# binary form, type, python native type
@@ -79,8 +79,8 @@
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'\x80\x00\x00\x01', 'SimpleDateType', date(1970,1,2)),
83-
(b'\x7f\xff\xff\xff', 'SimpleDateType', date(1969,12,31)),
82+
(b'\x80\x00\x00\x01', 'SimpleDateType', Date(1)),
83+
(b'\x7f\xff\xff\xff', 'SimpleDateType', Date('1969-12-31')),
8484
(b'\x00\x00\x00\x00\x00\x00\x00\x01', 'TimeType', Time(1))
8585
)
8686

tests/unit/test_types.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def test_simpledate(self):
130130
Test cassandra.cqltypes.SimpleDateType() construction
131131
"""
132132
# from string
133-
expected_date = datetime.date(1492, 10< 8245 /span>, 12)
133+
expected_date = cassandra.util.Date(datetime.date(1492, 10, 12))
134134
sd = SimpleDateType('1492-10-12')
135135
self.assertEqual(sd.val, expected_date)
136136

@@ -139,7 +139,7 @@ def test_simpledate(self):
139139
self.assertEqual(sd.val, expected_date)
140140

141141
# int
142-
expected_timestamp = calendar.timegm(expected_date.timetuple())
142+
expected_timestamp = calendar.timegm(expected_date.date().timetuple())
143143
sd = SimpleDateType(expected_timestamp)
144144
self.assertEqual(sd.val, expected_timestamp)
145145

0 commit comments

Comments
 (0)
0