Skip to content

Commit b338a1f

Browse files
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
1 parent fe6f9b1 commit b338a1f

File tree

3 files changed

+128
-21
lines changed

3 files changed

+128
-21
lines changed

CHANGELOG.md

+45
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
136136

137137
- Support iproto feature discovery (#206).
138138

139+
- Support pandas way to build datetime from timestamp (PR #252).
140+
141+
`timestamp_since_utc_epoch` is a parameter to set timestamp
142+
convertion behavior for timezone-aware datetimes.
143+
144+
If ``False`` (default), behaves similar to Tarantool `datetime.new()`:
145+
146+
```python
147+
>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=False)
148+
>>> dt
149+
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
150+
>>> dt.timestamp
151+
1640995200.0
152+
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
153+
... timestamp_since_utc_epoch=False)
154+
>>> dt
155+
datetime: Timestamp('2022-01-01 00:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
156+
>>> dt.timestamp
157+
1640984400.0
158+
```
159+
160+
Thus, if ``False``, datetime is computed from timestamp
161+
since epoch and then timezone is applied without any
162+
convertion. In that case, `dt.timestamp` won't be equal to
163+
initialization `timestamp` for all timezones with non-zero offset.
164+
165+
If ``True``, behaves similar to `pandas.Timestamp`:
166+
167+
```python
168+
>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=True)
169+
>>> dt
170+
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
171+
>>> dt.timestamp
172+
1640995200.0
173+
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
174+
... timestamp_since_utc_epoch=True)
175+
>>> dt
176+
datetime: Timestamp('2022-01-01 03:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
177+
>>> dt.timestamp
178+
1640995200.0
179+
```
180+
181+
Thus, if ``True``, datetime is computed in a way that `dt.timestamp` will
182+
always be equal to initialization `timestamp`.
183+
139184
### Changed
140185
- Bump msgpack requirement to 1.0.4 (PR #223).
141186
The only reason of this bump is various vulnerability fixes,

tarantool/msgpack_ext/types/datetime.py

+77-21
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ class Datetime():
279279

280280
def __init__(self, data=None, *, timestamp=None, year=None, month=None,
281281
day=None, hour=None, minute=None, sec=None, nsec=None,
282-
tzoffset=0, tz=''):
282+
tzoffset=0, tz='', timestamp_since_utc_epoch=False):
283283
"""
284284
:param data: MessagePack binary data to decode. If provided,
285285
all other parameters are ignored.
@@ -294,7 +294,10 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
294294
:paramref:`~tarantool.Datetime.params.minute`,
295295
:paramref:`~tarantool.Datetime.params.sec`.
296296
If :paramref:`~tarantool.Datetime.params.nsec` is provided,
297-
it must be :obj:`int`.
297+
it must be :obj:`int`. Refer to
298+
:paramref:`~tarantool.Datetime.params.timestamp_since_utc_epoch`
299+
to clarify how timezone-aware datetime is computed from
300+
the timestamp.
298301
:type timestamp: :obj:`float` or :obj:`int`, optional
299302
300303
:param year: Datetime year value. Must be a valid
@@ -344,8 +347,60 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
344347
:param tz: Timezone name from Olson timezone database.
345348
:type tz: :obj:`str`, optional
346349
350+
:param timestamp_since_utc_epoch: Parameter to set timestamp
351+
convertion behavior for timezone-aware datetimes.
352+
353+
If ``False`` (default), behaves similar to Tarantool
354+
`datetime.new()`_:
355+
356+
.. code-block:: python
357+
358+
>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=False)
359+
>>> dt
360+
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
361+
>>> dt.timestamp
362+
1640995200.0
363+
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
364+
... timestamp_since_utc_epoch=False)
365+
>>> dt
366+
datetime: Timestamp('2022-01-01 00:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
367+
>>> dt.timestamp
368+
1640984400.0
369+
370+
Thus, if ``False``, datetime is computed from timestamp
371+
since epoch and then timezone is applied without any
372+
convertion. In that case,
373+
:attr:`~tarantool.Datetime.timestamp` won't be equal to
374+
initialization
375+
:paramref:`~tarantool.Datetime.params.timestamp` for all
376+
timezones with non-zero offset.
377+
378+
If ``True``, behaves similar to :class:`pandas.Timestamp`:
379+
380+
.. code-block:: python
381+
382+
>>> dt = tarantool.Datetime(timestamp=1640995200, timestamp_since_utc_epoch=True)
383+
>>> dt
384+
datetime: Timestamp('2022-01-01 00:00:00'), tz: ""
385+
>>> dt.timestamp
386+
1640995200.0
387+
>>> dt = tarantool.Datetime(timestamp=1640995200, tz='Europe/Moscow',
388+
... timestamp_since_utc_epoch=True)
389+
>>> dt
390+
datetime: Timestamp('2022-01-01 03:00:00+0300', tz='Europe/Moscow'), tz: "Europe/Moscow"
391+
>>> dt.timestamp
392+
1640995200.0
393+
394+
Thus, if ``True``, datetime is computed in a way that
395+
:attr:`~tarantool.Datetime.timestamp` will always be equal
396+
to initialization
397+
:paramref:`~tarantool.Datetime.params.timestamp`.
398+
:type timestamp_since_utc_epoch: :obj:`bool`, optional
399+
347400
:raise: :exc:`ValueError`, :exc:`~tarantool.error.MsgpackError`,
348401
:class:`pandas.Timestamp` exceptions
402+
403+
.. _datetime.new(): https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/new/
349404
"""
350405

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

416+
tzinfo = None
417+
if tz != '':
418+
if tz not in tt_timezones.timezoneToIndex:
419+
raise ValueError(f'Unknown Tarantool timezone "{tz}"')
420+
421+
tzinfo = get_python_tzinfo(tz, ValueError)
422+
elif tzoffset != 0:
423+
tzinfo = pytz.FixedOffset(tzoffset)
424+
self._tz = tz
425+
361426
# The logic is same as in Tarantool, refer to datetime API.
362427
# https://www.tarantool.io/en/doc/latest/reference/reference_lua/datetime/new/
363428
if timestamp is not None:
@@ -375,6 +440,11 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
375440
datetime = pandas.to_datetime(total_nsec, unit='ns')
376441
else:
377442
datetime = pandas.to_datetime(timestamp, unit='s')
443+
444+
if not timestamp_since_utc_epoch:
445+
self._datetime = datetime.replace(tzinfo=tzinfo)
446+
else:
447+
self._datetime = datetime.replace(tzinfo=pytz.UTC).tz_convert(tzinfo)
378448
else:
379449
if nsec is not None:
380450
microsecond = nsec // NSEC_IN_MKSEC
@@ -383,25 +453,11 @@ def __init__(self, data=None, *, timestamp=None, year=None, month=None,
383453
microsecond = 0
384454
nanosecond = 0
385455

386-
datetime = pandas.Timestamp(year=year, month=month, day=day,
387-
hour=hour, minute=minute, second=sec,
388-
microsecond=microsecond,
389-
nanosecond=nanosecond)
390-
391-
if tz != '':
392-
if tz not in tt_timezones.timezoneToIndex:
393-
raise ValueError(f'Unknown Tarantool timezone "{tz}"')
394-
395-
tzinfo = get_python_tzinfo(tz, ValueError)
396-
self._datetime = datetime.replace(tzinfo=tzinfo)
397-
self._tz = tz
398-
elif tzoffset != 0:
399-
tzinfo = pytz.FixedOffset(tzoffset)
400-
self._datetime = datetime.replace(tzinfo=tzinfo)
401-
self._tz = ''
402-
else:
403-
self._datetime = datetime
404-
self._tz = ''
456+
self._datetime = pandas.Timestamp(
457+
year=year, month=month, day=day,
458+
hour=hour, minute=minute, second=sec,
459+
microsecond=microsecond,
460+
nanosecond=nanosecond, tzinfo=tzinfo)
405461

406462
def _interval_operation(self, other, sign=1):
407463
"""

test/suites/test_datetime.py

+6
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,12 @@ def test_Datetime_class_invalid_init(self):
270270
'tarantool': r"datetime.new({year=2022, month=8, day=31, hour=18, min=7, sec=54, " +
271271
r"nsec=308543321, tz='AZODT'})",
272272
},
273+
'timestamp_since_utc_epoch': {
274+
'python': tarantool.Datetime(timestamp=1661958474, nsec=308543321,
275+
tz='Europe/Moscow', timestamp_since_utc_epoch=True),
276+
'msgpack': (b'\x4a\x79\x0f\x63\x00\x00\x00\x00\x59\xff\x63\x12\xb4\x00\xb3\x03'),
277+
'tarantool': r"datetime.new({timestamp=1661969274, nsec=308543321, tz='Europe/Moscow'})",
278+
},
273279
}
274280

275281
def test_msgpack_decode(self):

0 commit comments

Comments
 (0)