1
1
from copy import deepcopy
2
2
3
3
import datetime
4
+ import pytz
4
5
import pandas
5
6
7
+ import tarantool .msgpack_ext .types .timezones as tt_timezones
8
+ from tarantool .error import MsgpackError , MsgpackWarning , warn
9
+
6
10
# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
7
11
#
8
12
# The datetime MessagePack representation looks like this:
45
49
SEC_IN_MIN = 60
46
50
MIN_IN_DAY = 60 * 24
47
51
52
+ DATETIME_TIMEZONE_NAME_POS = 1
48
53
49
54
def get_bytes_as_int (data , cursor , size ):
50
55
part = data [cursor :cursor + size ]
@@ -61,6 +66,80 @@ def compute_offset(timestamp):
61
66
# There is no precision loss since offset is in minutes
62
67
return utc_offset .days * MIN_IN_DAY + utc_offset .seconds // SEC_IN_MIN
63
68
69
+ def get_python_tzname (timestamp ):
70
+ tzinfo = timestamp .tzinfo
71
+
72
+ if tzinfo is None :
73
+ return None
74
+
75
+ if isinstance (tzinfo , pytz .tzinfo .BaseTzInfo ):
76
+ return tzinfo .zone
77
+
78
+ if isinstance (tzinfo , pytz ._FixedOffset ):
79
+ # pytz.FixedOffset(0) is actually pytz.utc timezone and
80
+ # even not a pytz._FixedOffset.
81
+ return None
82
+
83
+ if isinstance (tzinfo , datetime .timezone ):
84
+ # The only way to legally obtain datetime.timezone name is timezone.tzname(dt).
85
+ #
86
+ # But if name is not provided in the constructor, the name returned
87
+ # by tzname(dt) is generated from the value of the offset.
88
+ # We want to get `None` if name was not provided in the constructor.
89
+ # The only way to work with such behavior is to mess with init attributes.
90
+ #
91
+ # https://github.com/python/cpython/blob/1756ffd66a38755cd45de51316d66266ae30e132/Lib/datetime.py#L2323-L2327
92
+ initargs = tzinfo .__getinitargs__ ()
93
+ if len (initargs ) > DATETIME_TIMEZONE_NAME_POS :
94
+ return initargs [DATETIME_TIMEZONE_NAME_POS ]
95
+
96
+ return None
97
+
98
+ if isinstance (tzinfo , datetime .tzinfo ):
99
+ # If custom class, tzinfo is expected to have tzname(dt) method
100
+ # https://github.com/python/cpython/blob/1756ffd66a38755cd45de51316d66266ae30e132/Lib/datetime.py#L1591
101
+ return tzinfo .tzname (timestamp )
102
+
103
+ raise ValueError (f'Unsupported timezone type { type (tzinfo )} ' )
104
+
105
+
106
+ def is_abbrev_tz (tzname ):
107
+ return tzname in tt_timezones .timezoneAbbrevInfo
108
+
109
+ def assert_nonambiguous_tz (tzname , tt_tzinfo , error_class ):
110
+ if (tt_tzinfo ['category' ] & tt_timezones .TZ_AMBIGUOUS ) != 0 :
111
+ raise error_class (f'Failed to create datetime with ambiguous timezone "{ tzname } "' )
112
+
113
+ def get_python_tzinfo (tzindex ):
114
+ if tzindex not in tt_timezones .indexToTimezone :
115
+ raise MsgpackError (f'Failed to create datetime with unknown tzindex { tzindex } ' )
116
+
117
+ tzname = tt_timezones .indexToTimezone [tzindex ]
118
+
119
+ try :
120
+ tzinfo = pytz .timezone (tzname )
121
+ except pytz .exceptions .UnknownTimeZoneError :
122
+ tt_tzinfo = tt_timezones .timezoneAbbrevInfo [tzname ]
123
+ assert_nonambiguous_tz (tzname , tt_tzinfo , MsgpackError )
124
+
125
+ tzinfo = datetime .timezone (datetime .timedelta (minutes = tt_tzinfo ['offset' ]),
126
+ name = tzname )
127
+
128
+ return tzinfo
129
+
130
+ def validate_python_timezone (timestamp ):
131
+ tzname = get_python_tzname (timestamp )
132
+
133
+ if (tzname is not None ):
134
+ if tzname not in tt_timezones .timezoneToIndex :
135
+ raise ValueError (f'Failed to create datetime with unknown timezone "{ tzname } "' )
136
+
137
+ if not is_abbrev_tz (tzname ):
138
+ return
139
+
140
+ tt_tzinfo = tt_timezones .timezoneAbbrevInfo [tzname ]
141
+ assert_nonambiguous_tz (tzname , tt_tzinfo , ValueError )
142
+
64
143
def msgpack_decode (data ):
65
144
cursor = 0
66
145
seconds , cursor = get_bytes_as_int (data , cursor , SECONDS_SIZE_BYTES )
@@ -79,7 +158,10 @@ def msgpack_decode(data):
79
158
total_nsec = seconds * NSEC_IN_SEC + nsec
80
159
81
160
if (tzindex != 0 ):
82
- raise NotImplementedError
161
+ tzinfo = get_python_tzinfo (tzindex )
162
+ timestamp = pandas .to_datetime (total_nsec , unit = 'ns' )\
163
+ .replace (tzinfo = datetime .timezone .utc )\
164
+ .tz_convert (tzinfo )
83
165
elif (tzoffset != 0 ):
84
166
tzinfo = datetime .timezone (datetime .timedelta (minutes = tzoffset ))
85
167
return pandas .to_datetime (total_nsec , unit = 'ns' )\
@@ -92,14 +174,15 @@ def msgpack_decode(data):
92
174
return timestamp
93
175
94
176
class Datetime ():
95
- def __init__ (self , * args , ** kwargs ):
177
+ def __init__ (self , * args , tarantool_tzindex = None , ** kwargs ):
96
178
if len (args ) > 0 :
97
179
data = args [0 ]
98
180
if isinstance (data , bytes ):
99
181
self ._timestamp = msgpack_decode (data )
100
182
return
101
183
102
184
if isinstance (data , pandas .Timestamp ):
185
+ validate_python_timezone (data )
103
186
self ._timestamp = deepcopy (data )
104
187
return
105
188
@@ -108,6 +191,7 @@ def __init__(self, *args, **kwargs):
108
191
return
109
192
else :
110
193
self ._timestamp = pandas .Timestamp (* args , ** kwargs )
194
+ validate_python_timezone (self ._timestamp )
111
195
return
112
196
113
197
def __eq__ (self , other ):
@@ -127,13 +211,21 @@ def __repr__(self):
127
211
def to_pd_timestamp (self ):
128
212
return deepcopy (self ._timestamp )
129
213
214
+ def tzindex (self ):
215
+ return deepcopy (self ._tzindex )
216
+
130
217
def msgpack_encode (self ):
131
218
ts_value = self ._timestamp .value
132
219
133
220
seconds = ts_value // NSEC_IN_SEC
134
221
nsec = ts_value % NSEC_IN_SEC
222
+
135
223
tzoffset = compute_offset (self ._timestamp )
136
- tzindex = 0
224
+ tzname = get_python_tzname (self ._timestamp )
225
+ if tzname is not None :
226
+ tzindex = tt_timezones .timezoneToIndex [tzname ]
227
+ else :
228
+ tzindex = 0
137
229
138
230
buf = get_int_as_bytes (seconds , SECONDS_SIZE_BYTES )
139
231
0 commit comments