Repository: mapbox/mapbox-sdk-py
Pull request #161 (2017-02-24 14:29:58)

Analytics sdk
Here's the first cut for analytics-sdk. 

@batpad, @perrygeo Can you take a look at it. Will be glad to get suggestions to improve it.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 2627a67..a2b911a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,9 +10,6 @@ python:
 cache:
   directories:
     - $HOME/.pip-cache/
-env:
-  global:
-    - secure: "Y0TmwcQeUFm++0akAroEj+MYeTWOT/mtR97n4uHs3J//BhZHLtFNhUF9wINPaIioGfYhM3C7xZt3QStyF7APlC/ioPb+Jb/4CXIb1YwOPgN7BDSy8qsCGcjU6ieWGcjxjP+3qHIyepKhC8LFx2qJsnAGg9gJF+52z/7wiDBZGxgA6bGzIz8gQhu5aUcEHzICRwPyJSqH26QRg8ff+MGW9JYOat96WJiZWYsVoH3mBC21rpPvOEJsyYGuJzW0rYrjBPPKgS7HiPlCySWtslFpy5mqgM+puExjcGAR2QRBp42IB/8Sm9xRxSqSLx63vyni3vjic53/QGSylKn5c1JzOrss3+AJVb4YimjSvCS+POxtNxbgTS1R8ax0ZsSqzi3n9SiBWS0mfNkhxL72Aecm+g37ubJ9XRVndd+O8YAaQPCIqKVxcV/SHBtXehAX+PnUd2T4uTu1QC/upi6czh6R9ClPVwHpvFSv7VTUOzD1F3r/scWA3Y39Q1Y6oJcbVKIXMx7H5S4M978ha/49DbumohnnY7UAn6o62kfVqnNQmNw9fJWxrXCMoL0qlHJ/0AJk3pABz3HYuv/xU1zElzH2dOcrAj2JuCz+YEfb1ccAND3Nvq07Xt5ks5FCVKRipA+lDTX40EK4E3x3qgy8TYsGk2yH/np7bsaBselNICjanU0="
 install:
   - pip install -U pip --cache-dir $HOME/.pip-cache
   - pip install -e .[test] --cache-dir $HOME/.pip-cache
diff --git a/docs/analytics.md b/docs/analytics.md
new file mode 100644
index 0000000..958b603
--- /dev/null
+++ b/docs/analytics.md
@@ -0,0 +1,41 @@
+# Analytics
+
+The `Analytics` class from the `mapbox.services.analytics` module provides
+access to the Mapbox Analytics API. You can also import it directly from the
+`mapbox` module. 
+
+**Note**: This API is available only for premium and enterprise plans.
+
+```python
+>>> from mapbox import Analytics
+
+```
+
+
+See https://www.mapbox.com/api-documentation/#analytics for general documentation of the API.
+
+Your Mapbox access token should be set in your environment; see the [access
+tokens](access_tokens.md) documentation for more information.
+
+## Analytics methods
+
+The methods of `Analytics` class that provide access to the Analytics API return an instance of 
+[`requests.Response`](http://docs.python-requests.org/en/latest/api/#requests.Response). `Analytics` response also include the `json()` method which returns Python data parsed from API.
+
+## Usage
+
+The Mapbox Analytics API is used to get API usage for services by resource. It returns counts per day for given resource and period.
+
+```python
+>>> analytics = Analytics()
+
+```
+
+The input to `analytics` method are resource_type, username, id, period, access_token.
+
+```python
+>>> response = analytics.analytics('accounts', 'mapbox-sdk-py-user')
+>>> response.status_code
+200
+
+```
diff --git a/docs/directions.md b/docs/directions.md
index a9b5d5a..4c544d7 100644
--- a/docs/directions.md
+++ b/docs/directions.md
@@ -75,7 +75,7 @@ route(s) as a GeoJSON-like FeatureCollection dictionary.
 >>> driving_routes['features'][0]['geometry']['type']
 'LineString'
 >>> driving_routes['features'][0]['properties']['summary']
-'Mount Hood Highway...'
+'Robert Hugh Baldock Freeway...'
 
 ```
 
diff --git a/mapbox/__init__.py b/mapbox/__init__.py
index 99a8c9a..0a1e18f 100644
--- a/mapbox/__init__.py
+++ b/mapbox/__init__.py
@@ -9,3 +9,4 @@ from .services.mapmatching import MapMatcher
 from .services.surface import Surface
 from .services.static import Static
 from .services.uploads import Uploader
+from .services.analytics import Analytics
\ No newline at end of file
diff --git a/mapbox/errors.py b/mapbox/errors.py
index 4581a9b..005796a 100644
--- a/mapbox/errors.py
+++ b/mapbox/errors.py
@@ -45,3 +45,15 @@ class InvalidParameterError(ValidationError):
 
 class InvalidFileError(ValidationError):
     pass
+
+class InvalidResourceTypeError(ValidationError):
+    pass
+        
+class InvalidPeriodError(ValidationError):
+    pass
+
+class InvalidUsernameError(ValidationError):
+    pass
+
+class InvalidId(ValidationError):
+    pass
diff --git a/mapbox/services/analytics.py b/mapbox/services/analytics.py
new file mode 100644
index 0000000..dafb3c5
--- /dev/null
+++ b/mapbox/services/analytics.py
@@ -0,0 +1,67 @@
+import dateutil.parser
+from dateutil.relativedelta import relativedelta
+from uritemplate import URITemplate
+
+from mapbox.services.base import Service
+from mapbox import errors
+
+class Analytics(Service):
+    """Access to Analytics API"""
+
+    valid_resource_types = ['tokens', 'styles', 'accounts', 'tilesets']
+
+    @property
+    def baseuri(self):
+        return 'https://{0}/analytics/v1'.format(self.host)
+
+    def _validate_resource_type(self, resource_type):
+        if resource_type not in self.valid_resource_types:
+            raise errors.InvalidResourceTypeError(
+                "{0} is not a valid profile".format(resource_type))
+        return resource_type
+
+    def _validate_period(self, start, end):
+        if start is None and end is None:
+            return start, end
+        try:
+            start_date = dateutil.parser.parse(start)
+            end_date = dateutil.parser.parse(end)
+        except:
+            raise errors.InvalidPeriodError("Dates are not in ISO formatted string")
+        if start_date > end_date:
+            raise errors.InvalidPeriodError("The first date must be earlier than the second")
+        if relativedelta(end_date, start_date).years >= 1 and relativedelta(end_date, start_date).days >= 0:
+            raise errors.InvalidPeriodError("The maximum period can be 1 year")
+        return start, end
+
+    def _validate_username(self, username):
+        if username is None:
+            raise errors.InvalidUsernameError("Username is required")
+        return username
+
+    def _validate_id(self, resource_type, id):
+        if resource_type != 'accounts' and id is None:
+            raise errors.InvalidId("Id is required")
+        return id
+
+    def analytics(self, resource_type, username, id=None, start=None, end=None):
+        resource_type = self._validate_resource_type(resource_type)
+        username = self._validate_username(username)
+        start, end = self._validate_period(start, end)
+        id = self._validate_id(resource_type, id)
+
+        params = {}
+        if id is not None:
+            params.update({'id': id})
+
+        if start is not None and end is not None:
+            params.update({'period': start + ',' + end})
+
+        uri = URITemplate(self.baseuri + '/{resourceType}/{username}').expand(
+            resourceType=resource_type, username=username)
+
+        resp = self.session.get(uri, params=params)
+        resp.geojson = resp.json()
+        self.handle_http_error(resp)
+
+        return resp
diff --git a/setup.py b/setup.py
index 0247e92..0e2fa43 100644
--- a/setup.py
+++ b/setup.py
@@ -36,6 +36,7 @@ setup(name='mapbox',
           'click-plugins',
           'cligj',
           'iso3166',
+          'python-dateutil>=2.5.0'
           'requests',
           'polyline>=1.3.1',
           'uritemplate>=2.0'],


Test patch:
diff --git a/tests/test_analytics.py b/tests/test_analytics.py
new file mode 100644
index 0000000..0940e41
--- /dev/null
+++ b/tests/test_analytics.py
@@ -0,0 +1,68 @@
+import pytest
+
+import responses
+
+import mapbox
+from mapbox import errors
+
+def test_resource_type_invalid():
+    """'random' is not a valid resource type."""
+    with pytest.raises(errors.InvalidResourceTypeError):
+        mapbox.Analytics(access_token='pk.test')._validate_resource_type('random')
+
+@pytest.mark.parametrize('resource_type', ['tokens', 'styles', 'accounts', 'tilesets'])
+def test_profile_valid(resource_type):
+    """Resource types are valid."""
+    assert resource_type == mapbox.Analytics(
+        access_token='pk.test')._validate_resource_type(resource_type)
+
+@pytest.mark.parametrize(
+    'start, end',[('2016-03-22T00:00:00.000Z', None),
+                ('2016-03-22T00:00:00.000Z', '2016-03-20T00:00:00.000Z'),
+                ('2016-03-22T00:00:00.000Z', '2017-04-20T00:00:00.000Z')])
+def test_period_invalid(start, end):
+    with pytest.raises(errors.InvalidPeriodError):
+        mapbox.Analytics(access_token='pk.test')._validate_period(start, end)
+
+@pytest.mark.parametrize(
+    'start, end', [('2016-03-22T00:00:00.000Z', '2016-03-24T00:00:00.000Z'),
+                   (None, None)])
+def test_period_valid(start, end):
+    period = start, end
+    assert period == mapbox.Analytics(access_token='pk.test')._validate_period(start, end)
+
+def test_username_invalid():
+    """Username is requird."""
+    with pytest.raises(errors.InvalidUsernameError):
+        mapbox.Analytics(access_token='pk.test')._validate_username(None)
+
+def test_username_valid():
+    """Providing valid username"""
+    user = 'abc'
+    assert user == mapbox.Analytics(access_token='pk.test')._validate_username(user)
+
+def test_id_invalid():
+    """id is required when resource type is other than accounts"""
+    resource_type = 'tokens'
+    id = None
+    with pytest.raises(errors.InvalidId):
+        mapbox.Analytics(access_token='pk.test')._validate_id(resource_type, id)
+
+@pytest.mark.parametrize(
+    'resource_type, id', [('accounts', None),
+                   ('tokens', 'abc')])
+def test_id_valid(resource_type, id):
+    """id is not required when resource type is accounts"""
+    assert id == mapbox.Analytics(access_token='pk.test')._validate_id(resource_type, id)
+
+@responses.activate
+def test_analytics():
+    responses.add(
+        responses.GET,
+        'https://api.mapbox.com/analytics/v1/accounts/sanjayb?access_token=pk.test&id=abc&period=2016-03-22T00%3A00%3A00.000Z%2C2016-03-24T00%3A00%3A00.000Z',
+        match_querystring=True,
+        body='{"key": "value"}',
+        status=200)
+
+    res = mapbox.Analytics(access_token='pk.test').analytics('accounts', 'sanjayb', 'abc', '2016-03-22T00:00:00.000Z', '2016-03-24T00:00:00.000Z')
+    assert res.status_code == 200