Skip to content

Commit

Permalink
msgpack: support tzoffset in datetime
Browse files Browse the repository at this point in the history
Support non-zero tzoffset in datetime extended type. If tzoffset and
tzindex are not specified, return object with timezone-naive
pandas.Timestamp internals. If tzoffset is specified, return object with
timezone-aware pandas.Timestamp with pytz.FixedOffset [1] timezone info.
pytz module is already a dependency of pandas, but this patch adds it as
a requirement just in case something will change in the future.

1. https://pypi.org/project/pytz/

Part of #204
  • Loading branch information
DifferentialOrange committed Sep 8, 2022
1 parent c252fb7 commit 7561363
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
convert to a `pandas.Timestamp` and then use `to_datetime64()`
or `to_datetime()` converter.

- Offset in datetime type support (#204).

### Changed
- Bump msgpack requirement to 1.0.4 (PR #223).
The only reason of this bump is various vulnerability fixes,
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
msgpack>=1.0.4
pandas
pytz
33 changes: 28 additions & 5 deletions tarantool/msgpack_ext/types/datetime.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from copy import deepcopy

import pandas
import pytz

# https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-datetime-type
#
Expand Down Expand Up @@ -41,6 +42,8 @@
BYTEORDER = 'little'

NSEC_IN_SEC = 1000000000
SEC_IN_MIN = 60
MIN_IN_DAY = 60 * 24


def get_bytes_as_int(data, cursor, size):
Expand All @@ -50,6 +53,14 @@ def get_bytes_as_int(data, cursor, size):
def get_int_as_bytes(data, size):
return data.to_bytes(size, byteorder=BYTEORDER, signed=True)

def compute_offset(timestamp):
if timestamp.tz is None:
return 0

utc_offset = timestamp.tz.utcoffset(timestamp)
# There is no precision loss since offset is in minutes
return utc_offset.days * MIN_IN_DAY + utc_offset.seconds // SEC_IN_MIN

def msgpack_decode(data):
cursor = 0
seconds, cursor = get_bytes_as_int(data, cursor, SECONDS_SIZE_BYTES)
Expand All @@ -63,12 +74,17 @@ def msgpack_decode(data):
tzoffset = 0
tzindex = 0

if (tzoffset != 0) or (tzindex != 0):
raise NotImplementedError

total_nsec = seconds * NSEC_IN_SEC + nsec

timestamp = pandas.to_datetime(total_nsec, unit='ns')
if (tzindex != 0):
raise NotImplementedError
elif (tzoffset != 0):
tzinfo = pytz.FixedOffset(tzoffset)
timestamp = pandas.to_datetime(total_nsec, unit='ns').replace(tzinfo=pytz.utc).tz_convert(tzinfo)
else:
# return timezone-naive pandas.Timestamp
timestamp = pandas.to_datetime(total_nsec, unit='ns')

return timestamp, tzoffset, tzindex

class Datetime():
Expand All @@ -79,12 +95,16 @@ def __init__(self, *args, **kwargs):
timestamp, tzoffset, tzindex = msgpack_decode(data)
elif isinstance(data, pandas.Timestamp):
timestamp = deepcopy(data)
tzoffset = compute_offset(timestamp)
elif isinstance(data, Datetime):
timestamp = deepcopy(data._timestamp)
tzoffset = deepcopy(data._tzoffset)
else:
timestamp = pandas.Timestamp(*args, **kwargs)
tzoffset = compute_offset(timestamp)

self._timestamp = timestamp
self._tzoffset = tzoffset

def __eq__(self, other):
if isinstance(other, Datetime):
Expand All @@ -97,12 +117,15 @@ def __eq__(self, other):
def to_pd_timestamp(self):
return deepcopy(self._timestamp)

def tzoffset(self):
return deepcopy(self._tzoffset)

def msgpack_encode(self):
ts_value = self._timestamp.value

seconds = ts_value // NSEC_IN_SEC
nsec = ts_value % NSEC_IN_SEC
tzoffset = 0
tzoffset = self._tzoffset
tzindex = 0

buf = get_int_as_bytes(seconds, SECONDS_SIZE_BYTES)
Expand Down
33 changes: 33 additions & 0 deletions test/suites/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import warnings
import tarantool
import pandas
import pytz

from tarantool.msgpack_ext.packer import default as packer_default
from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook
Expand Down Expand Up @@ -97,6 +98,38 @@ def setUp(self):
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
r"nsec=308543321})",
},
'datetime_with_positive_offset': {
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
microsecond=308543, nanosecond=321,
tzinfo=pytz.FixedOffset(180)),
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
r"nsec=308543321, tzoffset=180})",
},
'datetime_with_negative_offset': {
'python': tarantool.Datetime(year=2022, month=8, day=31, hour=18, minute=7, second=54,
microsecond=308543, nanosecond=321,
tzinfo=pytz.FixedOffset(-60)),
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
r"nsec=308543321, tzoffset=-60})",
},
'pandas_timestamp_with_positive_offset': {
'python': pandas.Timestamp(year=2022, month=8, day=31, hour=18, minute=7, second=54,
microsecond=308543, nanosecond=321,
tzinfo=pytz.FixedOffset(180)),
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\x00\x00'),
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
r"nsec=308543321, tzoffset=180})",
},
'pandas_timestamp_with_negative_offset': {
'python': pandas.Timestamp(year=2022, month=8, day=31, hour=18, minute=7, second=54,
microsecond=308543, nanosecond=321,
tzinfo=pytz.FixedOffset(-60)),
'msgpack': (b'\x8a\xb1\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xc4\xff\x00\x00'),
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
r"nsec=308543321, tzoffset=-60})",
},
}

def test_msgpack_decode(self):
Expand Down

0 comments on commit 7561363

Please sign in to comment.