Skip to content

Commit 4ed8aba

Browse files
Fix timezone handling for datetime to unixtime conversions (#2213)
* Fix timezone handling for datetime to unixtime conversions datetime objects are supported to set expire, these can have timezones. mktime was used to convert these to unixtime. mktime in Python however is not timezone aware, it expects the input to be UTC and redis-py did not convert the datetime timestamps to UTC before calling mktime. This can lead to: 1) Setting incorrect expire times because the input datetime object has a timezone but is passed to mktime without converting to UTC first. 2) When the datetime timestamp is within DST, mktime fails with "OverflowError: mktime argument out of range" because UTC doesn't have DST. This depends on libc versions. * linters Co-authored-by: dvora-h <[email protected]>
1 parent fd9fea6 commit 4ed8aba

File tree

4 files changed

+13
-19
lines changed

4 files changed

+13
-19
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
* Fix timezone handling for datetime to unixtime conversions
12
* Fix start_id type for XAUTOCLAIM
23
* Remove verbose logging from cluster.py
34
* Add retry mechanism to async version of Connection

redis/commands/core.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import datetime
44
import hashlib
5-
import time
65
import warnings
76
from typing import (
87
TYPE_CHECKING,
@@ -1674,7 +1673,7 @@ def expireat(
16741673
For more information see https://redis.io/commands/expireat
16751674
"""
16761675
if isinstance(when, datetime.datetime):
1677-
when = int(time.mktime(when.timetuple()))
1676+
when = int(when.timestamp())
16781677

16791678
exp_option = list()
16801679
if nx:
@@ -1769,14 +1768,12 @@ def getex(
17691768
if exat is not None:
17701769
pieces.append("EXAT")
17711770
if isinstance(exat, datetime.datetime):
1772-
s = int(exat.microsecond / 1000000)
1773-
exat = int(time.mktime(exat.timetuple())) + s
1771+
exat = int(exat.timestamp())
17741772
pieces.append(exat)
17751773
if pxat is not None:
17761774
pieces.append("PXAT")
17771775
if isinstance(pxat, datetime.datetime):
1778-
ms = int(pxat.microsecond / 1000)
1779-
pxat = int(time.mktime(pxat.timetuple())) * 1000 + ms
1776+
pxat = int(pxat.timestamp() * 1000)
17801777
pieces.append(pxat)
17811778
if persist:
17821779
pieces.append("PERSIST")
@@ -1995,8 +1992,7 @@ def pexpireat(
19951992
For more information see https://redis.io/commands/pexpireat
19961993
"""
19971994
if isinstance(when, datetime.datetime):
1998-
ms = int(when.microsecond / 1000)
1999-
when = int(time.mktime(when.timetuple())) * 1000 + ms
1995+
when = int(when.timestamp() * 1000)
20001996
exp_option = list()
20011997
if nx:
20021998
exp_option.append("NX")
@@ -2197,14 +2193,12 @@ def set(
21972193
if exat is not None:
21982194
pieces.append("EXAT")
21992195
if isinstance(exat, datetime.datetime):
2200-
s = int(exat.microsecond / 1000000)
2201-
exat = int(time.mktime(exat.timetuple())) + s
2196+
exat = int(exat.timestamp())
22022197
pieces.append(exat)
22032198
if pxat is not None:
22042199
pieces.append("PXAT")
22052200
if isinstance(pxat, datetime.datetime):
2206-
ms = int(pxat.microsecond / 1000)
2207-
pxat = int(time.mktime(pxat.timetuple())) * 1000 + ms
2201+
pxat = int(pxat.timestamp() * 1000)
22082202
pieces.append(pxat)
22092203
if keepttl:
22102204
pieces.append("KEEPTTL")

tests/test_asyncio/test_commands.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import binascii
55
import datetime
66
import re
7-
import time
87
from string import ascii_letters
98

109
import pytest
@@ -750,7 +749,7 @@ async def test_expireat_no_key(self, r: redis.Redis):
750749
async def test_expireat_unixtime(self, r: redis.Redis):
751750
expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
752751
await r.set("a", "foo")
753-
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
752+
expire_at_seconds = int(expire_at.timestamp())
754753
assert await r.expireat("a", expire_at_seconds)
755754
assert 0 < await r.ttl("a") <= 61
756755

@@ -875,8 +874,8 @@ async def test_pexpireat_no_key(self, r: redis.Redis):
875874
async def test_pexpireat_unixtime(self, r: redis.Redis):
876875
expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
877876
await r.set("a", "foo")
878-
expire_at_seconds = int(time.mktime(expire_at.timetuple())) * 1000
879-
assert await r.pexpireat("a", expire_at_seconds)
877+
expire_at_milliseconds = int(expire_at.timestamp() * 1000)
878+
assert await r.pexpireat("a", expire_at_milliseconds)
880879
assert 0 < await r.pttl("a") <= 61000
881880

882881
@skip_if_server_version_lt("2.6.0")

tests/test_commands.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@ def test_expireat_no_key(self, r):
11851185
def test_expireat_unixtime(self, r):
11861186
expire_at = redis_server_time(r) + datetime.timedelta(minutes=1)
11871187
r["a"] = "foo"
1188-
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
1188+
expire_at_seconds = int(expire_at.timestamp())
11891189
assert r.expireat("a", expire_at_seconds) is True
11901190
assert 0 < r.ttl("a") <= 61
11911191

@@ -1428,8 +1428,8 @@ def test_pexpireat_no_key(self, r):
14281428
def test_pexpireat_unixtime(self, r):
14291429
expire_at = redis_server_time(r) + datetime.timedelta(minutes=1)
14301430
r["a"] = "foo"
1431-
expire_at_seconds = int(time.mktime(expire_at.timetuple())) * 1000
1432-
assert r.pexpireat("a", expire_at_seconds) is True
1431+
expire_at_milliseconds = int(expire_at.timestamp() * 1000)
1432+
assert r.pexpireat("a", expire_at_milliseconds) is True
14331433
assert 0 < r.pttl("a") <= 61000
14341434

14351435
@skip_if_server_version_lt("7.0.0")

0 commit comments

Comments
 (0)