From 4b0c97262851c5ce754bd3287c9a5ea41901a69e Mon Sep 17 00:00:00 2001 From: Parnell Springmeyer Date: Tue, 14 Jan 2014 16:16:39 -0600 Subject: [PATCH 1/4] Switching to urllib3 --- mixpanel/__init__.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/mixpanel/__init__.py b/mixpanel/__init__.py index 692c2f3..0f42090 100644 --- a/mixpanel/__init__.py +++ b/mixpanel/__init__.py @@ -1,8 +1,8 @@ import base64 import json import time -import urllib -import urllib2 +import urllib.parse +import urllib3 ''' The mixpanel package allows you to easily track events and @@ -17,7 +17,9 @@ VERSION = '3.0.0' + class Mixpanel(object): + ''' Use instances of Mixpanel to track events and send Mixpanel profile updates from your python code. @@ -33,8 +35,10 @@ def __init__(self, token, consumer=None): provided, Mixpanel will use the default Consumer, which communicates one synchronous request for every message. """ + self._token = token self._consumer = consumer or Consumer() + self.http = urllib3.PoolManager() def _now(self): return time.time() @@ -58,7 +62,7 @@ def track(self, distinct_id, event_name, properties={}, meta={}): }) """ all_properties = { - 'token' : self._token, + 'token': self._token, 'distinct_id': distinct_id, 'time': int(self._now()), 'mp_lib': 'python', @@ -228,7 +232,9 @@ def people_update(self, message, meta={}): record.update(meta) self._consumer.send('people', json.dumps(record)) + class MixpanelException(Exception): + ''' MixpanelExceptions will be thrown if the server can't recieve our events or updates for some reason- for example, if we can't @@ -236,12 +242,15 @@ class MixpanelException(Exception): ''' pass + class Consumer(object): + ''' The simple consumer sends an HTTP request directly to the Mixpanel service, with one request for every call. This is the default consumer for Mixpanel objects- if you don't provide your own, you get one of these. ''' + def __init__(self, events_url=None, people_url=None): self._endpoints = { 'events': events_url or 'https://api.mixpanel.com/track', @@ -267,31 +276,36 @@ def send(self, endpoint, json_message): if endpoint in self._endpoints: self._write_request(self._endpoints[endpoint], json_message) else: - raise MixpanelException('No such endpoint "{0}". Valid endpoints are one of {1}'.format(self._endpoints.keys())) + raise MixpanelException( + 'No such endpoint "{0}". Valid endpoints are one of {1}'.format(self._endpoints.keys())) def _write_request(self, request_url, json_message): - data = urllib.urlencode({ + data = urllib.parse.urlencode({ 'data': base64.b64encode(json_message), - 'verbose':1, - 'ip':0, + 'verbose': 1, + 'ip': 0, }) try: - request = urllib2.Request(request_url, data) - response = urllib2.urlopen(request).read() - except urllib2.HTTPError as e: + req = self.http.request("POST", request_url, data) + response = req.data + except urllib3.exceptions.HTTPError as e: raise MixpanelException(e) try: response = json.loads(response) except ValueError: - raise MixpanelException('Cannot interpret Mixpanel server response: {0}'.format(response)) + raise MixpanelException( + 'Cannot interpret Mixpanel server response: {0}'.format(response)) if response['status'] != 1: - raise MixpanelException('Mixpanel error: {0}'.format(response['error'])) + raise MixpanelException( + 'Mixpanel error: {0}'.format(response['error'])) return True + class BufferedConsumer(object): + ''' BufferedConsumer works just like Consumer, but holds messages in memory and sends them in batches. This can save bandwidth and @@ -301,6 +315,7 @@ class BufferedConsumer(object): when you're sure you're done sending them. calls to flush() will send all remaining unsent events being held by the BufferedConsumer. ''' + def __init__(self, max_size=50, events_url=None, people_url=None): self._consumer = Consumer(events_url, people_url) self._buffers = { @@ -326,7 +341,8 @@ def send(self, endpoint, json_message): :raises: MixpanelException ''' if endpoint not in self._buffers: - raise MixpanelException('No such endpoint "{0}". Valid endpoints are one of {1}'.format(self._buffers.keys())) + raise MixpanelException( + 'No such endpoint "{0}". Valid endpoints are one of {1}'.format(self._buffers.keys())) buf = self._buffers[endpoint] buf.append(json_message) From b1f41287f0d2673b674e3140b5a90267684c7564 Mon Sep 17 00:00:00 2001 From: Parnell Springmeyer Date: Tue, 14 Jan 2014 16:21:12 -0600 Subject: [PATCH 2/4] 2to3 in setup.py and tried fixing tests, not quite there yet --- setup.py | 1 + tests.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index e13c0e3..857e069 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ url='https://github.com/mixpanel/mixpanel-python', description='Official Mixpanel library for Python', long_description=open('README.txt').read(), + use_2to3=True, classifiers=[ 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', diff --git a/tests.py b/tests.py index 1677030..1f52a9b 100755 --- a/tests.py +++ b/tests.py @@ -3,11 +3,11 @@ import contextlib import json import unittest -import urlparse +import urllib.parse try: from mock import Mock, patch except ImportError: - print 'mixpanel-python requires the mock package to run the test suite' + print('mixpanel-python requires the mock package to run the test suite') raise import mixpanel @@ -236,7 +236,7 @@ def test_buffer_hold_and_flush(self): def test_buffer_fills_up(self): with patch('urllib2.urlopen', return_value = self.mock) as urlopen: - for i in xrange(self.MAX_LENGTH - 1): + for i in range(self.MAX_LENGTH - 1): self.consumer.send('events', '"Event"') self.assertTrue(not self.mock.called) @@ -263,7 +263,7 @@ def _assertRequested(self, expect_url, expect_data): self.assertEqual(urlopen.call_count, 1) ((request,),_) = urlopen.call_args self.assertEqual(request.get_full_url(), expect_url) - data = urlparse.parse_qs(request.get_data()) + data = urllib.parse.parse_qs(request.get_data()) self.assertEqual(len(data['data']), 1) payload_encoded = data['data'][0] payload_json = base64.b64decode(payload_encoded) @@ -273,12 +273,12 @@ def _assertRequested(self, expect_url, expect_data): def test_track_functional(self): # XXX this includes $lib_version, which means the test breaks # every time we release. - expect_data = {u'event': {u'color': u'blue', u'size': u'big'}, u'properties': {u'mp_lib': u'python', u'token': u'12345', u'distinct_id': u'button press', u'$lib_version': unicode(mixpanel.VERSION), u'time': 1000}} + expect_data = {'event': {'color': 'blue', 'size': 'big'}, 'properties': {'mp_lib': 'python', 'token': '12345', 'distinct_id': 'button press', '$lib_version': str(mixpanel.VERSION), 'time': 1000}} with self._assertRequested('https://api.mixpanel.com/track', expect_data): self.mp.track('button press', {'size': 'big', 'color': 'blue'}) def test_people_set_functional(self): - expect_data = {u'$distinct_id': u'amq', u'$set': {u'birth month': u'october', u'favorite color': u'purple'}, u'$time': 1000000, u'$token': u'12345'} + expect_data = {'$distinct_id': 'amq', '$set': {'birth month': 'october', 'favorite color': 'purple'}, '$time': 1000000, '$token': '12345'} with self._assertRequested('https://api.mixpanel.com/engage', expect_data): self.mp.people_set('amq', {'birth month': 'october', 'favorite color': 'purple'}) From db52d47eedf20da55a64042186784b351d7a6906 Mon Sep 17 00:00:00 2001 From: Parnell Springmeyer Date: Tue, 14 Jan 2014 16:40:48 -0600 Subject: [PATCH 3/4] Moving the pool manager into the consumer --- mixpanel/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mixpanel/__init__.py b/mixpanel/__init__.py index 0f42090..3422018 100644 --- a/mixpanel/__init__.py +++ b/mixpanel/__init__.py @@ -38,7 +38,6 @@ def __init__(self, token, consumer=None): self._token = token self._consumer = consumer or Consumer() - self.http = urllib3.PoolManager() def _now(self): return time.time() @@ -256,6 +255,7 @@ def __init__(self, events_url=None, people_url=None): 'events': events_url or 'https://api.mixpanel.com/track', 'people': people_url or 'https://api.mixpanel.com/engage', } + self.http = urllib3.PoolManager() def send(self, endpoint, json_message): ''' @@ -281,7 +281,7 @@ def send(self, endpoint, json_message): def _write_request(self, request_url, json_message): data = urllib.parse.urlencode({ - 'data': base64.b64encode(json_message), + 'data': base64.b64encode(json_message.encode()), 'verbose': 1, 'ip': 0, }) From aebd07ff269282e187c885a85ada6e8cb9bbbfa5 Mon Sep 17 00:00:00 2001 From: Parnell Springmeyer Date: Tue, 14 Jan 2014 17:09:24 -0600 Subject: [PATCH 4/4] Fixing encoding / decoding issues --- mixpanel/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mixpanel/__init__.py b/mixpanel/__init__.py index 3422018..34b77ef 100644 --- a/mixpanel/__init__.py +++ b/mixpanel/__init__.py @@ -280,19 +280,20 @@ def send(self, endpoint, json_message): 'No such endpoint "{0}". Valid endpoints are one of {1}'.format(self._endpoints.keys())) def _write_request(self, request_url, json_message): - data = urllib.parse.urlencode({ + data = { 'data': base64.b64encode(json_message.encode()), 'verbose': 1, 'ip': 0, - }) + } try: - req = self.http.request("POST", request_url, data) + req = self.http.request("GET", request_url, data, headers={ + "Content-Type": "application/x-www-form-urlencoded"}) response = req.data except urllib3.exceptions.HTTPError as e: raise MixpanelException(e) try: - response = json.loads(response) + response = json.loads(response.decode()) except ValueError: raise MixpanelException( 'Cannot interpret Mixpanel server response: {0}'.format(response))