Repository: mapbox/mapbox-sdk-py
Pull request #7 (2015-08-06 20:10:46)

Reverse Geocoding
Would love some :eyes: @sgillies @dnomadb. Biggest change is an alteration to the command line interface to make `click` handle passed coordinates with as little ugliness as possible.


Patch:
diff --git a/README.md b/README.md
index 3e156f2..d3e480f 100644
--- a/README.md
+++ b/README.md
@@ -19,24 +19,15 @@ $ pip install mapbox
 
 ## Usage
 
-Client methods return [Requests](http://www.python-requests.org/en/latest/)
-style response objects.
+Basic usage of the geocoder:
 
 ```python
 import mapbox
 
 geocoder = mapbox.Geocoder(access_token='YOUR_ACCESS_TOKEN')
-response = geocoder.fwd('Chester, NJ')
+response = geocoder.forward('Chester, NJ')
 
 # response.json() returns the geocoding result as GeoJSON.
-# response.status_code returns the HTTP API status code.
-```
-
-## Testing
-
-```bash
-pip install -e .[test]
-py.test
 ```
 
 ## See Also
diff --git a/mapbox/__init__.py b/mapbox/__init__.py
index 76ba458..fc30c44 100644
--- a/mapbox/__init__.py
+++ b/mapbox/__init__.py
@@ -27,10 +27,18 @@ class Geocoder(Service):
         self.baseuri = 'http://api.mapbox.com/v4/geocode'
         self.session = self.get_session(access_token)
 
-    def fwd(self, address, params=None):
+    def forward(self, address, params=None):
         """A forward geocoding request
 
-        See: https://www.mapbox.com/developers/api/geocoding/#forward."""
+        See: https://www.mapbox.com/developers/api/geocoding/#forward"""
         uri = URITemplate('%s/{dataset}/{query}.json' % self.baseuri).expand(
             dataset=self.name, query=address)
         return self.session.get(uri, params=params)
+
+    def reverse(self, lon, lat, params=None):
+        """A reverse geocoding request
+
+        See: https://www.mapbox.com/developers/api/geocoding/#reverse"""
+        uri = URITemplate('%s/{dataset}/{lon},{lat}.json' % self.baseuri).expand(
+            dataset=self.name, lon=lon, lat=lat)
+        return self.session.get(uri, params=params)
diff --git a/mapbox/scripts/cli.py b/mapbox/scripts/cli.py
index 66df017..9bbefa9 100644
--- a/mapbox/scripts/cli.py
+++ b/mapbox/scripts/cli.py
@@ -32,3 +32,6 @@ def main_group(ctx, verbose, quiet):
     configure_logging(verbosity)
     ctx.obj = {}
     ctx.obj['verbosity'] = verbosity
+
+if __name__ == '__main__':
+    main_group()
\ No newline at end of file
diff --git a/mapbox/scripts/geocoder.py b/mapbox/scripts/geocoder.py
index ad761af..7ccd5f1 100644
--- a/mapbox/scripts/geocoder.py
+++ b/mapbox/scripts/geocoder.py
@@ -8,18 +8,42 @@ import mapbox
 class MapboxException(click.ClickException):
     pass
 
+def _is_numeric(x):
+    try:
+        float(x)
+    except:
+        return False
+    return True
 
 @click.command(short_help="Geocode an address.")
-@click.argument('ADDRESS')
 @click.option('--access-token', help="Your access token")
+@click.option('--forward', default=False, help="Perform a forward geocode")
+@click.option('--reverse', default=False, help="Perform a reverse geocode")
 @click.pass_context
-def geocode(ctx, address, access_token):
+def geocode(ctx, access_token, forward, reverse):
     """Geocode an address"""
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 2
     logger = logging.getLogger('mapbox')
     geocoder = mapbox.Geocoder(access_token=access_token)
-    resp = geocoder.fwd(address)
+
+    if reverse and forward:
+        raise MapboxException('Cannot use forward and reverse geocoding simultaneously')
+
+    if not (reverse or forward):
+        raise MapboxException('You must specify --forward or --reverse (but not both)')
+
+    if forward:
+        resp = geocoder.forward(forward)
+    elif reverse:
+        coords = list(filter(lambda x: _is_numeric(x), [ x.strip() for x in reverse.split(',') ]))
+        if len(coords) != 2:
+            raise MapboxException('Reverse geocoding requires a query in decimal longitude,latitude format, e.g. --reverse="-100,37.7"')
+        resp = geocoder.reverse(coords[0], coords[1])
+
     if resp.status_code == 200:
         click.echo(resp.text)
     else:
         raise MapboxException(resp.text.strip())
+
+if __name__ == '__main__':
+    geocode()
\ No newline at end of file


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4a9b05b..6c0c64d 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,7 +1,6 @@
+import json
 from click.testing import CliRunner
-
 import responses
-
 from mapbox.scripts.cli import main_group
 
 
@@ -18,7 +17,7 @@ def test_cli_geocode_fwd():
     runner = CliRunner()
     result = runner.invoke(
         main_group,
-        ['geocode', '1600 pennsylvania ave nw', '--access-token', 'bogus'])
+        ['geocode', '--forward', '1600 pennsylvania ave nw', '--access-token', 'bogus'])
     assert result.exit_code == 0
     assert result.output == '{"query": ["1600", "pennsylvania", "ave", "nw"]}\n'
 
@@ -36,11 +35,52 @@ def test_cli_geocode_fwd_env_token():
     runner = CliRunner()
     result = runner.invoke(
         main_group,
-        ['geocode', '1600 pennsylvania ave nw'],
+        ['geocode', '--forward', '1600 pennsylvania ave nw'],
         env={'MapboxAccessToken': 'bogus'})
     assert result.exit_code == 0
     assert result.output == '{"query": ["1600", "pennsylvania", "ave", "nw"]}\n'
 
+@responses.activate
+def test_cli_geocode_reverse():
+
+    coords = [-77.4371, 37.5227]
+
+    responses.add(
+        responses.GET,
+        'http://api.mapbox.com/v4/geocode/mapbox.places/%s.json?access_token=pk.test' % ','.join([str(x) for x in coords]),
+        match_querystring=True,
+        body='{"query": %s}' % json.dumps(coords),
+        status=200,
+        content_type='application/json')
+
+    runner = CliRunner()
+    result = runner.invoke(
+        main_group,
+        ['geocode', '--reverse', ','.join([str(x) for x in coords]), '--access-token', 'pk.test'])
+    assert result.exit_code == 0
+    assert result.output == '{"query": %s}\n' % json.dumps(coords)
+
+@responses.activate
+def test_cli_geocode_reverse_env_token():
+
+    coords = [-77.4371, 37.5227]
+
+    responses.add(
+        responses.GET,
+        'http://api.mapbox.com/v4/geocode/mapbox.places/%s.json?access_token=bogus' % ','.join([str(x) for x in coords]),
+        match_querystring=True,
+        body='{"query": %s}' % json.dumps(coords),
+        status=200,
+        content_type='application/json')
+
+    runner = CliRunner()
+    result = runner.invoke(
+        main_group,
+        ['geocode', '--reverse', ','.join([str(x) for x in coords])],
+        env={'MapboxAccessToken': 'bogus'})
+    assert result.exit_code == 0
+    assert result.output == '{"query": %s}\n' % json.dumps(coords)
+
 
 @responses.activate
 def test_cli_geocode_unauthorized():
@@ -52,6 +92,6 @@ def test_cli_geocode_unauthorized():
         content_type='application/json')
 
     runner = CliRunner()
-    result = runner.invoke(main_group, ['geocode', '1600 pennsylvania ave nw'])
+    result = runner.invoke(main_group, ['geocode', '--forward', '1600 pennsylvania ave nw'])
     assert result.exit_code == 1
     assert result.output == 'Error: {"message":"Not Authorized - Invalid Token"}\n'
diff --git a/tests/test_geocoder.py b/tests/test_geocoder.py
index 51e9b4d..d816125 100644
--- a/tests/test_geocoder.py
+++ b/tests/test_geocoder.py
@@ -1,8 +1,24 @@
+import json
 import responses
-
 import mapbox
 
 
+def test_is_numeric():
+    """ test utility function in geocoder.py """
+    import mapbox.scripts.geocoder as gc
+    tests = (
+        (9.2, True),
+        ('9.2', True),
+        ('-9.2', True),
+        ('10000', True),
+        ('one thousand', False),
+        ('18f', False),
+        ([], False)
+    )
+    for test in tests:
+        assert gc._is_numeric(test[0]) == test[1]
+
+
 def test_service_session():
     """Get a session using a token"""
     session = mapbox.Service().get_session('pk.test')
@@ -36,7 +52,7 @@ def test_geocoder_name():
 
 
 @responses.activate
-def test_geocoder_fwd():
+def test_geocoder_forward():
     """Forward geocoding works"""
 
     responses.add(
@@ -46,7 +62,24 @@ def test_geocoder_fwd():
         body='{"query": ["1600", "pennsylvania", "ave", "nw"]}', status=200,
         content_type='application/json')
 
-    response = mapbox.Geocoder(
-        access_token='pk.test').fwd('1600 pennsylvania ave nw')
+    response = mapbox.Geocoder(access_token='pk.test').forward('1600 pennsylvania ave nw')
     assert response.status_code == 200
     assert response.json()['query'] == ["1600", "pennsylvania", "ave", "nw"]
+
+@responses.activate
+def test_geocoder_reverse():
+    """Reverse geocoding works"""
+
+    coords = [-77.4371, 37.5227]
+
+    responses.add(
+        responses.GET,
+        'http://api.mapbox.com/v4/geocode/mapbox.places/%s.json?access_token=pk.test' % ','.join([str(x) for x in coords]),
+        match_querystring=True,
+        body='{"query": %s}' % json.dumps(coords),
+        status=200,
+        content_type='application/json')
+
+    response = mapbox.Geocoder(access_token='pk.test').reverse(*[str(x) for x in coords])
+    assert response.status_code == 200
+    assert response.json()['query'] == coords