Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
api: pandas way to build datetime from timestamp
This option is required so it would be possible to decode Datetime with
external function without constructing excessive pandas.Timestamp
object.

Follows #204
  • Loading branch information
DifferentialOrange committed Oct 27, 2022
commit b338a1fbb14d4a0cb55d29d6ecaf87b2cadd6cde
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Support iproto feature discovery (#206).

- Support pandas way to build datetime from timestamp (PR #252).

`timestamp_since_utc_epoch` is a parameter to set timestamp
convertion behavior for timezone-aware datetimes.

If ``False`` (default), behaves similar to Tarantool `datetime.new()`:

```python
>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=False)
>>> dt
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
>>> dt.timestamp
1640995200.0
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
... timestamp_since_utc_epoch=False)
>>> dt
datetime: Timestamp('2022-01-01 00:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
>>> dt.timestamp
1640984400.0
```

Thus, if ``False``, datetime is computed from timestamp
since epoch and then timezone is applied without any
convertion. In that case, `dt.timestamp` won't be equal to
initialization `timestamp` for all timezones with non-zero offset.

If ``True``, behaves similar to `pandas.Timestamp`:

```python
>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=True)
>>> dt
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
>>> dt.timestamp
1640995200.0
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
... timestamp_since_utc_epoch=True)
>>> dt
datetime: Timestamp('2022-01-01 03:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
>>> dt.timestamp
1640995200.0
```

Thus, if ``True``, datetime is computed in a way that `dt.timestamp` will
always be equal to initialization `timestamp`.

### Changed
- Bump msgpack requirement to 1.0.4 (PR #223).
The only reason of this bump is various vulnerability fixes,
Expand Down
98 changes: 77 additions & 21 deletions tarantool/msgpack_ext/types/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class Datetime():

def __init__(self, data=None, *, timestamp=None, year=None, month=None,
day=None, hour=None, minute=None, sec=None, nsec=None,
tzoffset=0, tz=''):
tzoffset=0, tz='', timestamp_since_utc_epoch=False):
"""
:param data: MessagePack binary data to decode. If provided,
all other parameters are ignored.
Expand All @@ -294,7 +294,10 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
:paramref:`~tarantool.Datetime.params.minute`,
:paramref:`~tarantool.Datetime.params.sec`.
If :paramref:`~tarantool.Datetime.params.nsec` is provided,
it must be :obj:`int`.
it must be :obj:`int`. Refer to
:paramref:`~tarantool.Datetime.params.timestamp_since_utc_epoch`
to clarify how timezone-aware datetime is computed from
the timestamp.
:type timestamp: :obj:`float` or :obj:`int`, optional

:param year: Datetime year value. Must be a valid
Expand Down Expand Up @@ -344,8 +347,60 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
:param tz: Timezone name from Olson timezone database.
:type tz: :obj:`str`, optional

:param timestamp_since_utc_epoch: Parameter to set timestamp
convertion behavior for timezone-aware datetimes.

If ``False`` (default), behaves similar to Tarantool
`datetime.new()`_:

.. code-block:: python

>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=False)
>>> dt
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
>>> dt.timestamp
1640995200.0
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
... timestamp_since_utc_epoch=False)
>>> dt
datetime: Timestamp('2022-01-01 00:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
>>> dt.timestamp
1640984400.0

Thus, if ``False``, datetime is computed from timestamp
since epoch and then timezone is applied without any
convertion. In that case,
:attr:`~tarantool.Datetime.timestamp` won't be equal to
initialization
:paramref:`~tarantool.Datetime.params.timestamp` for all
timezones with non-zero offset.

If ``True``, behaves similar to :class:`pandas.Timestamp`:

.. code-block:: python

>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=True)
>>> dt
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
>>> dt.timestamp
1640995200.0
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
... timestamp_since_utc_epoch=True)
>>> dt
datetime: Timestamp('2022-01-01 03:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
>>> dt.timestamp
1640995200.0

Thus, if ``True``, datetime is computed in a way that
:attr:`~tarantool.Datetime.timestamp` will always be equal
to initialization
:paramref:`~tarantool.Datetime.params.timestamp`.
:type timestamp_since_utc_epoch: :obj:`bool`, optional

:raise: :exc:`ValueError`, :exc:`~tarantool.error.MsgpackError`,
:class:`pandas.Timestamp` exceptions

.. _datetime.new(): https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/new/
"""

if data is not None:
Expand All @@ -358,6 +413,16 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
self._tz = tz
return

tzinfo = None
if tz != '':
if tz not in tt_timezones.timezoneToIndex:
raise ValueError(f'Unknown Tarantool timezone "{tz}"')

tzinfo = get_python_tzinfo(tz, ValueError)
elif tzoffset != 0:
tzinfo = pytz.FixedOffset(tzoffset)
self._tz = tz

# The logic is same as in Tarantool, refer to datetime API.
# https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/new/
if timestamp is not None:
Expand All @@ -375,6 +440,11 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
datetime = pandas.to_datetime(total_nsec, unit='ns')
else:
datetime = pandas.to_datetime(timestamp, unit='s')

if not timestamp_since_utc_epoch:
self._datetime = datetime.replace(tzinfo=tzinfo)
else:
self._datetime = datetime.replace(tzinfo=pytz.UTC).tz_convert(tzinfo)
else:
if nsec is not None:
microsecond = nsec // NSEC_IN_MKSEC
Expand All @@ -383,25 +453,11 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
microsecond = 0
nanosecond = 0

datetime = pandas.Timestamp(year=year, month=month, day=day,
hour=hour, minute=minute, second=sec,
microsecond=microsecond,
nanosecond=nanosecond)

if tz != '':
if tz not in tt_timezones.timezoneToIndex:
raise ValueError(f'Unknown Tarantool timezone "{tz}"')

tzinfo = get_python_tzinfo(tz, ValueError)
self._datetime = datetime.replace(tzinfo=tzinfo)
self._tz = tz
elif tzoffset != 0:
tzinfo = pytz.FixedOffset(tzoffset)
self._datetime = datetime.replace(tzinfo=tzinfo)
self._tz = ''
else:
self._datetime = datetime
self._tz = ''
self._datetime = pandas.Timestamp(
year=year, month=month, day=day,
hour=hour, minute=minute, second=sec,
microsecond=microsecond,
nanosecond=nanosecond, tzinfo=tzinfo)

def _interval_operation(self, other, sign=1):
"""
Expand Down
6 changes: 6 additions & 0 deletions test/suites/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ def test_Datetime_class_invalid_init(self):
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
r"nsec=308543321, tz='AZODT'})",
},
'timestamp_since_utc_epoch': {
'python': tarantool.Datetime(timestamp=1661958474, nsec=308543321,
tz='Europe/Moscow', timestamp_since_utc_epoch=True),
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\xb3\x03'),
'tarantool': r"datetime.new({timestamp=1661969274, nsec=308543321, tz='Europe/Moscow'})",
},
}

def test_msgpack_decode(self):
Expand Down