diff --git a/README.rst b/README.rst index d19fdcb..cf31e3d 100644 --- a/README.rst +++ b/README.rst @@ -59,6 +59,10 @@ Services - Manage editable collections of GeoJSON features - Persistent storage for custom geographic data +- **Tilesets V1** `examples <./docs/tilesets.md#tilesets>`__, `website `__ + + - Read metadata for raster and vector tilesets + - **Tilequery V4** `examples <./docs/tilequery.md#tilequery>`__, `website `__ - Retrieve data about specific features from a vector tileset diff --git a/docs/index.rst b/docs/index.rst index 3f32719..f05218a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,6 +57,10 @@ Services - Manage editable collections of GeoJSON features - Persistent storage for custom geographic data +- **Tilesets V1** `examples <./tilesets.html#tilesets>`__, `website `__ + + - Read metadata for raster and vector tilesets + - **Tilequery V4** `examples <./tilequery.html#tilequery>`__, `website `__ - Retrieve data about specific features from a vector tileset @@ -115,6 +119,7 @@ Documentation analytics.md mapmatching.md static_style.md + tilesets.md tilequery.md maps.md api/mapbox.rst diff --git a/docs/tilesets.md b/docs/tilesets.md new file mode 100644 index 0000000..a275a2c --- /dev/null +++ b/docs/tilesets.md @@ -0,0 +1,43 @@ +# Tilesets + +The `Tilesets` class provides access to the Mapbox Tilesets API. You can import it from either the `mapbox` module or the `mapbox.services.tilesets` module. + +__mapbox__: + +```python +>>> from mapbox import Tilesets +``` + +__mapbox.services.tilesets__: + +```python +>>> from mapbox.services.tilesets import Tilesets +``` + +See https://www.mapbox.com/api-documentation/#tilesets for general documentation of the API. + +Use of the Tilesets API requires an access token, which you should set in your environment. For more information, see the [access tokens](access_tokens.md) documentation. + +## Tilesets Method + +The public method of the `Tilesets` class provides access to the Tilesets API and returns an instance of [`requests.Response`](http://docs.python-requests.org/en/latest/api/#requests.Response). + +## Usage: Listing Tilesets + +Instantiate `Tilesets`. + +```python +>>> tilesets = Tilesets() +``` + +Call the `list_tilesets` method, passing in values for optional arguments as necessary - `tileset_type`, `visibility`, `sortby`, and `limit`. + +```python +>>> response = tilesets.list_tilesets() +``` + +Evaluate whether the request succeeded, and retrieve the tileset object from the response object. + +```python +>>> if response.status_code == 200: +... tileset_object = response.get_json() diff --git a/mapbox/__init__.py b/mapbox/__init__.py index ddd59cd..e336247 100644 --- a/mapbox/__init__.py +++ b/mapbox/__init__.py @@ -12,5 +12,6 @@ from .services.static_style import StaticStyle from .services.uploads import Uploader from .services.analytics import Analytics +from .services.tilesets import Tilesets from .services.tilequery import Tilequery -from .services.maps import Maps +from .services.maps import Maps \ No newline at end of file diff --git a/mapbox/errors.py b/mapbox/errors.py index 4c18eae..8e3af4b 100644 --- a/mapbox/errors.py +++ b/mapbox/errors.py @@ -66,6 +66,54 @@ class MapboxDeprecationWarning(UserWarning): pass +class InvalidTilesetTypeError(ValidationError): + """InvalidTilesetTypeError + + Parameters + ---------- + message : str, optional + A human-readable string describing the error. + """ + + pass + + +class InvalidVisibilityError(ValidationError): + """InvalidVisibilityError + + Parameters + ---------- + message : str, optional + A human-readable string describing the error. + """ + + pass + + +class InvalidSortbyError(ValidationError): + """InvalidSortbyError + + Parameters + ---------- + message : str, optional + A human-readable string describing the error. + """ + + pass + + +class InvalidLimitError(ValidationError): + """InvalidLimitError + + Parameters + ---------- + message : str, optional + A human-readable string describing the error. + """ + + pass + + class InvalidZoomError(ValidationError): pass diff --git a/mapbox/services/tilesets.py b/mapbox/services/tilesets.py new file mode 100644 index 0000000..7be75e6 --- /dev/null +++ b/mapbox/services/tilesets.py @@ -0,0 +1,175 @@ +"""The Tilesets class provides access to Mapbox's Tilesets API.""" + +from mapbox.errors import ( + InvalidTilesetTypeError, + InvalidVisibilityError, + InvalidSortbyError, + InvalidLimitError +) + +from mapbox.services.base import Service + +from uritemplate import URITemplate + +class Tilesets(Service): + """Access to Tilesets API V1 + + Attributes + ---------- + api_name : str + The API's name. + + api_version : str + The API's version number. + + valid_tileset_types : list + The possible values for tileset_type. + + valid_visibilities : list + The possible values for visibility. + + valid_sortbys : list + The possible values for sortby. + + base_uri : str + The API's base URI, currently https://api.mapbox.com/tilesets/v1 + """ + + api_name = "tilesets" + + api_version = "v1" + + valid_tileset_types = [ + "raster", + "vector" + ] + + valid_visibilities = [ + "private", + "public" + ] + + valid_sortbys = [ + "created", + "modified" + ] + + @property + def base_uri(self): + """Forms base URI.""" + + return "https://{}/{}/{}".format( + self.host, + self.api_name, + self.api_version + ) + + def _validate_tileset_type(self, tileset_type): + """Validates tileset type, raising error if invalid.""" + + if tileset_type not in self.valid_tileset_types: + raise InvalidTilesetTypeError( + "{} is not a valid tileset type".format(tileset_type) + ) + + return tileset_type + + def _validate_visibility(self, visibility): + """Validates visibility, raising error if invalid.""" + + if visibility not in self.valid_visibilities: + raise InvalidVisibilityError( + "{} is not a valid value for visibility".format(visibility) + ) + + return visibility + + def _validate_sortby(self, sortby): + """Validates sortby, raising error if invalid.""" + + if sortby not in self.valid_sortbys: + raise InvalidSortbyError( + "{} is not a valid value for sortby".format(sortby) + ) + + return sortby + + def _validate_limit(self, limit): + """Validates limit, raising error if invalid.""" + + if (limit < 1) or (limit > 500): + raise InvalidLimitError( + "{} is not a valid value for limit".format(limit) + ) + + return limit + + def tilesets(self, tileset_type=None, visibility=None, + sortby=None, limit=None): + """Lists all tilesets for an account. + + tileset_type : str, optional + Filter results by tileset type. + + Valid values are raster or vector. + + visibility : str, optional + Filter results by visibility. + + Valid values are private or public. + + Private tilesets require an access token + belonging to the owner, while public + tilesets may be requested with an access + token belonging to any user. + + sortby : str, optional + Sort results by timestamp. + + Valid values are created or modified + + limit : int, optional + The maximum number of objects to return + (pagination), where 1 is the minimum value + and 500 is the maxium value. + + The default value is 100. + + Returns + ------- + request.Response + The response object with a tileset object. + """ + + # Build URI resource path. + + path_part = "/{username}" + path_values = dict(username=self.username) + uri = URITemplate(self.base_uri + path_part).expand(**path_values) + + # Validate tileset_type, visibility, sortby, and limit + # and build URI query parameters. + + query_parameters = dict() + + if tileset_type: + tileset_type = self._validate_tileset_type(tileset_type) + query_parameters["type"] = tileset_type + + if visibility: + visibility = self._validate_visibility(visibility) + query_parameters["visibility"] = visibility + + if sortby: + sortby = self._validate_sortby(sortby) + query_parameters["sortby"] = sortby + + if limit: + limit = self._validate_limit(limit) + query_parameters["limit"] = str(limit) + + # Send HTTP GET request. + + response = self.session.get(uri, params=query_parameters) + + return response diff --git a/tests/test_tilesets.py b/tests/test_tilesets.py new file mode 100644 index 0000000..3ae6037 --- /dev/null +++ b/tests/test_tilesets.py @@ -0,0 +1,313 @@ +from mapbox.errors import ( + InvalidTilesetTypeError, + InvalidVisibilityError, + InvalidSortbyError, + InvalidLimitError +) + +from mapbox.services.tilesets import Tilesets + +from base64 import b64encode + +from pytest import ( + mark, + raises +) + +from responses import ( + activate, + add, + GET +) + + +USERNAME = b64encode(b"{\"u\":\"user\"}").decode("utf-8") +ACCESS_TOKEN = "pk.{}.test".format(USERNAME) + + +def test_object_attributes(): + tilesets = Tilesets() + + assert tilesets.api_name + assert tilesets.api_version + assert tilesets.valid_tileset_types + assert tilesets.valid_visibilities + assert tilesets.valid_sortbys + assert tilesets.base_uri + + +def test_validate_tileset_type_invalid(): + tilesets = Tilesets() + + with raises(InvalidTilesetTypeError) as exception: + tileset_type = "invalid" + result = tilesets._validate_tileset_type(tileset_type) + + +@mark.parametrize("tileset_type", ["raster", "vector"]) +def test_validate_tileset_type_valid(tileset_type): + tilesets = Tilesets() + result = tilesets._validate_tileset_type(tileset_type) + assert result == tileset_type + + +def test_validate_visibility_invalid(): + tilesets = Tilesets() + + with raises(InvalidVisibilityError) as exception: + visibility = "invalid" + result = tilesets._validate_visibility(visibility) + + +@mark.parametrize("visibility", ["private", "public"]) +def test_validate_visibility_valid(visibility): + tilesets = Tilesets() + result = tilesets._validate_visibility(visibility) + assert result == visibility + + +def test_validate_sortby_invalid(): + tilesets = Tilesets() + + with raises(InvalidSortbyError) as exception: + sortby = "invalid" + result = tilesets._validate_sortby(sortby) + + +@mark.parametrize("sortby", ["created", "modified"]) +def test_validate_sortby_valid(sortby): + tilesets = Tilesets() + result = tilesets._validate_sortby(sortby) + assert result == sortby + + +@mark.parametrize("limit", [0, 501]) +def test_validate_limit_invalid(limit): + tilesets = Tilesets() + + with raises(InvalidLimitError) as exception: + result = tilesets._validate_limit(limit) + + +@mark.parametrize("limit", [1, 250, 500]) +def test_validate_limit_valid(limit): + tilesets = Tilesets() + result = tilesets._validate_limit(limit) + assert result == limit + + +@activate +def test_tilesets(): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN), + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets() + assert response.status_code == 200 + + +@activate +@mark.parametrize("tileset_type", ["raster", "vector"]) +def test_tilesets_with_tileset_type(tileset_type): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&type={}".format(tileset_type), + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(tileset_type=tileset_type) + assert response.status_code == 200 + + +@activate +@mark.parametrize("visibility", ["private", "public"]) +def test_tilesets_with_visibility(visibility): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&visibility={}".format(visibility), + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(visibility=visibility) + assert response.status_code == 200 + + +@activate +@mark.parametrize("sortby", ["created", "modified"]) +def test_tilesets_with_sortby(sortby): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&sortby={}".format(sortby), + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(sortby=sortby) + assert response.status_code == 200 + + +@activate +@mark.parametrize("limit", [1, 250, 500]) +def test_tilesets_with_limit(limit): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&limit={}".format(limit), + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(limit=limit) + assert response.status_code == 200 + + +@activate +def test_tilesets_with_tileset_type_and_visibility(): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&type=vector" + + "&visibility=public", + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(tileset_type="vector", visibility="public") + assert response.status_code == 200 + + +@activate +def test_tilesets_with_tileset_type_and_sortby(): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&type=vector" + + "&sortby=created", + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(tileset_type="vector", sortby="created") + assert response.status_code == 200 + + +@activate +def test_tilesets_with_tileset_type_and_limit(): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&type=vector" + + "&limit=500", + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(tileset_type="vector", limit=500) + assert response.status_code == 200 + + +@activate +def test_tilesets_with_visibility_and_sortby(): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&visibility=public" + + "&sortby=created", + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(visibility="public", sortby="created") + assert response.status_code == 200 + + +@activate +def test_tilesets_with_visibility_and_limit(): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user" + + "?access_token={}".format(ACCESS_TOKEN) + + "&visibility=public" + + "&limit=500", + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(visibility="public", limit=500) + assert response.status_code == 200 + + +@activate +def test_tilesets_with_sortby_and_limit(): + add( + method=GET, + url="https://api.mapbox.com" + + "/tilesets/v1" + + "/user?access_token={}".format(ACCESS_TOKEN) + + "&sortby=created" + + "&limit=500", + match_querystring=True, + body="{\"key\": \"value\"}", + status=200 + ) + + tilesets = Tilesets(access_token=ACCESS_TOKEN) + response = tilesets.tilesets(sortby="created", limit=500) + assert response.status_code == 200