Source code for interactive_python.oauth
import aiohttp
import asyncio
import datetime
from .errors import UnknownShortCodeError, ShortCodeAccessDeniedError, \
ShortCodeTimeoutError
class OAuthGrant:
"""Internal DTO for an OAuth grant"""
def __init__(self, client_id, client_secret, scopes, host):
self.client_id = client_id
self.client_secret = client_secret
self.scopes = scopes
self.host = host
def url(self, path):
return self.host + '/api/v1' + path
[docs]class OAuthTokens:
"""OAuthTokens is a bearer from an OAuth access and refresh token retrieved
via the :func:`~interactive_python.OAuthShortCode.accepted` method.
"""
def __init__(self, body):
self.access = body['access_token']
self.refresh = body['refresh_token']
self.expires_at = datetime.datetime.now() + \
datetime.timedelta(seconds=body['expires_in'])
[docs]class OAuthShortCode:
"""
OAuthShortCode is the shortcode handle returned by the `OAuthClient`. See
documentation on that class for more information and usage examples.
"""
def __init__(self, grant, client, body, check_interval=1):
self._handle = body['handle']
self._client = client
self._grant = grant
self._check_interval = check_interval
self.code = body['code']
self.expires_at = datetime.datetime.now() + \
datetime.timedelta(seconds=body['expires_in'])
[docs] async def accepted(self):
"""
Waits until the user enters the shortcode on the Mixer website. Throws
if they deny access or don't enter the code in time.
:raise ShortCodeAccessDeniedError: if the user denies access
:raise ShortCodeTimeoutError: if the user doesn't enter
:rtype: OAuthTokens
"""
address = self._grant.url('/oauth/shortcode/check/' + self._handle)
while True:
await asyncio.sleep(self._check_interval)
async with self._client.get(address) as res:
if res.status == 200:
return await self._get_tokens(await res.json())
elif res.status == 204:
continue
elif res.status == 403:
raise ShortCodeAccessDeniedError('User has denied access')
elif res.status == 404:
raise ShortCodeTimeoutError('Timeout waiting for the user '
'to enter the OAuth shortcode.')
async def _get_tokens(self, body):
address = self._grant.url('/oauth/token')
payload = {
'client_id': self._grant.client_id,
'grant_type': 'authorization_code',
'code': body['code'],
}
async with self._client.post(address, json=payload) as res:
if res.status != 200:
raise UnknownShortCodeError('Expected a 2xx status code, but'
'got {}'.format(res.status))
return OAuthTokens(await res.json())
[docs]class OAuthClient:
"""
The OAuth client implements our shortcode OAuth flow. From a user's
perspective, your app or game displays a 6-digit code to them and prompts
them to go to `mixer.com/go <https://mixer.com/go>`_. This library will
resolve back into your application once they enter that code.
Here's a full example of what a full usage might
look like, with error handling:
.. code:: python
import interactive_python as interactive
import asyncio
async def get_access_token(client):
code = await client.get_code()
print("Go to mixer.com/go and enter {}".format(code.code))
try:
return await code.accepted()
except interactive.ShortCodeAccessDeniedError:
print("The user denied access to our client")
except interactive.ShortCodeTimeoutError:
print("Yo, you're too slow! Let's try again...")
return await get_access_token(client)
async def run():
with interactive.OAuthClient(my_client_id) as client:
token = await get_access_token(client)
print("Access token: {}".format(token.access))
asyncio.get_event_loop().run_until_complete(run())
:param client_id: Your OAuth client ID
:type client_id: string
:param client_secret: Your OAuth client secret, if any
:type client_secret: string
:param host: Base address of the Mixer servers
:type host: string
:param scopes: A list of scopes to request. For Interactive, you only
need the 'interactive:robot:self' scope
:type scopes: list[string]
:param loop: asyncio event loop to attach to
:type loop: asyncio.Loop
"""
def __init__(self, client_id, client_secret=None, host='https://mixer.com',
scopes=['interactive:robot:self'], loop=None):
self._loop = loop
self._grant = OAuthGrant(client_id, client_secret, scopes, host)
def __enter__(self):
self._client = aiohttp.ClientSession(loop=self._loop)
return self
def __exit__(self, *args):
self._client.close()
[docs] async def get_code(self):
"""
Requests a shortcode from the Mixer servers and returns an
OAuthShortCode handle.
:rtype: OAuthShortCode
"""
address = self._grant.url('/oauth/shortcode')
payload = {
'client_id': self._grant.client_id,
'client_secret': self._grant.client_secret,
'scope': ' '.join(self._grant.scopes)
}
async with self._client.post(address, json=payload) as res:
if res.status >= 300:
raise UnknownShortCodeError('Expected a 2xx status code, but'
'got {}'.format(res.status))
return OAuthShortCode(self._grant, self._client, await res.json())