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

Issue:
Accept bare geometries as static map overlays
Where "bare" means not in the context of a Feature. See #132 for background.


Pull request description:
normalize feature inputs to collection
This allows the `features` input to the static map service to be a geojson-like mapping of geometry, feature or featurecollection (or sequences of them). All valid geojson objects will be normalized to a single featurecollection.

Resolves #134 


Patch:
diff --git a/mapbox/polyline/README.rst b/mapbox/polyline/README.rst
deleted file mode 100644
index 3e0b913..0000000
--- a/mapbox/polyline/README.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-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/services/static.py b/mapbox/services/static.py
index 1b51897..5f81a88 100644
--- a/mapbox/services/static.py
+++ b/mapbox/services/static.py
@@ -4,6 +4,7 @@ from uritemplate import URITemplate
 
 from mapbox import errors
 from mapbox.services.base import Service
+from mapbox.utils import normalize_geojson_featurecollection
 
 
 class Static(Service):
@@ -59,10 +60,9 @@ class Static(Service):
             fmt=image_format)
 
         if features:
-            values['overlay'] = json.dumps({'type': 'FeatureCollection',
-                                            'features': features},
-                                           separators=(',', ':'),
-                                           sort_keys=sort_keys)
+            collection = normalize_geojson_featurecollection(features)
+            values['overlay'] = json.dumps(
+                collection, separators=(',', ':'), sort_keys=sort_keys)
 
             self._validate_overlay(values['overlay'])
 
@@ -78,7 +78,7 @@ class Static(Service):
 
             # No overlay
             pth = '/{mapid}/{lon},{lat},{z}/{width}x{height}.{fmt}'
-        
+
         uri = URITemplate(self.baseuri + pth).expand(**values)
         res = self.session.get(uri)
         self.handle_http_error(res)
diff --git a/mapbox/utils.py b/mapbox/utils.py
new file mode 100644
index 0000000..b811212
--- /dev/null
+++ b/mapbox/utils.py
@@ -0,0 +1,30 @@
+from collections import Mapping, Sequence
+
+def normalize_geojson_featurecollection(obj):
+    """Takes a geojson-like mapping representing
+    geometry, Feature or FeatureCollection (or a sequence of such objects)
+    and returns a FeatureCollection-like dict
+    """
+    if not isinstance(obj, Sequence):
+        obj = [obj]
+
+    features = []
+    for x in obj:
+        if not isinstance(x, Mapping) or 'type' not in x:
+            raise ValueError(
+                "Expecting a geojson-like mapping or sequence of them")
+
+        if 'features' in x:
+            features.extend(x['features'])
+        elif 'geometry' in x:
+            features.append(x)
+        elif 'coordinates' in x:
+            feat = {'type': 'Feature',
+                    'properties': {},
+                    'geometry': x}
+            features.append(feat)
+        else:
+            raise ValueError(
+                "Expecting a geojson-like mapping or sequence of them")
+
+    return {'type': 'FeatureCollection', 'features': features}


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..35edc5e
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,66 @@
+import pytest
+
+from mapbox.utils import normalize_geojson_featurecollection
+
+
+geom = {'type': 'Point', 'coordinates': (-122, 45)}
+feat = {'type': 'Feature', 'geometry': geom, 'properties': {}}
+coll = {'type': 'FeatureCollection', 'features': [feat]}
+coll2 = {'type': 'FeatureCollection', 'features': [feat, feat]}
+
+
+def test_geom():
+    res = normalize_geojson_featurecollection(geom)
+    assert res['type'] == 'FeatureCollection'
+    assert res == coll
+
+
+def test_feat():
+    res = normalize_geojson_featurecollection(feat)
+    assert res['type'] == 'FeatureCollection'
+    assert res == coll
+
+
+def test_coll():
+    res = normalize_geojson_featurecollection(coll)
+    assert res['type'] == 'FeatureCollection'
+    assert res == coll
+
+
+def test_mult_geom():
+    geoms = (geom, geom)
+    res = normalize_geojson_featurecollection(geoms)
+    assert res['type'] == 'FeatureCollection'
+    assert res == coll2
+
+
+def test_mult_feat():
+    feats = (feat, feat)
+    res = normalize_geojson_featurecollection(feats)
+    assert res['type'] == 'FeatureCollection'
+    assert res == coll2
+
+
+def test_mult_coll():
+    colls = (coll, coll)
+    res = normalize_geojson_featurecollection(colls)
+    assert res['type'] == 'FeatureCollection'
+    assert res == coll2
+
+
+def test_mix():
+    objs = (geom, feat, coll, coll2)
+    res = normalize_geojson_featurecollection(objs)
+    assert res['type'] == 'FeatureCollection'
+    assert len(res['features']) == 5
+
+
+def test_nonsense():
+    with pytest.raises(ValueError):
+        normalize_geojson_featurecollection(123)
+
+    with pytest.raises(ValueError):
+        normalize_geojson_featurecollection({'foo': 'bar'})
+
+    with pytest.raises(ValueError):
+        normalize_geojson_featurecollection({'type': 'not-geojson'})