Skip to content
This repository was archived by the owner on Aug 2, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ca1f26e
Start of tokens API support
Feb 23, 2017
44c0a16
Add `list_tokens` method to list tokens of an account
iAmMrinal0 Jul 7, 2017
ed271d8
Add tests for token create and list
iAmMrinal0 Jul 7, 2017
fdc3425
Add `create_temp_token` to create temporary token
iAmMrinal0 Jul 8, 2017
5aa9707
Add test for temporary token creation
iAmMrinal0 Jul 8, 2017
bdebf61
Add `arrow` to dependencies
iAmMrinal0 Jul 8, 2017
07447ee
Remove `arrow` dependency and use `datetime` and `timedelta`
iAmMrinal0 Jul 11, 2017
14367af
Add `update_auth` method to update scopes or note of a token
iAmMrinal0 Jul 11, 2017
5ead778
Add test for token authorization update
iAmMrinal0 Jul 11, 2017
023cba7
Add `delete_auth` method to delete authorization
iAmMrinal0 Jul 11, 2017
874b31f
Add test for authorization deletion
iAmMrinal0 Jul 11, 2017
df071fe
Add `check_validity` method to retrieve a token, check validity
iAmMrinal0 Jul 11, 2017
6c226fc
Add test for retrieving validity information of a token
iAmMrinal0 Jul 11, 2017
10a109d
Add `list_scopes` to list scopes for a token
iAmMrinal0 Jul 11, 2017
55fe480
Add test for listing scopes of a token
iAmMrinal0 Jul 11, 2017
e0dd5b1
Remove `expires` arg to differentiate permanent and temporary token
iAmMrinal0 Jul 12, 2017
92639ce
Change default `expires` to 3600 seconds(1 hour)
iAmMrinal0 Jul 12, 2017
6ccab3c
Change `datetime.now()` to `datetime.utcnow()`
iAmMrinal0 Jul 12, 2017
73a38a5
Raise `ValidationError` instead of a `ValueError`
iAmMrinal0 Jul 12, 2017
2f27603
Use default username from base and make `username` as kwarg
iAmMrinal0 Jul 12, 2017
f4fd833
Update tests after `username` kwarg change, shifting of arguments
iAmMrinal0 Jul 12, 2017
6e2dfc7
Merge pull request #198 from iAmMrinal0/tokens-api-support
perrygeo Jul 12, 2017
05e4d3c
merge master
perrygeo Jul 12, 2017
3d459fa
break uri lines to <80 char
perrygeo Jul 12, 2017
fe8f66f
remove _auth from methods
perrygeo Jul 12, 2017
70b564d
initial documentation and doctests
perrygeo Jul 12, 2017
cd2a593
update unit tests
perrygeo Jul 12, 2017
487be09
make scopes a required arg
perrygeo Jul 12, 2017
25a8be7
function signatures, username kwarg at the end
perrygeo Jul 12, 2017
9e1c65c
unit tests to 100 percent coverage
perrygeo Jul 12, 2017
544c7cc
docstrings
perrygeo Jul 12, 2017
6cc333d
typo
perrygeo Jul 12, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions docs/tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Tokens

The `Tokens` class (`from mapbox import Tokens`) provides
access to the Mapbox Tokens API, allowing you to programmaticaly create
Mapbox access tokens to access Mapbox resources on behalf of a user.

```python

>>> from mapbox import Tokens
>>> service = Tokens()

```

See https://www.mapbox.com/api-documentation/#tokens for general documentation of the API.

This API requires an **initial token** with the `tokens:write` scope.
Your Mapbox access token should be set in your environment;
see the [access tokens](access_tokens.md) documentation for more information.

The Mapbox username associated with each account is determined by the access_token by default. All of the methods also take an optional `username` keyword argument to override this default.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@perrygeo I just noticed that we're using an account keyword arg in uploads. Should we follow suit here?

Copy link
Contributor

@perrygeo perrygeo Jul 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should make that consistent. But looks like the main API docs use username throughout; maybe the solution is to make the change to uploads, s/account/username/g?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ticketed ➡️ #202

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay we'll follow up there.


## List tokens

```python

>>> response = service.list_tokens()
>>> response.json()
[...]

```

## Create temporary tokens

Generate a token for temporary access to mapbox APIs using the
`create_temp_token` method. Tokens can bet set to expire at any time up to one hour.

```python

>>> response = service.create_temp_token(
... scopes=['styles:read'],
... expires=60) # seconds
>>> auth = response.json()
>>> auth['token'][:3]
'tk.'

```


## Create a permanent token


```python

>>> response = service.create(
... scopes=['styles:read'],
... note='test-token')
>>> auth = response.json()
>>> auth['scopes']
['styles:read']
>>> auth['token'][:3]
'pk.'

```

If you create a token with public/read scopes, your token with be a public token, starting with `pk`. If the token has secret/write scopes, the token will be secret, starting with `sk`.

If you want to create a token that may contain secret/write scopes, you must create the token with at least one such scope initially.

## Update a token

To update the scopes of a token

```python

>>> response = service.update(
... authorization_id=auth['id'],
... scopes=['styles:read', 'datasets:read'],
... note="updated")
>>> auth = response.json()
>>> assert response.status_code == 200


```

## Check validity of a token

```python

>>> service.check_validity().json()['code']
'TokenValid'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused about where the token gets passed to the method. I think it might be more clear like check_validity(token), no?

A quicker check for validity would be nice, too (like is_valid(token) --> True/False, but that's not really the style of this API.


```

Note that this applies only to the access token which is making the request.
If you want to check the validity of other tokens, you must make a separate instance of the `Tokens` service class using the desired `access_token`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answers my question above. I still don't think the current implementation feels quite right.

Copy link
Contributor

@perrygeo perrygeo Jul 12, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that the check_validity method feel a bit off. Similarly the list_scopes method.

They fit the http-response-first style of the rest of this module but usability suffers a bit. Still 🤔 about potential solutions...

Copy link
Contributor

@perrygeo perrygeo Jul 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-reading the docs for this endpoint (https://www.mapbox.com/api-documentation/#retrieve-a-token) it occurs to me that

  • validity is not a binary choice, there are five possible token codes.
  • the endpoint is intended to retrieve all metadata about the token, including the token code but also the authorization id, etc. check_validity might be a bit of a misnomer.

As such, I'm ok with leaving the current implementation as is for the purposes of this PR.

But perhaps we need to rename it. Consistency with the HTTP api docs would suggest retrieve_token?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And regarding the funky implementation detail of needing to create an instance of Tokens before checking validity, maybe we could write some @classmethods which do the instantiation silently, presenting a simpler interface that accepts a token as an argument?

Tokens.retrieve(token)

Copy link
Contributor Author

@sgillies sgillies Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@perrygeo yes, that's the way to go. I think get or get_token is better than retrieve, more true to the HTTP nature of the API.


```python

>>> new_service = Tokens(access_token=auth['token'])

```

## List the scopes of a token

```python

>>> response = service.list_scopes()
>>> response.json()
[...]

```

As with checking validity, this method applies only to the access token which is making the request.


## Delete a token

```python

>>> response = service.delete(
... authorization_id=auth['id'])
>>> assert response.status_code == 204

```


5 changes: 3 additions & 2 deletions mapbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# mapbox
__version__ = "0.14.0"

from .services.analytics import Analytics
from .services.datasets import Datasets
from .services.directions import Directions
from .services.distance import Distance
from .services.geocoding import (
Geocoder, InvalidCountryCodeError, InvalidPlaceTypeError)
from .services.mapmatching import MapMatcher
from .services.surface import Surface
from .services.static import Static
from .services.static_style import StaticStyle
from .services.surface import Surface
from .services.tokens import Tokens
from .services.uploads import Uploader
from .services.analytics import Analytics
196 changes: 196 additions & 0 deletions mapbox/services/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
from datetime import datetime, timedelta

from uritemplate import URITemplate

from mapbox.errors import ValidationError
from mapbox.services.base import Service


class Tokens(Service):
"""Access to the Tokens API."""

@property
def baseuri(self):
return 'https://{0}/tokens/v2'.format(self.host)

def create(self, scopes, note=None, username=None):
"""Create a permanent token

Parameters
----------
scopes: list
note: string
username: string, defaults to username in access token

Returns
-------
requests.Response
"""
if username is None:
username = self.username
if not note:
note = "SDK generated note"

uri = URITemplate(
self.baseuri + '/{username}').expand(username=username)

payload = {'scopes': scopes, 'note': note}

res = self.session.post(uri, json=payload)
self.handle_http_error(res)
return res

def list_tokens(self, limit=None, username=None):
"""List all permanent tokens

Parameters
----------
limit: int
username: string, defaults to username in access token

Returns
-------
requests.Response
"""
if username is None:
username = self.username

uri = URITemplate(
self.baseuri + '/{username}').expand(username=username)

params = {}
if limit:
params['limit'] = int(limit)

res = self.session.get(uri, params=params)
self.handle_http_error(res)
return res

def create_temp_token(self, scopes, expires=3600, username=None):
"""Create a temporary token

Parameters
----------
scopes: list
List of valid mapbox token scope strings
expires: int
seconds, defaults to 3600 (1 hr)
username: string
defaults to username in access token

Returns
-------
requests.Response
"""
if username is None:
username = self.username

uri = URITemplate(
self.baseuri + '/{username}').expand(username=username)

payload = {'scopes': scopes}

if expires <= 0 or expires > 3600:
raise ValidationError("Expiry should be within 1 hour from now")
payload['expires'] = (datetime.utcnow() + timedelta(seconds=expires)).isoformat()

res = self.session.post(uri, json=payload)
self.handle_http_error(res)
return res

def update(self, authorization_id, scopes=None, note=None, username=None):
"""Update a token's scopes or note

Parameters
----------
authorization_id: string
id of the token to update (not the token itself)
scopes: list
List of valid mapbox token scope strings
note: string
username: string
defaults to username in access token

Returns
-------
requests.Response
"""
if username is None:
username = self.username
if not scopes and not note:
raise ValidationError("Provide either scopes or a note to update token")

uri = URITemplate(
self.baseuri + '/{username}/{authorization_id}').expand(
username=username, authorization_id=authorization_id)

payload = {}
if scopes:
payload['scopes'] = scopes
if note:
payload['note'] = note

res = self.session.patch(uri, json=payload)
self.handle_http_error(res)
return res

def delete(self, authorization_id, username=None):
"""Delete a token

Parameters
----------
authorization_id: string
id of the token to update (not the token itself)
username: string
defaults to username in access token

Returns
-------
requests.Response
"""
if username is None:
username = self.username

uri = URITemplate(
self.baseuri + '/{username}/{authorization_id}').expand(
username=username, authorization_id=authorization_id)

res = self.session.delete(uri)
self.handle_http_error(res)
return res

def check_validity(self):
"""Check validity of the token

Returns
-------
requests.Response
"""
uri = URITemplate(self.baseuri)

res = self.session.get(uri)
self.handle_http_error(res)
return res

def list_scopes(self, username=None):
"""Delete a token

Parameters
----------
username: string
defaults to username in access token

Returns
-------
requests.Response
"""
if username is None:
username = self.username

uri = URITemplate(
'https://{host}/scopes/v1/{username}').expand(
host=self.host, username=username)

res = self.session.get(uri)
self.handle_http_error(res)
return res
Loading