Skip to content

Commit d453d1d

Browse files
ofekChristineTChen
authored andcommitted
Fix time utilities (#6692)
* Fix time utilities * address feedback - assert returned dt is same object * address feedback - remove upstream test * add comment * remove comment * address feedback * address
1 parent 78bb343 commit d453d1d

File tree

3 files changed

+96
-48
lines changed

3 files changed

+96
-48
lines changed

datadog_checks_base/datadog_checks/base/utils/time.py

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,68 +6,40 @@
66
from datetime import datetime
77
from time import time as epoch_offset
88

9-
import pytz
10-
from six import PY2
9+
from dateutil.tz import UTC
1110

12-
UTC = pytz.utc
13-
EPOCH = datetime(1970, 1, 1, tzinfo=UTC)
11+
EPOCH = datetime.fromtimestamp(0, UTC)
1412

1513

16-
if PY2:
17-
18-
def get_timestamp(dt=None):
19-
"""
20-
Returns the number of seconds since the Unix epoch.
21-
If `dt` is not specified or `None`, the current time in UTC is assumed.
22-
"""
23-
if dt is None:
24-
return epoch_offset()
25-
26-
return (dt - EPOCH).total_seconds()
27-
28-
29-
else:
30-
31-
def get_timestamp(dt=None):
32-
"""
33-
Returns the number of seconds since the Unix epoch.
34-
If `dt` is not specified or `None`, the current time in UTC is assumed.
35-
"""
36-
if dt is None:
37-
# NOTE: Although the epoch is platform dependent, it appears to be the same
38-
# for all platforms we've tested, therefore we use `time.time` for speed.
39-
#
40-
# Here is the test:
41-
#
42-
# $ python -c "import time;print(tuple(time.gmtime(0)[:9]))"
43-
# (1970, 1, 1, 0, 0, 0, 3, 1, 0)
44-
#
45-
# If you can reproduce, add to the following list of tested platforms:
46-
#
47-
# - Windows
48-
# - macOS
49-
# - Ubuntu
50-
# - Alpine
51-
return epoch_offset()
14+
def get_timestamp(dt=None):
15+
"""
16+
Returns the number of seconds since the Unix epoch.
17+
If `dt` is not specified or `None`, the current time in UTC is assumed.
18+
"""
19+
if dt is None:
20+
# The precision is different between Python 2 and 3
21+
return epoch_offset()
5222

53-
return normalize_datetime(dt).timestamp()
23+
# TODO: when we drop support for Python 2 switch to:
24+
# return ensure_aware_datetime(dt).timestamp()
25+
return (ensure_aware_datetime(dt) - EPOCH).total_seconds()
5426

5527

56-
def get_aware_datetime(dt=None, tz=UTC):
28+
def get_current_datetime(tz=UTC):
5729
"""
58-
Returns an aware datetime object. If `dt` is not specified or `None`, the current time in UTC is assumed.
30+
Returns an aware datetime object representing the current time. If `tz` is not specified, UTC is assumed.
5931
"""
60-
if dt is None:
61-
return datetime.now(tz)
62-
else:
63-
return normalize_datetime(dt)
32+
return datetime.now(tz)
6433

6534

66-
def normalize_datetime(dt, default_tz=UTC):
35+
def ensure_aware_datetime(dt, default_tz=UTC):
6736
"""
6837
Ensures that the returned datetime object is not naive.
6938
"""
7039
if dt.tzinfo is None:
7140
dt = dt.replace(tzinfo=default_tz)
7241

7342
return dt
43+
44+
45+
__all__ = ['EPOCH', 'UTC', 'ensure_aware_datetime', 'get_current_datetime', 'get_timestamp']

datadog_checks_base/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ orjson==2.6.1; python_version > '3.0'
1010
prometheus-client==0.7.1
1111
protobuf==3.7.0
1212
pysocks==1.7.0
13+
python-dateutil==2.8.0
1314
pytz==2019.3
1415
pywin32==227; sys_platform == 'win32'
1516
pyyaml==5.3.1
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# (C) Datadog, Inc. 2020-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from datetime import datetime
5+
6+
import pytest
7+
from dateutil import tz
8+
from six import PY2
9+
10+
from datadog_checks.base.utils.time import EPOCH, UTC, ensure_aware_datetime, get_current_datetime, get_timestamp
11+
12+
pytestmark = pytest.mark.time
13+
14+
15+
class TestNormalization:
16+
def test_replace(self):
17+
now = datetime.now()
18+
assert now.tzinfo is None
19+
20+
assert ensure_aware_datetime(now).tzinfo is UTC
21+
22+
def test_utc(self):
23+
nyc = tz.gettz('America/New_York')
24+
now = datetime.now(nyc)
25+
26+
normalized = ensure_aware_datetime(now)
27+
28+
assert normalized is now
29+
assert normalized.tzinfo is nyc
30+
31+
32+
class TestCurrentDatetime:
33+
# What follows is a workaround for being unable to patch directly:
34+
# TypeError: can't set attributes of built-in/extension type 'datetime.datetime'
35+
36+
def test_default_utc(self, mocker):
37+
datetime_obj = mocker.patch('datadog_checks.base.utils.time.datetime')
38+
datetime_obj.now = mocker.MagicMock()
39+
dt = datetime(2020, 2, 2, tzinfo=UTC)
40+
datetime_obj.now.return_value = dt
41+
assert get_current_datetime() is dt
42+
datetime_obj.now.assert_called_once_with(UTC)
43+
44+
def test_tz(self, mocker):
45+
datetime_obj = mocker.patch('datadog_checks.base.utils.time.datetime')
46+
datetime_obj.now = mocker.MagicMock()
47+
nyc = tz.gettz('America/New_York')
48+
dt = datetime(2020, 2, 2, tzinfo=nyc)
49+
datetime_obj.now.return_value = dt
50+
assert get_current_datetime(nyc) is dt
51+
datetime_obj.now.assert_called_once_with(nyc)
52+
53+
54+
class TestTimestamp:
55+
def test_type(self):
56+
assert isinstance(get_timestamp(), float)
57+
58+
def test_default(self, mocker):
59+
time_time = mocker.patch('datadog_checks.base.utils.time.epoch_offset')
60+
get_timestamp()
61+
time_time.assert_called_once()
62+
63+
def test_time_delta(self):
64+
now = datetime.now()
65+
expected = (now.replace(tzinfo=UTC) - EPOCH).total_seconds()
66+
67+
assert get_timestamp(now) == expected
68+
69+
70+
@pytest.mark.skipif(PY2, reason='Using Python 3 features')
71+
class TestConstants:
72+
def test_epoch(self):
73+
from datetime import timezone
74+
75+
assert EPOCH == datetime(1970, 1, 1, tzinfo=timezone.utc)

0 commit comments

Comments
 (0)