Repository: mapbox/mapbox-sdk-py

Issue:
Configurable baseuri
Currently, the endpoint for all services is hardcoded for each service as `baseuri`

```
mapbox/services/datasets.py:    baseuri = 'https://api.mapbox.com/datasets/v1'
mapbox/services/directions.py:    baseuri = 'https://api.mapbox.com/v4/directions'
mapbox/services/distance.py:    baseuri = 'https://api.mapbox.com/distances/v1/mapbox'
mapbox/services/geocoding.py:    baseuri = 'https://api.mapbox.com/geocoding/v5'
mapbox/services/mapmatching.py:    baseuri = 'https://api.mapbox.com/matching/v4'
mapbox/services/static.py:    baseuri = 'https://api.mapbox.com/v4'
mapbox/services/surface.py:    baseuri = 'https://api.mapbox.com/v4/surface'
mapbox/services/uploads.py:    baseuri = 'https://api.mapbox.com/uploads/v1'
```

There is a need for changing this in order to hit staging/testing endpoints that implement the same services. Ideally we'd have a true base URI (defaulting to `https://api.mapbox.com/`) that was easily configurable from any service class. Something like...

```
>>> service = mapbox.Upload()
>>> service.base = 'http://my-local-test-server/'
>>> service.baseuri
http://my-local-test-server/uploads/v1
```

Are there other mechanisms for changing the base uri? What about environment variables?

We'll also want to plan the corresponding flags in the command line interface.

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 3fcb802..d7154d0 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,7 +1,9 @@
-Next
-----
+0.11.0 (Soon)
+-------------
 - Require boto3>=1.4 to get `upload_fileobj()`.
 - Add a callback function for AWS SDK uploads to `uploads.stage()` (#147).
+- Allow configuration of API host using MAPBOX_HOST environment variable or
+  Service constructor keyword argument `host` (#143, #150).
 
 0.10.1 (2016-11-22)
 -------------------
diff --git a/mapbox/services/base.py b/mapbox/services/base.py
index 6b66533..421565e 100644
--- a/mapbox/services/base.py
+++ b/mapbox/services/base.py
@@ -32,13 +32,16 @@ def Session(access_token=None, env=os.environ):
 class Service(object):
     """Service base class."""
 
-    def __init__(self, access_token=None, cache=None):
+    default_host = 'api.mapbox.com'
+
+    def __init__(self, access_token=None, host=None, cache=None):
         """Constructs a Service object.
 
         :param access_token: Mapbox access token string.
         :param cache: CacheControl cache instance (Dict or FileCache).
         """
         self.session = Session(access_token)
+        self.host = host or os.environ.get('MAPBOX_HOST', self.default_host)
         if cache:
             self.session = CacheControl(self.session, cache=cache)
 
diff --git a/mapbox/services/datasets.py b/mapbox/services/datasets.py
index c913fc3..921897b 100644
--- a/mapbox/services/datasets.py
+++ b/mapbox/services/datasets.py
@@ -7,14 +7,12 @@ from uritemplate import URITemplate
 from mapbox.services.base import Service
 
 
-# Constants
-BASE_URI = 'https://api.mapbox.com/datasets/v1'
-
-
 class Datasets(Service):
     """Access to the Datasets API."""
 
-    baseuri = 'https://api.mapbox.com/datasets/v1'
+    @property
+    def baseuri(self):
+        return 'https://{0}/datasets/v1'.format(self.host)
 
     def _attribs(self, name=None, description=None):
         """Form an attributes dictionary from keyword args."""
@@ -94,7 +92,8 @@ class Datasets(Service):
         :param dataset: the dataset identifier string.
         """
         uri = URITemplate(self.baseuri + '/{owner}/{id}/features').expand(
-                owner=self.username, id=dataset)
+            owner=self.username, id=dataset)
+
         params = {}
         if reverse:
             params['reverse'] = 'true'
diff --git a/mapbox/services/directions.py b/mapbox/services/directions.py
index 9b6ba52..d6f6af3 100644
--- a/mapbox/services/directions.py
+++ b/mapbox/services/directions.py
@@ -8,13 +8,16 @@ from mapbox import errors
 class Directions(Service):
     """Access to the Directions API."""
 
-    baseuri = 'https://api.mapbox.com/v4/directions'
     valid_profiles = ['mapbox.driving',
                       'mapbox.cycling',
                       'mapbox.walking']
     valid_instruction_formats = ['text', 'html']
     valid_geom_encoding = ['geojson', 'polyline', 'false']
 
+    @property
+    def baseuri(self):
+        return 'https://{0}/v4/directions'.format(self.host)
+
     def _validate_profile(self, profile):
         if profile not in self.valid_profiles:
             raise errors.InvalidProfileError(
diff --git a/mapbox/services/distance.py b/mapbox/services/distance.py
index 7b89121..be27a34 100644
--- a/mapbox/services/distance.py
+++ b/mapbox/services/distance.py
@@ -8,9 +8,12 @@ from mapbox.services.base import Service
 class Distance(Service):
     """Access to the Distance API."""
 
-    baseuri = 'https://api.mapbox.com/distances/v1/mapbox'
     valid_profiles = ['driving', 'cycling', 'walking']
 
+    @property
+    def baseuri(self):
+        return 'https://{0}/distances/v1/mapbox'.format(self.host)
+
     def _validate_profile(self, profile):
         if profile not in self.valid_profiles:
             raise InvalidProfileError(
diff --git a/mapbox/services/geocoding.py b/mapbox/services/geocoding.py
index 8c66aba..85c03c7 100644
--- a/mapbox/services/geocoding.py
+++ b/mapbox/services/geocoding.py
@@ -9,9 +9,12 @@ from mapbox.services.base import Service
 class Geocoder(Service):
     """Access to the Geocoding API."""
 
-    baseuri = 'https://api.mapbox.com/geocoding/v5'
     precision = {'reverse': 5, 'proximity': 3}
 
+    @property
+    def baseuri(self):
+        return 'https://{0}/geocoding/v5'.format(self.host)
+
     def __init__(self, name='mapbox.places', access_token=None, cache=None):
         """Constructs a Geocoding Service object.
 
diff --git a/mapbox/services/mapmatching.py b/mapbox/services/mapmatching.py
index 6a68b96..cec6acc 100644
--- a/mapbox/services/mapmatching.py
+++ b/mapbox/services/mapmatching.py
@@ -9,9 +9,12 @@ from mapbox.services.base import Service
 class MapMatcher(Service):
     """Access to the Map Matching API."""
 
-    baseuri = 'https://api.mapbox.com/matching/v4'
     valid_profiles = ['mapbox.driving', 'mapbox.cycling', 'mapbox.walking']
 
+    @property
+    def baseuri(self):
+        return 'https://{0}/matching/v4'.format(self.host)
+
     def _validate_profile(self, profile):
         if profile not in self.valid_profiles:
             raise errors.InvalidProfileError(
diff --git a/mapbox/services/static.py b/mapbox/services/static.py
index 5f81a88..fe5a9fb 100644
--- a/mapbox/services/static.py
+++ b/mapbox/services/static.py
@@ -10,7 +10,9 @@ from mapbox.utils import normalize_geojson_featurecollection
 class Static(Service):
     """Access to the Static Map API."""
 
-    baseuri = 'https://api.mapbox.com/v4'
+    @property
+    def baseuri(self):
+        return 'https://{0}/v4'.format(self.host)
 
     def _validate_lat(self, val):
         if val < -85.0511 or val > 85.0511:
diff --git a/mapbox/services/surface.py b/mapbox/services/surface.py
index aabbc18..8c69073 100644
--- a/mapbox/services/surface.py
+++ b/mapbox/services/surface.py
@@ -7,7 +7,9 @@ from mapbox.services.base import Service
 class Surface(Service):
     """Access to the Surface API."""
 
-    baseuri = 'https://api.mapbox.com/v4/surface'
+    @property
+    def baseuri(self):
+        return 'https://{0}/v4/surface'.format(self.host)
 
     def surface(self,
                 features,
diff --git a/mapbox/services/uploads.py b/mapbox/services/uploads.py
index 3e765a0..1fe1f2f 100644
--- a/mapbox/services/uploads.py
+++ b/mapbox/services/uploads.py
@@ -27,7 +27,9 @@ class Uploader(Service):
         assert job not in u.list().json()
     """
 
-    baseuri = 'https://api.mapbox.com/uploads/v1'
+    @property
+    def baseuri(self):
+        return 'https://{0}/uploads/v1'.format(self.host)
 
     def _get_credentials(self):
         """Gets temporary S3 credentials to stage user-uploaded files
diff --git a/mapbox/utils.py b/mapbox/utils.py
index b811212..ad53c69 100644
--- a/mapbox/utils.py
+++ b/mapbox/utils.py
@@ -1,5 +1,6 @@
 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)


Test patch:
diff --git a/tests/test_base.py b/tests/test_base.py
index 24f2b57..2025043 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -85,6 +85,17 @@ def test_username(monkeypatch):
     assert service.username == 'testuser'
 
 
+def test_default_host():
+    service = base.Service()
+    assert service.host == 'api.mapbox.com'
+
+
+def test_configured_host(monkeypatch):
+    monkeypatch.setenv('MAPBOX_HOST', 'example.com')
+    service = base.Service()
+    assert service.host == 'example.com'
+
+
 def test_username_failures(monkeypatch):
     monkeypatch.delenv('MAPBOX_ACCESS_TOKEN', raising=False)
     monkeypatch.delenv('MapboxAccessToken', raising=False)