Repository: mapbox/mapbox-sdk-py
Pull request #71 (2015-11-16 18:47:45)

Add doctests of usage examples


Patch:
diff --git a/.travis.yml b/.travis.yml
index 9ede58d..d99e41d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,21 +1,25 @@
 sudo: false
 language: python
 python:
-- '2.6'
-- '2.7'
-- '3.3'
-- '3.4'
-- pypy
+  - '2.6'
+  - '2.7'
+  - '3.3'
+  - '3.4'
+  - pypy
 cache:
   directories:
-  - $HOME/.pip-cache/
+    - $HOME/.pip-cache/
+env:
+  global:
+    - secure: "RrKb6ss6d9e+f5CuadcPjsdY8oQ9zJYdPnNnvgMHknHTr7Xa09v5FrBtQoVowCJLvxzHPd0iQwoXocskZRGYirT+/sJ031hK2aFglxc6p+Z65AzObVU5CyBNKJq17GXQf+ziLbiXBDtpobj0QPmrfLiq0jFIGs+vzxHQ9fpZrPSmGRPkiDXLn0yn6RHGP3twmx27SnlIUbv5xSnhya8srJwo5WyakKDiKwfCPcevhm7uvsaf4S5uzUY6JSrmDScuZZx5m+glUM2mXfzf5S+Lz/r2o00qGh6Dx6xl9JRL5ST/uX3SakR//0RfAO3MAC2P/W8jUpnVKMarGbakoyZk8UigfsiP5vxInGA5qmLlS5ZExe46MK+D2Ea9wVvSbThHeBu8NW7TDpTokJTObIFVCqE/OG+UhQFa2V8KCzit6O8t7+bMSW0PU/uPfl6b3mtgG0LJbfKsAhYPu6XK7U+rJ9c3W0h36HWaENPMVB+V2RfG6Jj1I7VL0VHyJF+KcQq5pvhPQrtCJTuggVAbt3EgS6SDEGrQ3qZO8bd0ImLEiWnzJEogchnKOTD8w2RdJ2wHNRjKM1fH+V4DtyRJmUvq5fox9HNc2imOCeJVjsOZSoQB0rTgqYfnbGy8YV6p3f9X1+deWmxlL7c3v4xxMZQLy4Mf3n1Wvrz0MFYVn4uKRIE="
 install:
-- pip install -U pip --cache-dir $HOME/.pip-cache
-- pip install -e .[test] --cache-dir $HOME/.pip-cache
+  - pip install -U pip --cache-dir $HOME/.pip-cache
+  - pip install -e .[test] --cache-dir $HOME/.pip-cache
 script:
-- py.test --cov mapbox --cov-report term-missing
+  - py.test --cov mapbox --cov-report term-missing
+  - py.test --doctest-glob='*.md' docs/*.md
 after_success:
-- coveralls
+  - coveralls
 deploy:
   on:
     tags: true
diff --git a/docs/geocoding.md b/docs/geocoding.md
new file mode 100644
index 0000000..0afd6e1
--- /dev/null
+++ b/docs/geocoding.md
@@ -0,0 +1,99 @@
+Geocoding
+=========
+
+# Module import
+
+The Mapbox Python SDK is accessed from classes and methods of the `mapbox`
+module.
+
+```python
+
+>>> import mapbox
+
+```
+
+# Access Tokens
+
+Your Mapbox access token can be exported into your environment
+
+```bash
+
+export MAPBOX_ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
+
+```
+
+or can be passed explicitly to service constructors.
+
+```python
+
+>>> import os
+>>> YOUR_ACCESS_TOKEN = os.environ['MAPBOX_ACCESS_TOKEN']
+>>> geocoder = mapbox.Geocoder(access_token=YOUR_ACCESS_TOKEN)
+
+```
+
+# Geocoding
+
+```python
+
+>>> geocoder = mapbox.Geocoder()
+
+```
+
+## Coverage
+
+## Limits
+
+```python
+
+>>> response = geocoder.forward('Chester, NJ')
+>>> response.headers['x-rate-limit-interval']
+'60'
+>>> response.headers['x-rate-limit-limit']
+'600'
+>>> response.headers['x-rate-limit-remaining'] # doctest: +SKIP
+'599'
+>>> response.headers['x-rate-limit-reset'] # doctest: +SKIP
+'1447701074'
+
+```
+
+## Response format
+
+The JSON response extends GeoJSON's `FeatureCollection`.
+
+```python
+
+>>> response = geocoder.forward('Chester, NJ')
+>>> collection = response.json()
+>>> collection['type'] == 'FeatureCollection'
+True
+>>> sorted(collection.keys())
+['attribution', 'features', 'query', 'type']
+>>> collection['query']
+['chester', 'nj']
+
+```
+
+Zero or more objects that extend GeoJSON's `Feature` are contained in the
+collection, sorted by relevance to the query.
+
+```python
+
+>>> first = collection['features'][0]
+>>> first['type'] == 'Feature'
+True
+>>> sorted(first.keys())
+['bbox', 'center', 'context', 'geometry', 'id', 'place_name', 'properties', 'relevance', 'text', 'type']
+
+```
+
+## Forward geocoding
+
+```python
+
+>>> response = geocoder.forward('1600 pennsylvania ave nw')
+>>> response.status_code
+200
+
+```
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644
index 0000000..263ec56
--- /dev/null
+++ b/docs/usage.md
@@ -0,0 +1,99 @@
+SDK Usage Examples
+==================
+
+# Module import
+
+The Mapbox Python SDK is accessed from classes and methods of the `mapbox`
+module.
+
+```python
+
+>>> import mapbox
+
+```
+
+# Access Tokens
+
+Your Mapbox access token can be exported into your environment
+
+```bash
+
+export MAPBOX_ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
+
+```
+
+or can be passed explicitly to service constructors.
+
+```python
+
+>>> import os
+>>> YOUR_ACCESS_TOKEN = os.environ['MAPBOX_ACCESS_TOKEN']
+>>> geocoder = mapbox.Geocoder(access_token=YOUR_ACCESS_TOKEN)
+
+```
+
+# Geocoding
+
+```python
+
+>>> geocoder = mapbox.Geocoder()
+
+```
+
+## Coverage
+
+## Limits
+
+```python
+
+>>> response = geocoder.forward('Chester, NJ')
+>>> response.headers['x-rate-limit-interval']
+'60'
+>>> response.headers['x-rate-limit-limit']
+'600'
+>>> response.headers['x-rate-limit-remaining'] # doctest: +SKIP
+'599'
+>>> response.headers['x-rate-limit-reset'] # doctest: +SKIP
+'1447701074'
+
+```
+
+## Response format
+
+The JSON response extends GeoJSON's `FeatureCollection`.
+
+```python
+
+>>> response = geocoder.forward('Chester, NJ')
+>>> data = response.json()
+>>> data['type'] == 'FeatureCollection'
+True
+>>> sorted(data.keys())
+['attribution', 'features', 'query', 'type']
+>>> data['query']
+['chester', 'nj']
+
+```
+
+Zero or more objects that extend GeoJSON's `Feature` are contained in the
+collection, sorted by relevance to the query.
+
+```python
+
+>>> first = data['features'][0]
+>>> first['type'] == 'Feature'
+True
+>>> sorted(first.keys())
+['bbox', 'center', 'context', 'geometry', 'id', 'place_name', 'properties', 'relevance', 'text', 'type']
+
+```
+
+## Forward geocoding
+
+```python
+
+>>> response = geocoder.forward('1600 pennsylvania ave nw')
+>>> response.status_code
+200
+
+```
diff --git a/mapbox/encoding.py b/mapbox/encoding.py
index 62259d4..f009fb8 100644
--- a/mapbox/encoding.py
+++ b/mapbox/encoding.py
@@ -1,6 +1,7 @@
-from polyline.codec import PolylineCodec
 import json
 
+from .polyline.codec import PolylineCodec
+
 
 def _geom_points(geom):
     """GeoJSON geometry to a sequence of point tuples
diff --git a/mapbox/polyline/LICENSE b/mapbox/polyline/LICENSE
new file mode 100644
index 0000000..dd5d6d2
--- /dev/null
+++ b/mapbox/polyline/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014 Bruno M. Custódio
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/mapbox/polyline/README.rst b/mapbox/polyline/README.rst
new file mode 100644
index 0000000..e103b78
--- /dev/null
+++ b/mapbox/polyline/README.rst
@@ -0,0 +1,22 @@
+polyline
+========
+
+.. image:: https://travis-ci.org/bmcustodio/polyline.svg?branch=master
+    :target: https://travis-ci.org/bmcustodio/polyline
+
+.. image:: https://badge.fury.io/py/polyline.svg
+    :target: http://badge.fury.io/py/polyline
+
+.. image:: https://pypip.in/d/polyline/badge.png
+        :target: https://crate.io/packages/polyline/
+
+``polyline`` is a Python implementation of Google's Encoded Polyline Algorithm
+Format (http://goo.gl/PvXf8Y). It is essentially a port of
+https://github.com/mapbox/polyline built with Python 2 and 3 support in mind.
+
+The documentation for ``polyline`` can be found at http://polyline.rtfd.org/.
+
+License
+-------
+
+MIT © `Bruno M. Custódio <mailto:bruno@brunomcustodio.com>`_
diff --git a/mapbox/polyline/__init__.py b/mapbox/polyline/__init__.py
new file mode 100644
index 0000000..439eb0c
--- /dev/null
+++ b/mapbox/polyline/__init__.py
@@ -0,0 +1,1 @@
+__version__ = '1.1'
diff --git a/mapbox/polyline/codec.py b/mapbox/polyline/codec.py
new file mode 100644
index 0000000..f9a6c6a
--- /dev/null
+++ b/mapbox/polyline/codec.py
@@ -0,0 +1,53 @@
+import itertools
+import six
+
+
+class PolylineCodec(object):
+    def _pcitr(self, iterable):
+        return six.moves.zip(iterable, itertools.islice(iterable, 1, None))
+
+    def _write(self, output, value):
+        coord = int(round(value * 1e5, 0))
+        coord <<= 1
+        coord = coord if coord >= 0 else ~coord
+
+        while coord >= 0x20:
+            output.write(six.unichr((0x20 | (coord & 0x1f)) + 63))
+            coord >>= 5
+
+        output.write(six.unichr(coord + 63))
+
+    def _trans(self, value, index):
+        byte, result, shift = None, 0, 0
+
+        while (byte is None or byte >= 0x20):
+            byte = ord(value[index]) - 63
+            index += 1
+            result |= (byte & 0x1f) << shift
+            shift += 5
+            comp = result & 1
+
+        return ~(result >> 1) if comp else (result >> 1), index
+
+    def decode(self, expression):
+        coordinates, index, lat, lng, length = [], 0, 0, 0, len(expression)
+
+        while (index < length):
+            lat_change, index = self._trans(expression, index)
+            lng_change, index = self._trans(expression, index)
+            lat += lat_change
+            lng += lng_change
+            coordinates.append((lat / 1e5, lng / 1e5))
+
+        return coordinates
+
+    def encode(self, coordinates):
+        output = six.StringIO()
+        self._write(output, coordinates[0][0])
+        self._write(output, coordinates[0][1])
+
+        for prev, curr in self._pcitr(coordinates):
+            self._write(output, curr[0] - prev[0])
+            self._write(output, curr[1] - prev[1])
+
+        return output.getvalue()
diff --git a/setup.py b/setup.py
index ec1ef25..bc36a45 100644
--- a/setup.py
+++ b/setup.py
@@ -35,7 +35,6 @@ setup(name='mapbox',
           'requests',
           'uritemplate.py',
           'boto3',
-          'polyline'
       ],
       extras_require={
-          'test': ['coveralls', 'pytest', 'pytest-cov', 'responses']})
+          'test': ['coveralls', 'pytest>=2.8.3', 'pytest-cov', 'responses', 'tox']})
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..1b8a4d4
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,10 @@
+[tox]
+envlist = 
+    py27,py34
+
+[testenv]
+deps =
+    pytest-cov
+    responses
+commands = 
+    py.test --cov {envsitepackagesdir}/mapbox --cov-report term-missing


Test patch:
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..3988cc8
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+doctest_optionflags = ALLOW_UNICODE
diff --git a/tests/test_polyline_codec.py b/tests/test_polyline_codec.py
new file mode 100644
index 0000000..cf9d12c
--- /dev/null
+++ b/tests/test_polyline_codec.py
@@ -0,0 +1,70 @@
+import unittest
+
+from mapbox.polyline.codec import PolylineCodec
+
+
+class PolylineCodecTestCase(unittest.TestCase):
+    def setUp(self):
+        self.codec = PolylineCodec()
+
+    def test_decode_multiple_points(self):
+        d = self.codec.decode('gu`wFnfys@???nKgE??gE?????oK????fE??fE')
+        self.assertEqual(d, [
+            (40.641, -8.654),
+            (40.641, -8.654),
+            (40.641, -8.656),
+            (40.642, -8.656),
+            (40.642, -8.655),
+            (40.642, -8.655),
+            (40.642, -8.655),
+            (40.642, -8.653),
+            (40.642, -8.653),
+            (40.642, -8.653),
+            (40.641, -8.653),
+            (40.641, -8.654)
+        ])
+
+    def test_decode_official_example(self):
+        d = self.codec.decode('_p~iF~ps|U_ulLnnqC_mqNvxq`@')
+        self.assertEqual(d, [
+            (38.500, -120.200),
+            (40.700, -120.950),
+            (43.252, -126.453)
+        ])
+
+    def test_decode_single_point(self):
+        d = self.codec.decode('gu`wFf`ys@')
+        self.assertEqual(d, [
+            (40.641, -8.653)
+        ])
+
+    def test_encode_multiple_points(self):
+        e = self.codec.encode([
+            (40.641, -8.654),
+            (40.641, -8.654),
+            (40.641, -8.656),
+            (40.642, -8.656),
+            (40.642, -8.655),
+            (40.642, -8.655),
+            (40.642, -8.655),
+            (40.642, -8.653),
+            (40.642, -8.653),
+            (40.642, -8.653),
+            (40.641, -8.653),
+            (40.641, -8.654)
+        ])
+        self.assertEqual(e, 'gu`wFnfys@???nKgE??gE?????oK????fE??fE')
+
+    def test_encode_official_example(self):
+        e = self.codec.encode([
+            (38.500, -120.200),
+            (40.700, -120.950),
+            (43.252, -126.453)
+        ])
+        self.assertEqual(e, '_p~iF~ps|U_ulLnnqC_mqNvxq`@')
+
+    def test_encode_single_point(self):
+        e = self.codec.encode([
+            (40.641, -8.653)
+        ])
+        self.assertEqual(e, 'gu`wFf`ys@')