class CustomerIOWrapper(object): BASE_URL = 'https://beta-api.customer.io/v1/api' def __init__(self, site_id, api_key): self.site_id = site_id self.api_key = api_key self.cio = CustomerIO(site_id, api_key) def fetch(self, uri, payload): response = requests.get( self.BASE_URL + uri, data=payload, auth=(self.site_id, self.api_key), ) return response def get_messages(self, message_type=None): payload = { 'type': message_type, } response = self.fetch('/messages', payload) data = json.loads(response.content) return data.get('messages') def remove_bulk(self, filename): with open(filename) as f: reader = csv.DictReader(f, delimiter=',') items = list(reader) item_count = len(items) if not items: print(f'Error: no rows to in the file: {filename}') return False if 'id' not in items[0]: print(f'Error: id column not exists in the file: {filename}') return False for idx, item in enumerate(items): if 'id' not in item: continue try: self.cio.delete(customer_id=item['id']) item_label = item['email'] if 'email' in item else item[ 'id'] item_percent = round((idx / item_count) * 100, 2) print( f'{item_percent}% DELETE ({idx} of {item_count}): {item_label}' ) except Exception as e: print(f'Exception: {e}') return True
class CIOApi(object): """Wrapper for CustomerIO python library.""" def __init__(self): super(CIOApi, self).__init__() self.cio = CustomerIO(settings.CUSTOMERIO_SITE_ID, settings.CUSTOMERIO_API_KEY) def api_call(func): """Wrapper for API cals.""" def wrapped_func(*args, **kwargs): args_str = u', '.join(unicode(a) for a in args[1:]) args_str += u', '.join(unicode(v) for v in kwargs.values()) logger.debug(u'CustomerIO api call: %s %s' % (func.__name__, args_str)) if not settings.CUSTOMERIO_ENABLED: return None try: return func(*args, **kwargs) except Exception as e: logger.exception(e) return wrapped_func @api_call def create_or_update(self, membership): self.cio.identify( id=membership.id, email=membership.user.email, created_at=membership.user.date_joined, last_login=membership.last_login, short_name=membership.get_short_name(), full_name=membership.get_full_name(), role=membership.get_role_display(), account_name=membership.account.name, account_plan=membership.account.plan.get_name_display(), account_is_trial=membership.account.is_trial(), account_is_active=membership.account.is_active, account_date_cancel=membership.account.date_cancel, account_date_created=membership.account.date_created, ) @api_call def track_event(self, membership, name, **kwargs): self.cio.track(customer_id=membership.id, name=name, **kwargs) @api_call def delete(self, membership): self.cio.delete(customer_id=membership.id)
class TestCustomerIO(HTTPSTestCase): '''Starts server which the client connects to in the following tests''' def setUp(self): self.cio = CustomerIO( site_id='siteid', api_key='apikey', host=self.server.server_address[0], port=self.server.server_port, retries=5, backoff_factor=0) # do not verify the ssl certificate as it is self signed # should only be done for tests self.cio.http.verify = False def _check_request(self, resp, rq, *args, **kwargs): request = resp.request self.assertEqual(request.method, rq['method']) self.assertEqual(json.loads(request.body.decode('utf-8')), rq['body']) self.assertEqual(request.headers['Authorization'], rq['authorization']) self.assertEqual(request.headers['Content-Type'], rq['content_type']) self.assertEqual(int(request.headers['Content-Length']), len(json.dumps(rq['body']))) self.assertTrue(request.url.endswith(rq['url_suffix']), 'url: {} expected suffix: {}'.format(request.url, rq['url_suffix'])) def test_client_connection_handling(self): retries = self.cio.retries # should not raise exception as i should be less than retries and # therefore the last request should return a valid response for i in range(retries): self.cio.identify(i, fail_count=i) # should raise expection as we get invalid responses for all retries with self.assertRaises(CustomerIOException): self.cio.identify(retries, fail_count=retries) def test_identify_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1', 'body': {"name": "john", "email": "*****@*****.**"}, })) self.cio.identify(id=1, name='john', email='*****@*****.**') with self.assertRaises(TypeError): self.cio.identify(random_attr="some_value") def test_track_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/events', 'body': {"data": {"email": "*****@*****.**"}, "name": "sign_up"}, })) self.cio.track(customer_id=1, name='sign_up', email='*****@*****.**') with self.assertRaises(TypeError): self.cio.track(random_attr="some_value") def test_pageview_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/events', 'body': {"data": {"referer": "category_1"}, "type": "page", "name": "product_1"}, })) self.cio.pageview(customer_id=1, page='product_1', referer='category_1') with self.assertRaises(TypeError): self.cio.pageview(random_attr="some_value") def test_delete_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'DELETE', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1', 'body': {}, })) self.cio.delete(customer_id=1) with self.assertRaises(TypeError): self.cio.delete(random_attr="some_value") def test_backfill_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/events', 'body': {"timestamp": 1234567890, "data": {"email": "*****@*****.**"}, "name": "signup"}, })) self.cio.backfill(customer_id=1, name='signup', timestamp=1234567890, email='*****@*****.**') with self.assertRaises(TypeError): self.cio.backfill(random_attr="some_value") def test_base_url(self): test_cases = [ # host, port, prefix, result (None, None, None, 'https://track.customer.io/api/v1'), (None, None, 'v2', 'https://track.customer.io/v2'), (None, None, '/v2/', 'https://track.customer.io/v2'), ('sub.domain.com', 1337, '/v2/', 'https://sub.domain.com:1337/v2'), ('/sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'), ('http://sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'), ] for host, port, prefix, result in test_cases: cio = CustomerIO(host=host, port=port, url_prefix=prefix) self.assertEqual(cio.base_url, result) def test_device_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_1", "platform":"ios"}} })) self.cio.add_device(customer_id=1, device_id="device_1", platform="ios") with self.assertRaises(TypeError): self.cio.add_device(random_attr="some_value") def test_device_call_last_used(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_2", "platform": "android", "last_used": 1234567890}} })) self.cio.add_device(customer_id=1, device_id="device_2", platform="android", last_used=1234567890) def test_device_call_valid_platform(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_3", "platform": "notsupported"}} })) with self.assertRaises(CustomerIOException): self.cio.add_device(customer_id=1, device_id="device_3", platform=None) def test_device_call_has_customer_id(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_4", "platform": "ios"}} })) with self.assertRaises(CustomerIOException): self.cio.add_device(customer_id="", device_id="device_4", platform="ios") def test_device_call_has_device_id(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_5", "platform": "ios"}} })) with self.assertRaises(CustomerIOException): self.cio.add_device(customer_id=1, device_id="", platform="ios") def test_device_delete_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'DELETE', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices/device_1', 'body': {} })) self.cio.delete_device(customer_id=1, device_id="device_1") with self.assertRaises(TypeError): self.cio.delete_device(random_attr="some_value") def test_suppress_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/suppress', 'body': {}, })) self.cio.suppress(customer_id=1) with self.assertRaises(CustomerIOException): self.cio.suppress(None) def test_unsuppress_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/unsuppress', 'body': {}, })) self.cio.unsuppress(customer_id=1) with self.assertRaises(CustomerIOException): self.cio.unsuppress(None) @unittest.skipIf(sys.version_info.major > 2, "python2 specific test case") def test_sanitize_py2(self): data_in = dict(dt=datetime.fromtimestamp(1234567890)) data_out = self.cio._sanitize(data_in) self.assertEqual(data_out, dict(dt=1234567890)) @unittest.skipIf(sys.version_info.major < 3, "python3 specific test case") def test_sanitize_py3(self): from datetime import timezone data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc)) data_out = self.cio._sanitize(data_in) self.assertEqual(data_out, dict(dt=1234567890))
class TestCustomerIO(HTTPSTestCase): '''Starts server which the client connects to in the following tests''' def setUp(self): self.cio = CustomerIO( site_id='siteid', api_key='apikey', host=self.server.server_address[0], port=self.server.server_port, retries=5, backoff_factor=0) # do not verify the ssl certificate as it is self signed # should only be done for tests self.cio.http.verify = False def _check_request(self, resp, rq, *args, **kwargs): request = resp.request body = request.body.decode('utf-8') if isinstance(request.body, bytes) else request.body self.assertEqual(request.method, rq['method']) self.assertEqual(json.loads(body), rq['body']) self.assertEqual(request.headers['Authorization'], rq['authorization']) self.assertEqual(request.headers['Content-Type'], rq['content_type']) self.assertEqual(int(request.headers['Content-Length']), len(json.dumps(rq['body']))) self.assertTrue(request.url.endswith(rq['url_suffix']), 'url: {} expected suffix: {}'.format(request.url, rq['url_suffix'])) def test_client_connection_handling(self): retries = self.cio.retries # should not raise exception as i should be less than retries and # therefore the last request should return a valid response for i in range(retries): self.cio.identify(i, fail_count=i) # should raise expection as we get invalid responses for all retries with self.assertRaises(CustomerIOException): self.cio.identify(retries, fail_count=retries) def test_identify_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1', 'body': {"name": "john", "email": "*****@*****.**"}, })) self.cio.identify(id=1, name='john', email='*****@*****.**') with self.assertRaises(TypeError): self.cio.identify(random_attr="some_value") def test_track_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/events', 'body': {"data": {"email": "*****@*****.**"}, "name": "sign_up"}, })) self.cio.track(customer_id=1, name='sign_up', email='*****@*****.**') with self.assertRaises(TypeError): self.cio.track(random_attr="some_value") def test_pageview_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/events', 'body': {"data": {"referer": "category_1"}, "type": "page", "name": "product_1"}, })) self.cio.pageview(customer_id=1, page='product_1', referer='category_1') with self.assertRaises(TypeError): self.cio.pageview(random_attr="some_value") def test_delete_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'DELETE', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1', 'body': {}, })) self.cio.delete(customer_id=1) with self.assertRaises(TypeError): self.cio.delete(random_attr="some_value") def test_backfill_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/events', 'body': {"timestamp": 1234567890, "data": {"email": "*****@*****.**"}, "name": "signup"}, })) self.cio.backfill(customer_id=1, name='signup', timestamp=1234567890, email='*****@*****.**') with self.assertRaises(TypeError): self.cio.backfill(random_attr="some_value") def test_base_url(self): test_cases = [ # host, port, prefix, result (None, None, None, 'https://track.customer.io/api/v1'), (None, None, 'v2', 'https://track.customer.io/v2'), (None, None, '/v2/', 'https://track.customer.io/v2'), ('sub.domain.com', 1337, '/v2/', 'https://sub.domain.com:1337/v2'), ('/sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'), ('http://sub.domain.com/', 1337, '/v2/', 'https://sub.domain.com:1337/v2'), ] for host, port, prefix, result in test_cases: cio = CustomerIO(host=host, port=port, url_prefix=prefix) self.assertEqual(cio.base_url, result) def test_device_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_1", "platform":"ios"}} })) self.cio.add_device(customer_id=1, device_id="device_1", platform="ios") with self.assertRaises(TypeError): self.cio.add_device(random_attr="some_value") def test_device_call_last_used(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_2", "platform": "android", "last_used": 1234567890}} })) self.cio.add_device(customer_id=1, device_id="device_2", platform="android", last_used=1234567890) def test_device_call_valid_platform(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_3", "platform": "notsupported"}} })) with self.assertRaises(CustomerIOException): self.cio.add_device(customer_id=1, device_id="device_3", platform=None) def test_device_call_has_customer_id(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_4", "platform": "ios"}} })) with self.assertRaises(CustomerIOException): self.cio.add_device(customer_id="", device_id="device_4", platform="ios") def test_device_call_has_device_id(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'PUT', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices', 'body': {"device": {"id": "device_5", "platform": "ios"}} })) with self.assertRaises(CustomerIOException): self.cio.add_device(customer_id=1, device_id="", platform="ios") def test_device_delete_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'DELETE', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/devices/device_1', 'body': {} })) self.cio.delete_device(customer_id=1, device_id="device_1") with self.assertRaises(TypeError): self.cio.delete_device(random_attr="some_value") def test_suppress_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/suppress', 'body': {}, })) self.cio.suppress(customer_id=1) with self.assertRaises(CustomerIOException): self.cio.suppress(None) def test_unsuppress_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/customers/1/unsuppress', 'body': {}, })) self.cio.unsuppress(customer_id=1) with self.assertRaises(CustomerIOException): self.cio.unsuppress(None) def test_add_to_segment_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/segments/1/add_customers', 'body': {'ids': ['1','2','3']}, })) self.cio.add_to_segment(segment_id=1, customer_ids=[1,2,3]) with self.assertRaises(CustomerIOException): self.cio.add_to_segment(None, None) with self.assertRaises(CustomerIOException): self.cio.add_to_segment(segment_id=1, customer_ids=False) with self.assertRaises(CustomerIOException): self.cio.add_to_segment(segment_id=1, customer_ids=[False,True,False]) def test_remove_from_segment_call(self): self.cio.http.hooks=dict(response=partial(self._check_request, rq={ 'method': 'POST', 'authorization': _basic_auth_str('siteid', 'apikey'), 'content_type': 'application/json', 'url_suffix': '/segments/1/remove_customers', 'body': {'ids': ['1','2','3']}, })) self.cio.remove_from_segment(segment_id=1, customer_ids=[1,2,3]) with self.assertRaises(CustomerIOException): self.cio.remove_from_segment(None, None) with self.assertRaises(CustomerIOException): self.cio.add_to_segment(segment_id=1, customer_ids=False) with self.assertRaises(CustomerIOException): self.cio.add_to_segment(segment_id=1, customer_ids=[False,True,False]) @unittest.skipIf(sys.version_info.major > 2, "python2 specific test case") def test_sanitize_py2(self): data_in = dict(dt=datetime.fromtimestamp(1234567890)) data_out = self.cio._sanitize(data_in) self.assertEqual(data_out, dict(dt=1234567890)) @unittest.skipIf(sys.version_info.major < 3, "python3 specific test case") def test_sanitize_py3(self): from datetime import timezone data_in = dict(dt=datetime(2009, 2, 13, 23, 31, 30, 0, timezone.utc)) data_out = self.cio._sanitize(data_in) self.assertEqual(data_out, dict(dt=1234567890))