class CustomerIOBackend(BaseBackend): template_name = 'customerevents/customerio.html' def __init__(self, SITE_ID, API_KEY, **kwargs): self.site_id = SITE_ID self.connection = CustomerIO(SITE_ID, API_KEY) super(CustomerIOBackend, self).__init__(**kwargs) def get_context(self, identity, properties, events, **kwargs): if identity.startswith('session:'): raise NotImplementedError #anonymous sessions are not implemented context = { 'identify': { 'id': identity }, 'site_id': self.site_id, 'tracks': list() } context['identify'].update(properties) context['identify'] = json.dumps(context['identify']) context.update(kwargs) for event_name, event_properties in events: context['tracks'].append((event_name, json.dumps(event_properties))) return context def send(self, identity, properties, aliases, events, request_meta): if identity.startswith('session:'): #ignore anonymous sessions return self.connection.identify(id=identity, **properties) for event_name, event_properties in events: ep = dict(event_properties) ep.update({'customer_id': identity, 'name': event_name}) self.connection.track(**ep)
class CustomerIoIdentifyOperator(BaseOperator): """ Send Track Event to Segment for a specified user_id and event :param csv_file: a CSV (plain or zipped) path with data to be used as event properties. One of them *must* be called 'userId' (templated) :type csv_file: str :param customerio_site_id: The 'site_id' property from Customer.IO account we want to update :type customerio_site_id: str :param customerio_api_key: The 'api_key' property from Customer.IO account we want to update :type customerio_api_key: str """ template_fields = ('csv_file', 'event') ui_color = '#ffd700' @apply_defaults def __init__(self, csv_file, customerio_site_id, customerio_api_key, *args, **kwargs): super().__init__(*args, **kwargs) self.csv_file = csv_file self.cio = CustomerIO(site_id=customerio_site_id, api_key=customerio_api_key) def execute(self, context): if self.csv_file.endswith('.gz'): file_reader = gzip.open(self.csv_file, "rt", newline="") else: file_reader = open(self.csv_file, 'r') csv_reader = csv.DictReader(file_reader) for row in csv_reader: # converts a csv row into a map: header1 -> value, header2 -> value... props = dict(row) user_id = props.pop('userId', None) if user_id is None: self.log.info('No userId set in CSV row: %s >>> Skipping.', props) continue # fixing numerics types set as strings from csv clean_props = dict() for key in props: clean_props[key] = retype(props.get(key)) self.log.info('Sending identify for userId %s with properties: %s', user_id, clean_props) self.cio.identify(id=user_id, **clean_props)
def test_client_connection_handling(self): retries = 5 cio = CustomerIO(site_id="siteid", api_key="apikey", host=self.server.server_address[0], port=self.server.server_port, retries=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 xrange(retries): cio.identify(i, fail_count=i) # should raise expection as we get invalid responses for all retries with self.assertRaises(CustomerIOException): cio.identify(retries, fail_count=retries)
def test_client_connection_handling(self): retries = 5 cio = CustomerIO( site_id="siteid", api_key="apikey", host=self.server.server_address[0], port=self.server.server_port, retries=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 xrange(retries): cio.identify(i, fail_count=i) # should raise expection as we get invalid responses for all retries with self.assertRaises(CustomerIOException): cio.identify(retries, fail_count=retries)
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))
def deploy(self): instance = self._get_pusher_instance() cio = CustomerIO(settings.CUSTOMERIO_SITE_ID, settings.CUSTOMERIO_API_KEY) cio.identify(id=self.email, email=self.email) instance[self.deploy_id].trigger('info_update', { 'message': "Creating a new container...", 'percent': 30 }) headers = { 'content-type': 'application/json', } # run the container ports = self.project.ports.split(' ') hostnames = self.project.hostnames.split(' ') payload = { "image": self.project.image_name, "hosts": ["/api/v1/hosts/1/"], "ports": ports, "command": "", "links": "", "memory": "", "environment": self.project.env_vars, } if "edx" in self.project.name.lower(): edx_env = [] edx_env.append("EDX_LMS_BASE=lms-{0}.demo.appsembler.com".format(self.deploy_id)) edx_env.append("EDX_PREVIEW_LMS_BASE=lms-{0}.demo.appsembler.com".format(self.deploy_id)) edx_env.append("EDX_CMS_BASE=cms-{0}.demo.appsembler.com".format(self.deploy_id)) edx_env.append("INTERCOM_APP_ID={0}".format(settings.INTERCOM_EDX_APP_ID)) edx_env.append("INTERCOM_APP_SECRET={0}".format(settings.INTERCOM_EDX_APP_SECRET)) edx_env.append("INTERCOM_USER_EMAIL={0}".format(self.email)) env_string = " ".join(edx_env) env_string = " " + env_string payload['environment'] += env_string r = requests.post( "{0}/api/v1/containers/?username={1}&api_key={2}".format(settings.SHIPYARD_HOST, settings.SHIPYARD_USER, settings.SHIPYARD_KEY), data=json.dumps(payload), headers=headers ) if r.status_code == 201: # This sleep is needed to avoid problems with the API time.sleep(3) container_uri = urlparse(r.headers['location']).path self.remote_container_id = container_uri.split('/')[-2] # create the app (for dynamic routing) instance[self.deploy_id].trigger('info_update', { 'message': "Assigning an URL to the app...", 'percent': 60 }) time.sleep(2) app_ids = [] domains = [] for port, hostname in zip(ports, hostnames): domain_name = "{0}.demo.appsembler.com".format(self.deploy_id) if hostname: domain_name = "{0}-{1}".format(hostname, domain_name) domains.append(domain_name) payload = { "name": self.deploy_id, "description": "{0} for {1}".format(self.project.name, self.email), "domain_name": domain_name, "backend_port": port, "protocol": "https" if "443" in self.project.ports else "http", "containers": [container_uri] } r = requests.post( "{0}/api/v1/applications/?username={1}&api_key={2}".format(settings.SHIPYARD_HOST, settings.SHIPYARD_USER, settings.SHIPYARD_KEY), data=json.dumps(payload), headers=headers ) if r.status_code == 201: app_uri = urlparse(r.headers['location']).path app_ids.append(app_uri.split('/')[-2]) time.sleep(2) self.remote_app_id = " ".join(app_ids) status = r.status_code time.sleep(1) instance[self.deploy_id].trigger('info_update', { 'message': "Getting information...", 'percent': 90 }) time.sleep(1) if status == 201: scheme = "https" if "443" in self.project.ports else "http" app_urls = [] for domain in domains: app_url = "{0}://{1}".format(scheme, domain) app_urls.append(app_url) self.url = " ".join(app_urls) self.status = 'Completed' self.launch_time = timezone.now() self.expiration_time = self.expiration_datetime() instance[self.deploy_id].trigger('deployment_complete', { 'app_name': self.project.name, 'message': "Deployment complete!", 'app_url': self.url, 'username': self.project.default_username, 'password': self.project.default_password }) Event.create( event_name="deployed_app", email=self.email, metadata={ 'app_name': self.project.name, 'app_url': self.url, 'deploy_id': self.deploy_id, } ) if self.email: cio.track(customer_id=self.email, name='app_deploy_complete', app_url=self.url.replace(" ", "\n"), app_name=self.project.name, status_url="http://launcher.appsembler.com" + reverse('deployment_detail', kwargs={'deploy_id': self.deploy_id}), trial_duration=self.project.trial_duration, username=self.project.default_username, password=self.project.default_password ) else: self.status = 'Failed' error_log = DeploymentErrorLog(deployment=self, http_status=status, error_log=r.text) error_log.save() send_mail( "Deployment failed: {0}".format(self.deploy_id), "Error log link: {0}".format(reverse('admin:deployment_deploymenterrorlog_change', args=(error_log.id,))), '*****@*****.**', ['*****@*****.**', '*****@*****.**'] ) instance[self.deploy_id].trigger('deployment_failed', { 'message': "Deployment failed!", }) self.save()
try: import sys sys.path.insert(0, '/Users/nick/dev/theconversation') except: print "could not import -- must be running on heroku" from customerio import CustomerIO import settings from lib import userdb cio = CustomerIO(settings.get('customer_io_site_id'), settings.get('customer_io_api_key')) users = userdb.get_all() for user_info in users: if user_info and 'user' in user_info.keys() and 'email_address' in user_info.keys() and user_info['email_address'] != "": cio.identify(id=user_info['user']['username'], email=user_info['email_address'], name=user_info['user']['fullname']) print "added @" + user_info['user']['username']
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))
class MixpanelTrack: """Wrapper around the official `mixpanel` server-side integration for Mixpanel. You can track events and/or set people profiles. Uses https://pypi.python.org/pypi/mixpanel under the hood. Prepared as `request.mixpanel` for easy handling. """ events: Events event_properties: EventProperties global_event_props: PropertiesType profile_properties: ProfileProperties profile_meta_properties: ProfileMetaProperties @staticmethod def _resolve_events(dotted_name: t.Optional[object] = None) -> Events: """Resolve a dotted-name into an Events object.""" if not dotted_name: return Events() if not isinstance(dotted_name, str): raise ValueError( f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}" ) else: resolved = DottedNameResolver().resolve(dotted_name) if not issubclass(resolved, Events): raise ValueError( "class in dotted_name needs to be based on pyramid_mixpanel.Events" ) return resolved() @staticmethod def _resolve_event_properties( dotted_name: t.Optional[object] = None, ) -> EventProperties: """Resolve a dotted-name into an EventProperties object.""" if not dotted_name: return EventProperties() if not isinstance(dotted_name, str): raise ValueError( f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}" ) else: resolved = DottedNameResolver().resolve(dotted_name) if not issubclass(resolved, EventProperties): raise ValueError( "class in dotted_name needs to be based on pyramid_mixpanel.EventProperties" ) return resolved() @staticmethod def _resolve_profile_properties( dotted_name: t.Optional[object] = None, ) -> ProfileProperties: """Resolve a dotted-name into an ProfileProperties object.""" if not dotted_name: return ProfileProperties() if not isinstance(dotted_name, str): raise ValueError( f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}" ) else: resolved = DottedNameResolver().resolve(dotted_name) if not issubclass(resolved, ProfileProperties): raise ValueError( "class in dotted_name needs to be based on pyramid_mixpanel.ProfileProperties" ) return resolved() @staticmethod def _resolve_profile_meta_properties( dotted_name: t.Optional[object] = None, ) -> ProfileMetaProperties: """Resolve a dotted-name into an ProfileMetaProperties object.""" if not dotted_name: return ProfileMetaProperties() if not isinstance(dotted_name, str): raise ValueError( f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}" ) else: resolved = DottedNameResolver().resolve(dotted_name) if not issubclass(resolved, ProfileMetaProperties): raise ValueError( "class in dotted_name needs to be based on pyramid_mixpanel.ProfileMetaProperties" ) return resolved() @staticmethod def _resolve_consumer(dotted_name: t.Optional[object] = None, use_structlog: t.Optional[bool] = False) -> Consumer: """Resolve a dotted-name into a Consumer object.""" if not dotted_name: return PoliteBufferedConsumer(use_structlog) if not isinstance(dotted_name, str): raise ValueError( f"dotted_name must be a string, but it is: {dotted_name.__class__.__name__}" ) else: resolved = DottedNameResolver().resolve(dotted_name) if not (issubclass(resolved, Consumer) or issubclass(resolved, BufferedConsumer)): raise ValueError( "class in dotted_name needs to be based on mixpanel.(Buffered)Consumer" ) return resolved() def __init__(self, settings: SettingsType, distinct_id=None, global_event_props=None) -> None: """Initialize API connector.""" self.distinct_id = distinct_id self.events = self._resolve_events(settings.get("mixpanel.events")) self.event_properties = self._resolve_event_properties( settings.get("mixpanel.event_properties")) self.profile_properties = self._resolve_profile_properties( settings.get("mixpanel.profile_properties")) self.profile_meta_properties = self._resolve_profile_meta_properties( settings.get("mixpanel.profile_meta_properties")) use_structlog = settings.get("pyramid_heroku.structlog", False) is True consumer = self._resolve_consumer(settings.get("mixpanel.consumer"), use_structlog) if settings.get("mixpanel.token"): self.api = Mixpanel(token=settings["mixpanel.token"], consumer=consumer) else: self.api = Mixpanel(token="testing", consumer=MockedConsumer()) # nosec if global_event_props: self.global_event_props = global_event_props else: self.global_event_props = {} if (settings.get("customerio.tracking.site_id") and settings.get("customerio.tracking.api_key") and settings.get("customerio.tracking.region")): # This is here because customerio support is an install extra, # i.e. it is optional from customerio import CustomerIO from customerio import Regions if settings["customerio.tracking.region"] == "eu": region = Regions.EU elif settings["customerio.tracking.region"] == "us": region = Regions.US else: raise ValueError("Unknown customer.io region") self.cio = CustomerIO( settings["customerio.tracking.site_id"], settings["customerio.tracking.api_key"], region=region, ) else: self.cio = None @distinct_id_is_required def track(self, event: Event, props: t.Optional[PropertiesType] = None) -> None: """Track a Mixpanel event.""" if event not in self.events.__dict__.values(): raise ValueError(f"Event '{event}' is not a member of self.events") if props: props = {**self.global_event_props, **props} else: props = self.global_event_props for prop in props: if prop not in self.event_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.event_properties" ) self.api.track( self.distinct_id, event.name, {prop.name: value for (prop, value) in props.items()}, ) if self.cio: msg = { "customer_id": self.distinct_id, "name": event.name, **{ prop.name.replace("$", ""): value for (prop, value) in props.items() }, } if self.api._consumer.__class__ == MockedConsumer: self.api._consumer.mocked_messages.append( MockedMessage(endpoint="customer.io", msg=msg)) else: self.cio.track(**msg) @distinct_id_is_required def profile_set(self, props: PropertiesType, meta: t.Optional[PropertiesType] = None) -> None: """Set properties to a Profile. This creates a profile if one does not yet exist. Use `meta` to override are Mixpanel special properties, such as $ip. """ if not meta: meta = {} for prop in props: if prop not in self.profile_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_properties" ) for prop in meta: if prop not in self.profile_meta_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_meta_properties" ) # mixpanel and customerio expect different date formats # so we have to save the props here so we can format them # differently later on in the `if self.cio:` block customerio_props = deepcopy(props) for (prop, value) in props.items(): if isinstance(value, datetime): props[prop] = value.isoformat() self.api.people_set( self.distinct_id, {prop.name: value for (prop, value) in props.items()}, {prop.name: value for (prop, value) in meta.items()}, ) if self.cio: # customer.io expects dates in unix/epoch format for prop, value in customerio_props.items(): if isinstance(value, datetime): customerio_props[prop] = round(value.timestamp()) # customer.io expects created timestamp as `created_at` if customerio_props.get(Property("$created")): customerio_props[Property("created_at")] = customerio_props[ Property("$created")] del customerio_props[Property("$created")] msg = { "id": self.distinct_id, **{ prop.name.replace("$", ""): value for (prop, value) in customerio_props.items() }, **{ prop.name.replace("$", ""): value for (prop, value) in meta.items() }, } if self.api._consumer.__class__ == MockedConsumer: self.api._consumer.mocked_messages.append( MockedMessage(endpoint="customer.io", msg=msg)) else: self.cio.identify(**msg) @distinct_id_is_required def people_append(self, props: PropertiesType, meta: t.Optional[PropertiesType] = None) -> None: """Wrap around api.people_append to set distinct_id.""" if not meta: meta = {} for prop in props: if prop not in self.profile_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_properties" ) for prop in meta: if prop not in self.profile_meta_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_meta_properties" ) self.api.people_append( self.distinct_id, {prop.name: value for (prop, value) in props.items()}, {prop.name: value for (prop, value) in meta.items()}, ) @distinct_id_is_required def people_union(self, props: PropertiesType, meta: t.Optional[PropertiesType] = None) -> None: """Wrap around api.people_union to set properties.""" if not meta: meta = {} for prop in props: if prop not in self.profile_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_properties" ) if not isinstance(props[prop], list): raise TypeError(f"Property '{prop}' value is not a list") for prop in meta: if prop not in self.profile_meta_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_meta_properties" ) if not isinstance(meta[prop], list): raise TypeError(f"Property '{prop}' value is not a list") self.api.people_union( self.distinct_id, {prop.name: value for (prop, value) in props.items()}, {prop.name: value for (prop, value) in meta.items()}, ) @distinct_id_is_required def profile_increment(self, props: t.Dict[Property, int]) -> None: """Wrap around api.people_increment to set distinct_id.""" for prop in props: if prop not in self.profile_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_properties" ) self.api.people_increment( self.distinct_id, {prop.name: value for (prop, value) in props.items()}) @distinct_id_is_required def profile_track_charge( self, amount: int, props: t.Optional[t.Dict[Property, str]] = None) -> None: """Wrap around api.people_track_charge to set distinct_id.""" if not props: props = {} for prop in props: if prop not in self.profile_properties.__dict__.values(): raise ValueError( f"Property '{prop}' is not a member of self.profile_properties" ) self.api.people_track_charge( self.distinct_id, amount, {prop.name: value for (prop, value) in props.items()}, )