Advertisement
Robociety

__init__.py

Oct 21st, 2014
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.47 KB | None | 0 0
  1. # vim: set ts=4 sw=)
  2.  
  3. from functools import wraps
  4. from json import loads
  5. from datetime import datetime, timedelta
  6. from time import mktime
  7. try:
  8.     from urllib import urlencode
  9.     from urllib2 import Request, urlopen
  10.     from urlparse import urlsplit, urlunsplit, parse_qsl
  11.  
  12.     # monkeypatch httpmessage
  13.     from httplib import HTTPMessage
  14.     def get_charset(self):
  15.         try:
  16.             data = filter(lambda s: 'Content-Type' in s, self.headers)[0]
  17.             if 'charset' in data:
  18.                 cs = data[data.index(';') + 1:-2].split('=')[1].lower()
  19.                 return cs
  20.         except IndexError:
  21.             pass
  22.  
  23.         return 'utf-8'
  24.     HTTPMessage.get_content_charset = get_charset
  25. except ImportError: # pragma: no cover
  26.     from urllib.parse import urlencode, urlsplit, urlunsplit, parse_qsl
  27.     from urllib.request import Request, urlopen
  28.  
  29.  
  30. class Client(object):
  31.     """ OAuth 2.0 client object
  32.    """
  33.  
  34.     def __init__(self, auth_endpoint=None, token_endpoint=None,
  35.         resource_endpoint=None, client_id=None, client_secret=None,
  36.         token_transport=None):
  37.         """ Instantiates a `Client` to authorize and authenticate a user
  38.  
  39.        :param auth_endpoint: The authorization endpoint as issued by the
  40.                              provider. This is where the user should be
  41.                              redirect to provider authorization for your
  42.                              application.
  43.        :param token_endpoint: The endpoint against which a `code` will be
  44.                               exchanged for an access token.
  45.        :param resource_endpoint: The base url to use when accessing resources
  46.                                  via `Client.request`.
  47.        :param client_id: The client ID as issued by the provider.
  48.        :param client_secret: The client secret as issued by the provider. This
  49.                              must not be shared.
  50.        """
  51.         assert token_transport is None or hasattr(token_transport, '__call__')
  52.  
  53.         self.auth_endpoint = auth_endpoint
  54.         self.token_endpoint = token_endpoint
  55.         self.resource_endpoint = resource_endpoint
  56.         self.client_id = client_id
  57.         self.client_secret = client_secret
  58.         self.access_token = None
  59.         self.token_transport = token_transport or transport_query
  60.         self.token_expires = -1
  61.         self.refresh_token = None
  62.  
  63.     def auth_uri(self, redirect_uri=None, scope=None, scope_delim=None,
  64.         state=None, **kwargs):
  65.  
  66.         """  Builds the auth URI for the authorization endpoint
  67.  
  68.        :param scope: (optional) The `scope` parameter to pass for
  69.                      authorization. The format should match that expected by
  70.                      the provider (i.e. Facebook expects comma-delimited,
  71.                      while Google expects space-delimited)
  72.        :param state: (optional) The `state` parameter to pass for
  73.                      authorization. If the provider follows the OAuth 2.0
  74.                      spec, this will be returned to your `redirect_uri` after
  75.                      authorization. Generally used for CSRF protection.
  76.        :param **kwargs: Any other querystring parameters to be passed to the
  77.                         provider.
  78.        """
  79.         kwargs.update({
  80.             'client_id': self.client_id,
  81.             'response_type': 'code',
  82.         })
  83.  
  84.         if scope is not None:
  85.             kwargs['scope'] = scope
  86.  
  87.         if state is not None:
  88.             kwargs['state'] = state
  89.  
  90.         if redirect_uri is not None:
  91.             kwargs['redirect_uri'] = redirect_uri
  92.  
  93.         return '%s?%s' % (self.auth_endpoint, urlencode(kwargs))
  94.  
  95.     def request_token(self, parser=None, redirect_uri=None, **kwargs):
  96.         """ Request an access token from the token endpoint.
  97.        This is largely a helper method and expects the client code to
  98.        understand what the server expects. Anything that's passed into
  99.        ``**kwargs`` will be sent (``urlencode``d) to the endpoint. Client
  100.        secret and client ID are automatically included, so are not required
  101.        as kwargs. For example::
  102.  
  103.            # if requesting access token from auth flow:
  104.            {
  105.                'code': rval_from_auth,
  106.            }
  107.  
  108.            # if refreshing access token:
  109.            {
  110.                'refresh_token': stored_refresh_token,
  111.                'grant_type': 'refresh_token',
  112.            }
  113.  
  114.        :param parser: Callback to deal with returned data. Not all providers
  115.                       use JSON.
  116.        """
  117.         kwargs = kwargs and kwargs or {}
  118.  
  119.         parser = parser or _default_parser
  120.         kwargs.update({
  121.             'client_id': self.client_id,
  122.             'client_secret': self.client_secret,
  123.             'grant_type': 'grant_type' in kwargs and kwargs['grant_type'] or \
  124.                 'authorization_code'
  125.         })
  126.         if redirect_uri is not None:
  127.             kwargs.update({'redirect_uri': redirect_uri})
  128.  
  129.         # TODO: maybe raise an exception here if status code isn't 200?
  130.         msg = urlopen(self.token_endpoint, urlencode(kwargs).encode(
  131.             'utf-8'))
  132.         data = parser(msg.read().decode(msg.info().get_content_charset() or
  133.             'utf-8'))
  134.  
  135.         for key in data:
  136.             setattr(self, key, data[key])
  137.  
  138.         # expires_in is RFC-compliant. if anything else is used by the
  139.         # provider, token_expires must be set manually
  140.         if hasattr(self, 'expires_in'):
  141.             try:
  142.                 # python3 dosn't support long
  143.                 seconds = long(self.expires_in)
  144.             except:
  145.                 seconds = int(self.expires_in)
  146.             self.token_expires = mktime((datetime.utcnow() + timedelta(
  147.                 seconds=seconds)).timetuple())
  148.  
  149.     def refresh(self):
  150.         self.request_token(refresh_token=self.refresh_token,
  151.             grant_type='refresh_token')
  152.  
  153.     def request(self, url, method=None, data=None, headers=None, parser=None):
  154.         """ Request user data from the resource endpoint
  155.        :param url: The path to the resource and querystring if required
  156.        :param method: HTTP method. Defaults to ``GET`` unless data is not None
  157.                       in which case it defaults to ``POST``
  158.        :param data: Data to be POSTed to the resource endpoint
  159.        :param parser: Parser callback to deal with the returned data. Defaults
  160.                       to ``json.loads`.`
  161.        """
  162.         assert self.access_token is not None
  163.         parser = parser or loads
  164.  
  165.         if not method:
  166.             method = 'GET' if not data else 'POST'
  167.  
  168.         req = self.token_transport('{0}{1}'.format(self.resource_endpoint,
  169.             url), self.access_token, data=data, method=method, headers=headers)
  170.  
  171.         resp = urlopen(req)
  172.         data = resp.read()
  173.         try:
  174.             return parser(data.decode(resp.info().get_content_charset() or
  175.                 'utf-8'))
  176.             # try to decode it first using either the content charset, falling
  177.             # back to utf-8
  178.  
  179.         except UnicodeDecodeError:
  180.             # if we've gotten a decoder error, the calling code better know how
  181.             # to deal with it. some providers (i.e. stackexchange) like to gzip
  182.             # their responses, so this allows the client code to handle it
  183.             # directly.
  184.             return parser(data)
  185.  
  186.  
  187. def transport_headers(url, access_token, data=None, method=None, headers=None):
  188.     try:
  189.         req = Request(url, data=data, method=method)
  190.     except TypeError:
  191.         req = Request(url, data=data)
  192.         req.get_method = lambda: method
  193.  
  194.     add_headers = {'Authorization': 'Bearer {0}'.format(access_token)}
  195.     if headers is not None:
  196.         add_headers.update(headers)
  197.  
  198.     req.headers.update(add_headers)
  199.     return req
  200.  
  201.  
  202. def transport_query(url, access_token, data=None, method=None, headers=None):
  203.     parts = urlsplit(url)
  204.     query = dict(parse_qsl(parts.query))
  205.     query.update({
  206.         'access_token': access_token
  207.     })
  208.     url = urlunsplit((parts.scheme, parts.netloc, parts.path,
  209.         urlencode(query), parts.fragment))
  210.     try:
  211.         req = Request(url, data=data, method=method)
  212.     except TypeError:
  213.         req = Request(url, data=data)
  214.         req.get_method = lambda: method
  215.  
  216.     if headers is not None:
  217.         req.headers.update(headers)
  218.  
  219.     return req
  220.  
  221.  
  222. def _default_parser(data):
  223.     try:
  224.         return loads(data)
  225.     except ValueError:
  226.         return dict(parse_qsl(data))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement