Skip to content

Commit

Permalink
Initial commit. Core functionality to make queries against MixPanel's…
Browse files Browse the repository at this point in the history
… query API is working. A single method, `get_unique_events()` is implemented and returns th

e expected results.
  • Loading branch information
Sean Coonce committed Aug 3, 2014
0 parents commit ee9d4b0
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
build/
dist/
*.egg-info/
*.pyc
.DS_Store
1 change: 1 addition & 0 deletions MANIFEST.in
@@ -0,0 +1 @@
include README.md
62 changes: 62 additions & 0 deletions README.md
@@ -0,0 +1,62 @@
# mixpanel-query-py

The Python interface to fetch data from Mixpanel via [Mixpanel's Data Query API](https://mixpanel.com/docs/api-documentation/data-export-api). Note, this differs from the official [Python binding](https://github.com/mixpanel/mixpanel-python) which only provides an interface to send data to Mixpanel.

# Installation

To install mixpanel-query-py, simply:

```
$ sudo pip install mixpanel-query-py
```

or alternatively (you really should be using pip though):

```
$ sudo easy_install mixpanel-query-py
```
or from source:

```
$ git clone git@github.com:cooncesean/mixpanel-query-py.git
$ cd mixpanel-query-py
$ python setup.py install
```

# Usage

You will need a [Mixpanel account](https://mixpanel.com/register/) and your `API_KEY` + `API_SECRET` to access your project's data via their API; which can be found in "Account" > "Projects".

```python
from mixpanel_query.client import MixpanelQueryClient
from your_project.conf import MIXPANEL_API_KEY, MIXPANEL_API_SECRET

# Instantiate the client
query_client = MixpanelQueryClient(MIXPANEL_API_KEY, MIXPANEL_API_SECRET)

# Query your project's data
data = query_client.get_unique_events(['Some Event Name'], 'hour', 24)
print data
{
'data': {
'series': ['2010-05-29', '2010-05-30', '2010-05-31'],
'values': {
'account-page': {'2010-05-30': 1},
'splash features': {
'2010-05-29': 6,
'2010-05-30': 4,
'2010-05-31': 5, # Date + unique event counts
}
}
},
'legend_size': 2
}
```

View the [api reference](#api-reference) for details on accessing different endpoints.

# API Reference

Mixpanels' full [API reference is documented here](https://mixpanel.com/docs/api-documentation/data-export-api).

Empty file added __init__.py
Empty file.
1 change: 1 addition & 0 deletions mixpanel_query/__init__.py
@@ -0,0 +1 @@
__version__ = '0.0.1'
92 changes: 92 additions & 0 deletions mixpanel_query/client.py
@@ -0,0 +1,92 @@
from mixpanel_query.connection import Connection
from mixpanel_query.exceptions import InvalidUnitException, InvalidFormatException


class MixpanelQueryClient(object):
"""
Connects to the `Mixpanel Data Export API`
and provides an interface to query data based on the project
specified with your api credentials.
Full API Docs: https://mixpanel.com/docs/api-documentation/data-export-api
"""
ENDPOINT = 'http://mixpanel.com/api'
VERSION = '2.0'

UNIT_MINUTE = 'minute'
UNIT_HOUR = 'hour'
UNIT_DAY = 'day'
UNIT_WEEK = 'week'
UNIT_MONTH = 'month'
VALID_UNITS = (UNIT_MINUTE, UNIT_HOUR, UNIT_DAY, UNIT_WEEK, UNIT_MONTH)

FORMAT_JSON = 'json'
FORMAT_CSV = 'csv'
VALID_RESPONSE_FORMATS = (FORMAT_JSON, FORMAT_CSV)

def __init__(self, api_key, api_secret):
self.api_key = api_key
self.api_secret = api_secret
self.connection = Connection(self)

# Annotation methods ##############

# Event methods ###################
def get_unique_events(self, event_names, unit, interval, response_format=FORMAT_JSON):
"""
Get unique event data for a set of event types over the last N days, weeks, or months.
Args:
`event_names`: [list] The event or events that you wish to get data for.
[sample]: ["play song", "log in", "add playlist"]
`unit`: [str] Determines the level of granularity of the data you get back.
[sample]: "day" or "month" or "week"
`interval`: [int] The number of "units" to return data for. `1` will return data for the
current unit (minute, hour, day, week or month). `2` will return the
current and previous units, and so on.
`response_format`: [string (optional)]: The data return format.
[sample]: "json" or "csv"
Response format:
{
u'data': {
u'series': [u'2014-07-11', u'2014-07-12', u'2014-07-13'],
u'values': {
u'Guide Download': {
u'2014-07-11': 80,
u'2014-07-12': 100,
u'2014-07-13': 123, # Date + unique event counts
}
}
},
u'legend_size': 1
}
"""
self._validate_unit(unit)
self._validate_response_format(response_format)
return self.connection.request(
'events',
{
'event': event_names,
'unit': unit,
'interval': interval,
'type': 'unique'
}
)

# Event properties methods ########
# Funnel methods ##################
# Segmentation methods ############
# Retention methods ###############
# People methods ##################

# Util methods ####################
def _validate_unit(self, unit):
" Utility method used to validate a `unit` param. "
if unit not in self.VALID_UNITS:
raise InvalidUnitException('The `unit` specified is invalid. Must be: {0}'.format(self.VALID_UNITS))

def _validate_response_format(self, response_format):
" Utility method used to validate a `response_format` param. "
if response_format not in self.VALID_RESPONSE_FORMATS:
raise InvalidFormatException('The `response_format` specified is invalid. Must be {0}.'.format(self.VALID_RESPONSE_FORMATS))
88 changes: 88 additions & 0 deletions mixpanel_query/connection.py
@@ -0,0 +1,88 @@
"""
The class(es) in this module contain logic to make http
requests to the Mixpanel API.
"""
import hashlib
import json
import time
import urllib
import urllib2


class Connection(object):
"""
The `Connection` object's sole responsibility is to format, send to
and parse http responses from the Mixpanel API.
"""
ENDPOINT = 'http://mixpanel.com/api'
VERSION = '2.0'

def __init__(self, client):
self.client = client

def request(self, method_name, params, format='json'):
"""
Make a request to Mixpanel query endpoints and return the
response.
"""
params['api_key'] = self.client.api_key
params['expire'] = int(time.time()) + 600 # Grant this request 10 minutes.
params['format'] = format
if 'sig' in params:
del params['sig']
params['sig'] = self.hash_args(params)
request_url = '{base_url}/{version}/{method_name}/?{encoded_params}'.format(
base_url=self.ENDPOINT,
version=self.VERSION,
method_name=method_name,
encoded_params=self.unicode_urlencode(params)
)
request = urllib2.urlopen(request_url, timeout=120)
data = request.read()
return json.loads(data)

def unicode_urlencode(self, params):
"""
Convert lists to JSON encoded strings, and correctly handle any
unicode URL parameters.
"""
if isinstance(params, dict):
params = params.items()
for i, param in enumerate(params):
if isinstance(param[1], list):
params[i] = (param[0], json.dumps(param[1]),)

return urllib.urlencode(
[(k, isinstance(v, unicode) and v.encode('utf-8') or v) for k, v in params]
)

def hash_args(self, args, secret=None):
"""
Hashes arguments by joining key=value pairs, appending the api_secret, and
then taking the MD5 hex digest.
"""
for a in args:
if isinstance(args[a], list):
args[a] = json.dumps(args[a])

args_joined = ''
for a in sorted(args.keys()):
if isinstance(a, unicode):
args_joined += a.encode('utf-8')
else:
args_joined += str(a)

args_joined += '='

if isinstance(args[a], unicode):
args_joined += args[a].encode('utf-8')
else:
args_joined += str(args[a])

hash = hashlib.md5(args_joined)

if secret:
hash.update(secret)
elif self.client.api_secret:
hash.update(self.client.api_secret)
return hash.hexdigest()
22 changes: 22 additions & 0 deletions mixpanel_query/exceptions.py
@@ -0,0 +1,22 @@
class MixpanelQueryException(Exception):
pass

class InvalidUnitException(MixpanelQueryException):
" An invalid time unit was passed. "
pass

class InvalidFormatException(MixpanelQueryException):
" An invalid response format was passed. "
pass

class InvalidAPIKeyException(MixpanelQueryException):
" An invalid API key + secret were passed. Check your account page for the correct key. "
pass

class ExpiredRequestException(MixpanelQueryException):
" The request is past its expiration date (default 10 minutes). "
pass

class InvalidDateRangeException(MixpanelQueryException):
" The date range you have specified is not 30 days or less. "
pass
21 changes: 21 additions & 0 deletions setup.py
@@ -0,0 +1,21 @@
from setuptools import setup, find_packages


setup(
name='mixpanel-query-py',
version=__import__('mixpanel_query').__version__,
description='The Python interface to query data from Mixpanel.',
author='Sean Coonce',
author_email='cooncesean@gmail.com',
url='https://www.github.com/cooncesean/mixpanel-query-py',
packages=find_packages(),
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
],
include_package_data=True,
zip_safe=False,
)

0 comments on commit ee9d4b0

Please sign in to comment.