diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d615167 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +dist/ +*.egg-info/ +*.pyc +.DS_Store \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4bf4483 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e60be4f --- /dev/null +++ b/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). + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mixpanel_query/__init__.py b/mixpanel_query/__init__.py new file mode 100644 index 0000000..b8023d8 --- /dev/null +++ b/mixpanel_query/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/mixpanel_query/client.py b/mixpanel_query/client.py new file mode 100644 index 0000000..0a9158b --- /dev/null +++ b/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)) diff --git a/mixpanel_query/connection.py b/mixpanel_query/connection.py new file mode 100644 index 0000000..280272b --- /dev/null +++ b/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() diff --git a/mixpanel_query/exceptions.py b/mixpanel_query/exceptions.py new file mode 100644 index 0000000..68e7a67 --- /dev/null +++ b/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 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d166ab6 --- /dev/null +++ b/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, +)