diff --git a/setup.py b/setup.py index 154d54a..bf7c886 100644 --- a/setup.py +++ b/setup.py @@ -6,13 +6,17 @@ Copyright (c) 2012 TempoDB Inc. All rights reserved. """ -from setuptools import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup install_requires = [ - 'python-dateutil==1.5', - 'requests>=1.0', + 'python-dateutil >= 2.0', + 'requests >= 2.0', 'simplejson', + 'six' ] tests_require = [ diff --git a/tempodb/__init__.py b/tempodb/__init__.py index 6d2c111..a743916 100644 --- a/tempodb/__init__.py +++ b/tempodb/__init__.py @@ -9,7 +9,7 @@ try: VERSION = __import__('pkg_resources').get_distribution('tempodb').version -except Exception, e: +except Exception as e: VERSION = 'unknown' def get_version(): diff --git a/tempodb/base.py b/tempodb/base.py index e9403dd..4a3dde9 100644 --- a/tempodb/base.py +++ b/tempodb/base.py @@ -5,7 +5,9 @@ Copyright (c) 2012 TempoDB, Inc. All rights reserved. """ +import six from datetime import datetime +from dateutil.parser import parse class Database(object): @@ -65,7 +67,7 @@ def to_json(self): @staticmethod def from_json(json): - ts = datetime.strptime(json.get('t', ''), "%Y-%m-%dT%H:%M:%S.%fZ") + ts = parse(json.get('t', '')) value = json.get('v', None) dp = DataPoint(ts, value) return dp @@ -90,11 +92,14 @@ def __eq__(self, other): def from_json(json): series = Series.from_json(json.get('series', {})) - start_date = datetime.strptime(json.get('start', ''), "%Y-%m-%dT%H:%M:%S.%fZ") - end_date = datetime.strptime(json.get('end', ''), "%Y-%m-%dT%H:%M:%S.%fZ") + matcher = "%Y-%m-%dT%H:%M:%S.%fZ" + + start_date = parse(json.get('start', '')) + end_date = parse(json.get('end', '')) data = [DataPoint.from_json(dp) for dp in json.get("data", [])] - summary = Summary.from_json(json.get('summary', {})) if 'summary' in json else None + summary = Summary.from_json( + json.get('summary', {})) if 'summary' in json else None return DataSet(series, start_date, end_date, data, summary) @@ -112,11 +117,12 @@ def from_json(json): summary.__dict__.update(json) return summary + class DeleteSummary(object): + def __init__(self, deleted): self.deleted = deleted @staticmethod def from_json(json): return DeleteSummary(json['deleted']) - diff --git a/tempodb/client.py b/tempodb/client.py index 75ca682..32ffbb7 100644 --- a/tempodb/client.py +++ b/tempodb/client.py @@ -10,11 +10,11 @@ import re import requests import simplejson -import urllib -import urllib2 +from six.moves.urllib_parse import urlencode, quote import tempodb from tempodb import DataPoint, DataSet, DeleteSummary, Series, Summary +import six API_HOST = 'api.tempo-db.com' @@ -24,7 +24,8 @@ VALID_SERIES_KEY = r'^[a-zA-Z0-9\.:;\-_/\\ ]*$' RE_VALID_SERIES_KEY = re.compile(VALID_SERIES_KEY) -DATETIME_HANDLER = lambda obj: obj.isoformat() if isinstance(obj, datetime.datetime) else None +DATETIME_HANDLER = lambda obj: obj.isoformat() if isinstance( + obj, datetime.datetime) else None class Client(object): @@ -36,8 +37,10 @@ def __init__(self, key, secret, host=API_HOST, port=API_PORT, secure=True, pool_ self.port = port self.secure = secure self.session = requests.session() - self.session.mount('http://', requests.adapters.HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize)) - self.session.mount('https://', requests.adapters.HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize)) + self.session.mount( + 'http://', requests.adapters.HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize)) + self.session.mount( + 'https://', requests.adapters.HTTPAdapter(pool_connections=pool_connections, pool_maxsize=pool_maxsize)) def get_series(self, ids=[], keys=[], tags=[], attributes={}): params = self._normalize_params(ids, keys, tags, attributes) @@ -53,7 +56,8 @@ def delete_series(self, ids=[], keys=[], tags=[], attributes={}, allow_truncatio def create_series(self, key=None): if key and not RE_VALID_SERIES_KEY.match(key): - raise ValueError("Series key must match the following regex: %s" % (VALID_SERIES_KEY,)) + raise ValueError("Series key must match the following regex: %s" % + (VALID_SERIES_KEY,)) params = {} if key is not None: @@ -64,7 +68,8 @@ def create_series(self, key=None): return series def update_series(self, series): - json = self.request('/series/id/%s/' % (series.id,), method='PUT', params=series.to_json()) + json = self.request('/series/id/%s/' % + (series.id,), method='PUT', params=series.to_json()) series = Series.from_json(json) return series @@ -120,7 +125,8 @@ def write_id(self, series_id, data): def write_key(self, series_key, data): if series_key and not RE_VALID_SERIES_KEY.match(series_key): - raise ValueError("Series key must match the following regex: %s" % (VALID_SERIES_KEY,)) + raise ValueError("Series key must match the following regex: %s" % + (VALID_SERIES_KEY,)) series_type = 'key' series_val = series_key @@ -145,7 +151,8 @@ def increment_id(self, series_id, data): def increment_key(self, series_key, data): if series_key and not RE_VALID_SERIES_KEY.match(series_key): - raise ValueError("Series key must match the following regex: %s" % (VALID_SERIES_KEY,)) + raise ValueError("Series key must match the following regex: %s" % + (VALID_SERIES_KEY,)) series_type = 'key' series_val = series_key @@ -177,10 +184,11 @@ def _read(self, series_type, series_val, start, end, interval="", function="", t if tz: params['tz'] = tz - url = '/series/%s/%s/data/' % (series_type, urllib2.quote(series_val, "")) + url = '/series/%s/%s/data/' % (series_type, + quote(series_val, "")) json = self.request(url, method='GET', params=params) - #we got an error + # we got an error if 'error' in json: return json return DataSet.from_json(json) @@ -191,24 +199,28 @@ def _delete(self, series_type, series_val, start, end, options): 'end': end.isoformat(), } params.update(options) - url = '/series/%s/%s/data/' % (series_type, urllib2.quote(series_val, "")) + url = '/series/%s/%s/data/' % (series_type, + quote(series_val, "")) json = self.request(url, method='DELETE', params=params) return json def _write(self, series_type, series_val, data): - url = '/series/%s/%s/data/' % (series_type, urllib2.quote(series_val, "")) + url = '/series/%s/%s/data/' % (series_type, + quote(series_val, "")) body = [dp.to_json() for dp in data] json = self.request(url, method='POST', params=body) return json def _increment(self, series_type, series_val, data): - url = '/series/%s/%s/increment/' % (series_type, urllib2.quote(series_val, "")) + url = '/series/%s/%s/increment/' % (series_type, + quote(series_val, "")) body = [dp.to_json() for dp in data] json = self.request(url, method='POST', params=body) return json def request(self, target, method='GET', params={}): - assert method in ['GET', 'POST', 'PUT', 'DELETE'], "Only 'GET', 'POST', 'PUT', 'DELETE' are allowed for method." + assert method in ['GET', 'POST', 'PUT', + 'DELETE'], "Only 'GET', 'POST', 'PUT', 'DELETE' are allowed for method." headers = { 'User-Agent': 'tempodb-python/%s' % (tempodb.get_version(), ), @@ -219,27 +231,31 @@ def request(self, target, method='GET', params={}): headers['Content-Type'] = "application/json" base = self.build_full_url(target) json_data = simplejson.dumps(params, default=DATETIME_HANDLER) - response = self.session.post(base, data=json_data, auth=(self.key, self.secret), headers=headers) + response = self.session.post( + base, data=json_data, auth=(self.key, self.secret), headers=headers) elif method == 'PUT': headers['Content-Type'] = "application/json" base = self.build_full_url(target) json_data = simplejson.dumps(params, default=DATETIME_HANDLER) - response = self.session.put(base, data=json_data, auth=(self.key, self.secret), headers=headers) + response = self.session.put( + base, data=json_data, auth=(self.key, self.secret), headers=headers) elif method == 'DELETE': base = self.build_full_url(target, params) - response = self.session.delete(base, auth=(self.key, self.secret), headers=headers) + response = self.session.delete( + base, auth=(self.key, self.secret), headers=headers) else: base = self.build_full_url(target, params) - response = self.session.get(base, auth=(self.key, self.secret), headers=headers) + response = self.session.get( + base, auth=(self.key, self.secret), headers=headers) if response.status_code == 200: if response.text: json = simplejson.loads(response.text) else: json = '' - #try: + # try: # json = simplejson.loads(response.text) - #except simplejson.decoder.JSONDecodeError, err: + # except simplejson.decoder.JSONDecodeError, err: # json = dict(error="JSON Parse Error (%s):\n%s" % (err, response.text)) else: json = dict(error=response.text) @@ -260,7 +276,7 @@ def build_url(self, url, params={}): def _urlencode(self, params): p = [] - for key, value in params.iteritems(): + for key, value in six.iteritems(params): if isinstance(value, (list, tuple)): for v in value: p.append((key, v)) @@ -271,7 +287,10 @@ def _urlencode(self, params): p.append((key, str(value).lower())) else: p.append((key, str(value))) - return urllib.urlencode(p).encode("UTF-8") + + # We shouldn't need the explicit encoding cast at all, but I'm + # leaving it in for legacy's sake + return urlencode(p) if six.PY3 else urlencode(p).encode("UTF-8") def _normalize_params(self, ids=[], keys=[], tags=[], attributes={}): params = {} diff --git a/tempodb/demo/tempodb-bulk-write-demo.py b/tempodb/demo/tempodb-bulk-write-demo.py index 409e13e..46bf05d 100644 --- a/tempodb/demo/tempodb-bulk-write-demo.py +++ b/tempodb/demo/tempodb-bulk-write-demo.py @@ -1,6 +1,7 @@ """ http://tempo-db.com/api/write-series/#bulk-write-multiple-series """ +from __future__ import print_function import datetime from tempodb import Client @@ -20,4 +21,4 @@ { 'key': 'custom-series-key4', 'v': 4.44 }, ] -print client.write_bulk(ts, data) +print(client.write_bulk(ts, data)) diff --git a/tempodb/demo/tempodb-read-demo.py b/tempodb/demo/tempodb-read-demo.py index a3d70a7..ed64d8a 100644 --- a/tempodb/demo/tempodb-read-demo.py +++ b/tempodb/demo/tempodb-read-demo.py @@ -1,6 +1,7 @@ """ http://tempo-db.com/api/read-series/#read-series-by-key """ +from __future__ import print_function import datetime from tempodb import Client @@ -18,4 +19,4 @@ data = client.read_key(SERIES_KEY, start, end) for datapoint in data.data: - print datapoint + print(datapoint) diff --git a/tempodb/demo/tempodb-write-demo.py b/tempodb/demo/tempodb-write-demo.py index bccf505..e331c18 100644 --- a/tempodb/demo/tempodb-write-demo.py +++ b/tempodb/demo/tempodb-write-demo.py @@ -1,6 +1,7 @@ """ http://tempo-db.com/api/write-series/#write-series-by-key """ +from __future__ import print_function import datetime import random @@ -17,7 +18,7 @@ for day in range(1, 10): # print out the current day we are sending data for - print date + print(date) data = [] # 1440 minutes in one day diff --git a/tests/client/tests.py b/tests/client/tests.py index 972fe3b..cf81384 100644 --- a/tests/client/tests.py +++ b/tests/client/tests.py @@ -51,17 +51,23 @@ def test_defaults(self): def test_port_defaults(self): """ 80 is the default port for HTTP, 443 is the default for HTTPS """ client = Client('key', 'secret', 'example.com', 80, False) - self.assertEqual(client.build_full_url('/etc'), 'http://example.com/v1/etc') + self.assertEqual(client.build_full_url('/etc'), + 'http://example.com/v1/etc') client = Client('key', 'secret', 'example.com', 88, False) - self.assertEqual(client.build_full_url('/etc'), 'http://example.com:88/v1/etc') + self.assertEqual(client.build_full_url('/etc'), + 'http://example.com:88/v1/etc') client = Client('key', 'secret', 'example.com', 443, False) - self.assertEqual(client.build_full_url('/etc'), 'http://example.com:443/v1/etc') + self.assertEqual(client.build_full_url('/etc'), + 'http://example.com:443/v1/etc') client = Client('key', 'secret', 'example.com', 443, True) - self.assertEqual(client.build_full_url('/etc'), 'https://example.com/v1/etc') + self.assertEqual(client.build_full_url('/etc'), + 'https://example.com/v1/etc') client = Client('key', 'secret', 'example.com', 88, True) - self.assertEqual(client.build_full_url('/etc'), 'https://example.com:88/v1/etc') + self.assertEqual(client.build_full_url('/etc'), + 'https://example.com:88/v1/etc') client = Client('key', 'secret', 'example.com', 80, True) - self.assertEqual(client.build_full_url('/etc'), 'https://example.com:80/v1/etc') + self.assertEqual(client.build_full_url('/etc'), + 'https://example.com:80/v1/etc') def test_get_series(self): self.client.session.get.return_value = MockResponse(200, """[{ @@ -78,13 +84,16 @@ def test_get_series(self): auth=('key', 'secret'), headers=self.get_headers ) - expected = [Series('id', 'key', 'name', {'key1': 'value1'}, ['tag1', 'tag2'])] + expected = [ + Series('id', 'key', 'name', {'key1': 'value1'}, ['tag1', 'tag2'])] self.assertEqual(series, expected) def test_delete_series(self): - self.client.session.delete.return_value = MockResponse(200, """{"deleted":2}""") + self.client.session.delete.return_value = MockResponse( + 200, """{"deleted":2}""") - summary = self.client.delete_series([], [], [], {'key': 'one', 'key2': 'two'}) + summary = self.client.delete_series( + [], [], [], {'key': 'one', 'key2': 'two'}) self.assertEqual(summary.deleted, 2) def test_create_series(self): @@ -112,7 +121,8 @@ def test_create_series_validity_error(self): def test_update_series(self): update = Series('id', 'key', 'name', {'key1': 'value1'}, ['tag1']) - self.client.session.put.return_value = MockResponse(200, simplejson.dumps(update.to_json())) + self.client.session.put.return_value = MockResponse( + 200, simplejson.dumps(update.to_json())) updated = self.client.update_series(update) @@ -143,7 +153,8 @@ def test_read_id(self): end = datetime.datetime(2012, 3, 28) dataset = self.client.read_id('id', start, end) - expected = DataSet(Series('id', 'key'), start, end, [DataPoint(start, 12.34)], Summary()) + expected = DataSet(Series('id', 'key'), start, end, + [DataPoint(start, 12.34)], Summary()) self.client.session.get.assert_called_once_with( 'https://example.com/v1/series/id/id/data/?start=2012-03-27T00%3A00%3A00&end=2012-03-28T00%3A00%3A00', auth=('key', 'secret'), @@ -170,7 +181,8 @@ def test_read_key(self): end = datetime.datetime(2012, 3, 28) dataset = self.client.read_key('key1', start, end) - expected = DataSet(Series('id', 'key1'), start, end, [DataPoint(start, 12.34)], Summary()) + expected = DataSet(Series('id', 'key1'), start, end, + [DataPoint(start, 12.34)], Summary()) self.client.session.get.assert_called_once_with( 'https://example.com/v1/series/key/key1/data/?start=2012-03-27T00%3A00%3A00&end=2012-03-28T00%3A00%3A00', auth=('key', 'secret'), @@ -197,7 +209,8 @@ def test_read_key_escape(self): end = datetime.datetime(2012, 3, 28) dataset = self.client.read_key('ke:y/1', start, end) - expected = DataSet(Series('id', 'ke:y/1'), start, end, [DataPoint(start, 12.34)], Summary()) + expected = DataSet(Series('id', 'ke:y/1'), start, + end, [DataPoint(start, 12.34)], Summary()) self.client.session.get.assert_called_once_with( 'https://example.com/v1/series/key/ke%3Ay%2F1/data/?start=2012-03-27T00%3A00%3A00&end=2012-03-28T00%3A00%3A00', auth=('key', 'secret'), @@ -224,7 +237,8 @@ def test_read(self): end = datetime.datetime(2012, 3, 28) datasets = self.client.read(start, end, keys=['key1']) - expected = [DataSet(Series('id', 'key1'), start, end, [DataPoint(start, 12.34)], Summary())] + expected = [ + DataSet(Series('id', 'key1'), start, end, [DataPoint(start, 12.34)], Summary())] self.client.session.get.assert_called_once_with( 'https://example.com/v1/data/?start=2012-03-27T00%3A00%3A00&end=2012-03-28T00%3A00%3A00&key=key1', auth=('key', 'secret'), @@ -352,10 +366,10 @@ def test_increment_key_escape(self): def test_write_bulk(self): self.client.session.post.return_value = MockResponse(200, "") data = [ - { 'id': '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4.164 }, - { 'id': '38268c3b231f1266a392931e15e99231', 'v': 73.13 }, - { 'key': 'your-custom-key', 'v': 55.423 }, - { 'key': 'foo', 'v': 324.991 }, + {'id': '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4.164}, + {'id': '38268c3b231f1266a392931e15e99231', 'v': 73.13}, + {'key': 'your-custom-key', 'v': 55.423}, + {'key': 'foo', 'v': 324.991}, ] ts = datetime.datetime(2012, 3, 27) result = self.client.write_bulk(ts, data) @@ -371,10 +385,10 @@ def test_write_bulk(self): def test_increment_bulk(self): self.client.session.post.return_value = MockResponse(200, "") data = [ - { 'id': '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4 }, - { 'id': '38268c3b231f1266a392931e15e99231', 'v': 2 }, - { 'key': 'your-custom-key', 'v': 1 }, - { 'key': 'foo', 'v': 1 }, + {'id': '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4}, + {'id': '38268c3b231f1266a392931e15e99231', 'v': 2}, + {'key': 'your-custom-key', 'v': 1}, + {'key': 'foo', 'v': 1}, ] ts = datetime.datetime(2012, 3, 27) result = self.client.increment_bulk(ts, data) @@ -387,14 +401,16 @@ def test_increment_bulk(self): ) self.assertEqual(result, '') - def test_write_multi(self): self.client.session.post.return_value = MockResponse(200, "") data = [ - { 't': datetime.datetime(2013, 8, 21), 'id': '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4.164 }, - { 't': datetime.datetime(2013, 8, 22), 'id': '38268c3b231f1266a392931e15e99231', 'v': 73.13 }, - { 't': datetime.datetime(2013, 8, 23), 'key': 'your-custom-key', 'v': 55.423 }, - { 't': datetime.datetime(2013, 8, 24), 'key': 'foo', 'v': 324.991 }, + {'t': datetime.datetime(2013, 8, 21), 'id': + '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4.164}, + {'t': datetime.datetime(2013, 8, 22), 'id': + '38268c3b231f1266a392931e15e99231', 'v': 73.13}, + {'t': datetime.datetime(2013, 8, 23), 'key': + 'your-custom-key', 'v': 55.423}, + {'t': datetime.datetime(2013, 8, 24), 'key': 'foo', 'v': 324.991}, ] result = self.client.write_multi(data) @@ -413,11 +429,13 @@ def test_write_multi_207(self): { "status": "200", "messages": [] }, { "status": "422", "messages": [ "Must provide a numeric value", "Must provide a series ID or key" ] } ]}}""" - self.client.session.post.return_value = MockResponse(207, expected_response) + self.client.session.post.return_value = MockResponse( + 207, expected_response) data = [ - { 't': datetime.datetime(2013, 8, 21), 'v': 4.164 }, - { 't': datetime.datetime(2013, 8, 22), 'id': '38268c3b231f1266a392931e15e99231'}, + {'t': datetime.datetime(2013, 8, 21), 'v': 4.164}, + {'t': datetime.datetime(2013, 8, 22), 'id': + '38268c3b231f1266a392931e15e99231'}, {} ] result = self.client.write_multi(data) @@ -434,10 +452,13 @@ def test_write_multi_207(self): def test_write_multi(self): self.client.session.post.return_value = MockResponse(200, "") data = [ - { 't': datetime.datetime(2013, 8, 21), 'id': '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4164 }, - { 't': datetime.datetime(2013, 8, 22), 'id': '38268c3b231f1266a392931e15e99231', 'v': 7313 }, - { 't': datetime.datetime(2013, 8, 23), 'key': 'your-custom-key', 'v': 55423 }, - { 't': datetime.datetime(2013, 8, 24), 'key': 'foo', 'v': 324991 }, + {'t': datetime.datetime(2013, 8, 21), 'id': + '01868c1a2aaf416ea6cd8edd65e7a4b8', 'v': 4164}, + {'t': datetime.datetime(2013, 8, 22), 'id': + '38268c3b231f1266a392931e15e99231', 'v': 7313}, + {'t': datetime.datetime(2013, 8, 23), 'key': + 'your-custom-key', 'v': 55423}, + {'t': datetime.datetime(2013, 8, 24), 'key': 'foo', 'v': 324991}, ] result = self.client.write_multi(data)