Skip to content

Commit 3b7039e

Browse files
matt-snideraviau
authored andcommitted
Add time parameter to SeriesHelper (influxdata#306)
* Revert Allow setting the time of a point manually This reverts commit c25ec08, which this commit is part of PR influxdata#304. * Allow time to be specified in SeriesHelper.__init__() * Extract SeriesHelper default timestamp into method for testability * Use datetime.utcnow() as default timestamp in SeriesHelper This is preferable to time.time() because _convert_timestamp() from influxdb.line_protocol will do precision handling and conversion if a datetime object is given. * Get existing tests working by mocking SeriesHelper._current_timestamp() * Add additional tests for SeriesHelper time field * Move _reset_() calls in TestSeriesHelper to tearDown() * Use mock.patch() instead of unittest.mock.patch() for py27 * Update SeriesHelper docstring
1 parent e898317 commit 3b7039e

File tree

3 files changed

+107
-84
lines changed

3 files changed

+107
-84
lines changed

influxdb/helper.py

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,9 @@ class SeriesHelper(object):
1616
All data points are immutable, insuring they do not get overwritten.
1717
Each subclass can write to its own database.
1818
The time series names can also be based on one or more defined fields.
19-
20-
A field "time" can be used to write data points at a specific time,
21-
rather than the default current time. The time field can take any of
22-
the following forms:
23-
* An integer unix timestamp in nanoseconds, assumed to be in UTC.
24-
* A string in the ISO time format, including a timezone.
25-
* A naive python datetime, which will be treated as UTC.
26-
* A localized python datetime, which will use the chosen timezone.
27-
If no time field is provided, the current UTC system time in microseconds
28-
at the time of assembling the point data will be used.
19+
The field "time" can be specified when creating a point, and may be any of
20+
the time types supported by the client (i.e. str, datetime, int).
21+
If the time is not specified, the current system time (utc) will be used.
2922
3023
Annotated example::
3124
@@ -98,8 +91,11 @@ def __new__(cls, *args, **kwargs):
9891
' autocommit is false.'.format(cls.__name__))
9992

10093
cls._datapoints = defaultdict(list)
101-
cls._type = namedtuple(cls.__name__, cls._fields + cls._tags)
10294

95+
if 'time' in cls._fields:
96+
cls._fields.remove('time')
97+
cls._type = namedtuple(cls.__name__,
98+
cls._fields + cls._tags + ['time'])
10399
return super(SeriesHelper, cls).__new__(cls)
104100

105101
def __init__(self, **kw):
@@ -110,14 +106,17 @@ def __init__(self, **kw):
110106
:warning: Data points are *immutable* (`namedtuples`).
111107
"""
112108
cls = self.__class__
109+
timestamp = kw.pop('time', self._current_timestamp())
113110

114111
if sorted(cls._fields + cls._tags) != sorted(kw.keys()):
115112
raise NameError(
116113
'Expected {0}, got {1}.'.format(
117114
sorted(cls._fields + cls._tags),
118115
kw.keys()))
119116

120-
cls._datapoints[cls._series_name.format(**kw)].append(cls._type(**kw))
117+
cls._datapoints[cls._series_name.format(**kw)].append(
118+
cls._type(time=timestamp, **kw)
119+
)
121120

122121
if cls._autocommit and \
123122
sum(len(series) for series in cls._datapoints.values()) \
@@ -151,25 +150,11 @@ def _json_body_(cls):
151150
"measurement": series_name,
152151
"fields": {},
153152
"tags": {},
153+
"time": getattr(point, "time")
154154
}
155155

156-
ts = getattr(point, 'time', None)
157-
if not ts:
158-
# No time provided. Use current UTC time.
159-
ts = datetime.utcnow().isoformat() + "+00:00"
160-
elif isinstance(ts, datetime):
161-
if ts.tzinfo is None or ts.tzinfo.utcoffset(ts) is None:
162-
# Assuming naive datetime provided. Format with UTC tz.
163-
ts = ts.isoformat() + "+00:00"
164-
else:
165-
# Assuming localized datetime provided.
166-
ts = ts.isoformat()
167-
# Neither of the above match. Assuming correct string or int.
168-
json_point['time'] = ts
169-
170156
for field in cls._fields:
171-
if field != 'time':
172-
json_point['fields'][field] = getattr(point, field)
157+
json_point['fields'][field] = getattr(point, field)
173158

174159
for tag in cls._tags:
175160
json_point['tags'][tag] = getattr(point, tag)
@@ -183,3 +168,6 @@ def _reset_(cls):
183168
Reset data storage.
184169
"""
185170
cls._datapoints = defaultdict(list)
171+
172+
def _current_timestamp(self):
173+
return datetime.utcnow()

influxdb/tests/helper_test.py

Lines changed: 90 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# -*- coding: utf-8 -*-
22

3-
import datetime
4-
import pytz
53
import sys
64
if sys.version_info < (2, 7):
75
import unittest2 as unittest
@@ -10,6 +8,7 @@
108
import warnings
119

1210
import mock
11+
from datetime import datetime, timedelta
1312
from influxdb import SeriesHelper, InfluxDBClient
1413
from requests.exceptions import ConnectionError
1514

@@ -40,17 +39,13 @@ class Meta:
4039

4140
TestSeriesHelper.MySeriesHelper = MySeriesHelper
4241

43-
class MySeriesTimeHelper(SeriesHelper):
44-
45-
class Meta:
46-
client = TestSeriesHelper.client
47-
series_name = 'events.stats.{server_name}'
48-
fields = ['time', 'some_stat']
49-
tags = ['server_name', 'other_tag']
50-
bulk_size = 5
51-
autocommit = True
52-
53-
TestSeriesHelper.MySeriesTimeHelper = MySeriesTimeHelper
42+
def tearDown(self):
43+
super(TestSeriesHelper, self).tearDown()
44+
TestSeriesHelper.MySeriesHelper._reset_()
45+
self.assertEqual(
46+
TestSeriesHelper.MySeriesHelper._json_body_(),
47+
[],
48+
'Resetting helper did not empty datapoints.')
5449

5550
def test_auto_commit(self):
5651
"""
@@ -76,24 +71,20 @@ class Meta:
7671
AutoCommitTest(server_name='us.east-1', some_stat=3443, other_tag='gg')
7772
self.assertTrue(fake_write_points.called)
7873

79-
def testSingleSeriesName(self):
74+
@mock.patch('influxdb.helper.SeriesHelper._current_timestamp')
75+
def testSingleSeriesName(self, current_timestamp):
8076
"""
8177
Tests JSON conversion when there is only one series name.
8278
"""
83-
dt = datetime.datetime(2016, 1, 2, 3, 4, 5, 678912)
84-
ts1 = dt
85-
ts2 = "2016-10-11T01:02:03.123456789-04:00"
86-
ts3 = 1234567890123456789
87-
ts4 = pytz.timezone("Europe/Berlin").localize(dt)
88-
89-
TestSeriesHelper.MySeriesTimeHelper(
90-
time=ts1, server_name='us.east-1', other_tag='ello', some_stat=159)
91-
TestSeriesHelper.MySeriesTimeHelper(
92-
time=ts2, server_name='us.east-1', other_tag='ello', some_stat=158)
93-
TestSeriesHelper.MySeriesTimeHelper(
94-
time=ts3, server_name='us.east-1', other_tag='ello', some_stat=157)
95-
TestSeriesHelper.MySeriesTimeHelper(
96-
time=ts4, server_name='us.east-1', other_tag='ello', some_stat=156)
79+
current_timestamp.return_value = current_date = datetime.today()
80+
TestSeriesHelper.MySeriesHelper(
81+
server_name='us.east-1', other_tag='ello', some_stat=159)
82+
TestSeriesHelper.MySeriesHelper(
83+
server_name='us.east-1', other_tag='ello', some_stat=158)
84+
TestSeriesHelper.MySeriesHelper(
85+
server_name='us.east-1', other_tag='ello', some_stat=157)
86+
TestSeriesHelper.MySeriesHelper(
87+
server_name='us.east-1', other_tag='ello', some_stat=156)
9788
expectation = [
9889
{
9990
"measurement": "events.stats.us.east-1",
@@ -104,7 +95,7 @@ def testSingleSeriesName(self):
10495
"fields": {
10596
"some_stat": 159
10697
},
107-
"time": "2016-01-02T03:04:05.678912+00:00",
98+
"time": current_date,
10899
},
109100
{
110101
"measurement": "events.stats.us.east-1",
@@ -115,7 +106,7 @@ def testSingleSeriesName(self):
115106
"fields": {
116107
"some_stat": 158
117108
},
118-
"time": "2016-10-11T01:02:03.123456789-04:00",
109+
"time": current_date,
119110
},
120111
{
121112
"measurement": "events.stats.us.east-1",
@@ -126,7 +117,7 @@ def testSingleSeriesName(self):
126117
"fields": {
127118
"some_stat": 157
128119
},
129-
"time": 1234567890123456789,
120+
"time": current_date,
130121
},
131122
{
132123
"measurement": "events.stats.us.east-1",
@@ -137,25 +128,22 @@ def testSingleSeriesName(self):
137128
"fields": {
138129
"some_stat": 156
139130
},
140-
"time": "2016-01-02T03:04:05.678912+01:00",
131+
"time": current_date,
141132
}
142133
]
143134

144-
rcvd = TestSeriesHelper.MySeriesTimeHelper._json_body_()
135+
rcvd = TestSeriesHelper.MySeriesHelper._json_body_()
145136
self.assertTrue(all([el in expectation for el in rcvd]) and
146137
all([el in rcvd for el in expectation]),
147138
'Invalid JSON body of time series returned from '
148139
'_json_body_ for one series name: {0}.'.format(rcvd))
149-
TestSeriesHelper.MySeriesTimeHelper._reset_()
150-
self.assertEqual(
151-
TestSeriesHelper.MySeriesTimeHelper._json_body_(),
152-
[],
153-
'Resetting helper did not empty datapoints.')
154140

155-
def testSeveralSeriesNames(self):
156-
'''
141+
@mock.patch('influxdb.helper.SeriesHelper._current_timestamp')
142+
def testSeveralSeriesNames(self, current_timestamp):
143+
"""
157144
Tests JSON conversion when there are multiple series names.
158-
'''
145+
"""
146+
current_timestamp.return_value = current_date = datetime.today()
159147
TestSeriesHelper.MySeriesHelper(
160148
server_name='us.east-1', some_stat=159, other_tag='ello')
161149
TestSeriesHelper.MySeriesHelper(
@@ -173,7 +161,8 @@ def testSeveralSeriesNames(self):
173161
'tags': {
174162
'other_tag': 'ello',
175163
'server_name': 'lu.lux'
176-
}
164+
},
165+
"time": current_date,
177166
},
178167
{
179168
'fields': {
@@ -183,7 +172,8 @@ def testSeveralSeriesNames(self):
183172
'tags': {
184173
'other_tag': 'ello',
185174
'server_name': 'uk.london'
186-
}
175+
},
176+
"time": current_date,
187177
},
188178
{
189179
'fields': {
@@ -193,7 +183,8 @@ def testSeveralSeriesNames(self):
193183
'tags': {
194184
'other_tag': 'ello',
195185
'server_name': 'fr.paris-10'
196-
}
186+
},
187+
"time": current_date,
197188
},
198189
{
199190
'fields': {
@@ -203,25 +194,70 @@ def testSeveralSeriesNames(self):
203194
'tags': {
204195
'other_tag': 'ello',
205196
'server_name': 'us.east-1'
206-
}
197+
},
198+
"time": current_date,
207199
}
208200
]
209201

210202
rcvd = TestSeriesHelper.MySeriesHelper._json_body_()
211-
for r in rcvd:
212-
self.assertTrue(r.get('time'),
213-
"No time field in received JSON body.")
214-
del(r["time"])
215203
self.assertTrue(all([el in expectation for el in rcvd]) and
216204
all([el in rcvd for el in expectation]),
217205
'Invalid JSON body of time series returned from '
218206
'_json_body_ for several series names: {0}.'
219207
.format(rcvd))
220-
TestSeriesHelper.MySeriesHelper._reset_()
221-
self.assertEqual(
222-
TestSeriesHelper.MySeriesHelper._json_body_(),
223-
[],
224-
'Resetting helper did not empty datapoints.')
208+
209+
@mock.patch('influxdb.helper.SeriesHelper._current_timestamp')
210+
def testSeriesWithoutTimeField(self, current_timestamp):
211+
"""
212+
Tests that time is optional on a series without a time field.
213+
"""
214+
current_date = datetime.today()
215+
yesterday = current_date - timedelta(days=1)
216+
current_timestamp.return_value = yesterday
217+
TestSeriesHelper.MySeriesHelper(
218+
server_name='us.east-1', other_tag='ello',
219+
some_stat=159, time=current_date
220+
)
221+
TestSeriesHelper.MySeriesHelper(
222+
server_name='us.east-1', other_tag='ello',
223+
some_stat=158,
224+
)
225+
point1, point2 = TestSeriesHelper.MySeriesHelper._json_body_()
226+
self.assertTrue('time' in point1 and 'time' in point2)
227+
self.assertEqual(point1['time'], current_date)
228+
self.assertEqual(point2['time'], yesterday)
229+
230+
@mock.patch('influxdb.helper.SeriesHelper._current_timestamp')
231+
def testSeriesWithTimeField(self, current_timestamp):
232+
"""
233+
Test that time is optional on a series with a time field.
234+
"""
235+
current_date = datetime.today()
236+
yesterday = current_date - timedelta(days=1)
237+
current_timestamp.return_value = yesterday
238+
239+
class MyTimeFieldSeriesHelper(SeriesHelper):
240+
241+
class Meta:
242+
client = TestSeriesHelper.client
243+
series_name = 'events.stats.{server_name}'
244+
fields = ['some_stat', 'time']
245+
tags = ['server_name', 'other_tag']
246+
bulk_size = 5
247+
autocommit = True
248+
249+
MyTimeFieldSeriesHelper(
250+
server_name='us.east-1', other_tag='ello',
251+
some_stat=159, time=current_date
252+
)
253+
MyTimeFieldSeriesHelper(
254+
server_name='us.east-1', other_tag='ello',
255+
some_stat=158,
256+
)
257+
point1, point2 = MyTimeFieldSeriesHelper._json_body_()
258+
self.assertTrue('time' in point1 and 'time' in point2)
259+
self.assertEqual(point1['time'], current_date)
260+
self.assertEqual(point2['time'], yesterday)
225261

226262
def testInvalidHelpers(self):
227263
'''

test-requirements.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
nose
22
nose-cov
33
mock
4-
requests-mock
5-
pytz
4+
requests-mock

0 commit comments

Comments
 (0)