Repository: mapbox/mapbox-sdk-py
Pull request #28 (2015-10-14 17:00:22)

geocoder v5
- updates endpoints
- rounds precision of `proximity` flag & reverse geocoding
- tests for ^^^ 

@sgillies mind have a quick look?


Patch:
diff --git a/mapbox/__init__.py b/mapbox/__init__.py
index 3b3afb4..5bc7142 100644
--- a/mapbox/__init__.py
+++ b/mapbox/__init__.py
@@ -31,8 +31,9 @@ class Geocoder(Service):
 
     def __init__(self, name='mapbox.places', access_token=None):
         self.name = name
-        self.baseuri = 'https://api.mapbox.com/v4/geocode'
+        self.baseuri = 'https://api.mapbox.com/geocoding/v5'
         self.session = self.get_session(access_token)
+        self.precision = { 'reverse': 5, 'proximity': 3 }
 
     def _validate_place_types(self, types):
         """Validate place types and return a mapping for use in requests"""
@@ -58,7 +59,7 @@ class Geocoder(Service):
         if types:
             params.update(self._validate_place_types(types))
         if lon is not None and lat is not None:
-            params.update(proximity='{0},{1}'.format(lon, lat))
+            params.update(proximity='{0},{1}'.format(round(float(lon), self.precision.get('proximity', 3)), round(float(lat), self.precision.get('proximity', 3))))
         return self.session.get(uri, params=params)
 
     def reverse(self, lon=None, lat=None, types=None):
@@ -70,7 +71,7 @@ class Geocoder(Service):
 
         See: https://www.mapbox.com/developers/api/geocoding/#reverse."""
         uri = URITemplate(self.baseuri + '/{dataset}/{lon},{lat}.json').expand(
-            dataset=self.name, lon=str(lon), lat=str(lat))
+            dataset=self.name, lon=str(round(float(lon), self.precision.get('reverse', 5))), lat=str(round(float(lat), self.precision.get('reverse', 5))))
         params = {}
         if types:
             params.update(self._validate_place_types(types))
@@ -83,6 +84,7 @@ class Geocoder(Service):
             'address': "A street address with house number. Examples: 1600 Pennsylvania Ave NW, 1051 Market St, Oberbaumstrasse 7.",
             'country': "Sovereign states and other political entities. Examples: United States, France, China, Russia.",
             'place': "City, town, village or other municipality relevant to a country's address or postal system. Examples: Cleveland, Saratoga Springs, Berlin, Paris.",
+            'neighborhood': "A smaller area within a place, often without formal boundaries. Examples: Montparnasse, Downtown, Haight-Ashbury.",
             'poi': "Places of interest including commercial venues, major landmarks, parks, and other features. Examples: Yosemite National Park, Lake Superior.",
             'postcode': "Postal code, varies by a country's postal system. Examples: 20009, CR0 3RL.",
             'region': "First order administrative divisions within a country, usually provinces or states. Examples: California, Ontario, Essonne."}


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 74ef86b..fb4faf9 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -33,7 +33,7 @@ def test_cli_geocode_fwd():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=bogus',
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=bogus',
         match_querystring=True,
         body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
         content_type='application/json')
@@ -51,7 +51,7 @@ def test_cli_geocode_fwd_env_token():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=bogus',
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=bogus',
         match_querystring=True,
         body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
         content_type='application/json')
@@ -73,7 +73,7 @@ def test_cli_geocode_reverse():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/{0},{1}.json?access_token=pk.test'.format(lon, lat),
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/{0},{1}.json?access_token=pk.test'.format(lon, lat),
         match_querystring=True,
         body=body,
         status=200,
@@ -96,7 +96,7 @@ def test_cli_geocode_reverse_env_token():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/{0},{1}.json?access_token=bogus'.format(lon, lat),
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/{0},{1}.json?access_token=bogus'.format(lon, lat),
         match_querystring=True,
         body=body,
         status=200,
@@ -117,7 +117,7 @@ def test_cli_geocode_unauthorized():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json',
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json',
         body='{"message":"Not Authorized - Invalid Token"}', status=401,
         content_type='application/json')
 
@@ -134,7 +134,7 @@ def test_cli_geocode_rev_unauthorized():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/{0},{1}.json'.format(lon, lat),
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/{0},{1}.json'.format(lon, lat),
         body='{"message":"Not Authorized - Invalid Token"}', status=401,
         content_type='application/json')
 
@@ -152,7 +152,7 @@ def test_cli_geocode_fwd_headers():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json',
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json',
         body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
         content_type='application/json')
 
@@ -172,7 +172,7 @@ def test_cli_geocode_rev_headers():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/{0},{1}.json'.format(lon, lat),
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/{0},{1}.json'.format(lon, lat),
         body=body,
         status=200,
         content_type='application/json')
diff --git a/tests/test_geocoder.py b/tests/test_geocoder.py
index a970883..cfe95ef 100644
--- a/tests/test_geocoder.py
+++ b/tests/test_geocoder.py
@@ -1,4 +1,5 @@
 import json
+import re
 import responses
 
 import mapbox
@@ -44,6 +45,13 @@ def test_geocoder_name():
     geocoder = mapbox.Geocoder('mapbox.places-permanent')
     assert geocoder.name == 'mapbox.places-permanent'
 
+def _check_coordinate_precision(coord, precision):
+    """Coordinate precision is <= specified number of digits"""
+    if '.' not in coord:
+        return True
+    else:
+        return len(coord.split('.')[-1]) <= precision
+
 
 @responses.activate
 def test_geocoder_forward():
@@ -51,7 +59,7 @@ def test_geocoder_forward():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=pk.test',
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=pk.test',
         match_querystring=True,
         body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
         content_type='application/json')
@@ -70,7 +78,7 @@ def test_geocoder_reverse():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/{0},{1}.json?access_token=pk.test'.format(lon, lat),
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/{0},{1}.json?access_token=pk.test'.format(lon, lat),
         match_querystring=True,
         body=body,
         status=200,
@@ -86,6 +94,7 @@ def test_geocoder_place_types():
     assert sorted(mapbox.Geocoder().place_types.items()) == [
         ('address', "A street address with house number. Examples: 1600 Pennsylvania Ave NW, 1051 Market St, Oberbaumstrasse 7."),
         ('country', "Sovereign states and other political entities. Examples: United States, France, China, Russia."),
+        ('neighborhood', 'A smaller area within a place, often without formal boundaries. Examples: Montparnasse, Downtown, Haight-Ashbury.'),
         ('place', "City, town, village or other municipality relevant to a country's address or postal system. Examples: Cleveland, Saratoga Springs, Berlin, Paris."),
         ('poi', "Places of interest including commercial venues, major landmarks, parks, and other features. Examples: Yosemite National Park, Lake Superior."),
         ('postcode', "Postal code, varies by a country's postal system. Examples: 20009, CR0 3RL."),
@@ -110,7 +119,7 @@ def test_geocoder_forward_types():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?types=address,country,place,poi,postcode,region&access_token=pk.test',
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json?types=address,country,place,poi,postcode,region&access_token=pk.test',
         match_querystring=True,
         body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
         content_type='application/json')
@@ -132,7 +141,7 @@ def test_geocoder_reverse_types():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/{0},{1}.json?types=address,country,place,poi,postcode,region&access_token=pk.test'.format(lon, lat),
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/{0},{1}.json?types=address,country,place,poi,postcode,region&access_token=pk.test'.format(lon, lat),
         match_querystring=True,
         body=body,
         status=200,
@@ -152,7 +161,7 @@ def test_geocoder_forward_proximity():
 
     responses.add(
         responses.GET,
-        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?proximity=0,0&access_token=pk.test',
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json?proximity=0.0,0.0&access_token=pk.test',
         match_querystring=True,
         body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
         content_type='application/json')
@@ -162,3 +171,49 @@ def test_geocoder_forward_proximity():
             '1600 pennsylvania ave nw', lon=0, lat=0)
     assert response.status_code == 200
     assert response.json()['query'] == ["1600", "pennsylvania", "ave", "nw"]
+
+@responses.activate
+def test_geocoder_proximity_rounding():
+    """Proximity parameter is rounded to 3 decimal places"""
+
+    responses.add(
+        responses.GET,
+        'https://api.mapbox.com/geocoding/v5/mapbox.places/1600%20pennsylvania%20ave%20nw.json',
+        match_querystring=False,
+        body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
+        content_type='application/json')
+
+    response = mapbox.Geocoder(
+        access_token='pk.test').forward(
+            '1600 pennsylvania ave nw', lon=0.123456, lat=0.987654)
+
+    # check coordinate precision for proximity flag
+    match = re.search(r'[&\?]proximity=([^&$]+)', response.url)
+    assert match is not None
+    for coord in re.split(r'(%2C|,)', match.group(1)):
+        assert _check_coordinate_precision(coord, 3)
+
+@responses.activate
+def test_geocoder_reverse_rounding():
+    """Reverse geocoding parameters are rounded to 5 decimal places"""
+
+    lon, lat = -77.123456789, 37.987654321
+    body = json.dumps({"query": [lon, lat]})
+
+    responses.add(
+        responses.GET,
+        re.compile('https:\/\/api\.mapbox\.com\/geocoding\/v5\/mapbox\.places\/.+\.json'),
+        match_querystring=False,
+        body=body,
+        status=200,
+        content_type='application/json')
+
+    response = mapbox.Geocoder(
+        access_token='pk.test').reverse(
+            lon=lon, lat=lat)
+
+    # check coordinate precision for reverse geocoding coordinates
+    match = re.search(r'\/([\-\d\.\,]+)\.json', response.url)
+    assert match is not None
+    for coord in re.split(r'(%2C|,)', match.group(1)):
+        assert _check_coordinate_precision(coord, 5)
\ No newline at end of file