Skip to content

Commit 9cf8128

Browse files
authored
Merge pull request #30 from PredixDev/develop_tsfeedback
Account for time series attributes that may be numbers
2 parents 568aa27 + 10acb86 commit 9cf8128

File tree

2 files changed

+45
-9
lines changed

2 files changed

+45
-9
lines changed

predix/data/timeseries.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def get_datapoints(self, tags, start=None, end=None, order=None,
219219
- order: ascending (asc) or descending (desc)
220220
- limit: only return a few values (ie. 25)
221221
- qualities: data quality value (ie. [ts.GOOD, ts.UNCERTAIN])
222-
- attributes: data attributes (ie. {'unit': 'mph'})
222+
- attributes: dictionary of key-values (ie. {'unit': 'mph'})
223223
- measurement: tuple of operation and value (ie. ('gt', 30))
224224
- aggregations: summary statistics on data results (ie. 'avg')
225225
- post: POST query instead of GET (caching implication)
@@ -241,7 +241,7 @@ def get_datapoints(self, tags, start=None, end=None, order=None,
241241
# seems to be required all the time, so using sensible default.
242242
if not start:
243243
start = '1w-ago'
244-
logging.info("Defaulting to timeseries data since %s" % (start))
244+
logging.warning("Defaulting query for data with start date %s" % (start))
245245

246246
# Start date can be absolute or relative, only certain legal values
247247
# but service will throw error if used improperly. (ms, s, mi, h, d,
@@ -352,7 +352,12 @@ def get_latest(self, tags, post=False):
352352
and more complex query.
353353
"""
354354
params = {}
355-
params['tags'] = tags
355+
if isinstance(tags, list):
356+
params['tags'] = str.join(',', tags)
357+
elif isinstance(tags, str):
358+
params['tags'] = tags
359+
else:
360+
raise ValueError("Expect to get tags as a str or list")
356361

357362
if post:
358363
return self._post_latest(params)
@@ -459,17 +464,24 @@ def queue(self, name, value, quality=None, timestamp=None,
459464
"datapoints": [[timestamp, value, quality]]
460465
}
461466

462-
# Attributes are specified for datapoint
463-
if attributes:
467+
# Attributes are extra details for a datapoint
468+
469+
if attributes is not None:
470+
if not isinstance(attributes, dict):
471+
raise ValueError("Attributes are expected to be a dictionary.")
472+
464473
# Validate rules for attribute keys to provide guidance.
465474
invalid_value = ':;= '
466475
has_invalid_value = re.compile(r'[%s]' % (invalid_value)).search
467476
has_valid_key = re.compile(r'^[\w\.\/\-]+$').search
468477

469478
for (key, val) in list(attributes.items()):
470-
# Values cannot be NULL
471-
if not val:
472-
raise ValueError("Attribute (%s) must have value." % (key))
479+
# Values cannot be empty
480+
if (val == '') or (val is None):
481+
raise ValueError("Attribute (%s) must have a non-empty value." % (key))
482+
483+
# Values should be treated as a string for regex validation
484+
val = str(val)
473485

474486
# Values cannot contain certain arbitrary characters
475487
if bool(has_invalid_value(val)):

test/integration/test_timeseries.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ class TestTimeSeries(unittest.TestCase):
1414
More Testing
1515
- [ ] Query on multiple tags
1616
- [ ] Get aggregations
17-
- [ ] Get latest
1817
- [ ] Get values
1918
"""
2019

@@ -86,6 +85,16 @@ def test_ingest_attributes(self):
8685
self.assertEqual(values[0][2], 1)
8786
self.assertEqual(attrs, {'units':['F']})
8887

88+
# Test multiple attributes, including a number
89+
ts.send('TAG4b', 61, attributes={'units': 'F', 'Number': 1})
90+
time.sleep(2) # Allow time for ingestion to complete
91+
res = ts.get_datapoints('TAG4b')
92+
self.assertEqual('TAG4b', res['tags'][0]['name'])
93+
94+
# Test we get error if attributes is not a dictionary
95+
with self.assertRaises(ValueError):
96+
ts.send('TAG4b', 62, attributes="{'units': 'F'}")
97+
8998
def test_ingest_timestamp(self):
9099
ts = self.app.get_timeseries()
91100

@@ -102,6 +111,12 @@ def test_ingest_timestamp(self):
102111
values = res['tags'][0]['results'][0]['values']
103112
self.assertEqual(values[0][0], 306658800000)
104113

114+
# Verify can search with epoch dates, not just relative dates
115+
116+
res = ts.get_datapoints('TAG5', start=292185600000, end=313267200000)
117+
values = res['tags'][0]['results'][0]['values']
118+
self.assertEqual(values[0][1], 70)
119+
105120
def test_query_filter_aggregation(self):
106121
ts = self.app.get_timeseries()
107122
tag = 'STACK1'
@@ -115,6 +130,15 @@ def test_query_filter_aggregation(self):
115130
result = query['tags'][0]['results'][0]['values'][0][1]
116131
self.assertEqual(result, sum(values[1:]))
117132

133+
def test_get_latest(self):
134+
ts = self.app.get_timeseries()
135+
ts.send('LATEST-1', 1)
136+
ts.send('LATEST-2', 2)
137+
time.sleep(2) # Allow time for ingestion
138+
139+
res = ts.get_latest(['LATEST-1', 'LATEST-2'])
140+
self.assertEqual(len(res['tags']), 2)
141+
self.assertEqual(res['tags'][0]['name'], 'LATEST-1')
118142

119143
if __name__ == '__main__':
120144
if os.getenv('DEBUG'):

0 commit comments

Comments
 (0)