From fa838e50e5db7029c65bea1df2dc89964bcf5d55 Mon Sep 17 00:00:00 2001 From: Scott Vitale Date: Tue, 23 Oct 2018 15:36:12 -0600 Subject: [PATCH] Extend the default JSON serializer to support datetime instances at all levels. The existing _sanitize() method only cleaned datetime instances if they were root values in the request payload. By extending the default JSONEncoder class, we can easily support serialization of datetime values anywhere in the request payload structure (even several levels deep). This is particularly useful for calls to track(), where all extra event data is stored one level deeper in the payload. --- customerio/__init__.py | 51 +++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/customerio/__init__.py b/customerio/__init__.py index 252383d..eb16ce1 100644 --- a/customerio/__init__.py +++ b/customerio/__init__.py @@ -3,6 +3,7 @@ import math import time import warnings +import json from requests import Session from requests.adapters import HTTPAdapter @@ -19,6 +20,27 @@ class CustomerIOException(Exception): pass +class CustomJSONEncoder(json.JSONEncoder): + def _datetime_to_timestamp(self, dt): + if USE_PY3_TIMESTAMPS: + return int(dt.replace(tzinfo=timezone.utc).timestamp()) + else: + return int(time.mktime(dt.timetuple())) + + def default(self, obj): + ''' Add special handling for data types not supported by the standard json library. + + :param obj: Value being serialized to JSON + :return: The serialized value as a native python type. + ''' + if isinstance(obj, datetime): + return self._datetime_to_timestamp(obj) + if isinstance(obj, float) and math.isnan(obj): + return None + + return json.JSONEncoder.default(self, obj) + + class CustomerIO(object): def __init__(self, site_id=None, api_key=None, host=None, port=None, url_prefix=None, json_encoder=None, retries=3, timeout=10, backoff_factor=0.02): @@ -74,7 +96,12 @@ def send_request(self, method, url, data): '''Dispatches the request and returns a response''' try: - response = self.http.request(method, url=url, json=self._sanitize(data), timeout=self.timeout) + response = self.http.request( + method, + url=url, + data=json.dumps(data, cls=CustomJSONEncoder), + timeout=self.timeout, + ) except Exception as e: # Raise exception alerting user that the system might be # experiencing an outage and refer them to system status page. @@ -117,14 +144,6 @@ def backfill(self, customer_id, name, timestamp, **data): '''Backfill an event (track with timestamp) for a given customer_id''' url = self.get_event_query_string(customer_id) - if isinstance(timestamp, datetime): - timestamp = self._datetime_to_timestamp(timestamp) - elif not isinstance(timestamp, int): - try: - timestamp = int(timestamp) - except Exception as e: - raise CustomerIOException("{t} is not a valid timestamp ({err})".format(t=timestamp, err=e)) - post_data = { 'name': name, 'data': data, @@ -205,20 +224,6 @@ def remove_from_segment(self, segment_id, customer_ids): payload = {'ids': self._stringify_list(customer_ids)} self.send_request('POST', url, payload) - def _sanitize(self, data): - for k, v in data.items(): - if isinstance(v, datetime): - data[k] = self._datetime_to_timestamp(v) - if isinstance(v, float) and math.isnan(v): - data[k] = None - return data - - def _datetime_to_timestamp(self, dt): - if USE_PY3_TIMESTAMPS: - return int(dt.replace(tzinfo=timezone.utc).timestamp()) - else: - return int(time.mktime(dt.timetuple())) - def _stringify_list(self, customer_ids): customer_string_ids = [] for v in customer_ids: