Repository: mapbox/mapbox-sdk-py (python)

Issue:
Coordinate format for reverse geocoding input
I'm always looking for symmetry on the command line, and since a JSON array is the most straightforward output of forward geocoding, it would be great to be able to consume it when reverse geocoding. For example, this (using [jq](http://stedolan.github.io/jq/)) produces JSON:

```bash
$ mapbaux geocode "Bodega Bay, CA" --access-token MY_TOKEN | \
>  jq '.features[0].center' -c
[-123.0481,38.3332]
```

And the round trip could be

```bash
$ mapbaux geocode "Bodega Bay, CA" --access-token MY_TOKEN | \
> jq '.features[0].center' -c | \
> mapbaux geocode --rev --access-token MY_TOKEN
```

There's also precedent in rio-sample for JSON formatted coordinate pairs `[x, y]` or `[long, lat]`

  https://github.com/mapbox/rasterio/blob/master/docs/cli.rst#sample

but it seems like it would be relatively easy to also accept `x y` or `x, y`. There's a pattern that could be expressed as a simple regex.

/cc @dnomadb @sbma44 

Pull request:
Handle reverse geocoding CLI input flexibly
May be from a file, stream, or CLI arg and JSON or comma or ws-separated format. `[lng, lat]`, `lng, lat`, and `lng lat` are all acceptable.

Closes #8.

Also adds a helper function to make all inputs iterable and another to handle the various formats. The `--forward` and `--reverse` options are now one boolean option.

Patch:
diff --git a/mapbox/__init__.py b/mapbox/__init__.py
index fc30c44..c1889b3 100644
--- a/mapbox/__init__.py
+++ b/mapbox/__init__.py
@@ -24,7 +24,7 @@ class Geocoder(Service):
 
     def __init__(self, name='mapbox.places', access_token=None):
         self.name = name
-        self.baseuri = 'http://api.mapbox.com/v4/geocode'
+        self.baseuri = 'https://api.mapbox.com/v4/geocode'
         self.session = self.get_session(access_token)
 
     def forward(self, address, params=None):
@@ -39,6 +39,6 @@ class Geocoder(Service):
         """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)
+        uri = URITemplate(self.baseuri + '/{dataset}/{lon},{lat}.json').expand(
+            dataset=self.name, lon=str(lon), lat=str(lat))
         return self.session.get(uri, params=params)
diff --git a/mapbox/compat.py b/mapbox/compat.py
new file mode 100644
index 0000000..baf551f
--- /dev/null
+++ b/mapbox/compat.py
@@ -0,0 +1,10 @@
+# compatibility module.
+
+import itertools
+import sys
+
+
+if sys.version_info < (3,):
+    map = itertools.imap
+else:
+    map = map
diff --git a/mapbox/scripts/cli.py b/mapbox/scripts/cli.py
index 9bbefa9..66df017 100644
--- a/mapbox/scripts/cli.py
+++ b/mapbox/scripts/cli.py
@@ -32,6 +32,3 @@ 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 7ccd5f1..3a4a9bd 100644
--- a/mapbox/scripts/geocoder.py
+++ b/mapbox/scripts/geocoder.py
@@ -1,49 +1,62 @@
 import logging
+import json
+import re
 
 import click
 
 import mapbox
+from mapbox.compat import map
 
 
 class MapboxException(click.ClickException):
     pass
 
-def _is_numeric(x):
+
+def iter_query(query):
+    """Accept a filename, stream, or string.
+    Returns an iterator over lines of the query."""
+    try:
+        itr = click.open_file(query).readlines()
+    except IOError:
+        itr = [query]
+    return itr
+
+
+def coords_from_query(query):
+    """Transform a query line into a (lng, lat) pair of coordinates."""
     try:
-        float(x)
-    except:
-        return False
-    return True
+        coords = json.loads(query)
+    except ValueError:
+        vals = re.split(r"\,*\s*", query.strip())
+        coords = [float(v) for v in vals]
+    return tuple(coords[:2])
+
 
 @click.command(short_help="Geocode an address.")
+@click.argument('query', default='-', required=False)
 @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.option(
+    '--forward/--reverse',
+    default=True,
+    help="Perform a forward (default) or reverse geocode")
 @click.pass_context
-def geocode(ctx, access_token, forward, reverse):
+def geocode(ctx, query, access_token, forward):
     """Geocode an address"""
     verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 2
     logger = logging.getLogger('mapbox')
     geocoder = mapbox.Geocoder(access_token=access_token)
 
-    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)
+        for q in iter_query(query):
+            resp = geocoder.forward(q)
+            if resp.status_code == 200:
+                click.echo(resp.text)
+            else:
+                raise MapboxException(resp.text.strip())
     else:
-        raise MapboxException(resp.text.strip())
-
-if __name__ == '__main__':
-    geocode()
\ No newline at end of file
+        for coords in map(coords_from_query, iter_query(query)):
+            resp = geocoder.reverse(*coords)
+            if resp.status_code == 200:
+                click.echo(resp.text)
+            else:
+                raise MapboxException(resp.text.strip())


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 6c0c64d..ad1a75d 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2,6 +2,30 @@ import json
 from click.testing import CliRunner
 import responses
 from mapbox.scripts.cli import main_group
+from mapbox.scripts.geocoder import coords_from_query, iter_query
+
+
+def test_iter_query_string():
+    assert iter_query("lolwut") == ["lolwut"]
+
+
+def test_iter_query_file(tmpdir):
+    filename = str(tmpdir.join('test.txt'))
+    with open(filename, 'w') as f:
+        f.write("lolwut")
+    assert iter_query(filename) == ["lolwut"]
+
+
+def test_coords_from_query_json():
+    assert coords_from_query("[-100, 40]") == (-100, 40)
+
+
+def test_coords_from_query_csv():
+    assert coords_from_query("-100, 40") == (-100, 40)
+
+
+def test_coords_from_query_ws():
+    assert coords_from_query("-100 40") == (-100, 40)
 
 
 @responses.activate
@@ -9,7 +33,7 @@ def test_cli_geocode_fwd():
 
     responses.add(
         responses.GET,
-        'http://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=bogus',
+        'https://api.mapbox.com/v4/geocode/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')
@@ -27,7 +51,7 @@ def test_cli_geocode_fwd_env_token():
 
     responses.add(
         responses.GET,
-        'http://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=bogus',
+        'https://api.mapbox.com/v4/geocode/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')
@@ -40,6 +64,7 @@ def test_cli_geocode_fwd_env_token():
     assert result.exit_code == 0
     assert result.output == '{"query": ["1600", "pennsylvania", "ave", "nw"]}\n'
 
+
 @responses.activate
 def test_cli_geocode_reverse():
 
@@ -47,7 +72,7 @@ def test_cli_geocode_reverse():
 
     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]),
+        'https://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,
@@ -56,10 +81,12 @@ def test_cli_geocode_reverse():
     runner = CliRunner()
     result = runner.invoke(
         main_group,
-        ['geocode', '--reverse', ','.join([str(x) for x in coords]), '--access-token', 'pk.test'])
+        ['geocode', '--reverse', '--access-token', 'pk.test'],
+        input=','.join([str(x) for x in coords]))
     assert result.exit_code == 0
     assert result.output == '{"query": %s}\n' % json.dumps(coords)
 
+
 @responses.activate
 def test_cli_geocode_reverse_env_token():
 
@@ -67,7 +94,7 @@ def test_cli_geocode_reverse_env_token():
 
     responses.add(
         responses.GET,
-        'http://api.mapbox.com/v4/geocode/mapbox.places/%s.json?access_token=bogus' % ','.join([str(x) for x in coords]),
+        'https://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,
@@ -76,7 +103,8 @@ def test_cli_geocode_reverse_env_token():
     runner = CliRunner()
     result = runner.invoke(
         main_group,
-        ['geocode', '--reverse', ','.join([str(x) for x in coords])],
+        ['geocode', '--reverse'],
+        input=','.join([str(x) for x in coords]),
         env={'MapboxAccessToken': 'bogus'})
     assert result.exit_code == 0
     assert result.output == '{"query": %s}\n' % json.dumps(coords)
@@ -87,7 +115,7 @@ def test_cli_geocode_unauthorized():
 
     responses.add(
         responses.GET,
-        'http://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json',
+        'https://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json',
         body='{"message":"Not Authorized - Invalid Token"}', status=401,
         content_type='application/json')
 
@@ -95,3 +123,22 @@ def test_cli_geocode_unauthorized():
     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'
+
+
+@responses.activate
+def test_cli_geocode_rev_unauthorized():
+    coords = (-77.4371, 37.5227)
+
+    responses.add(
+        responses.GET,
+        'https://api.mapbox.com/v4/geocode/mapbox.places/%s,%s.json' % coords,
+        body='{"message":"Not Authorized - Invalid Token"}', status=401,
+        content_type='application/json')
+
+    runner = CliRunner()
+    result = runner.invoke(
+        main_group,
+        ['geocode', '--reverse'],
+        input='%s,%s' % coords)
+    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 d816125..84dd846 100644
--- a/tests/test_geocoder.py
+++ b/tests/test_geocoder.py
@@ -3,22 +3,6 @@ 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')
@@ -39,6 +23,7 @@ def test_service_session_os_environ(monkeypatch):
     assert session.params.get('access_token') == 'pk.test_os_environ'
     monkeypatch.undo()
 
+
 def test_geocoder_default_name():
     """Default name is set"""
     geocoder = mapbox.Geocoder()
@@ -57,7 +42,7 @@ def test_geocoder_forward():
 
     responses.add(
         responses.GET,
-        'http://api.mapbox.com/v4/geocode/mapbox.places/1600%20pennsylvania%20ave%20nw.json?access_token=pk.test',
+        'https://api.mapbox.com/v4/geocode/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')
@@ -66,6 +51,7 @@ def test_geocoder_forward():
     assert response.status_code == 200
     assert response.json()['query'] == ["1600", "pennsylvania", "ave", "nw"]
 
+
 @responses.activate
 def test_geocoder_reverse():
     """Reverse geocoding works"""
@@ -74,7 +60,7 @@ def test_geocoder_reverse():
 
     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]),
+        'https://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,