import requests_oauthlib (a dependency)
authorBenjamin Mako Hill <mako@atdot.cc>
Fri, 1 May 2015 18:23:17 +0000 (11:23 -0700)
committerBenjamin Mako Hill <mako@atdot.cc>
Fri, 1 May 2015 18:23:17 +0000 (11:23 -0700)
13 files changed:
docs/requests-oauthlib-AUTHORS.rst [new file with mode: 0644]
docs/requests-oauthlib-HISTORY.rst [new file with mode: 0644]
docs/requests-oauthlib-LICENSE [new file with mode: 0644]
docs/requests-oauthlib-README.rst [new file with mode: 0644]
requests_oauthlib/__init__.py [new file with mode: 0644]
requests_oauthlib/compliance_fixes/__init__.py [new file with mode: 0644]
requests_oauthlib/compliance_fixes/facebook.py [new file with mode: 0644]
requests_oauthlib/compliance_fixes/linkedin.py [new file with mode: 0644]
requests_oauthlib/oauth1_auth.py [new file with mode: 0644]
requests_oauthlib/oauth1_session.py [new file with mode: 0644]
requests_oauthlib/oauth2_auth.py [new file with mode: 0644]
requests_oauthlib/oauth2_session.py [new file with mode: 0644]
requests_oauthlib/utils.py [new file with mode: 0644]

diff --git a/docs/requests-oauthlib-AUTHORS.rst b/docs/requests-oauthlib-AUTHORS.rst
new file mode 100644 (file)
index 0000000..fb522a3
--- /dev/null
@@ -0,0 +1,23 @@
+Requests-oauthlib is written and maintained by Kenneth Reitz and various
+contributors:
+
+Development Lead
+----------------
+
+- Kenneth Reitz <me@kennethreitz.com>
+
+Patches and Suggestions
+-----------------------
+
+- Cory Benfield <cory@lukasa.co.uk>
+- Ib Lundgren <ib.lundgren@gmail.com>
+- Devin Sevilla <dasevilla@gmail.com>
+- Imad Mouhtassem <mouhtasi@gmail.com>
+- Johan Euphrosine <proppy@google.com>
+- Johannes Spielmann <js@shezi.de>
+- Martin Trigaux <me@mart-e.be>
+- Matt McClure <matt.mcclure@mapmyfitness.com>
+- Mikhail Sobolev <mss@mawhrin.net>
+- Paul Bonser <misterpib@gmail.com>
+- Vinay Raikar <rockraikar@gmail.com>
+- kracekumar <me@kracekumar.com>
diff --git a/docs/requests-oauthlib-HISTORY.rst b/docs/requests-oauthlib-HISTORY.rst
new file mode 100644 (file)
index 0000000..3cc6cca
--- /dev/null
@@ -0,0 +1,14 @@
+History
+-------
+
+v0.4.0 (29 September 2013)
+++++++++++++++++++++++++++
+- OAuth1Session methods only return unicode strings. #55.
+- Renamed requests_oauthlib.core to requests_oauthlib.oauth1_auth for consistency. #79.
+- Added Facebook compliance fix and access_token_response hook to OAuth2Session. #63.
+- Added LinkedIn compliance fix.
+- Added refresh_token_response compliance hook, invoked before parsing the refresh token.
+- Correctly limit compliance hooks to running only once!
+- Content type guessing should only be done when no content type is given
+- OAuth1 now updates r.headers instead of replacing it with non case insensitive dict
+- Remove last use of Response.content (in OAuth1Session). #44.
diff --git a/docs/requests-oauthlib-LICENSE b/docs/requests-oauthlib-LICENSE
new file mode 100644 (file)
index 0000000..5069506
--- /dev/null
@@ -0,0 +1,13 @@
+Copyright (c) 2013 Kenneth Reitz.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
\ No newline at end of file
diff --git a/docs/requests-oauthlib-README.rst b/docs/requests-oauthlib-README.rst
new file mode 100644 (file)
index 0000000..0207e7d
--- /dev/null
@@ -0,0 +1,57 @@
+Requests-OAuthlib
+=================
+
+This project provides first-class OAuth library support for `Requests <http://python-requests.org>`_.
+
+The OAuth 1 workflow
+--------------------
+
+OAuth 1 can seem overly complicated and it sure has its quirks. Luckily,
+requests_oauthlib hides most of these and let you focus at the task at hand.
+
+Accessing protected resources using requests_oauthlib is as simple as:
+
+.. code-block:: pycon
+
+    >>> from requests_oauthlib import OAuth1Session
+    >>> twitter = OAuth1Session('client_key',
+                                client_secret='client_secret',
+                                resource_owner_key='resource_owner_key',
+                                resource_owner_secret='resource_owner_secret')
+    >>> url = 'https://api.twitter.com/1/account/settings.json'
+    >>> r = twitter.get(url)
+
+Before accessing resources you will need to obtain a few credentials from your
+provider (i.e. Twitter) and authorization from the user for whom you wish to
+retrieve resources for. You can read all about this in the full
+`OAuth 1 workflow guide on RTD <http://requests-oauthlib.readthedocs.org/en/latest/oauth1_workflow.html>`_.
+
+The OAuth 2 workflow
+--------------------
+
+OAuth 2 is generally simpler than OAuth 1 but comes in more flavours. The most
+common being the Authorization Code Grant, also known as the WebApplication
+flow.
+
+Fetching a protected resource after obtaining an access token can be as simple as:
+
+.. code-block:: pycon
+
+    >>> from requests_oauthlib import OAuth2Session
+    >>> google = OAuth2Session(r'client_id', token=r'token')
+    >>> url = 'https://www.googleapis.com/oauth2/v1/userinfo'
+    >>> r = google.get(url)
+
+Before accessing resources you will need to obtain a few credentials from your
+provider (i.e. Google) and authorization from the user for whom you wish to
+retrieve resources for. You can read all about this in the full
+`OAuth 2 workflow guide on RTD <http://requests-oauthlib.readthedocs.org/en/latest/oauth2_workflow.html>`_.
+
+Installation
+-------------
+
+To install requests and requests_oauthlib you can use pip:
+
+.. code-block:: bash
+
+    $ pip install requests requests_oauthlib
diff --git a/requests_oauthlib/__init__.py b/requests_oauthlib/__init__.py
new file mode 100644 (file)
index 0000000..a89f9ce
--- /dev/null
@@ -0,0 +1,6 @@
+from .oauth1_auth import OAuth1
+from .oauth1_session import OAuth1Session
+from .oauth2_auth import OAuth2
+from .oauth2_session import OAuth2Session, TokenUpdated
+
+__version__ = '0.4.0'
diff --git a/requests_oauthlib/compliance_fixes/__init__.py b/requests_oauthlib/compliance_fixes/__init__.py
new file mode 100644 (file)
index 0000000..e828a27
--- /dev/null
@@ -0,0 +1,4 @@
+from __future__ import absolute_import
+
+from .facebook import facebook_compliance_fix
+from .linkedin import linkedin_compliance_fix
diff --git a/requests_oauthlib/compliance_fixes/facebook.py b/requests_oauthlib/compliance_fixes/facebook.py
new file mode 100644 (file)
index 0000000..c3c3012
--- /dev/null
@@ -0,0 +1,17 @@
+from json import dumps
+from oauthlib.common import urldecode
+
+
+def facebook_compliance_fix(session):
+
+    def _compliance_fix(r):
+        token = dict(urldecode(r.text))
+        expires = token.get('expires')
+        if expires is not None:
+            token['expires_in'] = expires
+        token['token_type'] = 'Bearer'
+        r._content = dumps(token)
+        return r
+
+    session.register_compliance_hook('access_token_response', _compliance_fix)
+    return session
diff --git a/requests_oauthlib/compliance_fixes/linkedin.py b/requests_oauthlib/compliance_fixes/linkedin.py
new file mode 100644 (file)
index 0000000..ad19004
--- /dev/null
@@ -0,0 +1,24 @@
+from json import loads, dumps
+
+from oauthlib.common import add_params_to_uri
+
+
+def linkedin_compliance_fix(session):
+
+    def _missing_token_type(r):
+        token = loads(r.text)
+        token['token_type'] = 'Bearer'
+        r._content = dumps(token)
+        return r
+
+    def _non_compliant_param_name(url, headers, data):
+        token = [('oauth2_access_token', session._client.access_token)]
+        url = add_params_to_uri(url, token)
+        return url, headers, data
+
+    session._client.default_token_placement = 'query'
+    session.register_compliance_hook('access_token_response',
+                                     _missing_token_type)
+    session.register_compliance_hook('protected_request',
+                                     _non_compliant_param_name)
+    return session
diff --git a/requests_oauthlib/oauth1_auth.py b/requests_oauthlib/oauth1_auth.py
new file mode 100644 (file)
index 0000000..e38a889
--- /dev/null
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from oauthlib.common import extract_params
+from oauthlib.oauth1 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER)
+
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
+
+import sys
+if sys.version > "3":
+    unicode = str
+
+    def to_native_str(string):
+        return string.decode('utf-8')
+else:
+    def to_native_str(string):
+        return string
+
+# OBS!: Correct signing of requests are conditional on invoking OAuth1
+# as the last step of preparing a request, or at least having the
+# content-type set properly.
+class OAuth1(object):
+    """Signs the request using OAuth 1 (RFC5849)"""
+    def __init__(self, client_key,
+            client_secret=None,
+            resource_owner_key=None,
+            resource_owner_secret=None,
+            callback_uri=None,
+            signature_method=SIGNATURE_HMAC,
+            signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+            rsa_key=None, verifier=None,
+            decoding='utf-8'):
+
+        try:
+            signature_type = signature_type.upper()
+        except AttributeError:
+            pass
+
+        self.client = Client(client_key, client_secret, resource_owner_key,
+            resource_owner_secret, callback_uri, signature_method,
+            signature_type, rsa_key, verifier, decoding=decoding)
+
+    def __call__(self, r):
+        """Add OAuth parameters to the request.
+
+        Parameters may be included from the body if the content-type is
+        urlencoded, if no content type is set a guess is made.
+        """
+        # Overwriting url is safe here as request will not modify it past
+        # this point.
+
+        content_type = r.headers.get('Content-Type', '')
+        if not content_type and extract_params(r.body):
+            content_type = CONTENT_TYPE_FORM_URLENCODED
+        if not isinstance(content_type, unicode):
+            content_type = content_type.decode('utf-8')
+
+        is_form_encoded = (CONTENT_TYPE_FORM_URLENCODED in content_type)
+
+        if is_form_encoded:
+            r.headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED
+            r.url, headers, r.body = self.client.sign(
+                unicode(r.url), unicode(r.method), r.body or '', r.headers)
+        else:
+            # Omit body data in the signing of non form-encoded requests
+            r.url, headers, _ = self.client.sign(
+                unicode(r.url), unicode(r.method), None, r.headers)
+
+        r.prepare_headers(headers)
+        r.url = to_native_str(r.url)
+        return r
diff --git a/requests_oauthlib/oauth1_session.py b/requests_oauthlib/oauth1_session.py
new file mode 100644 (file)
index 0000000..cadb09d
--- /dev/null
@@ -0,0 +1,266 @@
+from __future__ import unicode_literals
+
+try:
+    from urlparse import urlparse
+except ImportError:
+    from urllib.parse import urlparse
+
+from oauthlib.common import add_params_to_uri, urldecode
+from oauthlib.oauth1 import SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER
+import requests
+
+from . import OAuth1
+
+import sys
+if sys.version > "3":
+    unicode = str
+
+
+class OAuth1Session(requests.Session):
+    """Request signing and convenience methods for the oauth dance.
+
+    What is the difference between OAuth1Session and OAuth1?
+
+    OAuth1Session actually uses OAuth1 internally and it's purpose is to assist
+    in the OAuth workflow through convenience methods to prepare authorization
+    URLs and parse the various token and redirection responses. It also provide
+    rudimentary validation of responses.
+
+    An example of the OAuth workflow using a basic CLI app and Twitter.
+
+    >>> # Credentials obtained during the registration.
+    >>> client_key = 'client key'
+    >>> client_secret = 'secret'
+    >>> callback_uri = 'https://127.0.0.1/callback'
+    >>>
+    >>> # Endpoints found in the OAuth provider API documentation
+    >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+    >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+    >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
+    >>>
+    >>> oauth_session = OAuth1Session(client_key,client_secret=client_secret, callback_uri=callback_uri)
+    >>>
+    >>> # First step, fetch the request token.
+    >>> oauth_session.fetch_request_token(request_token_url)
+    {
+        'oauth_token': 'kjerht2309u',
+        'oauth_token_secret': 'lsdajfh923874',
+    }
+    >>>
+    >>> # Second step. Follow this link and authorize
+    >>> oauth_session.authorization_url(authorization_url)
+    'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
+    >>>
+    >>> # Third step. Fetch the access token
+    >>> redirect_response = raw_input('Paste the full redirect URL here.')
+    >>> oauth_session.parse_authorization_response(redirect_response)
+    {
+        'oauth_token: 'kjerht2309u',
+        'oauth_token_secret: 'lsdajfh923874',
+        'oauth_verifier: 'w34o8967345',
+    }
+    >>> oauth_session.fetch_access_token(access_token_url)
+    {
+        'oauth_token': 'sdf0o9823sjdfsdf',
+        'oauth_token_secret': '2kjshdfp92i34asdasd',
+    }
+    >>> # Done. You can now make OAuth requests.
+    >>> status_url = 'http://api.twitter.com/1/statuses/update.json'
+    >>> new_status = {'status':  'hello world!'}
+    >>> oauth_session.post(status_url, data=new_status)
+    <Response [200]>
+    """
+
+    def __init__(self, client_key,
+            client_secret=None,
+            resource_owner_key=None,
+            resource_owner_secret=None,
+            callback_uri=None,
+            signature_method=SIGNATURE_HMAC,
+            signature_type=SIGNATURE_TYPE_AUTH_HEADER,
+            rsa_key=None,
+            verifier=None):
+        """Construct the OAuth 1 session.
+
+        :param client_key: A client specific identifier.
+        :param client_secret: A client specific secret used to create HMAC and
+                              plaintext signatures.
+        :param resource_owner_key: A resource owner key, also referred to as
+                                   request token or access token depending on
+                                   when in the workflow it is used.
+        :param resource_owner_secret: A resource owner secret obtained with
+                                      either a request or access token. Often
+                                      referred to as token secret.
+        :param callback_uri: The URL the user is redirect back to after
+                             authorization.
+        :param signature_method: Signature methods determine how the OAuth
+                                 signature is created. The three options are
+                                 oauthlib.oauth1.SIGNATURE_HMAC (default),
+                                 oauthlib.oauth1.SIGNATURE_RSA and
+                                 oauthlib.oauth1.SIGNATURE_PLAIN.
+        :param signature_type: Signature type decides where the OAuth
+                               parameters are added. Either in the
+                               Authorization header (default) or to the URL
+                               query parameters or the request body. Defined as
+                               oauthlib.oauth1.SIGNATURE_TYPE_AUTH_HEADER,
+                               oauthlib.oauth1.SIGNATURE_TYPE_QUERY and
+                               oauthlib.oauth1.SIGNATURE_TYPE_BODY
+                               respectively.
+        :param rsa_key: The private RSA key as a string. Can only be used with
+                        signature_method=oauthlib.oauth1.SIGNATURE_RSA.
+        :param verifier: A verifier string to prove authorization was granted.
+        """
+        super(OAuth1Session, self).__init__()
+        self._client = OAuth1(client_key,
+                client_secret=client_secret,
+                resource_owner_key=resource_owner_key,
+                resource_owner_secret=resource_owner_secret,
+                callback_uri=callback_uri,
+                signature_method=signature_method,
+                signature_type=signature_type,
+                rsa_key=rsa_key,
+                verifier=verifier)
+        self.auth = self._client
+
+    def authorization_url(self, url, request_token=None, **kwargs):
+        """Create an authorization URL by appending request_token and optional
+        kwargs to url.
+
+        This is the second step in the OAuth 1 workflow. The user should be
+        redirected to this authorization URL, grant access to you, and then
+        be redirected back to you. The redirection back can either be specified
+        during client registration or by supplying a callback URI per request.
+
+        :param url: The authorization endpoint URL.
+        :param request_token: The previously obtained request token.
+        :param kwargs: Optional parameters to append to the URL.
+        :returns: The authorization URL with new parameters embedded.
+
+        An example using a registered default callback URI.
+
+        >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+        >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+        >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+        >>> oauth_session.fetch_request_token(request_token_url)
+        {
+            'oauth_token': 'sdf0o9823sjdfsdf',
+            'oauth_token_secret': '2kjshdfp92i34asdasd',
+        }
+        >>> oauth_session.authorization_url(authorization_url)
+        'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf'
+        >>> oauth_session.authorization_url(authorization_url, foo='bar')
+        'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&foo=bar'
+
+        An example using an explicit callback URI.
+
+        >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+        >>> authorization_url = 'https://api.twitter.com/oauth/authorize'
+        >>> oauth_session = OAuth1Session('client-key', client_secret='secret', callback_uri='https://127.0.0.1/callback')
+        >>> oauth_session.fetch_request_token(request_token_url)
+        {
+            'oauth_token': 'sdf0o9823sjdfsdf',
+            'oauth_token_secret': '2kjshdfp92i34asdasd',
+        }
+        >>> oauth_session.authorization_url(authorization_url)
+        'https://api.twitter.com/oauth/authorize?oauth_token=sdf0o9823sjdfsdf&oauth_callback=https%3A%2F%2F127.0.0.1%2Fcallback'
+        """
+        kwargs['oauth_token'] = request_token or self._client.client.resource_owner_key
+        return add_params_to_uri(url, kwargs.items())
+
+    def fetch_request_token(self, url, realm=None):
+        """Fetch a request token.
+
+        This is the first step in the OAuth 1 workflow. A request token is
+        obtained by making a signed post request to url. The token is then
+        parsed from the application/x-www-form-urlencoded response and ready
+        to be used to construct an authorization url.
+
+        :param url: The request token endpoint URL.
+        :param realm: A list of realms to request access to.
+        :returns: The response in dict format.
+
+        Note that a previously set callback_uri will be reset for your
+        convenience, or else signature creation will be incorrect on
+        consecutive requests.
+
+        >>> request_token_url = 'https://api.twitter.com/oauth/request_token'
+        >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+        >>> oauth_session.fetch_request_token(request_token_url)
+        {
+            'oauth_token': 'sdf0o9823sjdfsdf',
+            'oauth_token_secret': '2kjshdfp92i34asdasd',
+        }
+        """
+        self._client.client.realm = ' '.join(realm) if realm else None
+        token = self._fetch_token(url)
+        self._client.client.callback_uri = None
+        self._client.client.realm = None
+        return token
+
+    def fetch_access_token(self, url):
+        """Fetch an access token.
+
+        This is the final step in the OAuth 1 workflow. An access token is
+        obtained using all previously obtained credentials, including the
+        verifier from the authorization step.
+
+        Note that a previously set verifier will be reset for your
+        convenience, or else signature creation will be incorrect on
+        consecutive requests.
+
+        >>> access_token_url = 'https://api.twitter.com/oauth/access_token'
+        >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
+        >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+        >>> oauth_session.parse_authorization_response(redirect_response)
+        {
+            'oauth_token: 'kjerht2309u',
+            'oauth_token_secret: 'lsdajfh923874',
+            'oauth_verifier: 'w34o8967345',
+        }
+        >>> oauth_session.fetch_access_token(access_token_url)
+        {
+            'oauth_token': 'sdf0o9823sjdfsdf',
+            'oauth_token_secret': '2kjshdfp92i34asdasd',
+        }
+        """
+        if not hasattr(self._client.client, 'verifier'):
+            raise ValueError('No client verifier has been set.')
+        token = self._fetch_token(url)
+        self._client.client.verifier = None
+        return token
+
+    def parse_authorization_response(self, url):
+        """Extract parameters from the post authorization redirect response URL.
+
+        :param url: The full URL that resulted from the user being redirected
+                    back from the OAuth provider to you, the client.
+        :returns: A dict of parameters extracted from the URL.
+
+        >>> redirect_response = 'https://127.0.0.1/callback?oauth_token=kjerht2309uf&oauth_token_secret=lsdajfh923874&oauth_verifier=w34o8967345'
+        >>> oauth_session = OAuth1Session('client-key', client_secret='secret')
+        >>> oauth_session.parse_authorization_response(redirect_response)
+        {
+            'oauth_token: 'kjerht2309u',
+            'oauth_token_secret: 'lsdajfh923874',
+            'oauth_verifier: 'w34o8967345',
+        }
+        """
+        token = dict(urldecode(urlparse(url).query))
+        self._populate_attributes(token)
+        return token
+
+    def _populate_attributes(self, token):
+        if 'oauth_token' in token:
+            self._client.client.resource_owner_key = token['oauth_token']
+        else:
+            raise ValueError('Response does not contain a token. %s', token)
+        if 'oauth_token_secret' in token:
+            self._client.client.resource_owner_secret = (
+                token['oauth_token_secret'])
+        if 'oauth_verifier' in token:
+            self._client.client.verifier = token['oauth_verifier']
+
+    def _fetch_token(self, url):
+        token = dict(urldecode(self.post(url).text))
+        self._populate_attributes(token)
+        return token
diff --git a/requests_oauthlib/oauth2_auth.py b/requests_oauthlib/oauth2_auth.py
new file mode 100644 (file)
index 0000000..087b830
--- /dev/null
@@ -0,0 +1,36 @@
+from __future__ import unicode_literals
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+
+from .utils import is_secure_transport
+
+
+class OAuth2(object):
+    """Adds proof of authorization (OAuth2 token) to the request."""
+
+    def __init__(self, client_id=None, client=None, token=None):
+        """Construct a new OAuth 2 authorization object.
+
+        :param client_id: Client id obtained during registration
+        :param client: :class:`oauthlib.oauth2.Client` to be used. Default is
+                       WebApplicationClient which is useful for any
+                       hosted application but not mobile or desktop.
+        :param token: Token dictionary, must include access_token
+                      and token_type.
+        """
+        self._client = client or WebApplicationClient(client_id, token=token)
+        if token:
+            for k, v in token.items():
+                setattr(self._client, k, v)
+
+    def __call__(self, r):
+        """Append an OAuth 2 token to the request.
+
+        Note that currently HTTPS is required for all requests. There may be
+        a token type that allows for plain HTTP in the future and then this
+        should be updated to allow plain HTTP on a white list basis.
+        """
+        if not is_secure_transport(r.url):
+            raise InsecureTransportError()
+        r.url, r.headers, r.body = self._client.add_token(r.url,
+                http_method=r.method, body=r.body, headers=r.headers)
+        return r
diff --git a/requests_oauthlib/oauth2_session.py b/requests_oauthlib/oauth2_session.py
new file mode 100644 (file)
index 0000000..beb199e
--- /dev/null
@@ -0,0 +1,273 @@
+from __future__ import unicode_literals
+import os
+import requests
+from oauthlib.common import log, generate_token, urldecode
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+from oauthlib.oauth2 import TokenExpiredError
+
+from .utils import is_secure_transport
+
+
+class TokenUpdated(Warning):
+    def __init__(self, token):
+        super(TokenUpdated, self).__init__()
+        self.token = token
+
+
+class OAuth2Session(requests.Session):
+    """Versatile OAuth 2 extension to :class:`requests.Session`.
+
+    Supports any grant type adhering to :class:`oauthlib.oauth2.Client` spec
+    including the four core OAuth 2 grants.
+
+    Can be used to create authorization urls, fetch tokens and access protected
+    resources using the :class:`requests.Session` interface you are used to.
+
+    - :class:`oauthlib.oauth2.WebApplicationClient` (default): Authorization Code Grant
+    - :class:`oauthlib.oauth2.MobileApplicationClient`: Implicit Grant
+    - :class:`oauthlib.oauth2.LegacyApplicationClient`: Password Credentials Grant
+    - :class:`oauthlib.oauth2.BackendApplicationClient`: Client Credentials Grant
+
+    Note that the only time you will be using Implicit Grant from python is if
+    you are driving a user agent able to obtain URL fragments.
+    """
+
+    def __init__(self, client_id=None, client=None, auto_refresh_url=None,
+            auto_refresh_kwargs=None, scope=None, redirect_uri=None, token=None,
+            state=None, state_generator=None, token_updater=None, **kwargs):
+        """Construct a new OAuth 2 client session.
+
+        :param client_id: Client id obtained during registration
+        :param client: :class:`oauthlib.oauth2.Client` to be used. Default is
+                       WebApplicationClient which is useful for any
+                       hosted application but not mobile or desktop.
+        :param scope: List of scopes you wish to request access to
+        :param redirect_uri: Redirect URI you registered as callback
+        :param token: Token dictionary, must include access_token
+                      and token_type.
+        :param state: State string used to prevent CSRF. This will be given
+                      when creating the authorization url and must be supplied
+                      when parsing the authorization response.
+                      Can be either a string or a no argument callable.
+        :auto_refresh_url: Refresh token endpoint URL, must be HTTPS. Supply
+                           this if you wish the client to automatically refresh
+                           your access tokens.
+        :auto_refresh_kwargs: Extra arguments to pass to the refresh token
+                              endpoint.
+        :token_updater: Method with one argument, token, to be used to update
+                        your token databse on automatic token refresh. If not
+                        set a TokenUpdated warning will be raised when a token
+                        has been refreshed. This warning will carry the token
+                        in its token argument.
+        :param kwargs: Arguments to pass to the Session constructor.
+        """
+        super(OAuth2Session, self).__init__(**kwargs)
+        self.client_id = client_id or client.client_id
+        self.scope = scope
+        self.redirect_uri = redirect_uri
+        self.token = token or {}
+        self.state = state or generate_token
+        self._state = None
+        self.auto_refresh_url = auto_refresh_url
+        self.auto_refresh_kwargs = auto_refresh_kwargs or {}
+        self.token_updater = token_updater
+        self._client = client or WebApplicationClient(client_id, token=token)
+        self._client._populate_attributes(token or {})
+
+        # Allow customizations for non compliant providers through various
+        # hooks to adjust requests and responses.
+        self.compliance_hook = {
+            'access_token_response': set([]),
+            'refresh_token_response': set([]),
+            'protected_request': set([]),
+        }
+
+    def new_state(self):
+        """Generates a state string to be used in authorizations."""
+        try:
+            self._state = self.state()
+            log.debug('Generated new state %s.', self._state)
+        except TypeError:
+            self._state = self.state
+            log.debug('Re-using previously supplied state %s.', self._state)
+        return self._state
+
+    def authorization_url(self, url, **kwargs):
+        """Form an authorization URL.
+
+        :param url: Authorization endpoint url, must be HTTPS.
+        :param kwargs: Extra parameters to include.
+        :return: authorization_url, state
+        """
+        state = self.new_state()
+        return self._client.prepare_request_uri(url,
+                redirect_uri=self.redirect_uri,
+                scope=self.scope,
+                state=state,
+                **kwargs), state
+
+    def fetch_token(self, token_url, code=None, authorization_response=None,
+            body='', auth=None, username=None, password=None, **kwargs):
+        """Generic method for fetching an access token from the token endpoint.
+
+        If you are using the MobileApplicationClient you will want to use
+        token_from_fragment instead of fetch_token.
+
+        :param token_url: Token endpoint URL, must use HTTPS.
+        :param code: Authorization code (used by WebApplicationClients).
+        :param authorization_response: Authorization response URL, the callback
+                                       URL of the request back to you. Used by
+                                       WebApplicationClients instead of code.
+        :param body: Optional application/x-www-form-urlencoded body to add the
+                     include in the token request. Prefer kwargs over body.
+        :param auth: An auth tuple or method as accepted by requests.
+        :param username: Username used by LegacyApplicationClients.
+        :param password: Password used by LegacyApplicationClients.
+        :param kwargs: Extra parameters to include in the token request.
+        :return: A token dict
+        """
+        if not is_secure_transport(token_url):
+            raise InsecureTransportError()
+
+        if not code and authorization_response:
+            self._client.parse_request_uri_response(authorization_response,
+                    state=self._state)
+            code = self._client.code
+        elif not code and isinstance(self._client, WebApplicationClient):
+            code = self._client.code
+            if not code:
+                raise ValueError('Please supply either code or '
+                                 'authorization_code parameters.')
+
+
+        body = self._client.prepare_request_body(code=code, body=body,
+                redirect_uri=self.redirect_uri, username=username,
+                password=password, **kwargs)
+        # (ib-lundgren) All known, to me, token requests use POST.
+        r = self.post(token_url, data=dict(urldecode(body)),
+            headers={'Accept': 'application/json'}, auth=auth)
+        log.debug('Prepared fetch token request body %s', body)
+        log.debug('Request to fetch token completed with status %s.',
+                  r.status_code)
+        log.debug('Response headers were %s and content %s.',
+                  r.headers, r.text)
+        log.debug('Invoking %d token response hooks.',
+                  len(self.compliance_hook['access_token_response']))
+        for hook in self.compliance_hook['access_token_response']:
+            log.debug('Invoking hook %s.', hook)
+            r = hook(r)
+
+        self._client.parse_request_body_response(r.text, scope=self.scope)
+        self.token = self._client.token
+        log.debug('Obtained token %s.', self.token)
+        return self.token
+
+    def token_from_fragment(self, authorization_response):
+        """Parse token from the URI fragment, used by MobileApplicationClients.
+
+        :param authorization_response: The full URL of the redirect back to you
+        :return: A token dict
+        """
+        self._client.parse_request_uri_response(authorization_response,
+                state=self._state)
+        self.token = self._client.token
+        return self.token
+
+    def refresh_token(self, token_url, refresh_token=None, body='', auth=None,
+                      **kwargs):
+        """Fetch a new access token using a refresh token.
+
+        :param token_url: The token endpoint, must be HTTPS.
+        :param refresh_token: The refresh_token to use.
+        :param body: Optional application/x-www-form-urlencoded body to add the
+                     include in the token request. Prefer kwargs over body.
+        :param auth: An auth tuple or method as accepted by requests.
+        :param kwargs: Extra parameters to include in the token request.
+        :return: A token dict
+        """
+        if not token_url:
+            raise ValueError('No token endpoint set for auto_refresh.')
+
+        if not is_secure_transport(token_url):
+            raise InsecureTransportError()
+
+        # Need to nullify token to prevent it from being added to the request
+        refresh_token = refresh_token or self.token.get('refresh_token')
+        self.token = {}
+
+        log.debug('Adding auto refresh key word arguments %s.',
+                  self.auto_refresh_kwargs)
+        kwargs.update(self.auto_refresh_kwargs)
+        body = self._client.prepare_refresh_body(body=body,
+                refresh_token=refresh_token, scope=self.scope, **kwargs)
+        log.debug('Prepared refresh token request body %s', body)
+        r = self.post(token_url, data=dict(urldecode(body)), auth=auth)
+        log.debug('Request to refresh token completed with status %s.',
+                  r.status_code)
+        log.debug('Response headers were %s and content %s.',
+                  r.headers, r.text)
+        log.debug('Invoking %d token response hooks.',
+                  len(self.compliance_hook['refresh_token_response']))
+        for hook in self.compliance_hook['refresh_token_response']:
+            log.debug('Invoking hook %s.', hook)
+            r = hook(r)
+
+        self.token = self._client.parse_request_body_response(r.text, scope=self.scope)
+        if not 'refresh_token' in self.token:
+            log.debug('No new refresh token given. Re-using old.')
+            self.token['refresh_token'] = refresh_token
+        return self.token
+
+    def request(self, method, url, data=None, headers=None, **kwargs):
+        """Intercept all requests and add the OAuth 2 token if present."""
+        if not is_secure_transport(url):
+            raise InsecureTransportError()
+        if self.token:
+            log.debug('Invoking %d protected resource request hooks.',
+                      len(self.compliance_hook['protected_request']))
+            for hook in self.compliance_hook['protected_request']:
+                log.debug('Invoking hook %s.', hook)
+                url, headers, data = hook(url, headers, data)
+
+            log.debug('Adding token %s to request.', self.token)
+            try:
+                url, headers, data = self._client.add_token(url,
+                        http_method=method, body=data, headers=headers)
+            # Attempt to retrieve and save new access token if expired
+            except TokenExpiredError:
+                if self.auto_refresh_url:
+                    log.debug('Auto refresh is set, attempting to refresh at %s.',
+                              self.auto_refresh_url)
+                    token = self.refresh_token(self.auto_refresh_url)
+                    if self.token_updater:
+                        log.debug('Updating token to %s using %s.',
+                                  token, self.token_updater)
+                        self.token_updater(token)
+                        url, headers, data = self._client.add_token(url,
+                                http_method=method, body=data, headers=headers)
+                    else:
+                        raise TokenUpdated(token)
+                else:
+                    raise
+
+        log.debug('Requesting url %s using method %s.', url, method)
+        log.debug('Supplying headers %s and data %s', headers, data)
+        log.debug('Passing through key word arguments %s.', kwargs)
+        return super(OAuth2Session, self).request(method, url,
+                headers=headers, data=data, **kwargs)
+
+    def register_compliance_hook(self, hook_type, hook):
+        """Register a hook for request/response tweaking.
+
+        Available hooks are:
+            access_token_response invoked before token parsing.
+            refresh_token_response invoked before refresh token parsing.
+            protected_request invoked before making a request.
+
+        If you find a new hook is needed please send a GitHub PR request
+        or open an issue.
+        """
+        if hook_type not in self.compliance_hook:
+            raise ValueError('Hook type %s is not in %s.',
+                             hook_type, self.compliance_hook)
+        self.compliance_hook[hook_type].add(hook)
diff --git a/requests_oauthlib/utils.py b/requests_oauthlib/utils.py
new file mode 100644 (file)
index 0000000..239e0d9
--- /dev/null
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+import os
+
+
+def is_secure_transport(uri):
+    """Check if the uri is over ssl."""
+    if os.environ.get('DEBUG'):
+        return True
+    return uri.lower().startswith('https://')

Benjamin Mako Hill || Want to submit a patch?