Skip to content

Commit fd69623

Browse files
author
Joe Bowers
committed
Consolidate library into a single file
1 parent 430098c commit fd69623

5 files changed

Lines changed: 355 additions & 359 deletions

File tree

demo/subprocess_consumer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
import multiprocessing
33
import random
44

5-
from mixpanel import Mixpanel
6-
from mixpanel.consumer import BufferedConsumer
5+
from mixpanel import Mixpanel, BufferedConsumer
76

87
'''
98
As your application scales, it's likely you'll want to

mixpanel/__init__.py

Lines changed: 352 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,353 @@
1-
from .mixpanel import Mixpanel, VERSION
1+
import base64
2+
import json
3+
import time
4+
import urllib
5+
import urllib2
26

7+
'''
8+
The mixpanel package allows you to easily track events and
9+
update people properties from your python application.
10+
'''
11+
12+
VERSION = '2.0.0'
13+
14+
class Mixpanel(object):
15+
16+
def __init__(self, token, consumer=None):
17+
"""
18+
Creates a new Mixpanel object, which can be used for all tracking.
19+
20+
To use mixpanel, create a new Mixpanel object using your token.
21+
Use this object to start tracking.
22+
Takes in a user token and an optional base url. Base url defaults to
23+
https://api.mixpanel.com.
24+
Example:
25+
mp = Mixpanel('36ada5b10da39a1347559321baf13063')
26+
"""
27+
self._token = token
28+
self._consumer = consumer or Consumer()
29+
30+
def _now(self):
31+
return time.time()
32+
33+
def track(self, distinct_id, event_name, properties={}):
34+
"""
35+
Notes that an event has occurred, along with a distinct_id
36+
representing the source of that event (for example, a user id),
37+
an event name describing the event and a set of properties
38+
describing that event. Properties are provided as a Hash with
39+
string keys and strings, numbers or booleans as values.
40+
41+
# Track that user "12345"'s credit card was declined
42+
mp.track("12345", "Credit Card Declined")
43+
44+
# Properties describe the circumstances of the event,
45+
# or aspects of the source or user associated with the event
46+
mp.track("12345", "Welcome Email Sent", {
47+
'Email Template' => 'Pretty Pink Welcome',
48+
'User Sign-up Cohort' => 'July 2013'
49+
})
50+
"""
51+
all_properties = {
52+
'token' : self._token,
53+
'distinct_id': distinct_id,
54+
'time': int(self._now()),
55+
'mp_lib': 'python',
56+
'$lib_version': VERSION,
57+
}
58+
all_properties.update(properties)
59+
event = {
60+
'event': event_name,
61+
'properties': all_properties,
62+
}
63+
self._consumer.send('events', json.dumps(event))
64+
65+
def alias(self, alias_id, original):
66+
"""
67+
Gives custom alias to a people record.
68+
69+
Alias sends an update to our servers linking an existing distinct_id
70+
with a new id, so that events and profile updates associated with the
71+
new id will be associated with the existing user's profile and behavior.
72+
Example:
73+
mp.alias('amy@mixpanel.com', '13793')
74+
"""
75+
self.track(original, '$create_alias', {
76+
'distinct_id': original,
77+
'alias': alias_id,
78+
'token': self._token,
79+
})
80+
81+
def people_set(self, distinct_id, properties):
82+
"""
83+
Set properties of a people record.
84+
85+
Sets properties of a people record given in JSON object. If the profile
86+
does not exist, creates new profile with these properties.
87+
Example:
88+
mp.people_set('12345', {'Address': '1313 Mockingbird Lane',
89+
'Birthday': '1948-01-01'})
90+
"""
91+
return self.people_update({
92+
'$distinct_id': distinct_id,
93+
'$set': properties,
94+
})
95+
96+
def people_set_once(self, distinct_id, properties):
97+
"""
98+
Set immutable properties of a people record.
99+
100+
Sets properties of a people record given in JSON object. If the profile
101+
does not exist, creates new profile with these properties. Does not
102+
overwrite existing property values.
103+
Example:
104+
mp.people_set_once('12345', {'First Login': "2013-04-01T13:20:00"})
105+
"""
106+
return self.people_update({
107+
'$distinct_id': distinct_id,
108+
'$set_once': properties,
109+
})
110+
111+
def people_increment(self, distinct_id, properties):
112+
"""
113+
Increments/decrements numerical properties of people record.
114+
115+
Takes in JSON object with keys and numerical values. Adds numerical
116+
values to current property of profile. If property doesn't exist adds
117+
value to zero. Takes in negative values for subtraction.
118+
Example:
119+
mp.people_add('12345', {'Coins Gathered': 12})
120+
"""
121+
return self.people_update({
122+
'$distinct_id': distinct_id,
123+
'$add': properties,
124+
})
125+
126+
def people_append(self, distinct_id, properties):
127+
"""
128+
Appends to the list associated with a property.
129+
130+
Takes a JSON object containing keys and values, and appends each to a
131+
list associated with the corresponding property name. $appending to a
132+
property that doesn't exist will result in assigning a list with one
133+
element to that property.
134+
Example:
135+
mp.people_append('12345', { "Power Ups": "Bubble Lead" })
136+
"""
137+
return self.people_update({
138+
'$distinct_id': distinct_id,
139+
'$append': properties,
140+
})
141+
142+
def people_union(self, distinct_id, properties):
143+
"""
144+
Merges the values for a list associated with a property.
145+
146+
Takes a JSON object containing keys and list values. The list values in
147+
the request are merged with the existing list on the user profile,
148+
ignoring duplicate list values.
149+
Example:
150+
mp.people_union('12345', { "Items purchased": ["socks", "shirts"] } )
151+
"""
152+
return self.people_update({
153+
'$distinct_id': distinct_id,
154+
'$union': properties,
155+
})
156+
157+
def people_unset(self, distinct_id, properties):
158+
"""
159+
Removes properties from a profile.
160+
161+
Takes a JSON list of string property names, and permanently removes the
162+
properties and their values from a profile.
163+
Example:
164+
mp.people_unset('12345', ["Days Overdue"])
165+
"""
166+
return self.people_update({
167+
'$distinct_id': distinct_id,
168+
'$unset': properties,
169+
})
170+
171+
def people_delete(self, distinct_id):
172+
"""
173+
Permanently deletes a profile.
174+
175+
Permanently delete the profile from Mixpanel, along with all of its
176+
properties.
177+
Example:
178+
mp.people_delete('12345')
179+
"""
180+
return self.people_update({
181+
'$distinct_id': distinct_id,
182+
'$delete': "",
183+
})
184+
185+
def people_track_charge(self, distinct_id, amount, properties={}):
186+
"""
187+
Tracks a charge to a user.
188+
189+
Record that you have charged the current user a certain amount of
190+
money. Charges recorded with track_charge will appear in the Mixpanel
191+
revenue report.
192+
Example:
193+
#tracks a charge of $50 to user '1234'
194+
mp.track_charge('1234', 50)
195+
196+
#tracks a charge of $50 to user '1234' at a specific time
197+
mp.track_charge('1234', 50, {'$time': "2013-04-01T09:02:00"})
198+
"""
199+
properties.update({'$amount': amount})
200+
return self.people_append(distinct_id, {'$transactions': properties})
201+
202+
203+
"""
204+
Send a generic update to \Mixpanel people analytics.
205+
Caller is responsible for formatting the update message, as
206+
documented in the \Mixpanel HTTP specification, and passing
207+
the message as a dict to update. This
208+
method might be useful if you want to use very new
209+
or experimental features of people analytics from Ruby
210+
The \Mixpanel HTTP tracking API is documented at
211+
https://mixpanel.com/help/reference/http
212+
"""
213+
def people_update(self, message):
214+
record = {
215+
'$token': self._token,
216+
'$time': int(self._now() * 1000),
217+
}
218+
record.update(message)
219+
self._consumer.send('people', json.dumps(record))
220+
221+
222+
223+
class MixpanelException(Exception):
224+
'''
225+
MixpanelExceptions will be thrown if the server can't recieve
226+
our events or updates for some reason- for example, if we can't
227+
connect to the Internet.
228+
'''
229+
pass
230+
231+
class Consumer(object):
232+
'''
233+
The simple consumer sends an HTTP request directly to the Mixpanel service,
234+
with one request for every call. This is the default consumer for Mixpanel
235+
objects- if you don't provide your own, you get one of these.
236+
'''
237+
def __init__(self, events_url=None, people_url=None):
238+
self._endpoints = {
239+
'events': events_url or 'https://api.mixpanel.com/track',
240+
'people': people_url or 'https://api.mixpanel.com/people',
241+
}
242+
243+
def send(self, endpoint, json_message):
244+
'''
245+
Record an event or a profile update. Send is the only method
246+
associated with consumers. Will raise an exception if the endpoint
247+
doesn't exist, if the server is unreachable or for some reason
248+
can't process the message.
249+
250+
All you need to do to write your own consumer is to implement
251+
a send method of your own.
252+
253+
:param endpoint: One of 'events' or 'people', the Mixpanel endpoint for sending the data
254+
:type endpoint: str (one of 'events' or 'people')
255+
:param json_message: A json message formatted for the endpoint.
256+
:type json_message: str
257+
:raises: MixpanelException
258+
'''
259+
if endpoint in self._endpoints:
260+
self._write_request(self._endpoints[endpoint], json_message)
261+
else:
262+
raise MixpanelException('No such endpoint "{0}". Valid endpoints are one of {1}'.format(self._endpoints.keys()))
263+
264+
def _write_request(self, request_url, json_message):
265+
data = urllib.urlencode({'data': base64.b64encode(json_message),'verbose':1})
266+
try:
267+
request = urllib2.Request(request_url, data)
268+
response = urllib2.urlopen(request).read()
269+
except urllib2.HTTPError as e:
270+
raise MixpanelException(e)
271+
272+
try:
273+
response = json.loads(response)
274+
except ValueError:
275+
raise MixpanelException('Cannot interpret Mixpanel server response: {0}'.format(response))
276+
277+
if response['status'] != 1:
278+
raise MixpanelException('Mixpanel error: {0}'.format(response['error']))
279+
280+
return True
281+
282+
class BufferedConsumer(object):
283+
'''
284+
BufferedConsumer works just like Consumer, but holds messages in
285+
memory and sends them in batches. This can save bandwidth and
286+
reduce the total amount of time required to post your events.
287+
288+
Because BufferedConsumers hold events, you need to call flush()
289+
when you're sure you're done sending them. calls to flush() will
290+
send all remaining unsent events being held by the BufferedConsumer.
291+
'''
292+
def __init__(self, max_size=50, events_url=None, people_url=None):
293+
self._consumer = Consumer(events_url, people_url)
294+
self._buffers = {
295+
'events': [],
296+
'people': [],
297+
}
298+
self._max_size = min(50, max_size)
299+
300+
def send(self, endpoint, json_message):
301+
'''
302+
Record an event or a profile update. Calls to send() will store
303+
the given message in memory, and (when enough messages have been stored)
304+
may trigger a request to Mixpanel's servers.
305+
306+
Calls to send() may throw an exception, but the exception may be
307+
associated with the message given in an earlier call. If this is the case,
308+
the resulting MixpanelException e will have members e.message and e.endpoint
309+
310+
:param endpoint: One of 'events' or 'people', the Mixpanel endpoint for sending the data
311+
:type endpoint: str (one of 'events' or 'people')
312+
:param json_message: A json message formatted for the endpoint.
313+
:type json_message: str
314+
:raises: MixpanelException
315+
'''
316+
if endpoint not in self._buffers:
317+
raise MixpanelException('No such endpoint "{0}". Valid endpoints are one of {1}'.format(self._buffers.keys()))
318+
319+
buf = self._buffers[endpoint]
320+
buf.append(json_message)
321+
if len(buf) >= self._max_size:
322+
self._flush_endpoint(endpoint)
323+
324+
def flush(self):
325+
'''
326+
Send all remaining messages to Mixpanel. BufferedConsumers will
327+
flush automatically when you call send(), but you will need to call
328+
flush() when you are completely done using the consumer (for example,
329+
when your application exits) to ensure there are no messages remaining
330+
in memory.
331+
332+
Calls to flush() may raise a MixpanelException if there is a problem
333+
communicating with the Mixpanel servers. In this case, the exception
334+
thrown will have a message property, containing the text of the message,
335+
and an endpoint property containing the endpoint that failed.
336+
337+
:raises: MixpanelException
338+
'''
339+
for endpoint in self._buffers.keys():
340+
self._flush_endpoint(endpoint)
341+
342+
def _flush_endpoint(self, endpoint):
343+
buf = self._buffers[endpoint]
344+
while buf:
345+
batch = buf[:self._max_size]
346+
batch_json = '[{0}]'.format(','.join(batch))
347+
try:
348+
self._consumer.send(endpoint, batch_json)
349+
except MixpanelException as e:
350+
e.message = 'batch_json'
351+
e.endpoint = endpoint
352+
buf = buf[self._max_size:]
353+
self._buffers[endpoint] = buf

0 commit comments

Comments
 (0)