Updated packages and code to python3. Won't work with python 2
authorTommy Guy <richardtguy84@gmail.com>
Mon, 20 Apr 2015 03:23:39 +0000 (20:23 -0700)
committerTommy Guy <richardtguy84@gmail.com>
Mon, 20 Apr 2015 03:23:39 +0000 (20:23 -0700)
82 files changed:
oauth/._oauth.py [deleted file]
oauth/__init__.py [deleted file]
oauth/example/._server.py [deleted file]
oauth/example/client.py [deleted file]
oauth/example/server.py [deleted file]
oauth/oauth.py [deleted file]
oauthlib/__init__.py [new file with mode: 0644]
oauthlib/common.py [new file with mode: 0644]
oauthlib/oauth1/__init__.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/__init__.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/__init__.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/access_token.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/authorization.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/base.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/pre_configured.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/request_token.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/resource.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/endpoints/signature_only.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/errors.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/parameters.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/request_validator.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/signature.py [new file with mode: 0644]
oauthlib/oauth1/rfc5849/utils.py [new file with mode: 0644]
oauthlib/oauth2/__init__.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/__init__.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/clients/__init__.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/clients/backend_application.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/clients/base.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/clients/legacy_application.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/clients/mobile_application.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/clients/service_application.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/clients/web_application.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/endpoints/__init__.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/endpoints/authorization.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/endpoints/base.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/endpoints/pre_configured.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/endpoints/resource.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/endpoints/revocation.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/endpoints/token.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/errors.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/grant_types/__init__.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/grant_types/authorization_code.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/grant_types/base.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/grant_types/client_credentials.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/grant_types/implicit.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/grant_types/refresh_token.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/parameters.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/request_validator.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/tokens.py [new file with mode: 0644]
oauthlib/oauth2/rfc6749/utils.py [new file with mode: 0644]
oauthlib/signals.py [new file with mode: 0644]
oauthlib/uri_validate.py [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/douban.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/compliance_fixes/weibo.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]
tweepy/__init__.py
tweepy/api.py
tweepy/auth.py
tweepy/binder.py
tweepy/cache.py
tweepy/cursor.py
tweepy/error.py
tweepy/models.py
tweepy/oauth.py [deleted file]
tweepy/parsers.py
tweepy/streaming.py
tweepy/utils.py
twitter-stream-raw1.py
twitter-stream1.py
twitter-stream2.py
twitter1.py
twitter2.py
twitter3.py
twitter4.py

diff --git a/oauth/._oauth.py b/oauth/._oauth.py
deleted file mode 100644 (file)
index 02899ca..0000000
Binary files a/oauth/._oauth.py and /dev/null differ
diff --git a/oauth/__init__.py b/oauth/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/oauth/example/._server.py b/oauth/example/._server.py
deleted file mode 100644 (file)
index f4dda4e..0000000
Binary files a/oauth/example/._server.py and /dev/null differ
diff --git a/oauth/example/client.py b/oauth/example/client.py
deleted file mode 100644 (file)
index 34f7dcb..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-Example consumer. This is not recommended for production.
-Instead, you'll want to create your own subclass of OAuthClient
-or find one that works with your web framework.
-"""
-
-import httplib
-import time
-import oauth.oauth as oauth
-
-# settings for the local test consumer
-SERVER = 'localhost'
-PORT = 8080
-
-# fake urls for the test server (matches ones in server.py)
-REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
-ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
-AUTHORIZATION_URL = 'https://photos.example.net/authorize'
-CALLBACK_URL = 'http://printer.example.com/request_token_ready'
-RESOURCE_URL = 'http://photos.example.net/photos'
-
-# key and secret granted by the service provider for this consumer application - same as the MockOAuthDataStore
-CONSUMER_KEY = 'key'
-CONSUMER_SECRET = 'secret'
-
-# example client using httplib with headers
-class SimpleOAuthClient(oauth.OAuthClient):
-
-    def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='', access_token_url='', authorization_url=''):
-        self.server = server
-        self.port = port
-        self.request_token_url = request_token_url
-        self.access_token_url = access_token_url
-        self.authorization_url = authorization_url
-        self.connection = httplib.HTTPConnection("%s:%d" % (self.server, self.port))
-
-    def fetch_request_token(self, oauth_request):
-        # via headers
-        # -> OAuthToken
-        self.connection.request(oauth_request.http_method, self.request_token_url, headers=oauth_request.to_header()) 
-        response = self.connection.getresponse()
-        return oauth.OAuthToken.from_string(response.read())
-
-    def fetch_access_token(self, oauth_request):
-        # via headers
-        # -> OAuthToken
-        self.connection.request(oauth_request.http_method, self.access_token_url, headers=oauth_request.to_header()) 
-        response = self.connection.getresponse()
-        return oauth.OAuthToken.from_string(response.read())
-
-    def authorize_token(self, oauth_request):
-        # via url
-        # -> typically just some okay response
-        self.connection.request(oauth_request.http_method, oauth_request.to_url()) 
-        response = self.connection.getresponse()
-        return response.read()
-
-    def access_resource(self, oauth_request):
-        # via post body
-        # -> some protected resources
-        headers = {'Content-Type' :'application/x-www-form-urlencoded'}
-        self.connection.request('POST', RESOURCE_URL, body=oauth_request.to_postdata(), headers=headers)
-        response = self.connection.getresponse()
-        return response.read()
-
-def run_example():
-
-    # setup
-    print '** OAuth Python Library Example **'
-    client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZATION_URL)
-    consumer = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET)
-    signature_method_plaintext = oauth.OAuthSignatureMethod_PLAINTEXT()
-    signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
-    pause()
-
-    # get request token
-    print '* Obtain a request token ...'
-    pause()
-    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, callback=CALLBACK_URL, http_url=client.request_token_url)
-    oauth_request.sign_request(signature_method_plaintext, consumer, None)
-    print 'REQUEST (via headers)'
-    print 'parameters: %s' % str(oauth_request.parameters)
-    pause()
-    token = client.fetch_request_token(oauth_request)
-    print 'GOT'
-    print 'key: %s' % str(token.key)
-    print 'secret: %s' % str(token.secret)
-    print 'callback confirmed? %s' % str(token.callback_confirmed)
-    pause()
-
-    print '* Authorize the request token ...'
-    pause()
-    oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=client.authorization_url)
-    print 'REQUEST (via url query string)'
-    print 'parameters: %s' % str(oauth_request.parameters)
-    pause()
-    # this will actually occur only on some callback
-    response = client.authorize_token(oauth_request)
-    print 'GOT'
-    print response
-    # sad way to get the verifier
-    import urlparse, cgi
-    query = urlparse.urlparse(response)[4]
-    params = cgi.parse_qs(query, keep_blank_values=False)
-    verifier = params['oauth_verifier'][0]
-    print 'verifier: %s' % verifier
-    pause()
-
-    # get access token
-    print '* Obtain an access token ...'
-    pause()
-    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, verifier=verifier, http_url=client.access_token_url)
-    oauth_request.sign_request(signature_method_plaintext, consumer, token)
-    print 'REQUEST (via headers)'
-    print 'parameters: %s' % str(oauth_request.parameters)
-    pause()
-    token = client.fetch_access_token(oauth_request)
-    print 'GOT'
-    print 'key: %s' % str(token.key)
-    print 'secret: %s' % str(token.secret)
-    pause()
-
-    # access some protected resources
-    print '* Access protected resources ...'
-    pause()
-    parameters = {'file': 'vacation.jpg', 'size': 'original'} # resource specific params
-    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_method='POST', http_url=RESOURCE_URL, parameters=parameters)
-    oauth_request.sign_request(signature_method_hmac_sha1, consumer, token)
-    print 'REQUEST (via post body)'
-    print 'parameters: %s' % str(oauth_request.parameters)
-    pause()
-    params = client.access_resource(oauth_request)
-    print 'GOT'
-    print 'non-oauth parameters: %s' % params
-    pause()
-
-def pause():
-    print ''
-    time.sleep(1)
-
-if __name__ == '__main__':
-    run_example()
-    print 'Done.'
\ No newline at end of file
diff --git a/oauth/example/server.py b/oauth/example/server.py
deleted file mode 100644 (file)
index 5986b0e..0000000
+++ /dev/null
@@ -1,195 +0,0 @@
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
-import urllib
-
-import oauth.oauth as oauth
-
-# fake urls for the test server
-REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
-ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
-AUTHORIZATION_URL = 'https://photos.example.net/authorize'
-CALLBACK_URL = 'http://printer.example.com/request_token_ready'
-RESOURCE_URL = 'http://photos.example.net/photos'
-REALM = 'http://photos.example.net/'
-VERIFIER = 'verifier'
-
-# example store for one of each thing
-class MockOAuthDataStore(oauth.OAuthDataStore):
-
-    def __init__(self):
-        self.consumer = oauth.OAuthConsumer('key', 'secret')
-        self.request_token = oauth.OAuthToken('requestkey', 'requestsecret')
-        self.access_token = oauth.OAuthToken('accesskey', 'accesssecret')
-        self.nonce = 'nonce'
-        self.verifier = VERIFIER
-
-    def lookup_consumer(self, key):
-        if key == self.consumer.key:
-            return self.consumer
-        return None
-
-    def lookup_token(self, token_type, token):
-        token_attrib = getattr(self, '%s_token' % token_type)
-        if token == token_attrib.key:
-            ## HACK
-            token_attrib.set_callback(CALLBACK_URL)
-            return token_attrib
-        return None
-
-    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
-        if oauth_token and oauth_consumer.key == self.consumer.key and (oauth_token.key == self.request_token.key or oauth_token.key == self.access_token.key) and nonce == self.nonce:
-            return self.nonce
-        return None
-
-    def fetch_request_token(self, oauth_consumer, oauth_callback):
-        if oauth_consumer.key == self.consumer.key:
-            if oauth_callback:
-                # want to check here if callback is sensible
-                # for mock store, we assume it is
-                self.request_token.set_callback(oauth_callback)
-            return self.request_token
-        return None
-
-    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
-        if oauth_consumer.key == self.consumer.key and oauth_token.key == self.request_token.key and oauth_verifier == self.verifier:
-            # want to check here if token is authorized
-            # for mock store, we assume it is
-            return self.access_token
-        return None
-
-    def authorize_request_token(self, oauth_token, user):
-        if oauth_token.key == self.request_token.key:
-            # authorize the request token in the store
-            # for mock store, do nothing
-            return self.request_token
-        return None
-
-class RequestHandler(BaseHTTPRequestHandler):
-
-    def __init__(self, *args, **kwargs):
-        self.oauth_server = oauth.OAuthServer(MockOAuthDataStore())
-        self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
-        self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
-        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
-
-    # example way to send an oauth error
-    def send_oauth_error(self, err=None):
-        # send a 401 error
-        self.send_error(401, str(err.message))
-        # return the authenticate header
-        header = oauth.build_authenticate_header(realm=REALM)
-        for k, v in header.iteritems():
-            self.send_header(k, v) 
-
-    def do_GET(self):
-
-        # debug info
-        #print self.command, self.path, self.headers
-        
-        # get the post data (if any)
-        postdata = None
-        if self.command == 'POST':
-            try:
-                length = int(self.headers.getheader('content-length'))
-                postdata = self.rfile.read(length)
-            except:
-                pass
-
-        # construct the oauth request from the request parameters
-        oauth_request = oauth.OAuthRequest.from_request(self.command, self.path, headers=self.headers, query_string=postdata)
-
-        # request token
-        if self.path.startswith(REQUEST_TOKEN_URL):
-            try:
-                # create a request token
-                token = self.oauth_server.fetch_request_token(oauth_request)
-                # send okay response
-                self.send_response(200, 'OK')
-                self.end_headers()
-                # return the token
-                self.wfile.write(token.to_string())
-            except oauth.OAuthError, err:
-                self.send_oauth_error(err)
-            return
-
-        # user authorization
-        if self.path.startswith(AUTHORIZATION_URL):
-            try:
-                # get the request token
-                token = self.oauth_server.fetch_request_token(oauth_request)
-                # authorize the token (kind of does nothing for now)
-                token = self.oauth_server.authorize_token(token, None)
-                token.set_verifier(VERIFIER)
-                # send okay response
-                self.send_response(200, 'OK')
-                self.end_headers()
-                # return the callback url (to show server has it)
-                self.wfile.write(token.get_callback_url())
-            except oauth.OAuthError, err:
-                self.send_oauth_error(err)
-            return
-
-        # access token
-        if self.path.startswith(ACCESS_TOKEN_URL):
-            try:
-                # create an access token
-                token = self.oauth_server.fetch_access_token(oauth_request)
-                # send okay response
-                self.send_response(200, 'OK')
-                self.end_headers()
-                # return the token
-                self.wfile.write(token.to_string())
-            except oauth.OAuthError, err:
-                self.send_oauth_error(err)
-            return
-
-        # protected resources
-        if self.path.startswith(RESOURCE_URL):
-            try:
-                # verify the request has been oauth authorized
-                consumer, token, params = self.oauth_server.verify_request(oauth_request)
-                # send okay response
-                self.send_response(200, 'OK')
-                self.end_headers()
-                # return the extra parameters - just for something to return
-                self.wfile.write(str(params))
-            except oauth.OAuthError, err:
-                self.send_oauth_error(err)
-            return
-
-    def do_POST(self):
-        return self.do_GET()
-
-def main():
-    try:
-        server = HTTPServer(('', 8080), RequestHandler)
-        print 'Test server running...'
-        server.serve_forever()
-    except KeyboardInterrupt:
-        server.socket.close()
-
-if __name__ == '__main__':
-    main()
\ No newline at end of file
diff --git a/oauth/oauth.py b/oauth/oauth.py
deleted file mode 100644 (file)
index b6284c5..0000000
+++ /dev/null
@@ -1,655 +0,0 @@
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
-import binascii
-
-
-VERSION = '1.0' # Hi Blaine!
-HTTP_METHOD = 'GET'
-SIGNATURE_METHOD = 'PLAINTEXT'
-
-
-class OAuthError(RuntimeError):
-    """Generic exception class."""
-    def __init__(self, message='OAuth error occured.'):
-        self.message = message
-
-def build_authenticate_header(realm=''):
-    """Optional WWW-Authenticate header (401 error)"""
-    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-def escape(s):
-    """Escape a URL including any /."""
-    return urllib.quote(s, safe='~')
-
-def _utf8_str(s):
-    """Convert unicode to utf-8."""
-    if isinstance(s, unicode):
-        return s.encode("utf-8")
-    else:
-        return str(s)
-
-def generate_timestamp():
-    """Get seconds since epoch (UTC)."""
-    return int(time.time())
-
-def generate_nonce(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-def generate_verifier(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-
-class OAuthConsumer(object):
-    """Consumer of OAuth authentication.
-
-    OAuthConsumer is a data type that represents the identity of the Consumer
-    via its shared secret with the Service Provider.
-
-    """
-    key = None
-    secret = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-
-class OAuthToken(object):
-    """OAuthToken is a data type that represents an End User via either an access
-    or request token.
-    
-    key -- the token
-    secret -- the token secret
-
-    """
-    key = None
-    secret = None
-    callback = None
-    callback_confirmed = None
-    verifier = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-    def set_callback(self, callback):
-        self.callback = callback
-        self.callback_confirmed = 'true'
-
-    def set_verifier(self, verifier=None):
-        if verifier is not None:
-            self.verifier = verifier
-        else:
-            self.verifier = generate_verifier()
-
-    def get_callback_url(self):
-        if self.callback and self.verifier:
-            # Append the oauth_verifier.
-            parts = urlparse.urlparse(self.callback)
-            scheme, netloc, path, params, query, fragment = parts[:6]
-            if query:
-                query = '%s&oauth_verifier=%s' % (query, self.verifier)
-            else:
-                query = 'oauth_verifier=%s' % self.verifier
-            return urlparse.urlunparse((scheme, netloc, path, params,
-                query, fragment))
-        return self.callback
-
-    def to_string(self):
-        data = {
-            'oauth_token': self.key,
-            'oauth_token_secret': self.secret,
-        }
-        if self.callback_confirmed is not None:
-            data['oauth_callback_confirmed'] = self.callback_confirmed
-        return urllib.urlencode(data)
-    def from_string(s):
-        """ Returns a token from something like:
-        oauth_token_secret=xxx&oauth_token=xxx
-        """
-        params = cgi.parse_qs(s, keep_blank_values=False)
-        key = params['oauth_token'][0]
-        secret = params['oauth_token_secret'][0]
-        token = OAuthToken(key, secret)
-        try:
-            token.callback_confirmed = params['oauth_callback_confirmed'][0]
-        except KeyError:
-            pass # 1.0, no callback confirmed.
-        return token
-    from_string = staticmethod(from_string)
-
-    def __str__(self):
-        return self.to_string()
-
-
-class OAuthRequest(object):
-    """OAuthRequest represents the request and can be serialized.
-
-    OAuth parameters:
-        - oauth_consumer_key 
-        - oauth_token
-        - oauth_signature_method
-        - oauth_signature 
-        - oauth_timestamp 
-        - oauth_nonce
-        - oauth_version
-        - oauth_verifier
-        ... any additional parameters, as defined by the Service Provider.
-    """
-    parameters = None # OAuth parameters.
-    http_method = HTTP_METHOD
-    http_url = None
-    version = VERSION
-
-    def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
-        self.http_method = http_method
-        self.http_url = http_url
-        self.parameters = parameters or {}
-
-    def set_parameter(self, parameter, value):
-        self.parameters[parameter] = value
-
-    def get_parameter(self, parameter):
-        try:
-            return self.parameters[parameter]
-        except:
-            raise OAuthError('Parameter not found: %s' % parameter)
-
-    def _get_timestamp_nonce(self):
-        return self.get_parameter('oauth_timestamp'), self.get_parameter(
-            'oauth_nonce')
-
-    def get_nonoauth_parameters(self):
-        """Get any non-OAuth parameters."""
-        parameters = {}
-        for k, v in self.parameters.iteritems():
-            # Ignore oauth parameters.
-            if k.find('oauth_') < 0:
-                parameters[k] = v
-        return parameters
-
-    def to_header(self, realm=''):
-        """Serialize as a header for an HTTPAuth request."""
-        auth_header = 'OAuth realm="%s"' % realm
-        # Add the oauth parameters.
-        if self.parameters:
-            for k, v in self.parameters.iteritems():
-                if k[:6] == 'oauth_':
-                    auth_header += ', %s="%s"' % (k, escape(str(v)))
-        return {'Authorization': auth_header}
-
-    def to_postdata(self):
-        """Serialize as post data for a POST request."""
-        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
-            for k, v in self.parameters.iteritems()])
-
-    def to_url(self):
-        """Serialize as a URL for a GET request."""
-        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
-
-    def get_normalized_parameters(self):
-        """Return a string that contains the parameters that must be signed."""
-        params = self.parameters
-        try:
-            # Exclude the signature if it exists.
-            del params['oauth_signature']
-        except:
-            pass
-        # Escape key values before sorting.
-        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
-            for k,v in params.items()]
-        # Sort lexicographically, first after key, then after value.
-        key_values.sort()
-        # Combine key value pairs into a string.
-        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
-
-    def get_normalized_http_method(self):
-        """Uppercases the http method."""
-        return self.http_method.upper()
-
-    def get_normalized_http_url(self):
-        """Parses the URL and rebuilds it to be scheme://host/path."""
-        parts = urlparse.urlparse(self.http_url)
-        scheme, netloc, path = parts[:3]
-        # Exclude default port numbers.
-        if scheme == 'http' and netloc[-3:] == ':80':
-            netloc = netloc[:-3]
-        elif scheme == 'https' and netloc[-4:] == ':443':
-            netloc = netloc[:-4]
-        return '%s://%s%s' % (scheme, netloc, path)
-
-    def sign_request(self, signature_method, consumer, token):
-        """Set the signature parameter to the result of build_signature."""
-        # Set the signature method.
-        self.set_parameter('oauth_signature_method',
-            signature_method.get_name())
-        # Set the signature.
-        self.set_parameter('oauth_signature',
-            self.build_signature(signature_method, consumer, token))
-
-    def build_signature(self, signature_method, consumer, token):
-        """Calls the build signature method within the signature method."""
-        return signature_method.build_signature(self, consumer, token)
-
-    def from_request(http_method, http_url, headers=None, parameters=None,
-            query_string=None):
-        """Combines multiple parameter sources."""
-        if parameters is None:
-            parameters = {}
-
-        # Headers
-        if headers and 'Authorization' in headers:
-            auth_header = headers['Authorization']
-            # Check that the authorization header is OAuth.
-            if auth_header[:6] == 'OAuth ':
-                auth_header = auth_header[6:]
-                try:
-                    # Get the parameters from the header.
-                    header_params = OAuthRequest._split_header(auth_header)
-                    parameters.update(header_params)
-                except:
-                    raise OAuthError('Unable to parse OAuth parameters from '
-                        'Authorization header.')
-
-        # GET or POST query string.
-        if query_string:
-            query_params = OAuthRequest._split_url_string(query_string)
-            parameters.update(query_params)
-
-        # URL parameters.
-        param_str = urlparse.urlparse(http_url)[4] # query
-        url_params = OAuthRequest._split_url_string(param_str)
-        parameters.update(url_params)
-
-        if parameters:
-            return OAuthRequest(http_method, http_url, parameters)
-
-        return None
-    from_request = staticmethod(from_request)
-
-    def from_consumer_and_token(oauth_consumer, token=None,
-            callback=None, verifier=None, http_method=HTTP_METHOD,
-            http_url=None, parameters=None):
-        if not parameters:
-            parameters = {}
-
-        defaults = {
-            'oauth_consumer_key': oauth_consumer.key,
-            'oauth_timestamp': generate_timestamp(),
-            'oauth_nonce': generate_nonce(),
-            'oauth_version': OAuthRequest.version,
-        }
-
-        defaults.update(parameters)
-        parameters = defaults
-
-        if token:
-            parameters['oauth_token'] = token.key
-            if token.callback:
-                parameters['oauth_callback'] = token.callback
-            # 1.0a support for verifier.
-            if verifier:
-                parameters['oauth_verifier'] = verifier
-        elif callback:
-            # 1.0a support for callback in the request token request.
-            parameters['oauth_callback'] = callback
-
-        return OAuthRequest(http_method, http_url, parameters)
-    from_consumer_and_token = staticmethod(from_consumer_and_token)
-
-    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
-            http_url=None, parameters=None):
-        if not parameters:
-            parameters = {}
-
-        parameters['oauth_token'] = token.key
-
-        if callback:
-            parameters['oauth_callback'] = callback
-
-        return OAuthRequest(http_method, http_url, parameters)
-    from_token_and_callback = staticmethod(from_token_and_callback)
-
-    def _split_header(header):
-        """Turn Authorization: header into parameters."""
-        params = {}
-        parts = header.split(',')
-        for param in parts:
-            # Ignore realm parameter.
-            if param.find('realm') > -1:
-                continue
-            # Remove whitespace.
-            param = param.strip()
-            # Split key-value.
-            param_parts = param.split('=', 1)
-            # Remove quotes and unescape the value.
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
-        return params
-    _split_header = staticmethod(_split_header)
-
-    def _split_url_string(param_str):
-        """Turn URL string into parameters."""
-        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
-        return parameters
-    _split_url_string = staticmethod(_split_url_string)
-
-class OAuthServer(object):
-    """A worker to check the validity of a request against a data store."""
-    timestamp_threshold = 300 # In seconds, five minutes.
-    version = VERSION
-    signature_methods = None
-    data_store = None
-
-    def __init__(self, data_store=None, signature_methods=None):
-        self.data_store = data_store
-        self.signature_methods = signature_methods or {}
-
-    def set_data_store(self, data_store):
-        self.data_store = data_store
-
-    def get_data_store(self):
-        return self.data_store
-
-    def add_signature_method(self, signature_method):
-        self.signature_methods[signature_method.get_name()] = signature_method
-        return self.signature_methods
-
-    def fetch_request_token(self, oauth_request):
-        """Processes a request_token request and returns the
-        request token on success.
-        """
-        try:
-            # Get the request token for authorization.
-            token = self._get_token(oauth_request, 'request')
-        except OAuthError:
-            # No token required for the initial token request.
-            version = self._get_version(oauth_request)
-            consumer = self._get_consumer(oauth_request)
-            try:
-                callback = self.get_callback(oauth_request)
-            except OAuthError:
-                callback = None # 1.0, no callback specified.
-            self._check_signature(oauth_request, consumer, None)
-            # Fetch a new token.
-            token = self.data_store.fetch_request_token(consumer, callback)
-        return token
-
-    def fetch_access_token(self, oauth_request):
-        """Processes an access_token request and returns the
-        access token on success.
-        """
-        version = self._get_version(oauth_request)
-        consumer = self._get_consumer(oauth_request)
-        try:
-            verifier = self._get_verifier(oauth_request)
-        except OAuthError:
-            verifier = None
-        # Get the request token.
-        token = self._get_token(oauth_request, 'request')
-        self._check_signature(oauth_request, consumer, token)
-        new_token = self.data_store.fetch_access_token(consumer, token, verifier)
-        return new_token
-
-    def verify_request(self, oauth_request):
-        """Verifies an api call and checks all the parameters."""
-        # -> consumer and token
-        version = self._get_version(oauth_request)
-        consumer = self._get_consumer(oauth_request)
-        # Get the access token.
-        token = self._get_token(oauth_request, 'access')
-        self._check_signature(oauth_request, consumer, token)
-        parameters = oauth_request.get_nonoauth_parameters()
-        return consumer, token, parameters
-
-    def authorize_token(self, token, user):
-        """Authorize a request token."""
-        return self.data_store.authorize_request_token(token, user)
-
-    def get_callback(self, oauth_request):
-        """Get the callback URL."""
-        return oauth_request.get_parameter('oauth_callback')
-    def build_authenticate_header(self, realm=''):
-        """Optional support for the authenticate header."""
-        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-    def _get_version(self, oauth_request):
-        """Verify the correct version request for this server."""
-        try:
-            version = oauth_request.get_parameter('oauth_version')
-        except:
-            version = VERSION
-        if version and version != self.version:
-            raise OAuthError('OAuth version %s not supported.' % str(version))
-        return version
-
-    def _get_signature_method(self, oauth_request):
-        """Figure out the signature with some defaults."""
-        try:
-            signature_method = oauth_request.get_parameter(
-                'oauth_signature_method')
-        except:
-            signature_method = SIGNATURE_METHOD
-        try:
-            # Get the signature method object.
-            signature_method = self.signature_methods[signature_method]
-        except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
-            raise OAuthError('Signature method %s not supported try one of the '
-                'following: %s' % (signature_method, signature_method_names))
-
-        return signature_method
-
-    def _get_consumer(self, oauth_request):
-        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
-        consumer = self.data_store.lookup_consumer(consumer_key)
-        if not consumer:
-            raise OAuthError('Invalid consumer.')
-        return consumer
-
-    def _get_token(self, oauth_request, token_type='access'):
-        """Try to find the token for the provided request token key."""
-        token_field = oauth_request.get_parameter('oauth_token')
-        token = self.data_store.lookup_token(token_type, token_field)
-        if not token:
-            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
-        return token
-    
-    def _get_verifier(self, oauth_request):
-        return oauth_request.get_parameter('oauth_verifier')
-
-    def _check_signature(self, oauth_request, consumer, token):
-        timestamp, nonce = oauth_request._get_timestamp_nonce()
-        self._check_timestamp(timestamp)
-        self._check_nonce(consumer, token, nonce)
-        signature_method = self._get_signature_method(oauth_request)
-        try:
-            signature = oauth_request.get_parameter('oauth_signature')
-        except:
-            raise OAuthError('Missing signature.')
-        # Validate the signature.
-        valid_sig = signature_method.check_signature(oauth_request, consumer,
-            token, signature)
-        if not valid_sig:
-            key, base = signature_method.build_signature_base_string(
-                oauth_request, consumer, token)
-            raise OAuthError('Invalid signature. Expected signature base '
-                'string: %s' % base)
-        built = signature_method.build_signature(oauth_request, consumer, token)
-
-    def _check_timestamp(self, timestamp):
-        """Verify that timestamp is recentish."""
-        timestamp = int(timestamp)
-        now = int(time.time())
-        lapsed = now - timestamp
-        if lapsed > self.timestamp_threshold:
-            raise OAuthError('Expired timestamp: given %d and now %s has a '
-                'greater difference than threshold %d' %
-                (timestamp, now, self.timestamp_threshold))
-
-    def _check_nonce(self, consumer, token, nonce):
-        """Verify that the nonce is uniqueish."""
-        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
-        if nonce:
-            raise OAuthError('Nonce already used: %s' % str(nonce))
-
-
-class OAuthClient(object):
-    """OAuthClient is a worker to attempt to execute a request."""
-    consumer = None
-    token = None
-
-    def __init__(self, oauth_consumer, oauth_token):
-        self.consumer = oauth_consumer
-        self.token = oauth_token
-
-    def get_consumer(self):
-        return self.consumer
-
-    def get_token(self):
-        return self.token
-
-    def fetch_request_token(self, oauth_request):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_access_token(self, oauth_request):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def access_resource(self, oauth_request):
-        """-> Some protected resource."""
-        raise NotImplementedError
-
-
-class OAuthDataStore(object):
-    """A database abstraction used to lookup consumers and tokens."""
-
-    def lookup_consumer(self, key):
-        """-> OAuthConsumer."""
-        raise NotImplementedError
-
-    def lookup_token(self, oauth_consumer, token_type, token_token):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_request_token(self, oauth_consumer, oauth_callback):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def authorize_request_token(self, oauth_token, user):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-
-class OAuthSignatureMethod(object):
-    """A strategy class that implements a signature method."""
-    def get_name(self):
-        """-> str."""
-        raise NotImplementedError
-
-    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
-        """-> str key, str raw."""
-        raise NotImplementedError
-
-    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
-        """-> str."""
-        raise NotImplementedError
-
-    def check_signature(self, oauth_request, consumer, token, signature):
-        built = self.build_signature(oauth_request, consumer, token)
-        return built == signature
-
-
-class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
-
-    def get_name(self):
-        return 'HMAC-SHA1'
-        
-    def build_signature_base_string(self, oauth_request, consumer, token):
-        sig = (
-            escape(oauth_request.get_normalized_http_method()),
-            escape(oauth_request.get_normalized_http_url()),
-            escape(oauth_request.get_normalized_parameters()),
-        )
-
-        key = '%s&' % escape(consumer.secret)
-        if token:
-            key += escape(token.secret)
-        raw = '&'.join(sig)
-        return key, raw
-
-    def build_signature(self, oauth_request, consumer, token):
-        """Builds the base signature string."""
-        key, raw = self.build_signature_base_string(oauth_request, consumer,
-            token)
-
-        # HMAC object.
-        try:
-            import hashlib # 2.5
-            hashed = hmac.new(key, raw, hashlib.sha1)
-        except:
-            import sha # Deprecated
-            hashed = hmac.new(key, raw, sha)
-
-        # Calculate the digest base 64.
-        return binascii.b2a_base64(hashed.digest())[:-1]
-
-
-class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
-
-    def get_name(self):
-        return 'PLAINTEXT'
-
-    def build_signature_base_string(self, oauth_request, consumer, token):
-        """Concatenates the consumer key and secret."""
-        sig = '%s&' % escape(consumer.secret)
-        if token:
-            sig = sig + escape(token.secret)
-        return sig, sig
-
-    def build_signature(self, oauth_request, consumer, token):
-        key, raw = self.build_signature_base_string(oauth_request, consumer,
-            token)
-        return key
\ No newline at end of file
diff --git a/oauthlib/__init__.py b/oauthlib/__init__.py
new file mode 100644 (file)
index 0000000..300bdc7
--- /dev/null
@@ -0,0 +1,25 @@
+"""
+    oauthlib
+    ~~~~~~~~
+
+    A generic, spec-compliant, thorough implementation of the OAuth
+    request-signing logic.
+
+    :copyright: (c) 2011 by Idan Gazit.
+    :license: BSD, see LICENSE for details.
+"""
+
+__author__ = 'Idan Gazit <idan@gazit.me>'
+__version__ = '0.7.2'
+
+
+import logging
+try:  # Python 2.7+
+    from logging import NullHandler
+except ImportError:
+    class NullHandler(logging.Handler):
+
+        def emit(self, record):
+            pass
+
+logging.getLogger('oauthlib').addHandler(NullHandler())
diff --git a/oauthlib/common.py b/oauthlib/common.py
new file mode 100644 (file)
index 0000000..0179b8e
--- /dev/null
@@ -0,0 +1,414 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.common
+~~~~~~~~~~~~~~
+
+This module provides data structures and utilities common
+to all implementations of OAuth.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import collections
+import datetime
+import logging
+import random
+import re
+import sys
+import time
+
+try:
+    from urllib import quote as _quote
+    from urllib import unquote as _unquote
+    from urllib import urlencode as _urlencode
+except ImportError:
+    from urllib.parse import quote as _quote
+    from urllib.parse import unquote as _unquote
+    from urllib.parse import urlencode as _urlencode
+try:
+    import urlparse
+except ImportError:
+    import urllib.parse as urlparse
+
+UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
+                               'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+                               '0123456789')
+
+CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN'
+                           'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}')
+
+
+always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+               'abcdefghijklmnopqrstuvwxyz'
+               '0123456789' '_.-')
+
+log = logging.getLogger('oauthlib')
+
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+    unicode_type = str
+    bytes_type = bytes
+else:
+    unicode_type = unicode
+    bytes_type = str
+
+
+# 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
+def quote(s, safe=b'/'):
+    s = s.encode('utf-8') if isinstance(s, unicode_type) else s
+    s = _quote(s, safe)
+    # PY3 always returns unicode.  PY2 may return either, depending on whether
+    # it had to modify the string.
+    if isinstance(s, bytes_type):
+        s = s.decode('utf-8')
+    return s
+
+
+def unquote(s):
+    s = _unquote(s)
+    # PY3 always returns unicode.  PY2 seems to always return what you give it,
+    # which differs from quote's behavior.  Just to be safe, make sure it is
+    # unicode before we return.
+    if isinstance(s, bytes_type):
+        s = s.decode('utf-8')
+    return s
+
+
+def urlencode(params):
+    utf8_params = encode_params_utf8(params)
+    urlencoded = _urlencode(utf8_params)
+    if isinstance(urlencoded, unicode_type):  # PY3 returns unicode
+        return urlencoded
+    else:
+        return urlencoded.decode("utf-8")
+
+
+def encode_params_utf8(params):
+    """Ensures that all parameters in a list of 2-element tuples are encoded to
+    bytestrings using UTF-8
+    """
+    encoded = []
+    for k, v in params:
+        encoded.append((
+            k.encode('utf-8') if isinstance(k, unicode_type) else k,
+            v.encode('utf-8') if isinstance(v, unicode_type) else v))
+    return encoded
+
+
+def decode_params_utf8(params):
+    """Ensures that all parameters in a list of 2-element tuples are decoded to
+    unicode using UTF-8.
+    """
+    decoded = []
+    for k, v in params:
+        decoded.append((
+            k.decode('utf-8') if isinstance(k, bytes_type) else k,
+            v.decode('utf-8') if isinstance(v, bytes_type) else v))
+    return decoded
+
+
+urlencoded = set(always_safe) | set('=&;%+~,*@')
+
+
+def urldecode(query):
+    """Decode a query string in x-www-form-urlencoded format into a sequence
+    of two-element tuples.
+
+    Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
+    correct formatting of the query string by validation. If validation fails
+    a ValueError will be raised. urllib.parse_qsl will only raise errors if
+    any of name-value pairs omits the equals sign.
+    """
+    # Check if query contains invalid characters
+    if query and not set(query) <= urlencoded:
+        error = ("Error trying to decode a non urlencoded string. "
+                 "Found invalid characters: %s "
+                 "in the string: '%s'. "
+                 "Please ensure the request/response body is "
+                 "x-www-form-urlencoded.")
+        raise ValueError(error % (set(query) - urlencoded, query))
+
+    # Check for correctly hex encoded values using a regular expression
+    # All encoded values begin with % followed by two hex characters
+    # correct = %00, %A0, %0A, %FF
+    # invalid = %G0, %5H, %PO
+    invalid_hex = '%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]'
+    if len(re.findall(invalid_hex, query)):
+        raise ValueError('Invalid hex encoding in query string.')
+
+    # We encode to utf-8 prior to parsing because parse_qsl behaves
+    # differently on unicode input in python 2 and 3.
+    # Python 2.7
+    # >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
+    # u'\xe5\x95\xa6\xe5\x95\xa6'
+    # Python 2.7, non unicode input gives the same
+    # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
+    # '\xe5\x95\xa6\xe5\x95\xa6'
+    # but now we can decode it to unicode
+    # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
+    # u'\u5566\u5566'
+    # Python 3.3 however
+    # >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
+    # u'\u5566\u5566'
+    query = query.encode(
+        'utf-8') if not PY3 and isinstance(query, unicode_type) else query
+    # We want to allow queries such as "c2" whereas urlparse.parse_qsl
+    # with the strict_parsing flag will not.
+    params = urlparse.parse_qsl(query, keep_blank_values=True)
+
+    # unicode all the things
+    return decode_params_utf8(params)
+
+
+def extract_params(raw):
+    """Extract parameters and return them as a list of 2-tuples.
+
+    Will successfully extract parameters from urlencoded query strings,
+    dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
+    empty list of parameters. Any other input will result in a return
+    value of None.
+    """
+    if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
+        try:
+            params = urldecode(raw)
+        except ValueError:
+            params = None
+    elif hasattr(raw, '__iter__'):
+        try:
+            dict(raw)
+        except ValueError:
+            params = None
+        except TypeError:
+            params = None
+        else:
+            params = list(raw.items() if isinstance(raw, dict) else raw)
+            params = decode_params_utf8(params)
+    else:
+        params = None
+
+    return params
+
+
+def generate_nonce():
+    """Generate pseudorandom nonce that is unlikely to repeat.
+
+    Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
+    Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+    A random 64-bit number is appended to the epoch timestamp for both
+    randomness and to decrease the likelihood of collisions.
+
+    .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+    .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+    """
+    return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
+
+
+def generate_timestamp():
+    """Get seconds since epoch (UTC).
+
+    Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
+    Per `section 3.2.1`_ of the MAC Access Authentication spec.
+
+    .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
+    .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+    """
+    return unicode_type(int(time.time()))
+
+
+def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
+    """Generates a non-guessable OAuth token
+
+    OAuth (1 and 2) does not specify the format of tokens except that they
+    should be strings of random characters. Tokens should not be guessable
+    and entropy when generating the random characters is important. Which is
+    why SystemRandom is used instead of the default random.choice method.
+    """
+    rand = random.SystemRandom()
+    return ''.join(rand.choice(chars) for x in range(length))
+
+
+def generate_signed_token(private_pem, request):
+    import jwt
+
+    now = datetime.datetime.utcnow()
+
+    claims = {
+        'scope': request.scope,
+        'exp': now + datetime.timedelta(seconds=request.expires_in)
+    }
+
+    claims.update(request.claims)
+
+    token = jwt.encode(claims, private_pem, 'RS256')
+    token = to_unicode(token, "UTF-8")
+
+    return token
+
+
+def verify_signed_token(public_pem, token):
+    import jwt
+
+    return jwt.decode(token, public_pem, algorithms=['RS256'])
+
+
+def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
+    """Generates an OAuth client_id
+
+    OAuth 2 specify the format of client_id in
+    http://tools.ietf.org/html/rfc6749#appendix-A.
+    """
+    return generate_token(length, chars)
+
+
+def add_params_to_qs(query, params):
+    """Extend a query with a list of two-tuples."""
+    if isinstance(params, dict):
+        params = params.items()
+    queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
+    queryparams.extend(params)
+    return urlencode(queryparams)
+
+
+def add_params_to_uri(uri, params, fragment=False):
+    """Add a list of two-tuples to the uri query components."""
+    sch, net, path, par, query, fra = urlparse.urlparse(uri)
+    if fragment:
+        fra = add_params_to_qs(fra, params)
+    else:
+        query = add_params_to_qs(query, params)
+    return urlparse.urlunparse((sch, net, path, par, query, fra))
+
+
+def safe_string_equals(a, b):
+    """ Near-constant time string comparison.
+
+    Used in order to avoid timing attacks on sensitive information such
+    as secret keys during request verification (`rootLabs`_).
+
+    .. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
+
+    """
+    if len(a) != len(b):
+        return False
+
+    result = 0
+    for x, y in zip(a, b):
+        result |= ord(x) ^ ord(y)
+    return result == 0
+
+
+def to_unicode(data, encoding='UTF-8'):
+    """Convert a number of different types of objects to unicode."""
+    if isinstance(data, unicode_type):
+        return data
+
+    if isinstance(data, bytes_type):
+        return unicode_type(data, encoding=encoding)
+
+    if hasattr(data, '__iter__'):
+        try:
+            dict(data)
+        except TypeError:
+            pass
+        except ValueError:
+            # Assume it's a one dimensional data structure
+            return (to_unicode(i, encoding) for i in data)
+        else:
+            # We support 2.6 which lacks dict comprehensions
+            if hasattr(data, 'items'):
+                data = data.items()
+            return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data))
+
+    return data
+
+
+class CaseInsensitiveDict(dict):
+
+    """Basic case insensitive dict with strings only keys."""
+
+    proxy = {}
+
+    def __init__(self, data):
+        self.proxy = dict((k.lower(), k) for k in data)
+        for k in data:
+            self[k] = data[k]
+
+    def __contains__(self, k):
+        return k.lower() in self.proxy
+
+    def __delitem__(self, k):
+        key = self.proxy[k.lower()]
+        super(CaseInsensitiveDict, self).__delitem__(key)
+        del self.proxy[k.lower()]
+
+    def __getitem__(self, k):
+        key = self.proxy[k.lower()]
+        return super(CaseInsensitiveDict, self).__getitem__(key)
+
+    def get(self, k, default=None):
+        return self[k] if k in self else default
+
+    def __setitem__(self, k, v):
+        super(CaseInsensitiveDict, self).__setitem__(k, v)
+        self.proxy[k.lower()] = k
+
+
+class Request(object):
+
+    """A malleable representation of a signable HTTP request.
+
+    Body argument may contain any data, but parameters will only be decoded if
+    they are one of:
+
+    * urlencoded query string
+    * dict
+    * list of 2-tuples
+
+    Anything else will be treated as raw body data to be passed through
+    unmolested.
+    """
+
+    def __init__(self, uri, http_method='GET', body=None, headers=None,
+                 encoding='utf-8'):
+        # Convert to unicode using encoding if given, else assume unicode
+        encode = lambda x: to_unicode(x, encoding) if encoding else x
+
+        self.uri = encode(uri)
+        self.http_method = encode(http_method)
+        self.headers = CaseInsensitiveDict(encode(headers or {}))
+        self.body = encode(body)
+        self.decoded_body = extract_params(encode(body))
+        self.oauth_params = []
+
+        self._params = {}
+        self._params.update(dict(urldecode(self.uri_query)))
+        self._params.update(dict(self.decoded_body or []))
+        self._params.update(self.headers)
+
+    def __getattr__(self, name):
+        return self._params.get(name, None)
+
+    def __repr__(self):
+        return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
+            self.uri, self.http_method, self.headers, self.body)
+
+    @property
+    def uri_query(self):
+        return urlparse.urlparse(self.uri).query
+
+    @property
+    def uri_query_params(self):
+        if not self.uri_query:
+            return []
+        return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
+                                  strict_parsing=True)
+
+    @property
+    def duplicate_params(self):
+        seen_keys = collections.defaultdict(int)
+        all_keys = (p[0]
+                    for p in (self.decoded_body or []) + self.uri_query_params)
+        for k in all_keys:
+            seen_keys[k] += 1
+        return [k for k, c in seen_keys.items() if c > 1]
diff --git a/oauthlib/oauth1/__init__.py b/oauthlib/oauth1/__init__.py
new file mode 100644 (file)
index 0000000..b2bc0f9
--- /dev/null
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 1.0 Client
+and Server classes.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .rfc5849 import Client
+from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT
+from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY
+from .rfc5849 import SIGNATURE_TYPE_BODY
+from .rfc5849.request_validator import RequestValidator
+from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint
+from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint
+from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer
+from .rfc5849.errors import *
diff --git a/oauthlib/oauth1/rfc5849/__init__.py b/oauthlib/oauth1/rfc5849/__init__.py
new file mode 100644 (file)
index 0000000..ad9713c
--- /dev/null
@@ -0,0 +1,320 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+log = logging.getLogger(__name__)
+
+import sys
+try:
+    import urlparse
+except ImportError:
+    import urllib.parse as urlparse
+
+if sys.version_info[0] == 3:
+    bytes_type = bytes
+else:
+    bytes_type = str
+
+from oauthlib.common import Request, urlencode, generate_nonce
+from oauthlib.common import generate_timestamp, to_unicode
+from . import parameters, signature
+
+SIGNATURE_HMAC = "HMAC-SHA1"
+SIGNATURE_RSA = "RSA-SHA1"
+SIGNATURE_PLAINTEXT = "PLAINTEXT"
+SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
+
+SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
+SIGNATURE_TYPE_QUERY = 'QUERY'
+SIGNATURE_TYPE_BODY = 'BODY'
+
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+
+
+class Client(object):
+
+    """A client used to sign OAuth 1.0 RFC 5849 requests."""
+    SIGNATURE_METHODS = {
+        SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client,
+        SIGNATURE_RSA: signature.sign_rsa_sha1_with_client,
+        SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
+    }
+
+    @classmethod
+    def register_signature_method(cls, method_name, method_callback):
+        cls.SIGNATURE_METHODS[method_name] = method_callback
+
+    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, realm=None,
+                 encoding='utf-8', decoding=None,
+                 nonce=None, timestamp=None):
+        """Create an OAuth 1 client.
+
+        :param client_key: Client key (consumer key), mandatory.
+        :param resource_owner_key: Resource owner key (oauth token).
+        :param resource_owner_secret: Resource owner secret (oauth token secret).
+        :param callback_uri: Callback used when obtaining request token.
+        :param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT.
+        :param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default),
+                               SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY
+                               depending on where you want to embed the oauth
+                               credentials.
+        :param rsa_key: RSA key used with SIGNATURE_RSA.
+        :param verifier: Verifier used when obtaining an access token.
+        :param realm: Realm (scope) to which access is being requested.
+        :param encoding: If you provide non-unicode input you may use this
+                         to have oauthlib automatically convert.
+        :param decoding: If you wish that the returned uri, headers and body
+                         from sign be encoded back from unicode, then set
+                         decoding to your preferred encoding, i.e. utf-8.
+        :param nonce: Use this nonce instead of generating one. (Mainly for testing)
+        :param timestamp: Use this timestamp instead of using current. (Mainly for testing)
+        """
+        # Convert to unicode using encoding if given, else assume unicode
+        encode = lambda x: to_unicode(x, encoding) if encoding else x
+
+        self.client_key = encode(client_key)
+        self.client_secret = encode(client_secret)
+        self.resource_owner_key = encode(resource_owner_key)
+        self.resource_owner_secret = encode(resource_owner_secret)
+        self.signature_method = encode(signature_method)
+        self.signature_type = encode(signature_type)
+        self.callback_uri = encode(callback_uri)
+        self.rsa_key = encode(rsa_key)
+        self.verifier = encode(verifier)
+        self.realm = encode(realm)
+        self.encoding = encode(encoding)
+        self.decoding = encode(decoding)
+        self.nonce = encode(nonce)
+        self.timestamp = encode(timestamp)
+
+        if self.signature_method == SIGNATURE_RSA and self.rsa_key is None:
+            raise ValueError(
+                'rsa_key is required when using RSA signature method.')
+
+    def __repr__(self):
+        attrs = vars(self).copy()
+        attrs['client_secret'] = '****' if attrs['client_secret'] else None
+        attrs[
+            'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
+        attribute_str = ', '.join('%s=%s' % (k, v) for k, v in attrs.items())
+        return '<%s %s>' % (self.__class__.__name__, attribute_str)
+
+    def get_oauth_signature(self, request):
+        """Get an OAuth signature to be used in signing a request
+
+        To satisfy `section 3.4.1.2`_ item 2, if the request argument's
+        headers dict attribute contains a Host item, its value will
+        replace any netloc part of the request argument's uri attribute
+        value.
+
+        .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+        """
+        if self.signature_method == SIGNATURE_PLAINTEXT:
+            # fast-path
+            return signature.sign_plaintext(self.client_secret,
+                                            self.resource_owner_secret)
+
+        uri, headers, body = self._render(request)
+
+        collected_params = signature.collect_parameters(
+            uri_query=urlparse.urlparse(uri).query,
+            body=body,
+            headers=headers)
+        log.debug("Collected params: {0}".format(collected_params))
+
+        normalized_params = signature.normalize_parameters(collected_params)
+        normalized_uri = signature.normalize_base_string_uri(uri,
+                                                             headers.get('Host', None))
+        log.debug("Normalized params: {0}".format(normalized_params))
+        log.debug("Normalized URI: {0}".format(normalized_uri))
+
+        base_string = signature.construct_base_string(request.http_method,
+                                                      normalized_uri, normalized_params)
+
+        log.debug("Base signing string: {0}".format(base_string))
+
+        if self.signature_method not in self.SIGNATURE_METHODS:
+            raise ValueError('Invalid signature method.')
+
+        sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
+
+        log.debug("Signature: {0}".format(sig))
+        return sig
+
+    def get_oauth_params(self, request):
+        """Get the basic OAuth parameters to be used in generating a signature.
+        """
+        nonce = (generate_nonce()
+                 if self.nonce is None else self.nonce)
+        timestamp = (generate_timestamp()
+                     if self.timestamp is None else self.timestamp)
+        params = [
+            ('oauth_nonce', nonce),
+            ('oauth_timestamp', timestamp),
+            ('oauth_version', '1.0'),
+            ('oauth_signature_method', self.signature_method),
+            ('oauth_consumer_key', self.client_key),
+        ]
+        if self.resource_owner_key:
+            params.append(('oauth_token', self.resource_owner_key))
+        if self.callback_uri:
+            params.append(('oauth_callback', self.callback_uri))
+        if self.verifier:
+            params.append(('oauth_verifier', self.verifier))
+
+        return params
+
+    def _render(self, request, formencode=False, realm=None):
+        """Render a signed request according to signature type
+
+        Returns a 3-tuple containing the request URI, headers, and body.
+
+        If the formencode argument is True and the body contains parameters, it
+        is escaped and returned as a valid formencoded string.
+        """
+        # TODO what if there are body params on a header-type auth?
+        # TODO what if there are query params on a body-type auth?
+
+        uri, headers, body = request.uri, request.headers, request.body
+
+        # TODO: right now these prepare_* methods are very narrow in scope--they
+        # only affect their little thing. In some cases (for example, with
+        # header auth) it might be advantageous to allow these methods to touch
+        # other parts of the request, like the headers—so the prepare_headers
+        # method could also set the Content-Type header to x-www-form-urlencoded
+        # like the spec requires. This would be a fundamental change though, and
+        # I'm not sure how I feel about it.
+        if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
+            headers = parameters.prepare_headers(
+                request.oauth_params, request.headers, realm=realm)
+        elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
+            body = parameters.prepare_form_encoded_body(
+                request.oauth_params, request.decoded_body)
+            if formencode:
+                body = urlencode(body)
+            headers['Content-Type'] = 'application/x-www-form-urlencoded'
+        elif self.signature_type == SIGNATURE_TYPE_QUERY:
+            uri = parameters.prepare_request_uri_query(
+                request.oauth_params, request.uri)
+        else:
+            raise ValueError('Unknown signature type specified.')
+
+        return uri, headers, body
+
+    def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
+        """Sign a request
+
+        Signs an HTTP request with the specified parts.
+
+        Returns a 3-tuple of the signed request's URI, headers, and body.
+        Note that http_method is not returned as it is unaffected by the OAuth
+        signing process. Also worth noting is that duplicate parameters
+        will be included in the signature, regardless of where they are
+        specified (query, body).
+
+        The body argument may be a dict, a list of 2-tuples, or a formencoded
+        string. The Content-Type header must be 'application/x-www-form-urlencoded'
+        if it is present.
+
+        If the body argument is not one of the above, it will be returned
+        verbatim as it is unaffected by the OAuth signing process. Attempting to
+        sign a request with non-formencoded data using the OAuth body signature
+        type is invalid and will raise an exception.
+
+        If the body does contain parameters, it will be returned as a properly-
+        formatted formencoded string.
+
+        Body may not be included if the http_method is either GET or HEAD as
+        this changes the semantic meaning of the request.
+
+        All string data MUST be unicode or be encoded with the same encoding
+        scheme supplied to the Client constructor, default utf-8. This includes
+        strings inside body dicts, for example.
+        """
+        # normalize request data
+        request = Request(uri, http_method, body, headers,
+                          encoding=self.encoding)
+
+        # sanity check
+        content_type = request.headers.get('Content-Type', None)
+        multipart = content_type and content_type.startswith('multipart/')
+        should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
+        has_params = request.decoded_body is not None
+        # 3.4.1.3.1.  Parameter Sources
+        # [Parameters are collected from the HTTP request entity-body, but only
+        # if [...]:
+        #    *  The entity-body is single-part.
+        if multipart and has_params:
+            raise ValueError(
+                "Headers indicate a multipart body but body contains parameters.")
+        #    *  The entity-body follows the encoding requirements of the
+        #       "application/x-www-form-urlencoded" content-type as defined by
+        #       [W3C.REC-html40-19980424].
+        elif should_have_params and not has_params:
+            raise ValueError(
+                "Headers indicate a formencoded body but body was not decodable.")
+        #    *  The HTTP request entity-header includes the "Content-Type"
+        #       header field set to "application/x-www-form-urlencoded".
+        elif not should_have_params and has_params:
+            raise ValueError(
+                "Body contains parameters but Content-Type header was {0} "
+                "instead of {1}".format(content_type or "not set",
+                                        CONTENT_TYPE_FORM_URLENCODED))
+
+        # 3.5.2.  Form-Encoded Body
+        # Protocol parameters can be transmitted in the HTTP request entity-
+        # body, but only if the following REQUIRED conditions are met:
+        # o  The entity-body is single-part.
+        # o  The entity-body follows the encoding requirements of the
+        #    "application/x-www-form-urlencoded" content-type as defined by
+        #    [W3C.REC-html40-19980424].
+        # o  The HTTP request entity-header includes the "Content-Type" header
+        #    field set to "application/x-www-form-urlencoded".
+        elif self.signature_type == SIGNATURE_TYPE_BODY and not (
+                should_have_params and has_params and not multipart):
+            raise ValueError(
+                'Body signatures may only be used with form-urlencoded content')
+
+        # We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+        # with the clause that parameters from body should only be included
+        # in non GET or HEAD requests. Extracting the request body parameters
+        # and including them in the signature base string would give semantic
+        # meaning to the body, which it should not have according to the
+        # HTTP 1.1 spec.
+        elif http_method.upper() in ('GET', 'HEAD') and has_params:
+            raise ValueError('GET/HEAD requests should not include body.')
+
+        # generate the basic OAuth parameters
+        request.oauth_params = self.get_oauth_params(request)
+
+        # generate the signature
+        request.oauth_params.append(
+            ('oauth_signature', self.get_oauth_signature(request)))
+
+        # render the signed request and return it
+        uri, headers, body = self._render(request, formencode=True,
+                                          realm=(realm or self.realm))
+
+        if self.decoding:
+            log.debug('Encoding URI, headers and body to %s.', self.decoding)
+            uri = uri.encode(self.decoding)
+            body = body.encode(self.decoding) if body else body
+            new_headers = {}
+            for k, v in headers.items():
+                new_headers[k.encode(self.decoding)] = v.encode(self.decoding)
+            headers = new_headers
+        return uri, headers, body
diff --git a/oauthlib/oauth1/rfc5849/endpoints/__init__.py b/oauthlib/oauth1/rfc5849/endpoints/__init__.py
new file mode 100644 (file)
index 0000000..b16ccba
--- /dev/null
@@ -0,0 +1,9 @@
+from __future__ import absolute_import
+
+from .base import BaseEndpoint
+from .request_token import RequestTokenEndpoint
+from .authorization import AuthorizationEndpoint
+from .access_token import AccessTokenEndpoint
+from .resource import ResourceEndpoint
+from .signature_only import SignatureOnlyEndpoint
+from .pre_configured import WebApplicationServer
diff --git a/oauthlib/oauth1/rfc5849/endpoints/access_token.py b/oauthlib/oauth1/rfc5849/endpoints/access_token.py
new file mode 100644 (file)
index 0000000..26db919
--- /dev/null
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.access_token
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the access token provider logic of
+OAuth 1.0 RFC 5849. It validates the correctness of access token requests,
+creates and persists tokens as well as create the proper response to be
+returned to the client.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import urlencode
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class AccessTokenEndpoint(BaseEndpoint):
+
+    """An endpoint responsible for providing OAuth 1 access tokens.
+
+    Typical use is to instantiate with a request validator and invoke the
+    ``create_access_token_response`` from a view function. The tuple returned
+    has all information necessary (body, status, headers) to quickly form
+    and return a proper response. See :doc:`/oauth1/validator` for details on which
+    validator methods to implement for this endpoint.
+    """
+
+    def create_access_token(self, request, credentials):
+        """Create and save a new access token.
+
+        Similar to OAuth 2, indication of granted scopes will be included as a
+        space separated list in ``oauth_authorized_realms``.
+
+        :param request: An oauthlib.common.Request object.
+        :returns: The token as an urlencoded string.
+        """
+        request.realms = self.request_validator.get_realms(
+            request.resource_owner_key, request)
+        token = {
+            'oauth_token': self.token_generator(),
+            'oauth_token_secret': self.token_generator(),
+            # Backport the authorized scopes indication used in OAuth2
+            'oauth_authorized_realms': ' '.join(request.realms)
+        }
+        token.update(credentials)
+        self.request_validator.save_access_token(token, request)
+        return urlencode(token.items())
+
+    def create_access_token_response(self, uri, http_method='GET', body=None,
+                                     headers=None, credentials=None):
+        """Create an access token response, with a new request token if valid.
+
+        :param uri: The full URI of the token request.
+        :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+        :param body: The request body as a string.
+        :param headers: The request headers as a dict.
+        :param credentials: A list of extra credentials to include in the token.
+        :returns: A tuple of 3 elements.
+                  1. A dict of headers to set on the response.
+                  2. The response body as a string.
+                  3. The response status code as an integer.
+
+        An example of a valid request::
+
+            >>> from your_validator import your_validator
+            >>> from oauthlib.oauth1 import AccessTokenEndpoint
+            >>> endpoint = AccessTokenEndpoint(your_validator)
+            >>> h, b, s = endpoint.create_access_token_response(
+            ...     'https://your.provider/access_token?foo=bar',
+            ...     headers={
+            ...         'Authorization': 'OAuth oauth_token=234lsdkf....'
+            ...     },
+            ...     credentials={
+            ...         'my_specific': 'argument',
+            ...     })
+            >>> h
+            {'Content-Type': 'application/x-www-form-urlencoded'}
+            >>> b
+            'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_authorized_realms=movies+pics&my_specific=argument'
+            >>> s
+            200
+
+        An response to invalid request would have a different body and status::
+
+            >>> b
+            'error=invalid_request&description=missing+resource+owner+key'
+            >>> s
+            400
+
+        The same goes for an an unauthorized request:
+
+            >>> b
+            ''
+            >>> s
+            401
+        """
+        resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+        try:
+            request = self._create_request(uri, http_method, body, headers)
+            valid, processed_request = self.validate_access_token_request(
+                request)
+            if valid:
+                token = self.create_access_token(request, credentials or {})
+                self.request_validator.invalidate_request_token(
+                    request.client_key,
+                    request.resource_owner_key,
+                    request)
+                return resp_headers, token, 200
+            else:
+                return {}, None, 401
+        except errors.OAuth1Error as e:
+            return resp_headers, e.urlencoded, e.status_code
+
+    def validate_access_token_request(self, request):
+        """Validate an access token request.
+
+        :param request: An oauthlib.common.Request object.
+        :raises: OAuth1Error if the request is invalid.
+        :returns: A tuple of 2 elements.
+                  1. The validation result (True or False).
+                  2. The request object.
+        """
+        self._check_transport_security(request)
+        self._check_mandatory_parameters(request)
+
+        if not request.resource_owner_key:
+            raise errors.InvalidRequestError(
+                description='Missing resource owner.')
+
+        if not self.request_validator.check_request_token(
+                request.resource_owner_key):
+            raise errors.InvalidRequestError(
+                description='Invalid resource owner key format.')
+
+        if not request.verifier:
+            raise errors.InvalidRequestError(
+                description='Missing verifier.')
+
+        if not self.request_validator.check_verifier(request.verifier):
+            raise errors.InvalidRequestError(
+                description='Invalid verifier format.')
+
+        if not self.request_validator.validate_timestamp_and_nonce(
+                request.client_key, request.timestamp, request.nonce, request,
+                request_token=request.resource_owner_key):
+            return False, request
+
+        # The server SHOULD return a 401 (Unauthorized) status code when
+        # receiving a request with invalid client credentials.
+        # Note: This is postponed in order to avoid timing attacks, instead
+        # a dummy client is assigned and used to maintain near constant
+        # time request verification.
+        #
+        # Note that early exit would enable client enumeration
+        valid_client = self.request_validator.validate_client_key(
+            request.client_key, request)
+        if not valid_client:
+            request.client_key = self.request_validator.dummy_client
+
+        # The server SHOULD return a 401 (Unauthorized) status code when
+        # receiving a request with invalid or expired token.
+        # Note: This is postponed in order to avoid timing attacks, instead
+        # a dummy token is assigned and used to maintain near constant
+        # time request verification.
+        #
+        # Note that early exit would enable resource owner enumeration
+        valid_resource_owner = self.request_validator.validate_request_token(
+            request.client_key, request.resource_owner_key, request)
+        if not valid_resource_owner:
+            request.resource_owner_key = self.request_validator.dummy_request_token
+
+        # The server MUST verify (Section 3.2) the validity of the request,
+        # ensure that the resource owner has authorized the provisioning of
+        # token credentials to the client, and ensure that the temporary
+        # credentials have not expired or been used before.  The server MUST
+        # also verify the verification code received from the client.
+        # .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
+        #
+        # Note that early exit would enable resource owner authorization
+        # verifier enumertion.
+        valid_verifier = self.request_validator.validate_verifier(
+            request.client_key,
+            request.resource_owner_key,
+            request.verifier,
+            request)
+
+        valid_signature = self._check_signature(request, is_token_request=True)
+
+        # We delay checking validity until the very end, using dummy values for
+        # calculations and fetching secrets/keys to ensure the flow of every
+        # request remains almost identical regardless of whether valid values
+        # have been supplied. This ensures near constant time execution and
+        # prevents malicious users from guessing sensitive information
+        v = all((valid_client, valid_resource_owner, valid_verifier,
+                 valid_signature))
+        if not v:
+            log.info("[Failure] request verification failed.")
+            log.info("Valid client:, %s", valid_client)
+            log.info("Valid token:, %s", valid_resource_owner)
+            log.info("Valid verifier:, %s", valid_verifier)
+            log.info("Valid signature:, %s", valid_signature)
+        return v, request
diff --git a/oauthlib/oauth1/rfc5849/endpoints/authorization.py b/oauthlib/oauth1/rfc5849/endpoints/authorization.py
new file mode 100644 (file)
index 0000000..a93a517
--- /dev/null
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.authorization
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from oauthlib.common import Request, add_params_to_uri
+
+from .base import BaseEndpoint
+from .. import errors
+try:
+    from urllib import urlencode
+except ImportError:
+    from urllib.parse import urlencode
+
+
+class AuthorizationEndpoint(BaseEndpoint):
+
+    """An endpoint responsible for letting authenticated users authorize access
+    to their protected resources to a client.
+
+    Typical use would be to have two views, one for displaying the authorization
+    form and one to process said form on submission.
+
+    The first view will want to utilize ``get_realms_and_credentials`` to fetch
+    requested realms and useful client credentials, such as name and
+    description, to be used when creating the authorization form.
+
+    During form processing you can use ``create_authorization_response`` to
+    validate the request, create a verifier as well as prepare the final
+    redirection URI used to send the user back to the client.
+
+    See :doc:`/oauth1/validator` for details on which validator methods to implement
+    for this endpoint.
+    """
+
+    def create_verifier(self, request, credentials):
+        """Create and save a new request token.
+
+        :param request: An oauthlib.common.Request object.
+        :param credentials: A dict of extra token credentials.
+        :returns: The verifier as a dict.
+        """
+        verifier = {
+            'oauth_token': request.resource_owner_key,
+            'oauth_verifier': self.token_generator(),
+        }
+        verifier.update(credentials)
+        self.request_validator.save_verifier(
+            request.resource_owner_key, verifier, request)
+        return verifier
+
+    def create_authorization_response(self, uri, http_method='GET', body=None,
+                                      headers=None, realms=None, credentials=None):
+        """Create an authorization response, with a new request token if valid.
+
+        :param uri: The full URI of the token request.
+        :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+        :param body: The request body as a string.
+        :param headers: The request headers as a dict.
+        :param credentials: A list of credentials to include in the verifier.
+        :returns: A tuple of 3 elements.
+                  1. A dict of headers to set on the response.
+                  2. The response body as a string.
+                  3. The response status code as an integer.
+
+        If the callback URI tied to the current token is "oob", a response with
+        a 200 status code will be returned. In this case, it may be desirable to
+        modify the response to better display the verifier to the client.
+
+        An example of an authorization request::
+
+            >>> from your_validator import your_validator
+            >>> from oauthlib.oauth1 import AuthorizationEndpoint
+            >>> endpoint = AuthorizationEndpoint(your_validator)
+            >>> h, b, s = endpoint.create_authorization_response(
+            ...     'https://your.provider/authorize?oauth_token=...',
+            ...     credentials={
+            ...         'extra': 'argument',
+            ...     })
+            >>> h
+            {'Location': 'https://the.client/callback?oauth_verifier=...&extra=argument'}
+            >>> b
+            None
+            >>> s
+            302
+
+        An example of a request with an "oob" callback::
+
+            >>> from your_validator import your_validator
+            >>> from oauthlib.oauth1 import AuthorizationEndpoint
+            >>> endpoint = AuthorizationEndpoint(your_validator)
+            >>> h, b, s = endpoint.create_authorization_response(
+            ...     'https://your.provider/authorize?foo=bar',
+            ...     credentials={
+            ...         'extra': 'argument',
+            ...     })
+            >>> h
+            {'Content-Type': 'application/x-www-form-urlencoded'}
+            >>> b
+            'oauth_verifier=...&extra=argument'
+            >>> s
+            200
+        """
+        request = self._create_request(uri, http_method=http_method, body=body,
+                                       headers=headers)
+
+        if not request.resource_owner_key:
+            raise errors.InvalidRequestError(
+                'Missing mandatory parameter oauth_token.')
+        if not self.request_validator.verify_request_token(
+                request.resource_owner_key, request):
+            raise errors.InvalidClientError()
+
+        request.realms = realms
+        if (request.realms and not self.request_validator.verify_realms(
+                request.resource_owner_key, request.realms, request)):
+            raise errors.InvalidRequestError(
+                description=('User granted access to realms outside of '
+                             'what the client may request.'))
+
+        verifier = self.create_verifier(request, credentials or {})
+        redirect_uri = self.request_validator.get_redirect_uri(
+            request.resource_owner_key, request)
+        if redirect_uri == 'oob':
+            response_headers = {
+                'Content-Type': 'application/x-www-form-urlencoded'}
+            response_body = urlencode(verifier)
+            return response_headers, response_body, 200
+        else:
+            populated_redirect = add_params_to_uri(
+                redirect_uri, verifier.items())
+            return {'Location': populated_redirect}, None, 302
+
+    def get_realms_and_credentials(self, uri, http_method='GET', body=None,
+                                   headers=None):
+        """Fetch realms and credentials for the presented request token.
+
+        :param uri: The full URI of the token request.
+        :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+        :param body: The request body as a string.
+        :param headers: The request headers as a dict.
+        :returns: A tuple of 2 elements.
+                  1. A list of request realms.
+                  2. A dict of credentials which may be useful in creating the
+                  authorization form.
+        """
+        request = self._create_request(uri, http_method=http_method, body=body,
+                                       headers=headers)
+
+        if not self.request_validator.verify_request_token(
+                request.resource_owner_key, request):
+            raise errors.InvalidClientError()
+
+        realms = self.request_validator.get_realms(
+            request.resource_owner_key, request)
+        return realms, {'resource_owner_key': request.resource_owner_key}
diff --git a/oauthlib/oauth1/rfc5849/endpoints/base.py b/oauthlib/oauth1/rfc5849/endpoints/base.py
new file mode 100644 (file)
index 0000000..42006a1
--- /dev/null
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.base
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import time
+
+from oauthlib.common import Request, generate_token
+
+from .. import signature, utils, errors
+from .. import CONTENT_TYPE_FORM_URLENCODED
+from .. import SIGNATURE_HMAC, SIGNATURE_RSA
+from .. import SIGNATURE_TYPE_AUTH_HEADER
+from .. import SIGNATURE_TYPE_QUERY
+from .. import SIGNATURE_TYPE_BODY
+
+
+class BaseEndpoint(object):
+
+    def __init__(self, request_validator, token_generator=None):
+        self.request_validator = request_validator
+        self.token_generator = token_generator or generate_token
+
+    def _get_signature_type_and_params(self, request):
+        """Extracts parameters from query, headers and body. Signature type
+        is set to the source in which parameters were found.
+        """
+        # Per RFC5849, only the Authorization header may contain the 'realm'
+        # optional parameter.
+        header_params = signature.collect_parameters(headers=request.headers,
+                                                     exclude_oauth_signature=False, with_realm=True)
+        body_params = signature.collect_parameters(body=request.body,
+                                                   exclude_oauth_signature=False)
+        query_params = signature.collect_parameters(uri_query=request.uri_query,
+                                                    exclude_oauth_signature=False)
+
+        params = []
+        params.extend(header_params)
+        params.extend(body_params)
+        params.extend(query_params)
+        signature_types_with_oauth_params = list(filter(lambda s: s[2], (
+            (SIGNATURE_TYPE_AUTH_HEADER, params,
+                utils.filter_oauth_params(header_params)),
+            (SIGNATURE_TYPE_BODY, params,
+                utils.filter_oauth_params(body_params)),
+            (SIGNATURE_TYPE_QUERY, params,
+                utils.filter_oauth_params(query_params))
+        )))
+
+        if len(signature_types_with_oauth_params) > 1:
+            found_types = [s[0] for s in signature_types_with_oauth_params]
+            raise errors.InvalidRequestError(
+                description=('oauth_ params must come from only 1 signature'
+                             'type but were found in %s',
+                             ', '.join(found_types)))
+
+        try:
+            signature_type, params, oauth_params = signature_types_with_oauth_params[
+                0]
+        except IndexError:
+            raise errors.InvalidRequestError(
+                description='Missing mandatory OAuth parameters.')
+
+        return signature_type, params, oauth_params
+
+    def _create_request(self, uri, http_method, body, headers):
+        # Only include body data from x-www-form-urlencoded requests
+        headers = headers or {}
+        if ("Content-Type" in headers and
+                CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]):
+            request = Request(uri, http_method, body, headers)
+        else:
+            request = Request(uri, http_method, '', headers)
+
+        signature_type, params, oauth_params = (
+            self._get_signature_type_and_params(request))
+
+        # The server SHOULD return a 400 (Bad Request) status code when
+        # receiving a request with duplicated protocol parameters.
+        if len(dict(oauth_params)) != len(oauth_params):
+            raise errors.InvalidRequestError(
+                description='Duplicate OAuth2 entries.')
+
+        oauth_params = dict(oauth_params)
+        request.signature = oauth_params.get('oauth_signature')
+        request.client_key = oauth_params.get('oauth_consumer_key')
+        request.resource_owner_key = oauth_params.get('oauth_token')
+        request.nonce = oauth_params.get('oauth_nonce')
+        request.timestamp = oauth_params.get('oauth_timestamp')
+        request.redirect_uri = oauth_params.get('oauth_callback')
+        request.verifier = oauth_params.get('oauth_verifier')
+        request.signature_method = oauth_params.get('oauth_signature_method')
+        request.realm = dict(params).get('realm')
+        request.oauth_params = oauth_params
+
+        # Parameters to Client depend on signature method which may vary
+        # for each request. Note that HMAC-SHA1 and PLAINTEXT share parameters
+        request.params = [(k, v) for k, v in params if k != "oauth_signature"]
+
+        if 'realm' in request.headers.get('Authorization', ''):
+            request.params = [(k, v)
+                              for k, v in request.params if k != "realm"]
+
+        return request
+
+    def _check_transport_security(self, request):
+        # TODO: move into oauthlib.common from oauth2.utils
+        if (self.request_validator.enforce_ssl and
+                not request.uri.lower().startswith("https://")):
+            raise errors.InsecureTransportError()
+
+    def _check_mandatory_parameters(self, request):
+        # The server SHOULD return a 400 (Bad Request) status code when
+        # receiving a request with missing parameters.
+        if not all((request.signature, request.client_key,
+                    request.nonce, request.timestamp,
+                    request.signature_method)):
+            raise errors.InvalidRequestError(
+                description='Missing mandatory OAuth parameters.')
+
+        # OAuth does not mandate a particular signature method, as each
+        # implementation can have its own unique requirements.  Servers are
+        # free to implement and document their own custom methods.
+        # Recommending any particular method is beyond the scope of this
+        # specification.  Implementers should review the Security
+        # Considerations section (`Section 4`_) before deciding on which
+        # method to support.
+        # .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4
+        if (not request.signature_method in
+                self.request_validator.allowed_signature_methods):
+            raise errors.InvalidSignatureMethodError(
+                description="Invalid signature, %s not in %r." % (
+                    request.signature_method,
+                    self.request_validator.allowed_signature_methods))
+
+        # Servers receiving an authenticated request MUST validate it by:
+        #   If the "oauth_version" parameter is present, ensuring its value is
+        #   "1.0".
+        if ('oauth_version' in request.oauth_params and
+                request.oauth_params['oauth_version'] != '1.0'):
+            raise errors.InvalidRequestError(
+                description='Invalid OAuth version.')
+
+        # The timestamp value MUST be a positive integer. Unless otherwise
+        # specified by the server's documentation, the timestamp is expressed
+        # in the number of seconds since January 1, 1970 00:00:00 GMT.
+        if len(request.timestamp) != 10:
+            raise errors.InvalidRequestError(
+                description='Invalid timestamp size')
+
+        try:
+            ts = int(request.timestamp)
+
+        except ValueError:
+            raise errors.InvalidRequestError(
+                description='Timestamp must be an integer.')
+
+        else:
+            # To avoid the need to retain an infinite number of nonce values for
+            # future checks, servers MAY choose to restrict the time period after
+            # which a request with an old timestamp is rejected.
+            if abs(time.time() - ts) > self.request_validator.timestamp_lifetime:
+                raise errors.InvalidRequestError(
+                    description=('Timestamp given is invalid, differ from '
+                                 'allowed by over %s seconds.' % (
+                                     self.request_validator.timestamp_lifetime)))
+
+        # Provider specific validation of parameters, used to enforce
+        # restrictions such as character set and length.
+        if not self.request_validator.check_client_key(request.client_key):
+            raise errors.InvalidRequestError(
+                description='Invalid client key format.')
+
+        if not self.request_validator.check_nonce(request.nonce):
+            raise errors.InvalidRequestError(
+                description='Invalid nonce format.')
+
+    def _check_signature(self, request, is_token_request=False):
+        # ---- RSA Signature verification ----
+        if request.signature_method == SIGNATURE_RSA:
+            # The server verifies the signature per `[RFC3447] section 8.2.2`_
+            # .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1
+            rsa_key = self.request_validator.get_rsa_key(
+                request.client_key, request)
+            valid_signature = signature.verify_rsa_sha1(request, rsa_key)
+
+        # ---- HMAC or Plaintext Signature verification ----
+        else:
+            # Servers receiving an authenticated request MUST validate it by:
+            #   Recalculating the request signature independently as described in
+            #   `Section 3.4`_ and comparing it to the value received from the
+            #   client via the "oauth_signature" parameter.
+            # .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+            client_secret = self.request_validator.get_client_secret(
+                request.client_key, request)
+            resource_owner_secret = None
+            if request.resource_owner_key:
+                if is_token_request:
+                    resource_owner_secret = self.request_validator.get_request_token_secret(
+                        request.client_key, request.resource_owner_key, request)
+                else:
+                    resource_owner_secret = self.request_validator.get_access_token_secret(
+                        request.client_key, request.resource_owner_key, request)
+
+            if request.signature_method == SIGNATURE_HMAC:
+                valid_signature = signature.verify_hmac_sha1(request,
+                                                             client_secret, resource_owner_secret)
+            else:
+                valid_signature = signature.verify_plaintext(request,
+                                                             client_secret, resource_owner_secret)
+        return valid_signature
diff --git a/oauthlib/oauth1/rfc5849/endpoints/pre_configured.py b/oauthlib/oauth1/rfc5849/endpoints/pre_configured.py
new file mode 100644 (file)
index 0000000..f0705a8
--- /dev/null
@@ -0,0 +1,14 @@
+from __future__ import absolute_import, unicode_literals
+
+from . import RequestTokenEndpoint, AuthorizationEndpoint
+from . import AccessTokenEndpoint, ResourceEndpoint
+
+
+class WebApplicationServer(RequestTokenEndpoint, AuthorizationEndpoint,
+                           AccessTokenEndpoint, ResourceEndpoint):
+
+    def __init__(self, request_validator):
+        RequestTokenEndpoint.__init__(self, request_validator)
+        AuthorizationEndpoint.__init__(self, request_validator)
+        AccessTokenEndpoint.__init__(self, request_validator)
+        ResourceEndpoint.__init__(self, request_validator)
diff --git a/oauthlib/oauth1/rfc5849/endpoints/request_token.py b/oauthlib/oauth1/rfc5849/endpoints/request_token.py
new file mode 100644 (file)
index 0000000..e97c34b
--- /dev/null
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.request_token
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the request token provider logic of
+OAuth 1.0 RFC 5849. It validates the correctness of request token requests,
+creates and persists tokens as well as create the proper response to be
+returned to the client.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import urlencode
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class RequestTokenEndpoint(BaseEndpoint):
+
+    """An endpoint responsible for providing OAuth 1 request tokens.
+
+    Typical use is to instantiate with a request validator and invoke the
+    ``create_request_token_response`` from a view function. The tuple returned
+    has all information necessary (body, status, headers) to quickly form
+    and return a proper response. See :doc:`/oauth1/validator` for details on which
+    validator methods to implement for this endpoint.
+    """
+
+    def create_request_token(self, request, credentials):
+        """Create and save a new request token.
+
+        :param request: An oauthlib.common.Request object.
+        :param credentials: A dict of extra token credentials.
+        :returns: The token as an urlencoded string.
+        """
+        token = {
+            'oauth_token': self.token_generator(),
+            'oauth_token_secret': self.token_generator(),
+            'oauth_callback_confirmed': 'true'
+        }
+        token.update(credentials)
+        self.request_validator.save_request_token(token, request)
+        return urlencode(token.items())
+
+    def create_request_token_response(self, uri, http_method='GET', body=None,
+                                      headers=None, credentials=None):
+        """Create a request token response, with a new request token if valid.
+
+        :param uri: The full URI of the token request.
+        :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+        :param body: The request body as a string.
+        :param headers: The request headers as a dict.
+        :param credentials: A list of extra credentials to include in the token.
+        :returns: A tuple of 3 elements.
+                  1. A dict of headers to set on the response.
+                  2. The response body as a string.
+                  3. The response status code as an integer.
+
+        An example of a valid request::
+
+            >>> from your_validator import your_validator
+            >>> from oauthlib.oauth1 import RequestTokenEndpoint
+            >>> endpoint = RequestTokenEndpoint(your_validator)
+            >>> h, b, s = endpoint.create_request_token_response(
+            ...     'https://your.provider/request_token?foo=bar',
+            ...     headers={
+            ...         'Authorization': 'OAuth realm=movies user, oauth_....'
+            ...     },
+            ...     credentials={
+            ...         'my_specific': 'argument',
+            ...     })
+            >>> h
+            {'Content-Type': 'application/x-www-form-urlencoded'}
+            >>> b
+            'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_callback_confirmed=true&my_specific=argument'
+            >>> s
+            200
+
+        An response to invalid request would have a different body and status::
+
+            >>> b
+            'error=invalid_request&description=missing+callback+uri'
+            >>> s
+            400
+
+        The same goes for an an unauthorized request:
+
+            >>> b
+            ''
+            >>> s
+            401
+        """
+        resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+        try:
+            request = self._create_request(uri, http_method, body, headers)
+            valid, processed_request = self.validate_request_token_request(
+                request)
+            if valid:
+                token = self.create_request_token(request, credentials or {})
+                return resp_headers, token, 200
+            else:
+                return {}, None, 401
+        except errors.OAuth1Error as e:
+            return resp_headers, e.urlencoded, e.status_code
+
+    def validate_request_token_request(self, request):
+        """Validate a request token request.
+
+        :param request: An oauthlib.common.Request object.
+        :raises: OAuth1Error if the request is invalid.
+        :returns: A tuple of 2 elements.
+                  1. The validation result (True or False).
+                  2. The request object.
+        """
+        self._check_transport_security(request)
+        self._check_mandatory_parameters(request)
+
+        if request.realm:
+            request.realms = request.realm.split(' ')
+        else:
+            request.realms = self.request_validator.get_default_realms(
+                request.client_key, request)
+        if not self.request_validator.check_realms(request.realms):
+            raise errors.InvalidRequestError(
+                description='Invalid realm %s. Allowed are %r.' % (
+                    request.realms, self.request_validator.realms))
+
+        if not request.redirect_uri:
+            raise errors.InvalidRequestError(
+                description='Missing callback URI.')
+
+        if not self.request_validator.validate_timestamp_and_nonce(
+                request.client_key, request.timestamp, request.nonce, request,
+                request_token=request.resource_owner_key):
+            return False, request
+
+        # The server SHOULD return a 401 (Unauthorized) status code when
+        # receiving a request with invalid client credentials.
+        # Note: This is postponed in order to avoid timing attacks, instead
+        # a dummy client is assigned and used to maintain near constant
+        # time request verification.
+        #
+        # Note that early exit would enable client enumeration
+        valid_client = self.request_validator.validate_client_key(
+            request.client_key, request)
+        if not valid_client:
+            request.client_key = self.request_validator.dummy_client
+
+        # Note that `realm`_ is only used in authorization headers and how
+        # it should be interepreted is not included in the OAuth spec.
+        # However they could be seen as a scope or realm to which the
+        # client has access and as such every client should be checked
+        # to ensure it is authorized access to that scope or realm.
+        # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
+        #
+        # Note that early exit would enable client realm access enumeration.
+        #
+        # The require_realm indicates this is the first step in the OAuth
+        # workflow where a client requests access to a specific realm.
+        # This first step (obtaining request token) need not require a realm
+        # and can then be identified by checking the require_resource_owner
+        # flag and abscence of realm.
+        #
+        # Clients obtaining an access token will not supply a realm and it will
+        # not be checked. Instead the previously requested realm should be
+        # transferred from the request token to the access token.
+        #
+        # Access to protected resources will always validate the realm but note
+        # that the realm is now tied to the access token and not provided by
+        # the client.
+        valid_realm = self.request_validator.validate_requested_realms(
+            request.client_key, request.realms, request)
+
+        # Callback is normally never required, except for requests for
+        # a Temporary Credential as described in `Section 2.1`_
+        # .._`Section 2.1`: http://tools.ietf.org/html/rfc5849#section-2.1
+        valid_redirect = self.request_validator.validate_redirect_uri(
+            request.client_key, request.redirect_uri, request)
+        if not request.redirect_uri:
+            raise NotImplementedError('Redirect URI must either be provided '
+                                      'or set to a default during validation.')
+
+        valid_signature = self._check_signature(request)
+
+        # We delay checking validity until the very end, using dummy values for
+        # calculations and fetching secrets/keys to ensure the flow of every
+        # request remains almost identical regardless of whether valid values
+        # have been supplied. This ensures near constant time execution and
+        # prevents malicious users from guessing sensitive information
+        v = all((valid_client, valid_realm, valid_redirect, valid_signature))
+        if not v:
+            log.info("[Failure] request verification failed.")
+            log.info("Valid client: %s.", valid_client)
+            log.info("Valid realm: %s.", valid_realm)
+            log.info("Valid callback: %s.", valid_redirect)
+            log.info("Valid signature: %s.", valid_signature)
+        return v, request
diff --git a/oauthlib/oauth1/rfc5849/endpoints/resource.py b/oauthlib/oauth1/rfc5849/endpoints/resource.py
new file mode 100644 (file)
index 0000000..651a87c
--- /dev/null
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.resource
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the resource protection provider logic of
+OAuth 1.0 RFC 5849.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class ResourceEndpoint(BaseEndpoint):
+
+    """An endpoint responsible for protecting resources.
+
+    Typical use is to instantiate with a request validator and invoke the
+    ``validate_protected_resource_request`` in a decorator around a view
+    function. If the request is valid, invoke and return the response of the
+    view. If invalid create and return an error response directly from the
+    decorator.
+
+    See :doc:`/oauth1/validator` for details on which validator methods to implement
+    for this endpoint.
+
+    An example decorator::
+
+        from functools import wraps
+        from your_validator import your_validator
+        from oauthlib.oauth1 import ResourceEndpoint
+        endpoint = ResourceEndpoint(your_validator)
+
+        def require_oauth(realms=None):
+            def decorator(f):
+                @wraps(f)
+                def wrapper(request, *args, **kwargs):
+                    v, r = provider.validate_protected_resource_request(
+                            request.url,
+                            http_method=request.method,
+                            body=request.data,
+                            headers=request.headers,
+                            realms=realms or [])
+                    if v:
+                        return f(*args, **kwargs)
+                    else:
+                        return abort(403)
+    """
+
+    def validate_protected_resource_request(self, uri, http_method='GET',
+                                            body=None, headers=None, realms=None):
+        """Create a request token response, with a new request token if valid.
+
+        :param uri: The full URI of the token request.
+        :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+        :param body: The request body as a string.
+        :param headers: The request headers as a dict.
+        :param realms: A list of realms the resource is protected under.
+                       This will be supplied to the ``validate_realms``
+                       method of the request validator.
+        :returns: A tuple of 2 elements.
+                  1. True if valid, False otherwise.
+                  2. An oauthlib.common.Request object.
+        """
+        try:
+            request = self._create_request(uri, http_method, body, headers)
+        except errors.OAuth1Error:
+            return False, None
+
+        try:
+            self._check_transport_security(request)
+            self._check_mandatory_parameters(request)
+        except errors.OAuth1Error:
+            return False, request
+
+        if not request.resource_owner_key:
+            return False, request
+
+        if not self.request_validator.check_access_token(
+                request.resource_owner_key):
+            return False, request
+
+        if not self.request_validator.validate_timestamp_and_nonce(
+                request.client_key, request.timestamp, request.nonce, request,
+                access_token=request.resource_owner_key):
+            return False, request
+
+        # The server SHOULD return a 401 (Unauthorized) status code when
+        # receiving a request with invalid client credentials.
+        # Note: This is postponed in order to avoid timing attacks, instead
+        # a dummy client is assigned and used to maintain near constant
+        # time request verification.
+        #
+        # Note that early exit would enable client enumeration
+        valid_client = self.request_validator.validate_client_key(
+            request.client_key, request)
+        if not valid_client:
+            request.client_key = self.request_validator.dummy_client
+
+        # The server SHOULD return a 401 (Unauthorized) status code when
+        # receiving a request with invalid or expired token.
+        # Note: This is postponed in order to avoid timing attacks, instead
+        # a dummy token is assigned and used to maintain near constant
+        # time request verification.
+        #
+        # Note that early exit would enable resource owner enumeration
+        valid_resource_owner = self.request_validator.validate_access_token(
+            request.client_key, request.resource_owner_key, request)
+        if not valid_resource_owner:
+            request.resource_owner_key = self.request_validator.dummy_access_token
+
+        # Note that `realm`_ is only used in authorization headers and how
+        # it should be interepreted is not included in the OAuth spec.
+        # However they could be seen as a scope or realm to which the
+        # client has access and as such every client should be checked
+        # to ensure it is authorized access to that scope or realm.
+        # .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
+        #
+        # Note that early exit would enable client realm access enumeration.
+        #
+        # The require_realm indicates this is the first step in the OAuth
+        # workflow where a client requests access to a specific realm.
+        # This first step (obtaining request token) need not require a realm
+        # and can then be identified by checking the require_resource_owner
+        # flag and abscence of realm.
+        #
+        # Clients obtaining an access token will not supply a realm and it will
+        # not be checked. Instead the previously requested realm should be
+        # transferred from the request token to the access token.
+        #
+        # Access to protected resources will always validate the realm but note
+        # that the realm is now tied to the access token and not provided by
+        # the client.
+        valid_realm = self.request_validator.validate_realms(request.client_key,
+                                                             request.resource_owner_key, request, uri=request.uri,
+                                                             realms=realms)
+
+        valid_signature = self._check_signature(request)
+
+        # We delay checking validity until the very end, using dummy values for
+        # calculations and fetching secrets/keys to ensure the flow of every
+        # request remains almost identical regardless of whether valid values
+        # have been supplied. This ensures near constant time execution and
+        # prevents malicious users from guessing sensitive information
+        v = all((valid_client, valid_resource_owner, valid_realm,
+                 valid_signature))
+        if not v:
+            log.info("[Failure] request verification failed.")
+            log.info("Valid client: %s", valid_client)
+            log.info("Valid token: %s", valid_resource_owner)
+            log.info("Valid realm: %s", valid_realm)
+            log.info("Valid signature: %s", valid_signature)
+        return v, request
diff --git a/oauthlib/oauth1/rfc5849/endpoints/signature_only.py b/oauthlib/oauth1/rfc5849/endpoints/signature_only.py
new file mode 100644 (file)
index 0000000..2f8e7c9
--- /dev/null
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.endpoints.signature_only
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of the signing logic of OAuth 1.0 RFC 5849.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from .base import BaseEndpoint
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class SignatureOnlyEndpoint(BaseEndpoint):
+
+    """An endpoint only responsible for verifying an oauth signature."""
+
+    def validate_request(self, uri, http_method='GET',
+                         body=None, headers=None):
+        """Validate a signed OAuth request.
+
+        :param uri: The full URI of the token request.
+        :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
+        :param body: The request body as a string.
+        :param headers: The request headers as a dict.
+        :returns: A tuple of 2 elements.
+                  1. True if valid, False otherwise.
+                  2. An oauthlib.common.Request object.
+        """
+        try:
+            request = self._create_request(uri, http_method, body, headers)
+        except errors.OAuth1Error:
+            return False, None
+
+        try:
+            self._check_transport_security(request)
+            self._check_mandatory_parameters(request)
+        except errors.OAuth1Error:
+            return False, request
+
+        if not self.request_validator.validate_timestamp_and_nonce(
+                request.client_key, request.timestamp, request.nonce, request):
+            return False, request
+
+        # The server SHOULD return a 401 (Unauthorized) status code when
+        # receiving a request with invalid client credentials.
+        # Note: This is postponed in order to avoid timing attacks, instead
+        # a dummy client is assigned and used to maintain near constant
+        # time request verification.
+        #
+        # Note that early exit would enable client enumeration
+        valid_client = self.request_validator.validate_client_key(
+            request.client_key, request)
+        if not valid_client:
+            request.client_key = self.request_validator.dummy_client
+
+        valid_signature = self._check_signature(request)
+
+        # We delay checking validity until the very end, using dummy values for
+        # calculations and fetching secrets/keys to ensure the flow of every
+        # request remains almost identical regardless of whether valid values
+        # have been supplied. This ensures near constant time execution and
+        # prevents malicious users from guessing sensitive information
+        v = all((valid_client, valid_signature))
+        if not v:
+            log.info("[Failure] request verification failed.")
+            log.info("Valid client: %s", valid_client)
+            log.info("Valid signature: %s", valid_signature)
+        return v, request
diff --git a/oauthlib/oauth1/rfc5849/errors.py b/oauthlib/oauth1/rfc5849/errors.py
new file mode 100644 (file)
index 0000000..978035e
--- /dev/null
@@ -0,0 +1,79 @@
+# coding=utf-8
+"""
+oauthlib.oauth1.rfc5849.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 1 clients and provicers to represent the spec
+defined error responses for all four core grant types.
+"""
+from __future__ import unicode_literals
+
+from oauthlib.common import urlencode, add_params_to_uri
+
+
+class OAuth1Error(Exception):
+    error = None
+    description = ''
+
+    def __init__(self, description=None, uri=None, status_code=400,
+                 request=None):
+        """
+        description:    A human-readable ASCII [USASCII] text providing
+                        additional information, used to assist the client
+                        developer in understanding the error that occurred.
+                        Values for the "error_description" parameter MUST NOT
+                        include characters outside the set
+                        x20-21 / x23-5B / x5D-7E.
+
+        uri:    A URI identifying a human-readable web page with information
+                about the error, used to provide the client developer with
+                additional information about the error.  Values for the
+                "error_uri" parameter MUST conform to the URI- Reference
+                syntax, and thus MUST NOT include characters outside the set
+                x21 / x23-5B / x5D-7E.
+
+        state:  A CSRF protection value received from the client.
+
+        request:  Oauthlib Request object
+        """
+        self.description = description or self.description
+        message = '(%s) %s' % (self.error, self.description)
+        if request:
+            message += ' ' + repr(request)
+        super(OAuth1Error, self).__init__(message)
+
+        self.uri = uri
+        self.status_code = status_code
+
+    def in_uri(self, uri):
+        return add_params_to_uri(uri, self.twotuples)
+
+    @property
+    def twotuples(self):
+        error = [('error', self.error)]
+        if self.description:
+            error.append(('error_description', self.description))
+        if self.uri:
+            error.append(('error_uri', self.uri))
+        return error
+
+    @property
+    def urlencoded(self):
+        return urlencode(self.twotuples)
+
+
+class InsecureTransportError(OAuth1Error):
+    error = 'insecure_transport_protocol'
+    description = 'Only HTTPS connections are permitted.'
+
+
+class InvalidSignatureMethodError(OAuth1Error):
+    error = 'invalid_signature_method'
+
+
+class InvalidRequestError(OAuth1Error):
+    error = 'invalid_request'
+
+
+class InvalidClientError(OAuth1Error):
+    error = 'invalid_client'
diff --git a/oauthlib/oauth1/rfc5849/parameters.py b/oauthlib/oauth1/rfc5849/parameters.py
new file mode 100644 (file)
index 0000000..f0963ab
--- /dev/null
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.parameters
+~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
+
+.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5
+"""
+from __future__ import absolute_import, unicode_literals
+
+try:
+    from urlparse import urlparse, urlunparse
+except ImportError:
+    from urllib.parse import urlparse, urlunparse
+from . import utils
+from oauthlib.common import extract_params, urlencode
+
+
+# TODO: do we need filter_params now that oauth_params are handled by Request?
+#       We can easily pass in just oauth protocol params.
+@utils.filter_params
+def prepare_headers(oauth_params, headers=None, realm=None):
+    """**Prepare the Authorization header.**
+    Per `section 3.5.1`_ of the spec.
+
+    Protocol parameters can be transmitted using the HTTP "Authorization"
+    header field as defined by `RFC2617`_ with the auth-scheme name set to
+    "OAuth" (case insensitive).
+
+    For example::
+
+        Authorization: OAuth realm="Example",
+            oauth_consumer_key="0685bd9184jfhq22",
+            oauth_token="ad180jjd733klru7",
+            oauth_signature_method="HMAC-SHA1",
+            oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+            oauth_timestamp="137131200",
+            oauth_nonce="4572616e48616d6d65724c61686176",
+            oauth_version="1.0"
+
+
+    .. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
+    .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+    """
+    headers = headers or {}
+
+    # Protocol parameters SHALL be included in the "Authorization" header
+    # field as follows:
+    authorization_header_parameters_parts = []
+    for oauth_parameter_name, value in oauth_params:
+        # 1.  Parameter names and values are encoded per Parameter Encoding
+        #     (`Section 3.6`_)
+        #
+        # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+        escaped_name = utils.escape(oauth_parameter_name)
+        escaped_value = utils.escape(value)
+
+        # 2.  Each parameter's name is immediately followed by an "=" character
+        #     (ASCII code 61), a """ character (ASCII code 34), the parameter
+        #     value (MAY be empty), and another """ character (ASCII code 34).
+        part = '{0}="{1}"'.format(escaped_name, escaped_value)
+
+        authorization_header_parameters_parts.append(part)
+
+    # 3.  Parameters are separated by a "," character (ASCII code 44) and
+    #     OPTIONAL linear whitespace per `RFC2617`_.
+    #
+    # .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
+    authorization_header_parameters = ', '.join(
+        authorization_header_parameters_parts)
+
+    # 4.  The OPTIONAL "realm" parameter MAY be added and interpreted per
+    #     `RFC2617 section 1.2`_.
+    #
+    # .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2
+    if realm:
+        # NOTE: realm should *not* be escaped
+        authorization_header_parameters = ('realm="%s", ' % realm +
+                                           authorization_header_parameters)
+
+    # the auth-scheme name set to "OAuth" (case insensitive).
+    authorization_header = 'OAuth %s' % authorization_header_parameters
+
+    # contribute the Authorization header to the given headers
+    full_headers = {}
+    full_headers.update(headers)
+    full_headers['Authorization'] = authorization_header
+    return full_headers
+
+
+def _append_params(oauth_params, params):
+    """Append OAuth params to an existing set of parameters.
+
+    Both params and oauth_params is must be lists of 2-tuples.
+
+    Per `section 3.5.2`_ and `3.5.3`_ of the spec.
+
+    .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
+    .. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+
+    """
+    merged = list(params)
+    merged.extend(oauth_params)
+    # The request URI / entity-body MAY include other request-specific
+    # parameters, in which case, the protocol parameters SHOULD be appended
+    # following the request-specific parameters, properly separated by an "&"
+    # character (ASCII code 38)
+    merged.sort(key=lambda i: i[0].startswith('oauth_'))
+    return merged
+
+
+def prepare_form_encoded_body(oauth_params, body):
+    """Prepare the Form-Encoded Body.
+
+    Per `section 3.5.2`_ of the spec.
+
+    .. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
+
+    """
+    # append OAuth params to the existing body
+    return _append_params(oauth_params, body)
+
+
+def prepare_request_uri_query(oauth_params, uri):
+    """Prepare the Request URI Query.
+
+    Per `section 3.5.3`_ of the spec.
+
+    .. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
+
+    """
+    # append OAuth params to the existing set of query components
+    sch, net, path, par, query, fra = urlparse(uri)
+    query = urlencode(
+        _append_params(oauth_params, extract_params(query) or []))
+    return urlunparse((sch, net, path, par, query, fra))
diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py
new file mode 100644 (file)
index 0000000..e722029
--- /dev/null
@@ -0,0 +1,823 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849
+~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for signing and checking OAuth 1.0 RFC 5849 requests.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from . import SIGNATURE_METHODS, utils
+
+
+class RequestValidator(object):
+
+    """A validator/datastore interaction base class for OAuth 1 providers.
+
+    OAuth providers should inherit from RequestValidator and implement the
+    methods and properties outlined below. Further details are provided in the
+    documentation for each method and property.
+
+    Methods used to check the format of input parameters. Common tests include
+    length, character set, membership, range or pattern. These tests are
+    referred to as `whitelisting or blacklisting`_. Whitelisting is better
+    but blacklisting can be usefull to spot malicious activity.
+    The following have methods a default implementation:
+
+    - check_client_key
+    - check_request_token
+    - check_access_token
+    - check_nonce
+    - check_verifier
+    - check_realms
+
+    The methods above default to whitelist input parameters, checking that they
+    are alphanumerical and between a minimum and maximum length. Rather than
+    overloading the methods a few properties can be used to configure these
+    methods.
+
+    * @safe_characters -> (character set)
+    * @client_key_length -> (min, max)
+    * @request_token_length -> (min, max)
+    * @access_token_length -> (min, max)
+    * @nonce_length -> (min, max)
+    * @verifier_length -> (min, max)
+    * @realms -> [list, of, realms]
+
+    Methods used to validate/invalidate input parameters. These checks usually
+    hit either persistent or temporary storage such as databases or the
+    filesystem. See each methods documentation for detailed usage.
+    The following methods must be implemented:
+
+    - validate_client_key
+    - validate_request_token
+    - validate_access_token
+    - validate_timestamp_and_nonce
+    - validate_redirect_uri
+    - validate_requested_realms
+    - validate_realms
+    - validate_verifier
+    - invalidate_request_token
+
+    Methods used to retrieve sensitive information from storage.
+    The following methods must be implemented:
+
+    - get_client_secret
+    - get_request_token_secret
+    - get_access_token_secret
+    - get_rsa_key
+    - get_realms
+    - get_default_realms
+    - get_redirect_uri
+
+    Methods used to save credentials.
+    The following methods must be implemented:
+
+    - save_request_token
+    - save_verifier
+    - save_access_token
+
+    Methods used to verify input parameters. This methods are used during
+    authorizing request token by user (AuthorizationEndpoint), to check if
+    parameters are valid. During token authorization request is not signed,
+    thus 'validation' methods can not be used. The following methods must be
+    implemented:
+
+    - verify_realms
+    - verify_request_token
+
+    To prevent timing attacks it is necessary to not exit early even if the
+    client key or resource owner key is invalid. Instead dummy values should
+    be used during the remaining verification process. It is very important
+    that the dummy client and token are valid input parameters to the methods
+    get_client_secret, get_rsa_key and get_(access/request)_token_secret and
+    that the running time of those methods when given a dummy value remain
+    equivalent to the running time when given a valid client/resource owner.
+    The following properties must be implemented:
+
+    * @dummy_client
+    * @dummy_request_token
+    * @dummy_access_token
+
+    Example implementations have been provided, note that the database used is
+    a simple dictionary and serves only an illustrative purpose. Use whichever
+    database suits your project and how to access it is entirely up to you.
+    The methods are introduced in an order which should make understanding
+    their use more straightforward and as such it could be worth reading what
+    follows in chronological order.
+
+    .. _`whitelisting or blacklisting`: http://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
+    """
+
+    def __init__(self):
+        pass
+
+    @property
+    def allowed_signature_methods(self):
+        return SIGNATURE_METHODS
+
+    @property
+    def safe_characters(self):
+        return set(utils.UNICODE_ASCII_CHARACTER_SET)
+
+    @property
+    def client_key_length(self):
+        return 20, 30
+
+    @property
+    def request_token_length(self):
+        return 20, 30
+
+    @property
+    def access_token_length(self):
+        return 20, 30
+
+    @property
+    def timestamp_lifetime(self):
+        return 600
+
+    @property
+    def nonce_length(self):
+        return 20, 30
+
+    @property
+    def verifier_length(self):
+        return 20, 30
+
+    @property
+    def realms(self):
+        return []
+
+    @property
+    def enforce_ssl(self):
+        return True
+
+    def check_client_key(self, client_key):
+        """Check that the client key only contains safe characters
+        and is no shorter than lower and no longer than upper.
+        """
+        lower, upper = self.client_key_length
+        return (set(client_key) <= self.safe_characters and
+                lower <= len(client_key) <= upper)
+
+    def check_request_token(self, request_token):
+        """Checks that the request token contains only safe characters
+        and is no shorter than lower and no longer than upper.
+        """
+        lower, upper = self.request_token_length
+        return (set(request_token) <= self.safe_characters and
+                lower <= len(request_token) <= upper)
+
+    def check_access_token(self, request_token):
+        """Checks that the token contains only safe characters
+        and is no shorter than lower and no longer than upper.
+        """
+        lower, upper = self.access_token_length
+        return (set(request_token) <= self.safe_characters and
+                lower <= len(request_token) <= upper)
+
+    def check_nonce(self, nonce):
+        """Checks that the nonce only contains only safe characters
+        and is no shorter than lower and no longer than upper.
+        """
+        lower, upper = self.nonce_length
+        return (set(nonce) <= self.safe_characters and
+                lower <= len(nonce) <= upper)
+
+    def check_verifier(self, verifier):
+        """Checks that the verifier contains only safe characters
+        and is no shorter than lower and no longer than upper.
+        """
+        lower, upper = self.verifier_length
+        return (set(verifier) <= self.safe_characters and
+                lower <= len(verifier) <= upper)
+
+    def check_realms(self, realms):
+        """Check that the realm is one of a set allowed realms."""
+        return all((r in self.realms for r in realms))
+
+    @property
+    def dummy_client(self):
+        """Dummy client used when an invalid client key is supplied.
+
+        :returns: The dummy client key string.
+
+        The dummy client should be associated with either a client secret,
+        a rsa key or both depending on which signature methods are supported.
+        Providers should make sure that
+
+        get_client_secret(dummy_client)
+        get_rsa_key(dummy_client)
+
+        return a valid secret or key for the dummy client.
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        * RequestTokenEndpoint
+        * ResourceEndpoint
+        * SignatureOnlyEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    @property
+    def dummy_request_token(self):
+        """Dummy request token used when an invalid token was supplied.
+
+        :returns: The dummy request token string.
+
+        The dummy request token should be associated with a request token
+        secret such that get_request_token_secret(.., dummy_request_token)
+        returns a valid secret.
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    @property
+    def dummy_access_token(self):
+        """Dummy access token used when an invalid token was supplied.
+
+        :returns: The dummy access token string.
+
+        The dummy access token should be associated with an access token
+        secret such that get_access_token_secret(.., dummy_access_token)
+        returns a valid secret.
+
+        This method is used by
+
+        * ResourceEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def get_client_secret(self, client_key, request):
+        """Retrieves the client secret associated with the client key.
+
+        :param client_key: The client/consumer key.
+        :param request: An oauthlib.common.Request object.
+        :returns: The client secret as a string.
+
+        This method must allow the use of a dummy client_key value.
+        Fetching the secret using the dummy key must take the same amount of
+        time as fetching a secret for a valid client::
+
+            # Unlikely to be near constant time as it uses two database
+            # lookups for a valid client, and only one for an invalid.
+            from your_datastore import ClientSecret
+            if ClientSecret.has(client_key):
+                return ClientSecret.get(client_key)
+            else:
+                return 'dummy'
+
+            # Aim to mimic number of latency inducing operations no matter
+            # whether the client is valid or not.
+            from your_datastore import ClientSecret
+            return ClientSecret.get(client_key, 'dummy')
+
+        Note that the returned key must be in plaintext.
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        * RequestTokenEndpoint
+        * ResourceEndpoint
+        * SignatureOnlyEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def get_request_token_secret(self, client_key, token, request):
+        """Retrieves the shared secret associated with the request token.
+
+        :param client_key: The client/consumer key.
+        :param token: The request token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: The token secret as a string.
+
+        This method must allow the use of a dummy values and the running time
+        must be roughly equivalent to that of the running time of valid values::
+
+            # Unlikely to be near constant time as it uses two database
+            # lookups for a valid client, and only one for an invalid.
+            from your_datastore import RequestTokenSecret
+            if RequestTokenSecret.has(client_key):
+                return RequestTokenSecret.get((client_key, request_token))
+            else:
+                return 'dummy'
+
+            # Aim to mimic number of latency inducing operations no matter
+            # whether the client is valid or not.
+            from your_datastore import RequestTokenSecret
+            return ClientSecret.get((client_key, request_token), 'dummy')
+
+        Note that the returned key must be in plaintext.
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def get_access_token_secret(self, client_key, token, request):
+        """Retrieves the shared secret associated with the access token.
+
+        :param client_key: The client/consumer key.
+        :param token: The access token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: The token secret as a string.
+
+        This method must allow the use of a dummy values and the running time
+        must be roughly equivalent to that of the running time of valid values::
+
+            # Unlikely to be near constant time as it uses two database
+            # lookups for a valid client, and only one for an invalid.
+            from your_datastore import AccessTokenSecret
+            if AccessTokenSecret.has(client_key):
+                return AccessTokenSecret.get((client_key, request_token))
+            else:
+                return 'dummy'
+
+            # Aim to mimic number of latency inducing operations no matter
+            # whether the client is valid or not.
+            from your_datastore import AccessTokenSecret
+            return ClientSecret.get((client_key, request_token), 'dummy')
+
+        Note that the returned key must be in plaintext.
+
+        This method is used by
+
+        * ResourceEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def get_default_realms(self, client_key, request):
+        """Get the default realms for a client.
+
+        :param client_key: The client/consumer key.
+        :param request: An oauthlib.common.Request object.
+        :returns: The list of default realms associated with the client.
+
+        The list of default realms will be set during client registration and
+        is outside the scope of OAuthLib.
+
+        This method is used by
+
+        * RequestTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def get_realms(self, token, request):
+        """Get realms associated with a request token.
+
+        :param token: The request token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: The list of realms associated with the request token.
+
+        This method is used by
+
+        * AuthorizationEndpoint
+        * AccessTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def get_redirect_uri(self, token, request):
+        """Get the redirect URI associated with a request token.
+
+        :param token: The request token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: The redirect URI associated with the request token.
+
+        It may be desirable to return a custom URI if the redirect is set to "oob".
+        In this case, the user will be redirected to the returned URI and at that
+        endpoint the verifier can be displayed.
+
+        This method is used by
+
+        * AuthorizationEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def get_rsa_key(self, client_key, request):
+        """Retrieves a previously stored client provided RSA key.
+
+        :param client_key: The client/consumer key.
+        :param request: An oauthlib.common.Request object.
+        :returns: The rsa public key as a string.
+
+        This method must allow the use of a dummy client_key value. Fetching
+        the rsa key using the dummy key must take the same amount of time
+        as fetching a key for a valid client. The dummy key must also be of
+        the same bit length as client keys.
+
+        Note that the key must be returned in plaintext.
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        * RequestTokenEndpoint
+        * ResourceEndpoint
+        * SignatureOnlyEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def invalidate_request_token(self, client_key, request_token, request):
+        """Invalidates a used request token.
+
+        :param client_key: The client/consumer key.
+        :param request_token: The request token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: The rsa public key as a string.
+
+        Per `Section 2.3`__ of the spec:
+
+        "The server MUST (...) ensure that the temporary
+        credentials have not expired or been used before."
+
+        .. _`Section 2.3`: http://tools.ietf.org/html/rfc5849#section-2.3
+
+        This method should ensure that provided token won't validate anymore.
+        It can be simply removing RequestToken from storage or setting
+        specific flag that makes it invalid (note that such flag should be
+        also validated during request token validation).
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_client_key(self, client_key, request):
+        """Validates that supplied client key is a registered and valid client.
+
+        :param client_key: The client/consumer key.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        Note that if the dummy client is supplied it should validate in same
+        or nearly the same amount of time as a valid one.
+
+        Ensure latency inducing tasks are mimiced even for dummy clients.
+        For example, use::
+
+            from your_datastore import Client
+            try:
+                return Client.exists(client_key, access_token)
+            except DoesNotExist:
+                return False
+
+        Rather than::
+
+            from your_datastore import Client
+            if access_token == self.dummy_access_token:
+                return False
+            else:
+                return Client.exists(client_key, access_token)
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        * RequestTokenEndpoint
+        * ResourceEndpoint
+        * SignatureOnlyEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_request_token(self, client_key, token, request):
+        """Validates that supplied request token is registered and valid.
+
+        :param client_key: The client/consumer key.
+        :param token: The request token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        Note that if the dummy request_token is supplied it should validate in
+        the same nearly the same amount of time as a valid one.
+
+        Ensure latency inducing tasks are mimiced even for dummy clients.
+        For example, use::
+
+            from your_datastore import RequestToken
+            try:
+                return RequestToken.exists(client_key, access_token)
+            except DoesNotExist:
+                return False
+
+        Rather than::
+
+            from your_datastore import RequestToken
+            if access_token == self.dummy_access_token:
+                return False
+            else:
+                return RequestToken.exists(client_key, access_token)
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_access_token(self, client_key, token, request):
+        """Validates that supplied access token is registered and valid.
+
+        :param client_key: The client/consumer key.
+        :param token: The access token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        Note that if the dummy access token is supplied it should validate in
+        the same or nearly the same amount of time as a valid one.
+
+        Ensure latency inducing tasks are mimiced even for dummy clients.
+        For example, use::
+
+            from your_datastore import AccessToken
+            try:
+                return AccessToken.exists(client_key, access_token)
+            except DoesNotExist:
+                return False
+
+        Rather than::
+
+            from your_datastore import AccessToken
+            if access_token == self.dummy_access_token:
+                return False
+            else:
+                return AccessToken.exists(client_key, access_token)
+
+        This method is used by
+
+        * ResourceEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
+                                     request, request_token=None, access_token=None):
+        """Validates that the nonce has not been used before.
+
+        :param client_key: The client/consumer key.
+        :param timestamp: The ``oauth_timestamp`` parameter.
+        :param nonce: The ``oauth_nonce`` parameter.
+        :param request_token: Request token string, if any.
+        :param access_token: Access token string, if any.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        Per `Section 3.3`_ of the spec.
+
+        "A nonce is a random string, uniquely generated by the client to allow
+        the server to verify that a request has never been made before and
+        helps prevent replay attacks when requests are made over a non-secure
+        channel.  The nonce value MUST be unique across all requests with the
+        same timestamp, client credentials, and token combinations."
+
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
+
+        One of the first validation checks that will be made is for the validity
+        of the nonce and timestamp, which are associated with a client key and
+        possibly a token. If invalid then immediately fail the request
+        by returning False. If the nonce/timestamp pair has been used before and
+        you may just have detected a replay attack. Therefore it is an essential
+        part of OAuth security that you not allow nonce/timestamp reuse.
+        Note that this validation check is done before checking the validity of
+        the client and token.::
+
+           nonces_and_timestamps_database = [
+              (u'foo', 1234567890, u'rannoMstrInghere', u'bar')
+           ]
+
+           def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
+              request_token=None, access_token=None):
+
+              return ((client_key, timestamp, nonce, request_token or access_token)
+                       not in self.nonces_and_timestamps_database)
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        * RequestTokenEndpoint
+        * ResourceEndpoint
+        * SignatureOnlyEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_redirect_uri(self, client_key, redirect_uri, request):
+        """Validates the client supplied redirection URI.
+
+        :param client_key: The client/consumer key.
+        :param redirect_uri: The URI the client which to redirect back to after
+                             authorization is successful.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        It is highly recommended that OAuth providers require their clients
+        to register all redirection URIs prior to using them in requests and
+        register them as absolute URIs. See `CWE-601`_ for more information
+        about open redirection attacks.
+
+        By requiring registration of all redirection URIs it should be
+        straightforward for the provider to verify whether the supplied
+        redirect_uri is valid or not.
+
+        Alternatively per `Section 2.1`_ of the spec:
+
+        "If the client is unable to receive callbacks or a callback URI has
+        been established via other means, the parameter value MUST be set to
+        "oob" (case sensitive), to indicate an out-of-band configuration."
+
+        .. _`CWE-601`: http://cwe.mitre.org/top25/index.html#CWE-601
+        .. _`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
+
+        This method is used by
+
+        * RequestTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_requested_realms(self, client_key, realms, request):
+        """Validates that the client may request access to the realm.
+
+        :param client_key: The client/consumer key.
+        :param realms: The list of realms that client is requesting access to.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        This method is invoked when obtaining a request token and should
+        tie a realm to the request token and after user authorization
+        this realm restriction should transfer to the access token.
+
+        This method is used by
+
+        * RequestTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_realms(self, client_key, token, request, uri=None,
+                        realms=None):
+        """Validates access to the request realm.
+
+        :param client_key: The client/consumer key.
+        :param token: A request token string.
+        :param request: An oauthlib.common.Request object.
+        :param uri: The URI the realms is protecting.
+        :param realms: A list of realms that must have been granted to
+                       the access token.
+        :returns: True or False
+
+        How providers choose to use the realm parameter is outside the OAuth
+        specification but it is commonly used to restrict access to a subset
+        of protected resources such as "photos".
+
+        realms is a convenience parameter which can be used to provide
+        a per view method pre-defined list of allowed realms.
+
+        Can be as simple as::
+
+            from your_datastore import RequestToken
+            request_token = RequestToken.get(token, None)
+
+            if not request_token:
+                return False
+            return set(request_token.realms).issuperset(set(realms))
+
+        This method is used by
+
+        * ResourceEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def validate_verifier(self, client_key, token, verifier, request):
+        """Validates a verification code.
+
+        :param client_key: The client/consumer key.
+        :param token: A request token string.
+        :param verifier: The authorization verifier string.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        OAuth providers issue a verification code to clients after the
+        resource owner authorizes access. This code is used by the client to
+        obtain token credentials and the provider must verify that the
+        verifier is valid and associated with the client as well as the
+        resource owner.
+
+        Verifier validation should be done in near constant time
+        (to avoid verifier enumeration). To achieve this we need a
+        constant time string comparison which is provided by OAuthLib
+        in ``oauthlib.common.safe_string_equals``::
+
+            from your_datastore import Verifier
+            correct_verifier = Verifier.get(client_key, request_token)
+            from oauthlib.common import safe_string_equals
+            return safe_string_equals(verifier, correct_verifier)
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def verify_request_token(self, token, request):
+        """Verify that the given OAuth1 request token is valid.
+
+        :param token: A request token string.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        This method is used only in AuthorizationEndpoint to check whether the
+        oauth_token given in the authorization URL is valid or not.
+        This request is not signed and thus similar ``validate_request_token``
+        method can not be used.
+
+        This method is used by
+
+        * AuthorizationEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def verify_realms(self, token, realms, request):
+        """Verify authorized realms to see if they match those given to token.
+
+        :param token: An access token string.
+        :param realms: A list of realms the client attempts to access.
+        :param request: An oauthlib.common.Request object.
+        :returns: True or False
+
+        This prevents the list of authorized realms sent by the client during
+        the authorization step to be altered to include realms outside what
+        was bound with the request token.
+
+        Can be as simple as::
+
+            valid_realms = self.get_realms(token)
+            return all((r in valid_realms for r in realms))
+
+        This method is used by
+
+        * AuthorizationEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def save_access_token(self, token, request):
+        """Save an OAuth1 access token.
+
+        :param token: A dict with token credentials.
+        :param request: An oauthlib.common.Request object.
+
+        The token dictionary will at minimum include
+
+        * ``oauth_token`` the access token string.
+        * ``oauth_token_secret`` the token specific secret used in signing.
+        * ``oauth_authorized_realms`` a space separated list of realms.
+
+        Client key can be obtained from ``request.client_key``.
+
+        The list of realms (not joined string) can be obtained from
+        ``request.realm``.
+
+        This method is used by
+
+        * AccessTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def save_request_token(self, token, request):
+        """Save an OAuth1 request token.
+
+        :param token: A dict with token credentials.
+        :param request: An oauthlib.common.Request object.
+
+        The token dictionary will at minimum include
+
+        * ``oauth_token`` the request token string.
+        * ``oauth_token_secret`` the token specific secret used in signing.
+        * ``oauth_callback_confirmed`` the string ``true``.
+
+        Client key can be obtained from ``request.client_key``.
+
+        This method is used by
+
+        * RequestTokenEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
+
+    def save_verifier(self, token, verifier, request):
+        """Associate an authorization verifier with a request token.
+
+        :param token: A request token string.
+        :param verifier A dictionary containing the oauth_verifier and
+                        oauth_token
+        :param request: An oauthlib.common.Request object.
+
+        We need to associate verifiers with tokens for validation during the
+        access token request.
+
+        Note that unlike save_x_token token here is the ``oauth_token`` token
+        string from the request token saved previously.
+
+        This method is used by
+
+        * AuthorizationEndpoint
+        """
+        raise NotImplementedError("Subclasses must implement this function.")
diff --git a/oauthlib/oauth1/rfc5849/signature.py b/oauthlib/oauth1/rfc5849/signature.py
new file mode 100644 (file)
index 0000000..f57d80a
--- /dev/null
@@ -0,0 +1,609 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth1.rfc5849.signature
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module represents a direct implementation of `section 3.4`_ of the spec.
+
+Terminology:
+ * Client: software interfacing with an OAuth API
+ * Server: the API provider
+ * Resource Owner: the user who is granting authorization to the client
+
+Steps for signing a request:
+
+1. Collect parameters from the uri query, auth header, & body
+2. Normalize those parameters
+3. Normalize the uri
+4. Pass the normalized uri, normalized parameters, and http method to
+   construct the base string
+5. Pass the base string and any keys needed to a signing function
+
+.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+"""
+from __future__ import absolute_import, unicode_literals
+
+import binascii
+import hashlib
+import hmac
+try:
+    import urlparse
+except ImportError:
+    import urllib.parse as urlparse
+from . import utils
+from oauthlib.common import urldecode, extract_params, safe_string_equals
+from oauthlib.common import bytes_type, unicode_type
+
+
+def construct_base_string(http_method, base_string_uri,
+                          normalized_encoded_request_parameters):
+    """**String Construction**
+    Per `section 3.4.1.1`_ of the spec.
+
+    For example, the HTTP request::
+
+        POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
+        Host: example.com
+        Content-Type: application/x-www-form-urlencoded
+        Authorization: OAuth realm="Example",
+            oauth_consumer_key="9djdj82h48djs9d2",
+            oauth_token="kkk9d7dh3k39sjv7",
+            oauth_signature_method="HMAC-SHA1",
+            oauth_timestamp="137131201",
+            oauth_nonce="7d8f3e4a",
+            oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
+
+        c2&a3=2+q
+
+    is represented by the following signature base string (line breaks
+    are for display purposes only)::
+
+        POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
+        %26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
+        key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
+        ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
+        9d7dh3k39sjv7
+
+    .. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+    """
+
+    # The signature base string is constructed by concatenating together,
+    # in order, the following HTTP request elements:
+
+    # 1.  The HTTP request method in uppercase.  For example: "HEAD",
+    #     "GET", "POST", etc.  If the request uses a custom HTTP method, it
+    #     MUST be encoded (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+    base_string = utils.escape(http_method.upper())
+
+    # 2.  An "&" character (ASCII code 38).
+    base_string += '&'
+
+    # 3.  The base string URI from `Section 3.4.1.2`_, after being encoded
+    #     (`Section 3.6`_).
+    #
+    # .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+    # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+    base_string += utils.escape(base_string_uri)
+
+    # 4.  An "&" character (ASCII code 38).
+    base_string += '&'
+
+    # 5.  The request parameters as normalized in `Section 3.4.1.3.2`_, after
+    #     being encoded (`Section 3.6`).
+    #
+    # .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+    # .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
+    base_string += utils.escape(normalized_encoded_request_parameters)
+
+    return base_string
+
+
+def normalize_base_string_uri(uri, host=None):
+    """**Base String URI**
+    Per `section 3.4.1.2`_ of the spec.
+
+    For example, the HTTP request::
+
+        GET /r%20v/X?id=123 HTTP/1.1
+        Host: EXAMPLE.COM:80
+
+    is represented by the base string URI: "http://example.com/r%20v/X".
+
+    In another example, the HTTPS request::
+
+        GET /?q=1 HTTP/1.1
+        Host: www.example.net:8080
+
+    is represented by the base string URI: "https://www.example.net:8080/".
+
+    .. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
+
+    The host argument overrides the netloc part of the uri argument.
+    """
+    if not isinstance(uri, unicode_type):
+        raise ValueError('uri must be a unicode object.')
+
+    # FIXME: urlparse does not support unicode
+    scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
+
+    # The scheme, authority, and path of the request resource URI `RFC3986`
+    # are included by constructing an "http" or "https" URI representing
+    # the request resource (without the query or fragment) as follows:
+    #
+    # .. _`RFC3986`: http://tools.ietf.org/html/rfc3986
+
+    if not scheme or not netloc:
+        raise ValueError('uri must include a scheme and netloc')
+
+    # Per `RFC 2616 section 5.1.2`_:
+    #
+    # Note that the absolute path cannot be empty; if none is present in
+    # the original URI, it MUST be given as "/" (the server root).
+    #
+    # .. _`RFC 2616 section 5.1.2`: http://tools.ietf.org/html/rfc2616#section-5.1.2
+    if not path:
+        path = '/'
+
+    # 1.  The scheme and host MUST be in lowercase.
+    scheme = scheme.lower()
+    netloc = netloc.lower()
+
+    # 2.  The host and port values MUST match the content of the HTTP
+    #     request "Host" header field.
+    if host is not None:
+        netloc = host.lower()
+
+    # 3.  The port MUST be included if it is not the default port for the
+    #     scheme, and MUST be excluded if it is the default.  Specifically,
+    #     the port MUST be excluded when making an HTTP request `RFC2616`_
+    #     to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
+    #     All other non-default port numbers MUST be included.
+    #
+    # .. _`RFC2616`: http://tools.ietf.org/html/rfc2616
+    # .. _`RFC2818`: http://tools.ietf.org/html/rfc2818
+    default_ports = (
+        ('http', '80'),
+        ('https', '443'),
+    )
+    if ':' in netloc:
+        host, port = netloc.split(':', 1)
+        if (scheme, port) in default_ports:
+            netloc = host
+
+    return urlparse.urlunparse((scheme, netloc, path, params, '', ''))
+
+
+# ** Request Parameters **
+#
+#    Per `section 3.4.1.3`_ of the spec.
+#
+#    In order to guarantee a consistent and reproducible representation of
+#    the request parameters, the parameters are collected and decoded to
+#    their original decoded form.  They are then sorted and encoded in a
+#    particular manner that is often different from their original
+#    encoding scheme, and concatenated into a single string.
+#
+# .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+def collect_parameters(uri_query='', body=[], headers=None,
+                       exclude_oauth_signature=True, with_realm=False):
+    """**Parameter Sources**
+
+    Parameters starting with `oauth_` will be unescaped.
+
+    Body parameters must be supplied as a dict, a list of 2-tuples, or a
+    formencoded query string.
+
+    Headers must be supplied as a dict.
+
+    Per `section 3.4.1.3.1`_ of the spec.
+
+    For example, the HTTP request::
+
+        POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
+        Host: example.com
+        Content-Type: application/x-www-form-urlencoded
+        Authorization: OAuth realm="Example",
+            oauth_consumer_key="9djdj82h48djs9d2",
+            oauth_token="kkk9d7dh3k39sjv7",
+            oauth_signature_method="HMAC-SHA1",
+            oauth_timestamp="137131201",
+            oauth_nonce="7d8f3e4a",
+            oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"
+
+        c2&a3=2+q
+
+    contains the following (fully decoded) parameters used in the
+    signature base sting::
+
+        +------------------------+------------------+
+        |          Name          |       Value      |
+        +------------------------+------------------+
+        |           b5           |       =%3D       |
+        |           a3           |         a        |
+        |           c@           |                  |
+        |           a2           |        r b       |
+        |   oauth_consumer_key   | 9djdj82h48djs9d2 |
+        |       oauth_token      | kkk9d7dh3k39sjv7 |
+        | oauth_signature_method |     HMAC-SHA1    |
+        |     oauth_timestamp    |     137131201    |
+        |       oauth_nonce      |     7d8f3e4a     |
+        |           c2           |                  |
+        |           a3           |        2 q       |
+        +------------------------+------------------+
+
+    Note that the value of "b5" is "=%3D" and not "==".  Both "c@" and
+    "c2" have empty values.  While the encoding rules specified in this
+    specification for the purpose of constructing the signature base
+    string exclude the use of a "+" character (ASCII code 43) to
+    represent an encoded space character (ASCII code 32), this practice
+    is widely used in "application/x-www-form-urlencoded" encoded values,
+    and MUST be properly decoded, as demonstrated by one of the "a3"
+    parameter instances (the "a3" parameter is used twice in this
+    request).
+
+    .. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
+    """
+    headers = headers or {}
+    params = []
+
+    # The parameters from the following sources are collected into a single
+    # list of name/value pairs:
+
+    # *  The query component of the HTTP request URI as defined by
+    #    `RFC3986, Section 3.4`_.  The query component is parsed into a list
+    #    of name/value pairs by treating it as an
+    #    "application/x-www-form-urlencoded" string, separating the names
+    #    and values and decoding them as defined by
+    #    `W3C.REC-html40-19980424`_, Section 17.13.4.
+    #
+    # .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4
+    # .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+    if uri_query:
+        params.extend(urldecode(uri_query))
+
+    # *  The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if
+    #    present.  The header's content is parsed into a list of name/value
+    #    pairs excluding the "realm" parameter if present.  The parameter
+    #    values are decoded as defined by `Section 3.5.1`_.
+    #
+    # .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
+    if headers:
+        headers_lower = dict((k.lower(), v) for k, v in headers.items())
+        authorization_header = headers_lower.get('authorization')
+        if authorization_header is not None:
+            params.extend([i for i in utils.parse_authorization_header(
+                authorization_header) if with_realm or i[0] != 'realm'])
+
+    # *  The HTTP request entity-body, but only if all of the following
+    #    conditions are met:
+    #     *  The entity-body is single-part.
+    #
+    #     *  The entity-body follows the encoding requirements of the
+    #        "application/x-www-form-urlencoded" content-type as defined by
+    #        `W3C.REC-html40-19980424`_.
+
+    #     *  The HTTP request entity-header includes the "Content-Type"
+    #        header field set to "application/x-www-form-urlencoded".
+    #
+    # .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
+
+    # TODO: enforce header param inclusion conditions
+    bodyparams = extract_params(body) or []
+    params.extend(bodyparams)
+
+    # ensure all oauth params are unescaped
+    unescaped_params = []
+    for k, v in params:
+        if k.startswith('oauth_'):
+            v = utils.unescape(v)
+        unescaped_params.append((k, v))
+
+    # The "oauth_signature" parameter MUST be excluded from the signature
+    # base string if present.
+    if exclude_oauth_signature:
+        unescaped_params = list(filter(lambda i: i[0] != 'oauth_signature',
+                                       unescaped_params))
+
+    return unescaped_params
+
+
+def normalize_parameters(params):
+    """**Parameters Normalization**
+    Per `section 3.4.1.3.2`_ of the spec.
+
+    For example, the list of parameters from the previous section would
+    be normalized as follows:
+
+    Encoded::
+
+    +------------------------+------------------+
+    |          Name          |       Value      |
+    +------------------------+------------------+
+    |           b5           |     %3D%253D     |
+    |           a3           |         a        |
+    |          c%40          |                  |
+    |           a2           |       r%20b      |
+    |   oauth_consumer_key   | 9djdj82h48djs9d2 |
+    |       oauth_token      | kkk9d7dh3k39sjv7 |
+    | oauth_signature_method |     HMAC-SHA1    |
+    |     oauth_timestamp    |     137131201    |
+    |       oauth_nonce      |     7d8f3e4a     |
+    |           c2           |                  |
+    |           a3           |       2%20q      |
+    +------------------------+------------------+
+
+    Sorted::
+
+    +------------------------+------------------+
+    |          Name          |       Value      |
+    +------------------------+------------------+
+    |           a2           |       r%20b      |
+    |           a3           |       2%20q      |
+    |           a3           |         a        |
+    |           b5           |     %3D%253D     |
+    |          c%40          |                  |
+    |           c2           |                  |
+    |   oauth_consumer_key   | 9djdj82h48djs9d2 |
+    |       oauth_nonce      |     7d8f3e4a     |
+    | oauth_signature_method |     HMAC-SHA1    |
+    |     oauth_timestamp    |     137131201    |
+    |       oauth_token      | kkk9d7dh3k39sjv7 |
+    +------------------------+------------------+
+
+    Concatenated Pairs::
+
+    +-------------------------------------+
+    |              Name=Value             |
+    +-------------------------------------+
+    |               a2=r%20b              |
+    |               a3=2%20q              |
+    |                 a3=a                |
+    |             b5=%3D%253D             |
+    |                c%40=                |
+    |                 c2=                 |
+    | oauth_consumer_key=9djdj82h48djs9d2 |
+    |         oauth_nonce=7d8f3e4a        |
+    |   oauth_signature_method=HMAC-SHA1  |
+    |      oauth_timestamp=137131201      |
+    |     oauth_token=kkk9d7dh3k39sjv7    |
+    +-------------------------------------+
+
+    and concatenated together into a single string (line breaks are for
+    display purposes only)::
+
+        a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj
+        dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
+        &oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
+
+    .. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
+    """
+
+    # The parameters collected in `Section 3.4.1.3`_ are normalized into a
+    # single string as follows:
+    #
+    # .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
+
+    # 1.  First, the name and value of each parameter are encoded
+    #     (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+    key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
+
+    # 2.  The parameters are sorted by name, using ascending byte value
+    #     ordering.  If two or more parameters share the same name, they
+    #     are sorted by their value.
+    key_values.sort()
+
+    # 3.  The name of each parameter is concatenated to its corresponding
+    #     value using an "=" character (ASCII code 61) as a separator, even
+    #     if the value is empty.
+    parameter_parts = ['{0}={1}'.format(k, v) for k, v in key_values]
+
+    # 4.  The sorted name/value pairs are concatenated together into a
+    #     single string by using an "&" character (ASCII code 38) as
+    #     separator.
+    return '&'.join(parameter_parts)
+
+
+def sign_hmac_sha1_with_client(base_string, client):
+    return sign_hmac_sha1(base_string,
+                          client.client_secret,
+                          client.resource_owner_secret
+                          )
+
+
+def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
+    """**HMAC-SHA1**
+
+    The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
+    algorithm as defined in `RFC2104`_::
+
+        digest = HMAC-SHA1 (key, text)
+
+    Per `section 3.4.2`_ of the spec.
+
+    .. _`RFC2104`: http://tools.ietf.org/html/rfc2104
+    .. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2
+    """
+
+    # The HMAC-SHA1 function variables are used in following way:
+
+    # text is set to the value of the signature base string from
+    # `Section 3.4.1.1`_.
+    #
+    # .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
+    text = base_string
+
+    # key is set to the concatenated values of:
+    # 1.  The client shared-secret, after being encoded (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+    key = utils.escape(client_secret or '')
+
+    # 2.  An "&" character (ASCII code 38), which MUST be included
+    #     even when either secret is empty.
+    key += '&'
+
+    # 3.  The token shared-secret, after being encoded (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+    key += utils.escape(resource_owner_secret or '')
+
+    # FIXME: HMAC does not support unicode!
+    key_utf8 = key.encode('utf-8')
+    text_utf8 = text.encode('utf-8')
+    signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
+
+    # digest  is used to set the value of the "oauth_signature" protocol
+    #         parameter, after the result octet string is base64-encoded
+    #         per `RFC2045, Section 6.8`.
+    #
+    # .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8
+    return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
+
+_jwtrs1 = None
+
+#jwt has some nice pycrypto/cryptography abstractions
+def _jwt_rs1_signing_algorithm():
+    global _jwtrs1
+    if _jwtrs1 is None:
+        import jwt.algorithms as jwtalgo
+        _jwtrs1 = jwtalgo.RSAAlgorithm(jwtalgo.hashes.SHA1)
+    return _jwtrs1
+
+def sign_rsa_sha1(base_string, rsa_private_key):
+    """**RSA-SHA1**
+
+    Per `section 3.4.3`_ of the spec.
+
+    The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
+    algorithm as defined in `RFC3447, Section 8.2`_ (also known as
+    PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5.  To
+    use this method, the client MUST have established client credentials
+    with the server that included its RSA public key (in a manner that is
+    beyond the scope of this specification).
+
+    .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
+    .. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2
+
+    """
+    if isinstance(base_string, unicode_type):
+        base_string = base_string.encode('utf-8')
+    # TODO: finish RSA documentation
+    alg = _jwt_rs1_signing_algorithm()
+    key = _prepare_key_plus(alg, rsa_private_key)
+    s=alg.sign(base_string, key)
+    return binascii.b2a_base64(s)[:-1].decode('utf-8')
+
+
+def sign_rsa_sha1_with_client(base_string, client):
+    return sign_rsa_sha1(base_string, client.rsa_key)
+
+
+def sign_plaintext(client_secret, resource_owner_secret):
+    """Sign a request using plaintext.
+
+    Per `section 3.4.4`_ of the spec.
+
+    The "PLAINTEXT" method does not employ a signature algorithm.  It
+    MUST be used with a transport-layer mechanism such as TLS or SSL (or
+    sent over a secure channel with equivalent protections).  It does not
+    utilize the signature base string or the "oauth_timestamp" and
+    "oauth_nonce" parameters.
+
+    .. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4
+
+    """
+
+    # The "oauth_signature" protocol parameter is set to the concatenated
+    # value of:
+
+    # 1.  The client shared-secret, after being encoded (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+    signature = utils.escape(client_secret or '')
+
+    # 2.  An "&" character (ASCII code 38), which MUST be included even
+    #     when either secret is empty.
+    signature += '&'
+
+    # 3.  The token shared-secret, after being encoded (`Section 3.6`_).
+    #
+    # .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+    signature += utils.escape(resource_owner_secret or '')
+
+    return signature
+
+
+def sign_plaintext_with_client(base_string, client):
+    return sign_plaintext(client.client_secret, client.resource_owner_secret)
+
+
+def verify_hmac_sha1(request, client_secret=None,
+                     resource_owner_secret=None):
+    """Verify a HMAC-SHA1 signature.
+
+    Per `section 3.4`_ of the spec.
+
+    .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+
+    To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+    attribute MUST be an absolute URI whose netloc part identifies the
+    origin server or gateway on which the resource resides. Any Host
+    item of the request argument's headers dict attribute will be
+    ignored.
+
+    .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
+
+    """
+    norm_params = normalize_parameters(request.params)
+    uri = normalize_base_string_uri(request.uri)
+    base_string = construct_base_string(request.http_method, uri, norm_params)
+    signature = sign_hmac_sha1(base_string, client_secret,
+                               resource_owner_secret)
+    return safe_string_equals(signature, request.signature)
+
+def _prepare_key_plus(alg, keystr):
+    if isinstance(keystr, bytes_type):
+        keystr = keystr.decode('utf-8')
+    return alg.prepare_key(keystr)
+
+def verify_rsa_sha1(request, rsa_public_key):
+    """Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.
+
+    Per `section 3.4.3`_ of the spec.
+
+    Note this method requires the jwt and cryptography libraries.
+
+    .. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
+
+    To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
+    attribute MUST be an absolute URI whose netloc part identifies the
+    origin server or gateway on which the resource resides. Any Host
+    item of the request argument's headers dict attribute will be
+    ignored.
+
+    .. _`RFC2616 section 5.2`: http://tools.ietf.org/html/rfc2616#section-5.2
+    """
+    norm_params = normalize_parameters(request.params)
+    uri = normalize_base_string_uri(request.uri)
+    message = construct_base_string(request.http_method, uri, norm_params).encode('utf-8')
+    sig = binascii.a2b_base64(request.signature.encode('utf-8'))
+
+    alg = _jwt_rs1_signing_algorithm()
+    key = _prepare_key_plus(alg, rsa_public_key)
+    return alg.verify(message, key, sig)
+
+
+def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
+    """Verify a PLAINTEXT signature.
+
+    Per `section 3.4`_ of the spec.
+
+    .. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
+    """
+    signature = sign_plaintext(client_secret, resource_owner_secret)
+    return safe_string_equals(signature, request.signature)
diff --git a/oauthlib/oauth1/rfc5849/utils.py b/oauthlib/oauth1/rfc5849/utils.py
new file mode 100644 (file)
index 0000000..eafb303
--- /dev/null
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth
+spec.
+"""
+from __future__ import absolute_import, unicode_literals
+
+try:
+    import urllib2
+except ImportError:
+    import urllib.request as urllib2
+
+from oauthlib.common import quote, unquote, bytes_type, unicode_type
+
+UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
+                               'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+                               '0123456789')
+
+
+def filter_params(target):
+    """Decorator which filters params to remove non-oauth_* parameters
+
+    Assumes the decorated method takes a params dict or list of tuples as its
+    first argument.
+    """
+    def wrapper(params, *args, **kwargs):
+        params = filter_oauth_params(params)
+        return target(params, *args, **kwargs)
+
+    wrapper.__doc__ = target.__doc__
+    return wrapper
+
+
+def filter_oauth_params(params):
+    """Removes all non oauth parameters from a dict or a list of params."""
+    is_oauth = lambda kv: kv[0].startswith("oauth_")
+    if isinstance(params, dict):
+        return list(filter(is_oauth, list(params.items())))
+    else:
+        return list(filter(is_oauth, params))
+
+
+def escape(u):
+    """Escape a unicode string in an OAuth-compatible fashion.
+
+    Per `section 3.6`_ of the spec.
+
+    .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
+
+    """
+    if not isinstance(u, unicode_type):
+        raise ValueError('Only unicode objects are escapable. ' +
+                         'Got %s of type %s.' % (u, type(u)))
+    # Letters, digits, and the characters '_.-' are already treated as safe
+    # by urllib.quote(). We need to add '~' to fully support rfc5849.
+    return quote(u, safe=b'~')
+
+
+def unescape(u):
+    if not isinstance(u, unicode_type):
+        raise ValueError('Only unicode objects are unescapable.')
+    return unquote(u)
+
+
+def parse_keqv_list(l):
+    """A unicode-safe version of urllib2.parse_keqv_list"""
+    # With Python 2.6, parse_http_list handles unicode fine
+    return urllib2.parse_keqv_list(l)
+
+
+def parse_http_list(u):
+    """A unicode-safe version of urllib2.parse_http_list"""
+    # With Python 2.6, parse_http_list handles unicode fine
+    return urllib2.parse_http_list(u)
+
+
+def parse_authorization_header(authorization_header):
+    """Parse an OAuth authorization header into a list of 2-tuples"""
+    auth_scheme = 'OAuth '.lower()
+    if authorization_header[:len(auth_scheme)].lower().startswith(auth_scheme):
+        items = parse_http_list(authorization_header[len(auth_scheme):])
+        try:
+            return list(parse_keqv_list(items).items())
+        except (IndexError, ValueError):
+            pass
+    raise ValueError('Malformed authorization header')
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py
new file mode 100644 (file)
index 0000000..a13e484
--- /dev/null
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2
+~~~~~~~~~~~~~~
+
+This module is a wrapper for the most recent implementation of OAuth 2.0 Client
+and Server classes.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .rfc6749.clients import Client
+from .rfc6749.clients import WebApplicationClient
+from .rfc6749.clients import MobileApplicationClient
+from .rfc6749.clients import LegacyApplicationClient
+from .rfc6749.clients import BackendApplicationClient
+from .rfc6749.clients import ServiceApplicationClient
+from .rfc6749.endpoints import AuthorizationEndpoint
+from .rfc6749.endpoints import TokenEndpoint
+from .rfc6749.endpoints import ResourceEndpoint
+from .rfc6749.endpoints import RevocationEndpoint
+from .rfc6749.endpoints import Server
+from .rfc6749.endpoints import WebApplicationServer
+from .rfc6749.endpoints import MobileApplicationServer
+from .rfc6749.endpoints import LegacyApplicationServer
+from .rfc6749.endpoints import BackendApplicationServer
+from .rfc6749.errors import *
+from .rfc6749.grant_types import AuthorizationCodeGrant
+from .rfc6749.grant_types import ImplicitGrant
+from .rfc6749.grant_types import ResourceOwnerPasswordCredentialsGrant
+from .rfc6749.grant_types import ClientCredentialsGrant
+from .rfc6749.grant_types import RefreshTokenGrant
+from .rfc6749.request_validator import RequestValidator
+from .rfc6749.tokens import BearerToken, OAuth2Token
+from .rfc6749.utils import is_secure_transport
diff --git a/oauthlib/oauth2/rfc6749/__init__.py b/oauthlib/oauth2/rfc6749/__init__.py
new file mode 100644 (file)
index 0000000..aff0ed8
--- /dev/null
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import functools
+import logging
+
+from .errors import TemporarilyUnavailableError, ServerError
+from .errors import FatalClientError, OAuth2Error
+
+
+log = logging.getLogger(__name__)
+
+
+class BaseEndpoint(object):
+
+    def __init__(self):
+        self._available = True
+        self._catch_errors = False
+
+    @property
+    def available(self):
+        return self._available
+
+    @available.setter
+    def available(self, available):
+        self._available = available
+
+    @property
+    def catch_errors(self):
+        return self._catch_errors
+
+    @catch_errors.setter
+    def catch_errors(self, catch_errors):
+        self._catch_errors = catch_errors
+
+
+def catch_errors_and_unavailability(f):
+    @functools.wraps(f)
+    def wrapper(endpoint, uri, *args, **kwargs):
+        if not endpoint.available:
+            e = TemporarilyUnavailableError()
+            log.info('Endpoint unavailable, ignoring request %s.' % uri)
+            return {}, e.json, 503
+
+        if endpoint.catch_errors:
+            try:
+                return f(endpoint, uri, *args, **kwargs)
+            except OAuth2Error:
+                raise
+            except FatalClientError:
+                raise
+            except Exception as e:
+                error = ServerError()
+                log.warning(
+                    'Exception caught while processing request, %s.' % e)
+                return {}, error.json, 500
+        else:
+            return f(endpoint, uri, *args, **kwargs)
+    return wrapper
diff --git a/oauthlib/oauth2/rfc6749/clients/__init__.py b/oauthlib/oauth2/rfc6749/clients/__init__.py
new file mode 100644 (file)
index 0000000..65bea50
--- /dev/null
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import *
+from .web_application import WebApplicationClient
+from .mobile_application import MobileApplicationClient
+from .legacy_application import LegacyApplicationClient
+from .backend_application import BackendApplicationClient
+from .service_application import ServiceApplicationClient
diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py
new file mode 100644 (file)
index 0000000..445bdd5
--- /dev/null
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_token_request
+from ..parameters import parse_token_response
+
+
+class BackendApplicationClient(Client):
+
+    """A public client utilizing the client credentials grant workflow.
+
+    The client can request an access token using only its client
+    credentials (or other supported means of authentication) when the
+    client is requesting access to the protected resources under its
+    control, or those of another resource owner which has been previously
+    arranged with the authorization server (the method of which is beyond
+    the scope of this specification).
+
+    The client credentials grant type MUST only be used by confidential
+    clients.
+
+    Since the client authentication is used as the authorization grant,
+    no additional authorization request is needed.
+    """
+
+    def prepare_request_body(self, body='', scope=None, **kwargs):
+        """Add the client credentials to the request body.
+
+        The client makes a request to the token endpoint by adding the
+        following parameters using the "application/x-www-form-urlencoded"
+        format per `Appendix B`_ in the HTTP request entity-body:
+
+        :param scope:   The scope of the access request as described by
+                        `Section 3.3`_.
+        :param kwargs:  Extra credentials to include in the token request.
+
+        The client MUST authenticate with the authorization server as
+        described in `Section 3.2.1`_.
+
+        The prepared body will include all provided credentials as well as
+        the ``grant_type`` parameter set to ``client_credentials``::
+
+            >>> from oauthlib.oauth2 import BackendApplicationClient
+            >>> client = BackendApplicationClient('your_id')
+            >>> client.prepare_request_body(scope=['hello', 'world'])
+            'grant_type=client_credentials&scope=hello+world'
+
+        .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+        """
+        return prepare_token_request('client_credentials', body=body,
+                                     scope=scope, **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py
new file mode 100644 (file)
index 0000000..e53ccc1
--- /dev/null
@@ -0,0 +1,490 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import time
+
+from oauthlib.common import generate_token
+from oauthlib.oauth2.rfc6749 import tokens
+from oauthlib.oauth2.rfc6749.parameters import parse_token_response
+from oauthlib.oauth2.rfc6749.parameters import prepare_token_request
+from oauthlib.oauth2.rfc6749.parameters import prepare_token_revocation_request
+from oauthlib.oauth2.rfc6749.errors import TokenExpiredError
+from oauthlib.oauth2.rfc6749.errors import InsecureTransportError
+from oauthlib.oauth2.rfc6749.utils import is_secure_transport
+
+
+AUTH_HEADER = 'auth_header'
+URI_QUERY = 'query'
+BODY = 'body'
+
+FORM_ENC_HEADERS = {
+    'Content-Type': 'application/x-www-form-urlencoded'
+}
+
+class Client(object):
+
+    """Base OAuth2 client responsible for access token management.
+
+    This class also acts as a generic interface providing methods common to all
+    client types such as ``prepare_authorization_request`` and
+    ``prepare_token_revocation_request``. The ``prepare_x_request`` methods are
+    the recommended way of interacting with clients (as opposed to the abstract
+    prepare uri/body/etc methods). They are recommended over the older set
+    because they are easier to use (more consistent) and add a few additional
+    security checks, such as HTTPS and state checking.
+
+    Some of these methods require further implementation only provided by the
+    specific purpose clients such as
+    :py:class:`oauthlib.oauth2.MobileApplicationClient` and thus you should always
+    seek to use the client class matching the OAuth workflow you need. For
+    Python, this is usually :py:class:`oauthlib.oauth2.WebApplicationClient`.
+
+    """
+
+    def __init__(self, client_id,
+                 default_token_placement=AUTH_HEADER,
+                 token_type='Bearer',
+                 access_token=None,
+                 refresh_token=None,
+                 mac_key=None,
+                 mac_algorithm=None,
+                 token=None,
+                 scope=None,
+                 state=None,
+                 redirect_url=None,
+                 state_generator=generate_token,
+                 **kwargs):
+        """Initialize a client with commonly used attributes.
+
+        :param client_id: Client identifier given by the OAuth provider upon
+        registration.
+
+        :param default_token_placement: Tokens can be supplied in the Authorization
+        header (default), the URL query component (``query``) or the request
+        body (``body``).
+
+        :param token_type: OAuth 2 token type. Defaults to Bearer. Change this
+        if you specify the ``access_token`` parameter and know it is of a
+        different token type, such as a MAC, JWT or SAML token. Can
+        also be supplied as ``token_type`` inside the ``token`` dict parameter.
+
+        :param access_token: An access token (string) used to authenticate
+        requests to protected resources. Can also be supplied inside the
+        ``token`` dict parameter.
+
+        :param refresh_token: A refresh token (string) used to refresh expired
+        tokens. Can also be supplide inside the ``token`` dict parameter.
+
+        :param mac_key: Encryption key used with MAC tokens.
+
+        :param mac_algorithm:  Hashing algorithm for MAC tokens.
+
+        :param token: A dict of token attributes such as ``access_token``,
+        ``token_type`` and ``expires_at``.
+
+        :param scope: A list of default scopes to request authorization for.
+
+        :param state: A CSRF protection string used during authorization.
+
+        :param redirect_url: The redirection endpoint on the client side to which
+        the user returns after authorization.
+
+        :param state_generator: A no argument state generation callable. Defaults
+        to :py:meth:`oauthlib.common.generate_token`.
+        """
+
+        self.client_id = client_id
+        self.default_token_placement = default_token_placement
+        self.token_type = token_type
+        self.access_token = access_token
+        self.refresh_token = refresh_token
+        self.mac_key = mac_key
+        self.mac_algorithm = mac_algorithm
+        self.token = token or {}
+        self.scope = scope
+        self.state_generator = state_generator
+        self.state = state
+        self.redirect_url = redirect_url
+        self._expires_at = None
+        self._populate_attributes(self.token)
+
+    @property
+    def token_types(self):
+        """Supported token types and their respective methods
+
+        Additional tokens can be supported by extending this dictionary.
+
+        The Bearer token spec is stable and safe to use.
+
+        The MAC token spec is not yet stable and support for MAC tokens
+        is experimental and currently matching version 00 of the spec.
+        """
+        return {
+            'Bearer': self._add_bearer_token,
+            'MAC': self._add_mac_token
+        }
+
+    def prepare_request_uri(self, *args, **kwargs):
+        """Abstract method used to create request URIs."""
+        raise NotImplementedError("Must be implemented by inheriting classes.")
+
+    def prepare_request_body(self, *args, **kwargs):
+        """Abstract method used to create request bodies."""
+        raise NotImplementedError("Must be implemented by inheriting classes.")
+
+    def parse_request_uri_response(self, *args, **kwargs):
+        """Abstract method used to parse redirection responses."""
+
+    def add_token(self, uri, http_method='GET', body=None, headers=None,
+                  token_placement=None, **kwargs):
+        """Add token to the request uri, body or authorization header.
+
+        The access token type provides the client with the information
+        required to successfully utilize the access token to make a protected
+        resource request (along with type-specific attributes).  The client
+        MUST NOT use an access token if it does not understand the token
+        type.
+
+        For example, the "bearer" token type defined in
+        [`I-D.ietf-oauth-v2-bearer`_] is utilized by simply including the access
+        token string in the request:
+
+        .. code-block:: http
+
+            GET /resource/1 HTTP/1.1
+            Host: example.com
+            Authorization: Bearer mF_9.B5f-4.1JqM
+
+        while the "mac" token type defined in [`I-D.ietf-oauth-v2-http-mac`_] is
+        utilized by issuing a MAC key together with the access token which is
+        used to sign certain components of the HTTP requests:
+
+        .. code-block:: http
+
+            GET /resource/1 HTTP/1.1
+            Host: example.com
+            Authorization: MAC id="h480djs93hd8",
+                                nonce="274312:dj83hs9s",
+                                mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
+
+        .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/rfc6749#section-12.2
+        .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/rfc6749#section-12.2
+        """
+        if not is_secure_transport(uri):
+            raise InsecureTransportError()
+
+        token_placement = token_placement or self.default_token_placement
+
+        case_insensitive_token_types = dict(
+            (k.lower(), v) for k, v in self.token_types.items())
+        if not self.token_type.lower() in case_insensitive_token_types:
+            raise ValueError("Unsupported token type: %s" % self.token_type)
+
+        if not self.access_token:
+            raise ValueError("Missing access token.")
+
+        if self._expires_at and self._expires_at < time.time():
+            raise TokenExpiredError()
+
+        return case_insensitive_token_types[self.token_type.lower()](uri, http_method, body,
+                                                                     headers, token_placement, **kwargs)
+
+    def prepare_authorization_request(self, authorization_url, state=None,
+            redirect_url=None, scope=None, **kwargs):
+        """Prepare the authorization request.
+
+        This is the first step in many OAuth flows in which the user is
+        redirected to a certain authorization URL. This method adds
+        required parameters to the authorization URL.
+
+        :param authorization_url: Provider authorization endpoint URL.
+
+        :param state: CSRF protection string. Will be automatically created if
+        not provided. The generated state is available via the ``state``
+        attribute. Clients should verify that the state is unchanged and
+        present in the authorization response. This verification is done
+        automatically if using the ``authorization_response`` parameter
+        with ``prepare_token_request``.
+
+        :param redirect_url: Redirect URL to which the user will be returned
+        after authorization. Must be provided unless previously setup with
+        the provider. If provided then it must also be provided in the
+        token request.
+
+        :param kwargs: Additional parameters to included in the request.
+
+        :returns: The prepared request tuple with (url, headers, body).
+        """
+        if not is_secure_transport(authorization_url):
+            raise InsecureTransportError()
+
+        self.state = state or self.state_generator()
+        self.redirect_url = redirect_url or self.redirect_url
+        self.scope = scope or self.scope
+        auth_url = self.prepare_request_uri(
+                authorization_url, redirect_uri=self.redirect_uri,
+                scope=self.scope, state=self.state, **kwargs)
+        return auth_url, FORM_ENC_HEADERS, ''
+
+    def prepare_token_request(self, token_url, authorization_response=None,
+            redirect_url=None, state=None, body='', **kwargs):
+        """Prepare a token creation request.
+
+        Note that these requests usually require client authentication, either
+        by including client_id or a set of provider specific authentication
+        credentials.
+
+        :param token_url: Provider token creation endpoint URL.
+
+        :param authorization_response: The full redirection URL string, i.e.
+        the location to which the user was redirected after successfull
+        authorization. Used to mine credentials needed to obtain a token
+        in this step, such as authorization code.
+
+        :param redirect_url: The redirect_url supplied with the authorization
+        request (if there was one).
+
+        :param body: Request body (URL encoded string).
+
+        :param kwargs: Additional parameters to included in the request.
+
+        :returns: The prepared request tuple with (url, headers, body).
+        """
+        if not is_secure_transport(token_url):
+            raise InsecureTransportError()
+
+        state = state or self.state
+        if authorization_response:
+            self.parse_request_uri_response(
+                    authorization_response, state=state)
+        self.redirect_url = redirect_url or self.redirect_url
+        body = self.prepare_request_body(body=body,
+                redirect_uri=self.redirect_url, **kwargs)
+
+        return token_url, FORM_ENC_HEADERS, body
+
+    def prepare_refresh_token_request(self, token_url, refresh_token=None,
+            body='', scope=None, **kwargs):
+        """Prepare an access token refresh request.
+
+        Expired access tokens can be replaced by new access tokens without
+        going through the OAuth dance if the client obtained a refresh token.
+        This refresh token and authentication credentials can be used to
+        obtain a new access token, and possibly a new refresh token.
+
+        :param token_url: Provider token refresh endpoint URL.
+
+        :param refresh_token: Refresh token string.
+
+        :param body: Request body (URL encoded string).
+
+        :param scope: List of scopes to request. Must be equal to
+        or a subset of the scopes granted when obtaining the refresh
+        token.
+
+        :param kwargs: Additional parameters to included in the request.
+
+        :returns: The prepared request tuple with (url, headers, body).
+        """
+        if not is_secure_transport(token_url):
+            raise InsecureTransportError()
+
+        self.scope = scope or self.scope
+        body = self._client.prepare_refresh_body(body=body,
+                refresh_token=refresh_token, scope=self.scope, **kwargs)
+        return token_url, FORM_ENC_HEADERS, body
+
+    def prepare_token_revocation_request(self, revocation_url, token,
+            token_type_hint="access_token", body='', callback=None, **kwargs):
+        """Prepare a token revocation request.
+
+        :param revocation_url: Provider token revocation endpoint URL.
+
+        :param token: The access or refresh token to be revoked (string).
+
+        :param token_type_hint: ``"access_token"`` (default) or
+        ``"refresh_token"``. This is optional and if you wish to not pass it you
+        must provide ``token_type_hint=None``.
+
+        :param callback: A jsonp callback such as ``package.callback`` to be invoked
+        upon receiving the response. Not that it should not include a () suffix.
+
+        :param kwargs: Additional parameters to included in the request.
+
+        :returns: The prepared request tuple with (url, headers, body).
+
+        Note that JSONP request may use GET requests as the parameters will
+        be added to the request URL query as opposed to the request body.
+
+        An example of a revocation request
+
+        .. code-block: http
+
+            POST /revoke HTTP/1.1
+            Host: server.example.com
+            Content-Type: application/x-www-form-urlencoded
+            Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
+
+            token=45ghiukldjahdnhzdauz&token_type_hint=refresh_token
+
+        An example of a jsonp revocation request
+
+        .. code-block: http
+
+            GET /revoke?token=agabcdefddddafdd&callback=package.myCallback HTTP/1.1
+            Host: server.example.com
+            Content-Type: application/x-www-form-urlencoded
+            Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
+
+        and an error response
+
+        .. code-block: http
+
+        package.myCallback({"error":"unsupported_token_type"});
+
+        Note that these requests usually require client credentials, client_id in
+        the case for public clients and provider specific authentication
+        credentials for confidential clients.
+        """
+        if not is_secure_transport(revocation_url):
+            raise InsecureTransportError()
+
+        return prepare_token_revocation_request(revocation_url, token,
+                token_type_hint=token_type_hint, body=body, callback=callback,
+                **kwargs)
+
+    def parse_request_body_response(self, body, scope=None, **kwargs):
+        """Parse the JSON response body.
+
+        If the access token request is valid and authorized, the
+        authorization server issues an access token as described in
+        `Section 5.1`_.  A refresh token SHOULD NOT be included.  If the request
+        failed client authentication or is invalid, the authorization server
+        returns an error response as described in `Section 5.2`_.
+
+        :param body: The response body from the token request.
+        :param scope: Scopes originally requested.
+        :return: Dictionary of token parameters.
+        :raises: Warning if scope has changed. OAuth2Error if response is invalid.
+
+        These response are json encoded and could easily be parsed without
+        the assistance of OAuthLib. However, there are a few subtle issues
+        to be aware of regarding the response which are helpfully addressed
+        through the raising of various errors.
+
+        A successful response should always contain
+
+        **access_token**
+                The access token issued by the authorization server. Often
+                a random string.
+
+        **token_type**
+            The type of the token issued as described in `Section 7.1`_.
+            Commonly ``Bearer``.
+
+        While it is not mandated it is recommended that the provider include
+
+        **expires_in**
+            The lifetime in seconds of the access token.  For
+            example, the value "3600" denotes that the access token will
+            expire in one hour from the time the response was generated.
+            If omitted, the authorization server SHOULD provide the
+            expiration time via other means or document the default value.
+
+        **scope**
+            Providers may supply this in all responses but are required to only
+            if it has changed since the authorization request.
+
+        .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+        .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+        .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+        """
+        self.token = parse_token_response(body, scope=scope)
+        self._populate_attributes(self.token)
+        return self.token
+
+    def prepare_refresh_body(self, body='', refresh_token=None, scope=None, **kwargs):
+        """Prepare an access token request, using a refresh token.
+
+        If the authorization server issued a refresh token to the client, the
+        client makes a refresh request to the token endpoint by adding the
+        following parameters using the "application/x-www-form-urlencoded"
+        format in the HTTP request entity-body:
+
+        grant_type
+                REQUIRED.  Value MUST be set to "refresh_token".
+        refresh_token
+                REQUIRED.  The refresh token issued to the client.
+        scope
+                OPTIONAL.  The scope of the access request as described by
+                Section 3.3.  The requested scope MUST NOT include any scope
+                not originally granted by the resource owner, and if omitted is
+                treated as equal to the scope originally granted by the
+                resource owner.
+        """
+        refresh_token = refresh_token or self.refresh_token
+        return prepare_token_request('refresh_token', body=body, scope=scope,
+                                     refresh_token=refresh_token, **kwargs)
+
+    def _add_bearer_token(self, uri, http_method='GET', body=None,
+                          headers=None, token_placement=None):
+        """Add a bearer token to the request uri, body or authorization header."""
+        if token_placement == AUTH_HEADER:
+            headers = tokens.prepare_bearer_headers(self.access_token, headers)
+
+        elif token_placement == URI_QUERY:
+            uri = tokens.prepare_bearer_uri(self.access_token, uri)
+
+        elif token_placement == BODY:
+            body = tokens.prepare_bearer_body(self.access_token, body)
+
+        else:
+            raise ValueError("Invalid token placement.")
+        return uri, headers, body
+
+    def _add_mac_token(self, uri, http_method='GET', body=None,
+                       headers=None, token_placement=AUTH_HEADER, ext=None, **kwargs):
+        """Add a MAC token to the request authorization header.
+
+        Warning: MAC token support is experimental as the spec is not yet stable.
+        """
+        headers = tokens.prepare_mac_header(self.access_token, uri,
+                                            self.mac_key, http_method, headers=headers, body=body, ext=ext,
+                                            hash_algorithm=self.mac_algorithm, **kwargs)
+        return uri, headers, body
+
+    def _populate_attributes(self, response):
+        """Add commonly used values such as access_token to self."""
+
+        if 'access_token' in response:
+            self.access_token = response.get('access_token')
+
+        if 'refresh_token' in response:
+            self.refresh_token = response.get('refresh_token')
+
+        if 'token_type' in response:
+            self.token_type = response.get('token_type')
+
+        if 'expires_in' in response:
+            self.expires_in = response.get('expires_in')
+            self._expires_at = time.time() + int(self.expires_in)
+
+        if 'expires_at' in response:
+            self._expires_at = int(response.get('expires_at'))
+
+        if 'code' in response:
+            self.code = response.get('code')
+
+        if 'mac_key' in response:
+            self.mac_key = response.get('mac_key')
+
+        if 'mac_algorithm' in response:
+            self.mac_algorithm = response.get('mac_algorithm')
+
diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py
new file mode 100644 (file)
index 0000000..e4e522a
--- /dev/null
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_token_request
+from ..parameters import parse_token_response
+
+
+class LegacyApplicationClient(Client):
+
+    """A public client using the resource owner password and username directly.
+
+    The resource owner password credentials grant type is suitable in
+    cases where the resource owner has a trust relationship with the
+    client, such as the device operating system or a highly privileged
+    application.  The authorization server should take special care when
+    enabling this grant type, and only allow it when other flows are not
+    viable.
+
+    The grant type is suitable for clients capable of obtaining the
+    resource owner's credentials (username and password, typically using
+    an interactive form).  It is also used to migrate existing clients
+    using direct authentication schemes such as HTTP Basic or Digest
+    authentication to OAuth by converting the stored credentials to an
+    access token.
+
+    The method through which the client obtains the resource owner
+    credentials is beyond the scope of this specification.  The client
+    MUST discard the credentials once an access token has been obtained.
+    """
+
+    def __init__(self, client_id, **kwargs):
+        super(LegacyApplicationClient, self).__init__(client_id, **kwargs)
+
+    def prepare_request_body(self, username, password, body='', scope=None, **kwargs):
+        """Add the resource owner password and username to the request body.
+
+        The client makes a request to the token endpoint by adding the
+        following parameters using the "application/x-www-form-urlencoded"
+        format per `Appendix B`_ in the HTTP request entity-body:
+
+        :param username:    The resource owner username.
+        :param password:    The resource owner password.
+        :param scope:   The scope of the access request as described by
+                        `Section 3.3`_.
+        :param kwargs:  Extra credentials to include in the token request.
+
+        If the client type is confidential or the client was issued client
+        credentials (or assigned other authentication requirements), the
+        client MUST authenticate with the authorization server as described
+        in `Section 3.2.1`_.
+
+        The prepared body will include all provided credentials as well as
+        the ``grant_type`` parameter set to ``password``::
+
+            >>> from oauthlib.oauth2 import LegacyApplicationClient
+            >>> client = LegacyApplicationClient('your_id')
+            >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world'])
+            'grant_type=password&username=foo&scope=hello+world&password=bar'
+
+        .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+        """
+        return prepare_token_request('password', body=body, username=username,
+                                     password=password, scope=scope, **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py
new file mode 100644 (file)
index 0000000..9e3ef21
--- /dev/null
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_grant_uri
+from ..parameters import parse_implicit_response
+
+
+class MobileApplicationClient(Client):
+
+    """A public client utilizing the implicit code grant workflow.
+
+    A user-agent-based application is a public client in which the
+    client code is downloaded from a web server and executes within a
+    user-agent (e.g. web browser) on the device used by the resource
+    owner.  Protocol data and credentials are easily accessible (and
+    often visible) to the resource owner.  Since such applications
+    reside within the user-agent, they can make seamless use of the
+    user-agent capabilities when requesting authorization.
+
+    The implicit grant type is used to obtain access tokens (it does not
+    support the issuance of refresh tokens) and is optimized for public
+    clients known to operate a particular redirection URI.  These clients
+    are typically implemented in a browser using a scripting language
+    such as JavaScript.
+
+    As a redirection-based flow, the client must be capable of
+    interacting with the resource owner's user-agent (typically a web
+    browser) and capable of receiving incoming requests (via redirection)
+    from the authorization server.
+
+    Unlike the authorization code grant type in which the client makes
+    separate requests for authorization and access token, the client
+    receives the access token as the result of the authorization request.
+
+    The implicit grant type does not include client authentication, and
+    relies on the presence of the resource owner and the registration of
+    the redirection URI.  Because the access token is encoded into the
+    redirection URI, it may be exposed to the resource owner and other
+    applications residing on the same device.
+    """
+
+    def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
+                            state=None, **kwargs):
+        """Prepare the implicit grant request URI.
+
+        The client constructs the request URI by adding the following
+        parameters to the query component of the authorization endpoint URI
+        using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+        :param redirect_uri:  OPTIONAL. The redirect URI must be an absolute URI
+                              and it should have been registerd with the OAuth
+                              provider prior to use. As described in `Section 3.1.2`_.
+
+        :param scope:  OPTIONAL. The scope of the access request as described by
+                       Section 3.3`_. These may be any string but are commonly
+                       URIs or various categories such as ``videos`` or ``documents``.
+
+        :param state:   RECOMMENDED.  An opaque value used by the client to maintain
+                        state between the request and callback.  The authorization
+                        server includes this value when redirecting the user-agent back
+                        to the client.  The parameter SHOULD be used for preventing
+                        cross-site request forgery as described in `Section 10.12`_.
+
+        :param kwargs:  Extra arguments to include in the request URI.
+
+        In addition to supplied parameters, OAuthLib will append the ``client_id``
+        that was provided in the constructor as well as the mandatory ``response_type``
+        argument, set to ``token``::
+
+            >>> from oauthlib.oauth2 import MobileApplicationClient
+            >>> client = MobileApplicationClient('your_id')
+            >>> client.prepare_request_uri('https://example.com')
+            'https://example.com?client_id=your_id&response_type=token'
+            >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
+            'https://example.com?client_id=your_id&response_type=token&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
+            >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
+            'https://example.com?client_id=your_id&response_type=token&scope=profile+pictures'
+            >>> client.prepare_request_uri('https://example.com', foo='bar')
+            'https://example.com?client_id=your_id&response_type=token&foo=bar'
+
+        .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+        .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+        .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+        """
+        return prepare_grant_uri(uri, self.client_id, 'token',
+                                 redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
+
+    def parse_request_uri_response(self, uri, state=None, scope=None):
+        """Parse the response URI fragment.
+
+        If the resource owner grants the access request, the authorization
+        server issues an access token and delivers it to the client by adding
+        the following parameters to the fragment component of the redirection
+        URI using the "application/x-www-form-urlencoded" format:
+
+        :param uri: The callback URI that resulted from the user being redirected
+                    back from the provider to you, the client.
+        :param state: The state provided in the authorization request.
+        :param scope: The scopes provided in the authorization request.
+        :return: Dictionary of token parameters.
+        :raises: OAuth2Error if response is invalid.
+
+        A successful response should always contain
+
+        **access_token**
+                The access token issued by the authorization server. Often
+                a random string.
+
+        **token_type**
+            The type of the token issued as described in `Section 7.1`_.
+            Commonly ``Bearer``.
+
+        **state**
+            If you provided the state parameter in the authorization phase, then
+            the provider is required to include that exact state value in the
+            response.
+
+        While it is not mandated it is recommended that the provider include
+
+        **expires_in**
+            The lifetime in seconds of the access token.  For
+            example, the value "3600" denotes that the access token will
+            expire in one hour from the time the response was generated.
+            If omitted, the authorization server SHOULD provide the
+            expiration time via other means or document the default value.
+
+        **scope**
+            Providers may supply this in all responses but are required to only
+            if it has changed since the authorization request.
+
+        A few example responses can be seen below::
+
+            >>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world'
+            >>> from oauthlib.oauth2 import MobileApplicationClient
+            >>> client = MobileApplicationClient('your_id')
+            >>> client.parse_request_uri_response(response_uri)
+            {
+                'access_token': 'sdlfkj452',
+                'token_type': 'Bearer',
+                'state': 'ss345asyht',
+                'scope': [u'hello', u'world']
+            }
+            >>> client.parse_request_uri_response(response_uri, state='other')
+            Traceback (most recent call last):
+                File "<stdin>", line 1, in <module>
+                File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response
+                    **scope**
+                File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response
+                    raise ValueError("Mismatching or missing state in params.")
+            ValueError: Mismatching or missing state in params.
+            >>> def alert_scope_changed(message, old, new):
+            ...     print(message, old, new)
+            ...
+            >>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
+            >>> client.parse_request_body_response(response_body, scope=['other'])
+            ('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
+
+        .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        """
+        self.token = parse_implicit_response(uri, state=state, scope=scope)
+        self._populate_attributes(self.token)
+        return self.token
diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py
new file mode 100644 (file)
index 0000000..36da98b
--- /dev/null
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import time
+
+from oauthlib.common import to_unicode
+
+from .base import Client
+from ..parameters import prepare_token_request
+from ..parameters import parse_token_response
+
+
+class ServiceApplicationClient(Client):
+    """A public client utilizing the JWT bearer grant.
+
+    JWT bearer tokes can be used to request an access token when a client
+    wishes to utilize an existing trust relationship, expressed through the
+    semantics of (and digital signature or keyed message digest calculated
+    over) the JWT, without a direct user approval step at the authorization
+    server.
+
+    This grant type does not involve an authorization step. It may be
+    used by both public and confidential clients.
+    """
+
+    grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
+
+    def __init__(self, client_id, private_key=None, subject=None, issuer=None,
+                 audience=None, **kwargs):
+        """Initalize a JWT client with defaults for implicit use later.
+
+        :param client_id: Client identifier given by the OAuth provider upon
+                          registration.
+
+        :param private_key: Private key used for signing and encrypting.
+                            Must be given as a string.
+
+        :param subject: The principal that is the subject of the JWT, i.e. 
+                        which user is the token requested on behalf of.
+                        For example, ``foo@example.com.
+
+        :param issuer: The JWT MUST contain an "iss" (issuer) claim that
+                       contains a unique identifier for the entity that issued
+                       the JWT. For example, ``your-client@provider.com``. 
+
+        :param audience: A value identifying the authorization server as an
+                         intended audience, e.g.
+                         ``https://provider.com/oauth2/token``.
+
+        :param kwargs: Additional arguments to pass to base client, such as
+                       state and token. See Client.__init__.__doc__ for 
+                       details.
+        """
+        super(ServiceApplicationClient, self).__init__(client_id, **kwargs)
+        self.private_key = private_key
+        self.subject = subject
+        self.issuer = issuer
+        self.audience = audience
+
+    def prepare_request_body(self, 
+                             private_key=None,
+                             subject=None, 
+                             issuer=None, 
+                             audience=None, 
+                             expires_at=None, 
+                             issued_at=None,
+                             extra_claims=None,
+                             body='', 
+                             scope=None, 
+                             **kwargs):
+        """Create and add a JWT assertion to the request body.
+
+        :param private_key: Private key used for signing and encrypting.
+                            Must be given as a string.
+
+        :param subject: (sub) The principal that is the subject of the JWT,
+                        i.e.  which user is the token requested on behalf of.
+                        For example, ``foo@example.com.
+
+        :param issuer: (iss) The JWT MUST contain an "iss" (issuer) claim that
+                       contains a unique identifier for the entity that issued
+                       the JWT. For example, ``your-client@provider.com``. 
+
+        :param audience: (aud) A value identifying the authorization server as an
+                         intended audience, e.g.
+                         ``https://provider.com/oauth2/token``.
+
+        :param expires_at: A unix expiration timestamp for the JWT. Defaults
+                           to an hour from now, i.e. ``time.time() + 3600``.
+
+        :param issued_at: A unix timestamp of when the JWT was created.
+                          Defaults to now, i.e. ``time.time()``.
+
+        :param not_before: A unix timestamp after which the JWT may be used.
+                           Not included unless provided.
+
+        :param jwt_id: A unique JWT token identifier. Not included unless
+                       provided.
+
+        :param extra_claims: A dict of additional claims to include in the JWT.
+
+        :param scope: The scope of the access request.
+
+        :param body: Request body (string) with extra parameters.
+
+        :param kwargs: Extra credentials to include in the token request.
+
+        The "scope" parameter may be used, as defined in the Assertion
+        Framework for OAuth 2.0 Client Authentication and Authorization Grants
+        [I-D.ietf-oauth-assertions] specification, to indicate the requested
+        scope.
+
+        Authentication of the client is optional, as described in 
+        `Section 3.2.1`_ of OAuth 2.0 [RFC6749] and consequently, the
+        "client_id" is only needed when a form of client authentication that
+        relies on the parameter is used.
+
+        The following non-normative example demonstrates an Access Token
+        Request with a JWT as an authorization grant (with extra line breaks
+        for display purposes only):
+
+        .. code-block: http
+
+            POST /token.oauth2 HTTP/1.1
+            Host: as.example.com
+            Content-Type: application/x-www-form-urlencoded
+
+            grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
+            &assertion=eyJhbGciOiJFUzI1NiJ9.
+            eyJpc3Mi[...omitted for brevity...].
+            J9l-ZhwP[...omitted for brevity...]
+
+        .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+        """
+        import jwt
+
+        key = private_key or self.private_key
+        if not key:
+            raise ValueError('An encryption key must be supplied to make JWT'
+                             ' token requests.')
+        claim = {
+            'iss': issuer or self.issuer,
+            'aud': audience or self.issuer,
+            'sub': subject or self.issuer,
+            'exp': int(expires_at or time.time() + 3600),
+            'iat': int(issued_at or time.time()),
+        }
+
+        for attr in ('iss', 'aud', 'sub'):
+            if claim[attr] is None:
+                raise ValueError(
+                        'Claim must include %s but none was given.' % attr)
+
+        if 'not_before' in kwargs:
+            claim['nbf'] = kwargs.pop('not_before')
+
+        if 'jwt_id' in kwargs:
+            claim['jti'] = kwargs.pop('jwt_id')
+
+        claim.update(extra_claims or {})
+
+        assertion = jwt.encode(claim, key, 'RS256')
+        assertion = to_unicode(assertion)
+
+        return prepare_token_request(self.grant_type,
+                                     body=body,
+                                     assertion=assertion,
+                                     scope=scope, 
+                                     **kwargs)
diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py
new file mode 100644 (file)
index 0000000..c685d3c
--- /dev/null
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .base import Client
+from ..parameters import prepare_grant_uri, prepare_token_request
+from ..parameters import parse_authorization_code_response
+from ..parameters import parse_token_response
+
+
+class WebApplicationClient(Client):
+
+    """A client utilizing the authorization code grant workflow.
+
+    A web application is a confidential client running on a web
+    server.  Resource owners access the client via an HTML user
+    interface rendered in a user-agent on the device used by the
+    resource owner.  The client credentials as well as any access
+    token issued to the client are stored on the web server and are
+    not exposed to or accessible by the resource owner.
+
+    The authorization code grant type is used to obtain both access
+    tokens and refresh tokens and is optimized for confidential clients.
+    As a redirection-based flow, the client must be capable of
+    interacting with the resource owner's user-agent (typically a web
+    browser) and capable of receiving incoming requests (via redirection)
+    from the authorization server.
+    """
+
+    def __init__(self, client_id, code=None, **kwargs):
+        super(WebApplicationClient, self).__init__(client_id, **kwargs)
+        self.code = code
+
+    def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
+                            state=None, **kwargs):
+        """Prepare the authorization code request URI
+
+        The client constructs the request URI by adding the following
+        parameters to the query component of the authorization endpoint URI
+        using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+        :param redirect_uri:  OPTIONAL. The redirect URI must be an absolute URI
+                              and it should have been registerd with the OAuth
+                              provider prior to use. As described in `Section 3.1.2`_.
+
+        :param scope:  OPTIONAL. The scope of the access request as described by
+                       Section 3.3`_. These may be any string but are commonly
+                       URIs or various categories such as ``videos`` or ``documents``.
+
+        :param state:   RECOMMENDED.  An opaque value used by the client to maintain
+                        state between the request and callback.  The authorization
+                        server includes this value when redirecting the user-agent back
+                        to the client.  The parameter SHOULD be used for preventing
+                        cross-site request forgery as described in `Section 10.12`_.
+
+        :param kwargs:  Extra arguments to include in the request URI.
+
+        In addition to supplied parameters, OAuthLib will append the ``client_id``
+        that was provided in the constructor as well as the mandatory ``response_type``
+        argument, set to ``code``::
+
+            >>> from oauthlib.oauth2 import WebApplicationClient
+            >>> client = WebApplicationClient('your_id')
+            >>> client.prepare_request_uri('https://example.com')
+            'https://example.com?client_id=your_id&response_type=code'
+            >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback')
+            'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback'
+            >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures'])
+            'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures'
+            >>> client.prepare_request_uri('https://example.com', foo='bar')
+            'https://example.com?client_id=your_id&response_type=code&foo=bar'
+
+        .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+        .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+        .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+        """
+        return prepare_grant_uri(uri, self.client_id, 'code',
+                                 redirect_uri=redirect_uri, scope=scope, state=state, **kwargs)
+
+    def prepare_request_body(self, client_id=None, code=None, body='',
+                             redirect_uri=None, **kwargs):
+        """Prepare the access token request body.
+
+        The client makes a request to the token endpoint by adding the
+        following parameters using the "application/x-www-form-urlencoded"
+        format in the HTTP request entity-body:
+
+        :param client_id:   REQUIRED, if the client is not authenticating with the
+                            authorization server as described in `Section 3.2.1`_.
+
+        :param code:    REQUIRED. The authorization code received from the
+                        authorization server.
+
+        :param redirect_uri:    REQUIRED, if the "redirect_uri" parameter was included in the
+                                authorization request as described in `Section 4.1.1`_, and their
+                                values MUST be identical.
+
+        :param kwargs: Extra parameters to include in the token request.
+
+        In addition OAuthLib will add the ``grant_type`` parameter set to
+        ``authorization_code``.
+
+        If the client type is confidential or the client was issued client
+        credentials (or assigned other authentication requirements), the
+        client MUST authenticate with the authorization server as described
+        in `Section 3.2.1`_::
+
+            >>> from oauthlib.oauth2 import WebApplicationClient
+            >>> client = WebApplicationClient('your_id')
+            >>> client.prepare_request_body(code='sh35ksdf09sf')
+            'grant_type=authorization_code&code=sh35ksdf09sf'
+            >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar')
+            'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar'
+
+        .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
+        .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+        """
+        code = code or self.code
+        return prepare_token_request('authorization_code', code=code, body=body,
+                                     client_id=self.client_id, redirect_uri=redirect_uri, **kwargs)
+
+    def parse_request_uri_response(self, uri, state=None):
+        """Parse the URI query for code and state.
+
+        If the resource owner grants the access request, the authorization
+        server issues an authorization code and delivers it to the client by
+        adding the following parameters to the query component of the
+        redirection URI using the "application/x-www-form-urlencoded" format:
+
+        :param uri: The callback URI that resulted from the user being redirected
+                    back from the provider to you, the client.
+        :param state: The state provided in the authorization request.
+
+        **code**
+            The authorization code generated by the authorization server.
+            The authorization code MUST expire shortly after it is issued
+            to mitigate the risk of leaks. A maximum authorization code
+            lifetime of 10 minutes is RECOMMENDED. The client MUST NOT
+            use the authorization code more than once. If an authorization
+            code is used more than once, the authorization server MUST deny
+            the request and SHOULD revoke (when possible) all tokens
+            previously issued based on that authorization code.
+            The authorization code is bound to the client identifier and
+            redirection URI.
+
+        **state**
+                If the "state" parameter was present in the authorization request.
+
+        This method is mainly intended to enforce strict state checking with
+        the added benefit of easily extracting parameters from the URI::
+
+            >>> from oauthlib.oauth2 import WebApplicationClient
+            >>> client = WebApplicationClient('your_id')
+            >>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45'
+            >>> client.parse_request_uri_response(uri, state='sfetw45')
+            {'state': 'sfetw45', 'code': 'sdfkjh345'}
+            >>> client.parse_request_uri_response(uri, state='other')
+            Traceback (most recent call last):
+                File "<stdin>", line 1, in <module>
+                File "oauthlib/oauth2/rfc6749/__init__.py", line 357, in parse_request_uri_response
+                    back from the provider to you, the client.
+                File "oauthlib/oauth2/rfc6749/parameters.py", line 153, in parse_authorization_code_response
+                    raise MismatchingStateError()
+            oauthlib.oauth2.rfc6749.errors.MismatchingStateError
+        """
+        response = parse_authorization_code_response(uri, state=state)
+        self._populate_attributes(response)
+        return response
diff --git a/oauthlib/oauth2/rfc6749/endpoints/__init__.py b/oauthlib/oauth2/rfc6749/endpoints/__init__.py
new file mode 100644 (file)
index 0000000..848bec6
--- /dev/null
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from .authorization import AuthorizationEndpoint
+from .token import TokenEndpoint
+from .resource import ResourceEndpoint
+from .revocation import RevocationEndpoint
+from .pre_configured import Server
+from .pre_configured import WebApplicationServer
+from .pre_configured import MobileApplicationServer
+from .pre_configured import LegacyApplicationServer
+from .pre_configured import BackendApplicationServer
diff --git a/oauthlib/oauth2/rfc6749/endpoints/authorization.py b/oauthlib/oauth2/rfc6749/endpoints/authorization.py
new file mode 100644 (file)
index 0000000..ada24d7
--- /dev/null
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class AuthorizationEndpoint(BaseEndpoint):
+
+    """Authorization endpoint - used by the client to obtain authorization
+    from the resource owner via user-agent redirection.
+
+    The authorization endpoint is used to interact with the resource
+    owner and obtain an authorization grant.  The authorization server
+    MUST first verify the identity of the resource owner.  The way in
+    which the authorization server authenticates the resource owner (e.g.
+    username and password login, session cookies) is beyond the scope of
+    this specification.
+
+    The endpoint URI MAY include an "application/x-www-form-urlencoded"
+    formatted (per `Appendix B`_) query component,
+    which MUST be retained when adding additional query parameters.  The
+    endpoint URI MUST NOT include a fragment component::
+
+        https://example.com/path?query=component             # OK
+        https://example.com/path?query=component#fragment    # Not OK
+
+    Since requests to the authorization endpoint result in user
+    authentication and the transmission of clear-text credentials (in the
+    HTTP response), the authorization server MUST require the use of TLS
+    as described in Section 1.6 when sending requests to the
+    authorization endpoint::
+
+        # We will deny any request which URI schema is not with https
+
+    The authorization server MUST support the use of the HTTP "GET"
+    method [RFC2616] for the authorization endpoint, and MAY support the
+    use of the "POST" method as well::
+
+        # HTTP method is currently not enforced
+
+    Parameters sent without a value MUST be treated as if they were
+    omitted from the request.  The authorization server MUST ignore
+    unrecognized request parameters.  Request and response parameters
+    MUST NOT be included more than once::
+
+        # Enforced through the design of oauthlib.common.Request
+
+    .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+    """
+
+    def __init__(self, default_response_type, default_token_type,
+                 response_types):
+        BaseEndpoint.__init__(self)
+        self._response_types = response_types
+        self._default_response_type = default_response_type
+        self._default_token_type = default_token_type
+
+    @property
+    def response_types(self):
+        return self._response_types
+
+    @property
+    def default_response_type(self):
+        return self._default_response_type
+
+    @property
+    def default_response_type_handler(self):
+        return self.response_types.get(self.default_response_type)
+
+    @property
+    def default_token_type(self):
+        return self._default_token_type
+
+    @catch_errors_and_unavailability
+    def create_authorization_response(self, uri, http_method='GET', body=None,
+                                      headers=None, scopes=None, credentials=None):
+        """Extract response_type and route to the designated handler."""
+        request = Request(
+            uri, http_method=http_method, body=body, headers=headers)
+        request.scopes = scopes
+        # TODO: decide whether this should be a required argument
+        request.user = None     # TODO: explain this in docs
+        for k, v in (credentials or {}).items():
+            setattr(request, k, v)
+        response_type_handler = self.response_types.get(
+            request.response_type, self.default_response_type_handler)
+        log.debug('Dispatching response_type %s request to %r.',
+                  request.response_type, response_type_handler)
+        return response_type_handler.create_authorization_response(
+            request, self.default_token_type)
+
+    @catch_errors_and_unavailability
+    def validate_authorization_request(self, uri, http_method='GET', body=None,
+                                       headers=None):
+        """Extract response_type and route to the designated handler."""
+        request = Request(
+            uri, http_method=http_method, body=body, headers=headers)
+        request.scopes = None
+        response_type_handler = self.response_types.get(
+            request.response_type, self.default_response_type_handler)
+        return response_type_handler.validate_authorization_request(request)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/base.py b/oauthlib/oauth2/rfc6749/endpoints/base.py
new file mode 100644 (file)
index 0000000..759c6c8
--- /dev/null
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import functools
+import logging
+
+from ..errors import TemporarilyUnavailableError, ServerError
+from ..errors import FatalClientError, OAuth2Error
+
+log = logging.getLogger(__name__)
+
+
+class BaseEndpoint(object):
+
+    def __init__(self):
+        self._available = True
+        self._catch_errors = False
+
+    @property
+    def available(self):
+        return self._available
+
+    @available.setter
+    def available(self, available):
+        self._available = available
+
+    @property
+    def catch_errors(self):
+        return self._catch_errors
+
+    @catch_errors.setter
+    def catch_errors(self, catch_errors):
+        self._catch_errors = catch_errors
+
+
+def catch_errors_and_unavailability(f):
+    @functools.wraps(f)
+    def wrapper(endpoint, uri, *args, **kwargs):
+        if not endpoint.available:
+            e = TemporarilyUnavailableError()
+            log.info('Endpoint unavailable, ignoring request %s.' % uri)
+            return {}, e.json, 503
+
+        if endpoint.catch_errors:
+            try:
+                return f(endpoint, uri, *args, **kwargs)
+            except OAuth2Error:
+                raise
+            except FatalClientError:
+                raise
+            except Exception as e:
+                error = ServerError()
+                log.warning(
+                    'Exception caught while processing request, %s.' % e)
+                return {}, error.json, 500
+        else:
+            return f(endpoint, uri, *args, **kwargs)
+    return wrapper
diff --git a/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py b/oauthlib/oauth2/rfc6749/endpoints/pre_configured.py
new file mode 100644 (file)
index 0000000..21ed2dd
--- /dev/null
@@ -0,0 +1,209 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+from ..tokens import BearerToken
+from ..grant_types import AuthorizationCodeGrant
+from ..grant_types import ImplicitGrant
+from ..grant_types import ResourceOwnerPasswordCredentialsGrant
+from ..grant_types import ClientCredentialsGrant
+from ..grant_types import RefreshTokenGrant
+
+from .authorization import AuthorizationEndpoint
+from .token import TokenEndpoint
+from .resource import ResourceEndpoint
+from .revocation import RevocationEndpoint
+
+
+class Server(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
+             RevocationEndpoint):
+
+    """An all-in-one endpoint featuring all four major grant types."""
+
+    def __init__(self, request_validator, token_expires_in=None,
+                 token_generator=None, refresh_token_generator=None,
+                 *args, **kwargs):
+        """Construct a new all-grants-in-one server.
+
+        :param request_validator: An implementation of
+                                  oauthlib.oauth2.RequestValidator.
+        :param token_expires_in: An int or a function to generate a token
+                                 expiration offset (in seconds) given a
+                                 oauthlib.common.Request object.
+        :param token_generator: A function to generate a token from a request.
+        :param refresh_token_generator: A function to generate a token from a
+                                        request for the refresh token.
+        :param kwargs: Extra parameters to pass to authorization-,
+                       token-, resource-, and revocation-endpoint constructors.
+        """
+        auth_grant = AuthorizationCodeGrant(request_validator)
+        implicit_grant = ImplicitGrant(request_validator)
+        password_grant = ResourceOwnerPasswordCredentialsGrant(
+            request_validator)
+        credentials_grant = ClientCredentialsGrant(request_validator)
+        refresh_grant = RefreshTokenGrant(request_validator)
+        bearer = BearerToken(request_validator, token_generator,
+                             token_expires_in, refresh_token_generator)
+        AuthorizationEndpoint.__init__(self, default_response_type='code',
+                                       response_types={
+                                           'code': auth_grant,
+                                           'token': implicit_grant,
+                                       },
+                                       default_token_type=bearer)
+        TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+                               grant_types={
+                                   'authorization_code': auth_grant,
+                                   'password': password_grant,
+                                   'client_credentials': credentials_grant,
+                                   'refresh_token': refresh_grant,
+                               },
+                               default_token_type=bearer)
+        ResourceEndpoint.__init__(self, default_token='Bearer',
+                                  token_types={'Bearer': bearer})
+        RevocationEndpoint.__init__(self, request_validator)
+
+
+class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint,
+                           RevocationEndpoint):
+
+    """An all-in-one endpoint featuring Authorization code grant and Bearer tokens."""
+
+    def __init__(self, request_validator, token_generator=None,
+                 token_expires_in=None, refresh_token_generator=None, **kwargs):
+        """Construct a new web application server.
+
+        :param request_validator: An implementation of
+                                  oauthlib.oauth2.RequestValidator.
+        :param token_expires_in: An int or a function to generate a token
+                                 expiration offset (in seconds) given a
+                                 oauthlib.common.Request object.
+        :param token_generator: A function to generate a token from a request.
+        :param refresh_token_generator: A function to generate a token from a
+                                        request for the refresh token.
+        :param kwargs: Extra parameters to pass to authorization-,
+                       token-, resource-, and revocation-endpoint constructors.
+        """
+        auth_grant = AuthorizationCodeGrant(request_validator)
+        refresh_grant = RefreshTokenGrant(request_validator)
+        bearer = BearerToken(request_validator, token_generator,
+                             token_expires_in, refresh_token_generator)
+        AuthorizationEndpoint.__init__(self, default_response_type='code',
+                                       response_types={'code': auth_grant},
+                                       default_token_type=bearer)
+        TokenEndpoint.__init__(self, default_grant_type='authorization_code',
+                               grant_types={
+                                   'authorization_code': auth_grant,
+                                   'refresh_token': refresh_grant,
+                               },
+                               default_token_type=bearer)
+        ResourceEndpoint.__init__(self, default_token='Bearer',
+                                  token_types={'Bearer': bearer})
+        RevocationEndpoint.__init__(self, request_validator)
+
+
+class MobileApplicationServer(AuthorizationEndpoint, ResourceEndpoint,
+                              RevocationEndpoint):
+
+    """An all-in-one endpoint featuring Implicit code grant and Bearer tokens."""
+
+    def __init__(self, request_validator, token_generator=None,
+                 token_expires_in=None, refresh_token_generator=None, **kwargs):
+        """Construct a new implicit grant server.
+
+        :param request_validator: An implementation of
+                                  oauthlib.oauth2.RequestValidator.
+        :param token_expires_in: An int or a function to generate a token
+                                 expiration offset (in seconds) given a
+                                 oauthlib.common.Request object.
+        :param token_generator: A function to generate a token from a request.
+        :param refresh_token_generator: A function to generate a token from a
+                                        request for the refresh token.
+        :param kwargs: Extra parameters to pass to authorization-,
+                       token-, resource-, and revocation-endpoint constructors.
+        """
+        implicit_grant = ImplicitGrant(request_validator)
+        bearer = BearerToken(request_validator, token_generator,
+                             token_expires_in, refresh_token_generator)
+        AuthorizationEndpoint.__init__(self, default_response_type='token',
+                                       response_types={
+                                           'token': implicit_grant},
+                                       default_token_type=bearer)
+        ResourceEndpoint.__init__(self, default_token='Bearer',
+                                  token_types={'Bearer': bearer})
+        RevocationEndpoint.__init__(self, request_validator,
+                                    supported_token_types=['access_token'])
+
+
+class LegacyApplicationServer(TokenEndpoint, ResourceEndpoint,
+                              RevocationEndpoint):
+
+    """An all-in-one endpoint featuring Resource Owner Password Credentials grant and Bearer tokens."""
+
+    def __init__(self, request_validator, token_generator=None,
+                 token_expires_in=None, refresh_token_generator=None, **kwargs):
+        """Construct a resource owner password credentials grant server.
+
+        :param request_validator: An implementation of
+                                  oauthlib.oauth2.RequestValidator.
+        :param token_expires_in: An int or a function to generate a token
+                                 expiration offset (in seconds) given a
+                                 oauthlib.common.Request object.
+        :param token_generator: A function to generate a token from a request.
+        :param refresh_token_generator: A function to generate a token from a
+                                        request for the refresh token.
+        :param kwargs: Extra parameters to pass to authorization-,
+                       token-, resource-, and revocation-endpoint constructors.
+        """
+        password_grant = ResourceOwnerPasswordCredentialsGrant(
+            request_validator)
+        refresh_grant = RefreshTokenGrant(request_validator)
+        bearer = BearerToken(request_validator, token_generator,
+                             token_expires_in, refresh_token_generator)
+        TokenEndpoint.__init__(self, default_grant_type='password',
+                               grant_types={
+                                   'password': password_grant,
+                                   'refresh_token': refresh_grant,
+                               },
+                               default_token_type=bearer)
+        ResourceEndpoint.__init__(self, default_token='Bearer',
+                                  token_types={'Bearer': bearer})
+        RevocationEndpoint.__init__(self, request_validator)
+
+
+class BackendApplicationServer(TokenEndpoint, ResourceEndpoint,
+                               RevocationEndpoint):
+
+    """An all-in-one endpoint featuring Client Credentials grant and Bearer tokens."""
+
+    def __init__(self, request_validator, token_generator=None,
+                 token_expires_in=None, refresh_token_generator=None, **kwargs):
+        """Construct a client credentials grant server.
+
+        :param request_validator: An implementation of
+                                  oauthlib.oauth2.RequestValidator.
+        :param token_expires_in: An int or a function to generate a token
+                                 expiration offset (in seconds) given a
+                                 oauthlib.common.Request object.
+        :param token_generator: A function to generate a token from a request.
+        :param refresh_token_generator: A function to generate a token from a
+                                        request for the refresh token.
+        :param kwargs: Extra parameters to pass to authorization-,
+                       token-, resource-, and revocation-endpoint constructors.
+        """
+        credentials_grant = ClientCredentialsGrant(request_validator)
+        bearer = BearerToken(request_validator, token_generator,
+                             token_expires_in, refresh_token_generator)
+        TokenEndpoint.__init__(self, default_grant_type='client_credentials',
+                               grant_types={
+                                   'client_credentials': credentials_grant},
+                               default_token_type=bearer)
+        ResourceEndpoint.__init__(self, default_token='Bearer',
+                                  token_types={'Bearer': bearer})
+        RevocationEndpoint.__init__(self, request_validator,
+                                    supported_token_types=['access_token'])
diff --git a/oauthlib/oauth2/rfc6749/endpoints/resource.py b/oauthlib/oauth2/rfc6749/endpoints/resource.py
new file mode 100644 (file)
index 0000000..d03ed21
--- /dev/null
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+log = logging.getLogger(__name__)
+
+
+class ResourceEndpoint(BaseEndpoint):
+
+    """Authorizes access to protected resources.
+
+    The client accesses protected resources by presenting the access
+    token to the resource server.  The resource server MUST validate the
+    access token and ensure that it has not expired and that its scope
+    covers the requested resource.  The methods used by the resource
+    server to validate the access token (as well as any error responses)
+    are beyond the scope of this specification but generally involve an
+    interaction or coordination between the resource server and the
+    authorization server::
+
+        # For most cases, returning a 403 should suffice.
+
+    The method in which the client utilizes the access token to
+    authenticate with the resource server depends on the type of access
+    token issued by the authorization server.  Typically, it involves
+    using the HTTP "Authorization" request header field [RFC2617] with an
+    authentication scheme defined by the specification of the access
+    token type used, such as [RFC6750]::
+
+        # Access tokens may also be provided in query and body
+        https://example.com/protected?access_token=kjfch2345sdf   # Query
+        access_token=sdf23409df   # Body
+    """
+
+    def __init__(self, default_token, token_types):
+        BaseEndpoint.__init__(self)
+        self._tokens = token_types
+        self._default_token = default_token
+
+    @property
+    def default_token(self):
+        return self._default_token
+
+    @property
+    def default_token_type_handler(self):
+        return self.tokens.get(self.default_token)
+
+    @property
+    def tokens(self):
+        return self._tokens
+
+    @catch_errors_and_unavailability
+    def verify_request(self, uri, http_method='GET', body=None, headers=None,
+                       scopes=None):
+        """Validate client, code etc, return body + headers"""
+        request = Request(uri, http_method, body, headers)
+        request.token_type = self.find_token_type(request)
+        request.scopes = scopes
+        token_type_handler = self.tokens.get(request.token_type,
+                                             self.default_token_type_handler)
+        log.debug('Dispatching token_type %s request to %r.',
+                  request.token_type, token_type_handler)
+        return token_type_handler.validate_request(request), request
+
+    def find_token_type(self, request):
+        """Token type identification.
+
+        RFC 6749 does not provide a method for easily differentiating between
+        different token types during protected resource access. We estimate
+        the most likely token type (if any) by asking each known token type
+        to give an estimation based on the request.
+        """
+        estimates = sorted(((t.estimate_type(request), n)
+                            for n, t in self.tokens.items()))
+        return estimates[0][1] if len(estimates) else None
diff --git a/oauthlib/oauth2/rfc6749/endpoints/revocation.py b/oauthlib/oauth2/rfc6749/endpoints/revocation.py
new file mode 100644 (file)
index 0000000..b73131c
--- /dev/null
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.endpoint.revocation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An implementation of the OAuth 2 `Token Revocation`_ spec (draft 11).
+
+.. _`Token Revocation`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+from ..errors import InvalidClientError, UnsupportedTokenTypeError
+from ..errors import InvalidRequestError, OAuth2Error
+
+log = logging.getLogger(__name__)
+
+
+class RevocationEndpoint(BaseEndpoint):
+
+    """Token revocation endpoint.
+
+    Endpoint used by authenticated clients to revoke access and refresh tokens.
+    Commonly this will be part of the Authorization Endpoint.
+    """
+
+    valid_token_types = ('access_token', 'refresh_token')
+
+    def __init__(self, request_validator, supported_token_types=None,
+            enable_jsonp=False):
+        BaseEndpoint.__init__(self)
+        self.request_validator = request_validator
+        self.supported_token_types = (
+            supported_token_types or self.valid_token_types)
+        self.enable_jsonp = enable_jsonp
+
+    @catch_errors_and_unavailability
+    def create_revocation_response(self, uri, http_method='POST', body=None,
+                                   headers=None):
+        """Revoke supplied access or refresh token.
+
+
+        The authorization server responds with HTTP status code 200 if the
+        token has been revoked sucessfully or if the client submitted an
+        invalid token.
+
+        Note: invalid tokens do not cause an error response since the client
+        cannot handle such an error in a reasonable way.  Moreover, the purpose
+        of the revocation request, invalidating the particular token, is
+        already achieved.
+
+        The content of the response body is ignored by the client as all
+        necessary information is conveyed in the response code.
+
+        An invalid token type hint value is ignored by the authorization server
+        and does not influence the revocation response.
+        """
+        request = Request(
+            uri, http_method=http_method, body=body, headers=headers)
+        try:
+            self.validate_revocation_request(request)
+            log.debug('Token revocation valid for %r.', request)
+        except OAuth2Error as e:
+            log.debug('Client error during validation of %r. %r.', request, e)
+            response_body = e.json
+            if self.enable_jsonp and request.callback:
+                response_body = '%s(%s);' % (request.callback, response_body)
+            return {}, response_body, e.status_code
+
+        self.request_validator.revoke_token(request.token,
+                                            request.token_type_hint, request)
+
+        response_body = None
+        if self.enable_jsonp and request.callback:
+            response_body = request.callback + '();'
+        return {}, response_body, 200
+
+    def validate_revocation_request(self, request):
+        """Ensure the request is valid.
+
+        The client constructs the request by including the following parameters
+        using the "application/x-www-form-urlencoded" format in the HTTP
+        request entity-body:
+
+        token (REQUIRED).  The token that the client wants to get revoked.
+
+        token_type_hint (OPTIONAL).  A hint about the type of the token
+        submitted for revocation.  Clients MAY pass this parameter in order to
+        help the authorization server to optimize the token lookup.  If the
+        server is unable to locate the token using the given hint, it MUST
+        extend its search accross all of its supported token types.  An
+        authorization server MAY ignore this parameter, particularly if it is
+        able to detect the token type automatically.  This specification
+        defines two such values:
+
+                *  access_token: An Access Token as defined in [RFC6749],
+                    `section 1.4`_
+
+                *  refresh_token: A Refresh Token as defined in [RFC6749],
+                    `section 1.5`_
+
+                Specific implementations, profiles, and extensions of this
+                specification MAY define other values for this parameter using
+                the registry defined in `Section 4.1.2`_.
+
+        The client also includes its authentication credentials as described in
+        `Section 2.3`_. of [`RFC6749`_].
+
+        .. _`section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
+        .. _`section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
+        .. _`section 2.3`: http://tools.ietf.org/html/rfc6749#section-2.3
+        .. _`Section 4.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-revocation-11#section-4.1.2
+        .. _`RFC6749`: http://tools.ietf.org/html/rfc6749
+        """
+        if not request.token:
+            raise InvalidRequestError(request=request,
+                                      description='Missing token parameter.')
+
+        if not self.request_validator.authenticate_client(request):
+            raise InvalidClientError(request=request)
+
+        if (request.token_type_hint and
+                request.token_type_hint in self.valid_token_types and
+                request.token_type_hint not in self.supported_token_types):
+            raise UnsupportedTokenTypeError(request=request)
diff --git a/oauthlib/oauth2/rfc6749/endpoints/token.py b/oauthlib/oauth2/rfc6749/endpoints/token.py
new file mode 100644 (file)
index 0000000..cda56ad
--- /dev/null
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749
+~~~~~~~~~~~~~~~~~~~~~~~
+
+This module is an implementation of various logic needed
+for consuming and providing OAuth 2.0 RFC6749.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import logging
+
+from oauthlib.common import Request
+
+from .base import BaseEndpoint, catch_errors_and_unavailability
+
+
+log = logging.getLogger(__name__)
+
+
+class TokenEndpoint(BaseEndpoint):
+
+    """Token issuing endpoint.
+
+    The token endpoint is used by the client to obtain an access token by
+    presenting its authorization grant or refresh token.  The token
+    endpoint is used with every authorization grant except for the
+    implicit grant type (since an access token is issued directly).
+
+    The means through which the client obtains the location of the token
+    endpoint are beyond the scope of this specification, but the location
+    is typically provided in the service documentation.
+
+    The endpoint URI MAY include an "application/x-www-form-urlencoded"
+    formatted (per `Appendix B`_) query component,
+    which MUST be retained when adding additional query parameters.  The
+    endpoint URI MUST NOT include a fragment component::
+
+        https://example.com/path?query=component             # OK
+        https://example.com/path?query=component#fragment    # Not OK
+
+    Since requests to the authorization endpoint result in user
+    Since requests to the token endpoint result in the transmission of
+    clear-text credentials (in the HTTP request and response), the
+    authorization server MUST require the use of TLS as described in
+    Section 1.6 when sending requests to the token endpoint::
+
+        # We will deny any request which URI schema is not with https
+
+    The client MUST use the HTTP "POST" method when making access token
+    requests::
+
+        # HTTP method is currently not enforced
+
+    Parameters sent without a value MUST be treated as if they were
+    omitted from the request.  The authorization server MUST ignore
+    unrecognized request parameters.  Request and response parameters
+    MUST NOT be included more than once::
+
+        # Delegated to each grant type.
+
+    .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+    """
+
+    def __init__(self, default_grant_type, default_token_type, grant_types):
+        BaseEndpoint.__init__(self)
+        self._grant_types = grant_types
+        self._default_token_type = default_token_type
+        self._default_grant_type = default_grant_type
+
+    @property
+    def grant_types(self):
+        return self._grant_types
+
+    @property
+    def default_grant_type(self):
+        return self._default_grant_type
+
+    @property
+    def default_grant_type_handler(self):
+        return self.grant_types.get(self.default_grant_type)
+
+    @property
+    def default_token_type(self):
+        return self._default_token_type
+
+    @catch_errors_and_unavailability
+    def create_token_response(self, uri, http_method='GET', body=None,
+                              headers=None, credentials=None):
+        """Extract grant_type and route to the designated handler."""
+        request = Request(
+            uri, http_method=http_method, body=body, headers=headers)
+        request.scopes = None
+        request.extra_credentials = credentials
+        grant_type_handler = self.grant_types.get(request.grant_type,
+                                                  self.default_grant_type_handler)
+        log.debug('Dispatching grant_type %s request to %r.',
+                  request.grant_type, grant_type_handler)
+        return grant_type_handler.create_token_response(
+            request, self.default_token_type)
diff --git a/oauthlib/oauth2/rfc6749/errors.py b/oauthlib/oauth2/rfc6749/errors.py
new file mode 100644 (file)
index 0000000..a21d0bd
--- /dev/null
@@ -0,0 +1,259 @@
+# coding=utf-8
+"""
+oauthlib.oauth2.rfc6749.errors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Error used both by OAuth 2 clients and provicers to represent the spec
+defined error responses for all four core grant types.
+"""
+from __future__ import unicode_literals
+import json
+from oauthlib.common import urlencode, add_params_to_uri
+
+
+class OAuth2Error(Exception):
+    error = None
+    status_code = 400
+    description = ''
+
+    def __init__(self, description=None, uri=None, state=None, status_code=None,
+                 request=None):
+        """
+        description:    A human-readable ASCII [USASCII] text providing
+                        additional information, used to assist the client
+                        developer in understanding the error that occurred.
+                        Values for the "error_description" parameter MUST NOT
+                        include characters outside the set
+                        x20-21 / x23-5B / x5D-7E.
+
+        uri:    A URI identifying a human-readable web page with information
+                about the error, used to provide the client developer with
+                additional information about the error.  Values for the
+                "error_uri" parameter MUST conform to the URI- Reference
+                syntax, and thus MUST NOT include characters outside the set
+                x21 / x23-5B / x5D-7E.
+
+        state:  A CSRF protection value received from the client.
+
+        request:  Oauthlib Request object
+        """
+        self.description = description or self.description
+        message = '(%s) %s' % (self.error, self.description)
+        if request:
+            message += ' ' + repr(request)
+        super(OAuth2Error, self).__init__(message)
+
+        self.uri = uri
+        self.state = state
+
+        if status_code:
+            self.status_code = status_code
+
+        if request:
+            self.redirect_uri = request.redirect_uri
+            self.client_id = request.client_id
+            self.scopes = request.scopes
+            self.response_type = request.response_type
+            self.grant_type = request.grant_type
+            if not state:
+                self.state = request.state
+
+    def in_uri(self, uri):
+        return add_params_to_uri(uri, self.twotuples)
+
+    @property
+    def twotuples(self):
+        error = [('error', self.error)]
+        if self.description:
+            error.append(('error_description', self.description))
+        if self.uri:
+            error.append(('error_uri', self.uri))
+        if self.state:
+            error.append(('state', self.state))
+        return error
+
+    @property
+    def urlencoded(self):
+        return urlencode(self.twotuples)
+
+    @property
+    def json(self):
+        return json.dumps(dict(self.twotuples))
+
+
+class TokenExpiredError(OAuth2Error):
+    error = 'token_expired'
+
+
+class InsecureTransportError(OAuth2Error):
+    error = 'insecure_transport'
+    description = 'OAuth 2 MUST utilize https.'
+
+
+class MismatchingStateError(OAuth2Error):
+    error = 'mismatching_state'
+    description = 'CSRF Warning! State not equal in request and response.'
+
+
+class MissingCodeError(OAuth2Error):
+    error = 'missing_code'
+
+
+class MissingTokenError(OAuth2Error):
+    error = 'missing_token'
+
+
+class MissingTokenTypeError(OAuth2Error):
+    error = 'missing_token_type'
+
+
+class FatalClientError(OAuth2Error):
+
+    """Errors during authorization where user should not be redirected back.
+
+    If the request fails due to a missing, invalid, or mismatching
+    redirection URI, or if the client identifier is missing or invalid,
+    the authorization server SHOULD inform the resource owner of the
+    error and MUST NOT automatically redirect the user-agent to the
+    invalid redirection URI.
+
+    Instead the user should be informed of the error by the provider itself.
+    """
+    pass
+
+
+class InvalidRedirectURIError(FatalClientError):
+    error = 'invalid_redirect_uri'
+
+
+class MissingRedirectURIError(FatalClientError):
+    error = 'missing_redirect_uri'
+
+
+class MismatchingRedirectURIError(FatalClientError):
+    error = 'mismatching_redirect_uri'
+
+
+class MissingClientIdError(FatalClientError):
+    error = 'invalid_client_id'
+
+
+class InvalidClientIdError(FatalClientError):
+    error = 'invalid_client_id'
+
+
+class InvalidRequestError(OAuth2Error):
+
+    """The request is missing a required parameter, includes an invalid
+    parameter value, includes a parameter more than once, or is
+    otherwise malformed.
+    """
+    error = 'invalid_request'
+
+
+class AccessDeniedError(OAuth2Error):
+
+    """The resource owner or authorization server denied the request."""
+    error = 'access_denied'
+    status_code = 401
+
+
+class UnsupportedResponseTypeError(OAuth2Error):
+
+    """The authorization server does not support obtaining an authorization
+    code using this method.
+    """
+    error = 'unsupported_response_type'
+
+
+class InvalidScopeError(OAuth2Error):
+
+    """The requested scope is invalid, unknown, or malformed."""
+    error = 'invalid_scope'
+    status_code = 401
+
+
+class ServerError(OAuth2Error):
+
+    """The authorization server encountered an unexpected condition that
+    prevented it from fulfilling the request.  (This error code is needed
+    because a 500 Internal Server Error HTTP status code cannot be returned
+    to the client via a HTTP redirect.)
+    """
+    error = 'server_error'
+
+
+class TemporarilyUnavailableError(OAuth2Error):
+
+    """The authorization server is currently unable to handle the request
+    due to a temporary overloading or maintenance of the server.
+    (This error code is needed because a 503 Service Unavailable HTTP
+    status code cannot be returned to the client via a HTTP redirect.)
+    """
+    error = 'temporarily_unavailable'
+
+
+class InvalidClientError(OAuth2Error):
+
+    """Client authentication failed (e.g. unknown client, no client
+    authentication included, or unsupported authentication method).
+    The authorization server MAY return an HTTP 401 (Unauthorized) status
+    code to indicate which HTTP authentication schemes are supported.
+    If the client attempted to authenticate via the "Authorization" request
+    header field, the authorization server MUST respond with an
+    HTTP 401 (Unauthorized) status code, and include the "WWW-Authenticate"
+    response header field matching the authentication scheme used by the
+    client.
+    """
+    error = 'invalid_client'
+    status_code = 401
+
+
+class InvalidGrantError(OAuth2Error):
+
+    """The provided authorization grant (e.g. authorization code, resource
+    owner credentials) or refresh token is invalid, expired, revoked, does
+    not match the redirection URI used in the authorization request, or was
+    issued to another client.
+    """
+    error = 'invalid_grant'
+    status_code = 401
+
+
+class UnauthorizedClientError(OAuth2Error):
+
+    """The authenticated client is not authorized to use this authorization
+    grant type.
+    """
+    error = 'unauthorized_client'
+    status_code = 401
+
+
+class UnsupportedGrantTypeError(OAuth2Error):
+
+    """The authorization grant type is not supported by the authorization
+    server.
+    """
+    error = 'unsupported_grant_type'
+
+
+class UnsupportedTokenTypeError(OAuth2Error):
+
+    """The authorization server does not support the revocation of the
+    presented token type.  I.e. the client tried to revoke an access token
+    on a server not supporting this feature.
+    """
+    error = 'unsupported_token_type'
+
+
+def raise_from_error(error, params=None):
+    import inspect
+    import sys
+    kwargs = {
+        'description': params.get('error_description'),
+        'uri': params.get('error_uri'),
+        'state': params.get('state')
+    }
+    for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
+        if cls.error == error:
+            raise cls(**kwargs)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/__init__.py b/oauthlib/oauth2/rfc6749/grant_types/__init__.py
new file mode 100644 (file)
index 0000000..2ec8e4f
--- /dev/null
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+from .authorization_code import AuthorizationCodeGrant
+from .implicit import ImplicitGrant
+from .resource_owner_password_credentials import ResourceOwnerPasswordCredentialsGrant
+from .client_credentials import ClientCredentialsGrant
+from .refresh_token import RefreshTokenGrant
diff --git a/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py b/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py
new file mode 100644 (file)
index 0000000..b6ff07c
--- /dev/null
@@ -0,0 +1,393 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from oauthlib import common
+from oauthlib.uri_validate import is_absolute_uri
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class AuthorizationCodeGrant(GrantTypeBase):
+
+    """`Authorization Code Grant`_
+
+    The authorization code grant type is used to obtain both access
+    tokens and refresh tokens and is optimized for confidential clients.
+    Since this is a redirection-based flow, the client must be capable of
+    interacting with the resource owner's user-agent (typically a web
+    browser) and capable of receiving incoming requests (via redirection)
+    from the authorization server::
+
+        +----------+
+        | Resource |
+        |   Owner  |
+        |          |
+        +----------+
+             ^
+             |
+            (B)
+        +----|-----+          Client Identifier      +---------------+
+        |         -+----(A)-- & Redirection URI ---->|               |
+        |  User-   |                                 | Authorization |
+        |  Agent  -+----(B)-- User authenticates --->|     Server    |
+        |          |                                 |               |
+        |         -+----(C)-- Authorization Code ---<|               |
+        +-|----|---+                                 +---------------+
+          |    |                                         ^      v
+         (A)  (C)                                        |      |
+          |    |                                         |      |
+          ^    v                                         |      |
+        +---------+                                      |      |
+        |         |>---(D)-- Authorization Code ---------'      |
+        |  Client |          & Redirection URI                  |
+        |         |                                             |
+        |         |<---(E)----- Access Token -------------------'
+        +---------+       (w/ Optional Refresh Token)
+
+    Note: The lines illustrating steps (A), (B), and (C) are broken into
+    two parts as they pass through the user-agent.
+
+    Figure 3: Authorization Code Flow
+
+    The flow illustrated in Figure 3 includes the following steps:
+
+    (A)  The client initiates the flow by directing the resource owner's
+         user-agent to the authorization endpoint.  The client includes
+         its client identifier, requested scope, local state, and a
+         redirection URI to which the authorization server will send the
+         user-agent back once access is granted (or denied).
+
+    (B)  The authorization server authenticates the resource owner (via
+         the user-agent) and establishes whether the resource owner
+         grants or denies the client's access request.
+
+    (C)  Assuming the resource owner grants access, the authorization
+         server redirects the user-agent back to the client using the
+         redirection URI provided earlier (in the request or during
+         client registration).  The redirection URI includes an
+         authorization code and any local state provided by the client
+         earlier.
+
+    (D)  The client requests an access token from the authorization
+         server's token endpoint by including the authorization code
+         received in the previous step.  When making the request, the
+         client authenticates with the authorization server.  The client
+         includes the redirection URI used to obtain the authorization
+         code for verification.
+
+    (E)  The authorization server authenticates the client, validates the
+         authorization code, and ensures that the redirection URI
+         received matches the URI used to redirect the client in
+         step (C).  If valid, the authorization server responds back with
+         an access token and, optionally, a refresh token.
+
+    .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1
+    """
+
+    def __init__(self, request_validator=None):
+        self.request_validator = request_validator or RequestValidator()
+
+    def create_authorization_code(self, request):
+        """Generates an authorization grant represented as a dictionary."""
+        grant = {'code': common.generate_token()}
+        if hasattr(request, 'state') and request.state:
+            grant['state'] = request.state
+        log.debug('Created authorization code grant %r for request %r.',
+                  grant, request)
+        return grant
+
+    def create_authorization_response(self, request, token_handler):
+        """
+        The client constructs the request URI by adding the following
+        parameters to the query component of the authorization endpoint URI
+        using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+        response_type
+                REQUIRED.  Value MUST be set to "code".
+        client_id
+                REQUIRED.  The client identifier as described in `Section 2.2`_.
+        redirect_uri
+                OPTIONAL.  As described in `Section 3.1.2`_.
+        scope
+                OPTIONAL.  The scope of the access request as described by
+                `Section 3.3`_.
+        state
+                RECOMMENDED.  An opaque value used by the client to maintain
+                state between the request and callback.  The authorization
+                server includes this value when redirecting the user-agent back
+                to the client.  The parameter SHOULD be used for preventing
+                cross-site request forgery as described in `Section 10.12`_.
+
+        The client directs the resource owner to the constructed URI using an
+        HTTP redirection response, or by other means available to it via the
+        user-agent.
+
+        :param request: oauthlib.commong.Request
+        :param token_handler: A token handler instace, for example of type
+                              oauthlib.oauth2.BearerToken.
+        :returns: headers, body, status
+        :raises: FatalClientError on invalid redirect URI or client id.
+                 ValueError if scopes are not set on the request object.
+
+        A few examples::
+
+            >>> from your_validator import your_validator
+            >>> request = Request('https://example.com/authorize?client_id=valid'
+            ...                   '&redirect_uri=http%3A%2F%2Fclient.com%2F')
+            >>> from oauthlib.common import Request
+            >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
+            >>> token = BearerToken(your_validator)
+            >>> grant = AuthorizationCodeGrant(your_validator)
+            >>> grant.create_authorization_response(request, token)
+            Traceback (most recent call last):
+                File "<stdin>", line 1, in <module>
+                File "oauthlib/oauth2/rfc6749/grant_types.py", line 513, in create_authorization_response
+                    raise ValueError('Scopes must be set on post auth.')
+            ValueError: Scopes must be set on post auth.
+            >>> request.scopes = ['authorized', 'in', 'some', 'form']
+            >>> grant.create_authorization_response(request, token)
+            (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
+            >>> request = Request('https://example.com/authorize?client_id=valid'
+            ...                   '&redirect_uri=http%3A%2F%2Fclient.com%2F'
+            ...                   '&response_type=code')
+            >>> request.scopes = ['authorized', 'in', 'some', 'form']
+            >>> grant.create_authorization_response(request, token)
+            (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200)
+            >>> # If the client id or redirect uri fails validation
+            >>> grant.create_authorization_response(request, token)
+            Traceback (most recent call last):
+                File "<stdin>", line 1, in <module>
+                File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response
+                    >>> grant.create_authorization_response(request, token)
+                File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
+            oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
+
+        .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+        .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+        .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+        """
+        try:
+            # request.scopes is only mandated in post auth and both pre and
+            # post auth use validate_authorization_request
+            if not request.scopes:
+                raise ValueError('Scopes must be set on post auth.')
+
+            self.validate_authorization_request(request)
+            log.debug('Pre resource owner authorization validation ok for %r.',
+                      request)
+
+        # If the request fails due to a missing, invalid, or mismatching
+        # redirection URI, or if the client identifier is missing or invalid,
+        # the authorization server SHOULD inform the resource owner of the
+        # error and MUST NOT automatically redirect the user-agent to the
+        # invalid redirection URI.
+        except errors.FatalClientError as e:
+            log.debug('Fatal client error during validation of %r. %r.',
+                      request, e)
+            raise
+
+        # If the resource owner denies the access request or if the request
+        # fails for reasons other than a missing or invalid redirection URI,
+        # the authorization server informs the client by adding the following
+        # parameters to the query component of the redirection URI using the
+        # "application/x-www-form-urlencoded" format, per Appendix B:
+        # http://tools.ietf.org/html/rfc6749#appendix-B
+        except errors.OAuth2Error as e:
+            log.debug('Client error during validation of %r. %r.', request, e)
+            request.redirect_uri = request.redirect_uri or self.error_uri
+            return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples)}, None, 302
+
+        grant = self.create_authorization_code(request)
+        log.debug('Saving grant %r for %r.', grant, request)
+        self.request_validator.save_authorization_code(
+            request.client_id, grant, request)
+        return {'Location': common.add_params_to_uri(request.redirect_uri, grant.items())}, None, 302
+
+    def create_token_response(self, request, token_handler):
+        """Validate the authorization code.
+
+        The client MUST NOT use the authorization code more than once. If an
+        authorization code is used more than once, the authorization server
+        MUST deny the request and SHOULD revoke (when possible) all tokens
+        previously issued based on that authorization code. The authorization
+        code is bound to the client identifier and redirection URI.
+        """
+        headers = {
+            'Content-Type': 'application/json',
+            'Cache-Control': 'no-store',
+            'Pragma': 'no-cache',
+        }
+        try:
+            self.validate_token_request(request)
+            log.debug('Token request validation ok for %r.', request)
+        except errors.OAuth2Error as e:
+            log.debug('Client error during validation of %r. %r.', request, e)
+            return headers, e.json, e.status_code
+
+        token = token_handler.create_token(request, refresh_token=True)
+        self.request_validator.invalidate_authorization_code(
+            request.client_id, request.code, request)
+        return headers, json.dumps(token), 200
+
+    def validate_authorization_request(self, request):
+        """Check the authorization request for normal and fatal errors.
+
+        A normal error could be a missing response_type parameter or the client
+        attempting to access scope it is not allowed to ask authorization for.
+        Normal errors can safely be included in the redirection URI and
+        sent back to the client.
+
+        Fatal errors occur when the client_id or redirect_uri is invalid or
+        missing. These must be caught by the provider and handled, how this
+        is done is outside of the scope of OAuthLib but showing an error
+        page describing the issue is a good idea.
+        """
+
+        # First check for fatal errors
+
+        # If the request fails due to a missing, invalid, or mismatching
+        # redirection URI, or if the client identifier is missing or invalid,
+        # the authorization server SHOULD inform the resource owner of the
+        # error and MUST NOT automatically redirect the user-agent to the
+        # invalid redirection URI.
+
+        # REQUIRED. The client identifier as described in Section 2.2.
+        # http://tools.ietf.org/html/rfc6749#section-2.2
+        if not request.client_id:
+            raise errors.MissingClientIdError(request=request)
+
+        if not self.request_validator.validate_client_id(request.client_id, request):
+            raise errors.InvalidClientIdError(request=request)
+
+        # OPTIONAL. As described in Section 3.1.2.
+        # http://tools.ietf.org/html/rfc6749#section-3.1.2
+        log.debug('Validating redirection uri %s for client %s.',
+                  request.redirect_uri, request.client_id)
+        if request.redirect_uri is not None:
+            request.using_default_redirect_uri = False
+            log.debug('Using provided redirect_uri %s', request.redirect_uri)
+            if not is_absolute_uri(request.redirect_uri):
+                raise errors.InvalidRedirectURIError(request=request)
+
+            if not self.request_validator.validate_redirect_uri(
+                    request.client_id, request.redirect_uri, request):
+                raise errors.MismatchingRedirectURIError(request=request)
+        else:
+            request.redirect_uri = self.request_validator.get_default_redirect_uri(
+                request.client_id, request)
+            request.using_default_redirect_uri = True
+            log.debug('Using default redirect_uri %s.', request.redirect_uri)
+            if not request.redirect_uri:
+                raise errors.MissingRedirectURIError(request=request)
+
+        # Then check for normal errors.
+
+        # If the resource owner denies the access request or if the request
+        # fails for reasons other than a missing or invalid redirection URI,
+        # the authorization server informs the client by adding the following
+        # parameters to the query component of the redirection URI using the
+        # "application/x-www-form-urlencoded" format, per Appendix B.
+        # http://tools.ietf.org/html/rfc6749#appendix-B
+
+        # Note that the correct parameters to be added are automatically
+        # populated through the use of specific exceptions.
+        if request.response_type is None:
+            raise errors.InvalidRequestError(description='Missing response_type parameter.', request=request)
+
+        for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+            if param in request.duplicate_params:
+                raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+
+        if not self.request_validator.validate_response_type(request.client_id,
+                                                             request.response_type, request.client, request):
+            log.debug('Client %s is not authorized to use response_type %s.',
+                      request.client_id, request.response_type)
+            raise errors.UnauthorizedClientError(request=request)
+
+        # REQUIRED. Value MUST be set to "code".
+        if request.response_type != 'code':
+            raise errors.UnsupportedResponseTypeError(request=request)
+
+        # OPTIONAL. The scope of the access request as described by Section 3.3
+        # http://tools.ietf.org/html/rfc6749#section-3.3
+        self.validate_scopes(request)
+
+        return request.scopes, {
+            'client_id': request.client_id,
+            'redirect_uri': request.redirect_uri,
+            'response_type': request.response_type,
+            'state': request.state,
+            'request': request,
+        }
+
+    def validate_token_request(self, request):
+        # REQUIRED. Value MUST be set to "authorization_code".
+        if request.grant_type != 'authorization_code':
+            raise errors.UnsupportedGrantTypeError(request=request)
+
+        if request.code is None:
+            raise errors.InvalidRequestError(
+                description='Missing code parameter.', request=request)
+
+        for param in ('client_id', 'grant_type', 'redirect_uri'):
+            if param in request.duplicate_params:
+                raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
+                                                 request=request)
+
+        if self.request_validator.client_authentication_required(request):
+            # If the client type is confidential or the client was issued client
+            # credentials (or assigned other authentication requirements), the
+            # client MUST authenticate with the authorization server as described
+            # in Section 3.2.1.
+            # http://tools.ietf.org/html/rfc6749#section-3.2.1
+            if not self.request_validator.authenticate_client(request):
+                log.debug('Client authentication failed, %r.', request)
+                raise errors.InvalidClientError(request=request)
+        elif not self.request_validator.authenticate_client_id(request.client_id, request):
+            # REQUIRED, if the client is not authenticating with the
+            # authorization server as described in Section 3.2.1.
+            # http://tools.ietf.org/html/rfc6749#section-3.2.1
+            log.debug('Client authentication failed, %r.', request)
+            raise errors.InvalidClientError(request=request)
+
+        if not hasattr(request.client, 'client_id'):
+            raise NotImplementedError('Authenticate client must set the '
+                                      'request.client.client_id attribute '
+                                      'in authenticate_client.')
+
+        # Ensure client is authorized use of this grant type
+        self.validate_grant_type(request)
+
+        # REQUIRED. The authorization code received from the
+        # authorization server.
+        if not self.request_validator.validate_code(request.client_id,
+                                                    request.code, request.client, request):
+            log.debug('Client, %r (%r), is not allowed access to scopes %r.',
+                      request.client_id, request.client, request.scopes)
+            raise errors.InvalidGrantError(request=request)
+
+        for attr in ('user', 'state', 'scopes'):
+            if getattr(request, attr) is None:
+                log.debug('request.%s was not set on code validation.', attr)
+
+        # REQUIRED, if the "redirect_uri" parameter was included in the
+        # authorization request as described in Section 4.1.1, and their
+        # values MUST be identical.
+        if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
+                                                           request.redirect_uri, request.client):
+            log.debug('Redirect_uri (%r) invalid for client %r (%r).',
+                      request.redirect_uri, request.client_id, request.client)
+            raise errors.AccessDeniedError(request=request)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/base.py b/oauthlib/oauth2/rfc6749/grant_types/base.py
new file mode 100644 (file)
index 0000000..4a8017f
--- /dev/null
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import logging
+
+from oauthlib.oauth2.rfc6749 import errors, utils
+
+log = logging.getLogger(__name__)
+
+
+class GrantTypeBase(object):
+    error_uri = None
+    request_validator = None
+
+    def create_authorization_response(self, request, token_handler):
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def create_token_response(self, request, token_handler):
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_grant_type(self, request):
+        if not self.request_validator.validate_grant_type(request.client_id,
+                                                          request.grant_type, request.client, request):
+            log.debug('Unauthorized from %r (%r) access to grant type %s.',
+                      request.client_id, request.client, request.grant_type)
+            raise errors.UnauthorizedClientError(request=request)
+
+    def validate_scopes(self, request):
+        if not request.scopes:
+            request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list(
+                self.request_validator.get_default_scopes(request.client_id, request))
+        log.debug('Validating access to scopes %r for client %r (%r).',
+                  request.scopes, request.client_id, request.client)
+        if not self.request_validator.validate_scopes(request.client_id,
+                                                      request.scopes, request.client, request):
+            raise errors.InvalidScopeError(request=request)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/client_credentials.py
new file mode 100644 (file)
index 0000000..30df247
--- /dev/null
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class ClientCredentialsGrant(GrantTypeBase):
+
+    """`Client Credentials Grant`_
+
+    The client can request an access token using only its client
+    credentials (or other supported means of authentication) when the
+    client is requesting access to the protected resources under its
+    control, or those of another resource owner that have been previously
+    arranged with the authorization server (the method of which is beyond
+    the scope of this specification).
+
+    The client credentials grant type MUST only be used by confidential
+    clients::
+
+        +---------+                                  +---------------+
+        :         :                                  :               :
+        :         :>-- A - Client Authentication --->: Authorization :
+        : Client  :                                  :     Server    :
+        :         :<-- B ---- Access Token ---------<:               :
+        :         :                                  :               :
+        +---------+                                  +---------------+
+
+    Figure 6: Client Credentials Flow
+
+    The flow illustrated in Figure 6 includes the following steps:
+
+    (A)  The client authenticates with the authorization server and
+            requests an access token from the token endpoint.
+
+    (B)  The authorization server authenticates the client, and if valid,
+            issues an access token.
+
+    .. _`Client Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.4
+    """
+
+    def __init__(self, request_validator=None):
+        self.request_validator = request_validator or RequestValidator()
+
+    def create_token_response(self, request, token_handler):
+        """Return token or error in JSON format.
+
+        If the access token request is valid and authorized, the
+        authorization server issues an access token as described in
+        `Section 5.1`_.  A refresh token SHOULD NOT be included.  If the request
+        failed client authentication or is invalid, the authorization server
+        returns an error response as described in `Section 5.2`_.
+
+        .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+        .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+        """
+        headers = {
+            'Content-Type': 'application/json',
+            'Cache-Control': 'no-store',
+            'Pragma': 'no-cache',
+        }
+        try:
+            log.debug('Validating access token request, %r.', request)
+            self.validate_token_request(request)
+        except errors.OAuth2Error as e:
+            log.debug('Client error in token request. %s.', e)
+            return headers, e.json, e.status_code
+
+        token = token_handler.create_token(request, refresh_token=False)
+        log.debug('Issuing token to client id %r (%r), %r.',
+                  request.client_id, request.client, token)
+        return headers, json.dumps(token), 200
+
+    def validate_token_request(self, request):
+        if not getattr(request, 'grant_type'):
+            raise errors.InvalidRequestError('Request is missing grant type.',
+                                             request=request)
+
+        if not request.grant_type == 'client_credentials':
+            raise errors.UnsupportedGrantTypeError(request=request)
+
+        for param in ('grant_type', 'scope'):
+            if param in request.duplicate_params:
+                raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
+                                                 request=request)
+
+        log.debug('Authenticating client, %r.', request)
+        if not self.request_validator.authenticate_client(request):
+            log.debug('Client authentication failed, %r.', request)
+            raise errors.InvalidClientError(request=request)
+        else:
+            if not hasattr(request.client, 'client_id'):
+                raise NotImplementedError('Authenticate client must set the '
+                                          'request.client.client_id attribute '
+                                          'in authenticate_client.')
+        # Ensure client is authorized use of this grant type
+        self.validate_grant_type(request)
+
+        log.debug('Authorizing access to user %r.', request.user)
+        request.client_id = request.client_id or request.client.client_id
+        self.validate_scopes(request)
diff --git a/oauthlib/oauth2/rfc6749/grant_types/implicit.py b/oauthlib/oauth2/rfc6749/grant_types/implicit.py
new file mode 100644 (file)
index 0000000..27bcb24
--- /dev/null
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import logging
+
+from oauthlib import common
+from oauthlib.uri_validate import is_absolute_uri
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class ImplicitGrant(GrantTypeBase):
+
+    """`Implicit Grant`_
+
+    The implicit grant type is used to obtain access tokens (it does not
+    support the issuance of refresh tokens) and is optimized for public
+    clients known to operate a particular redirection URI.  These clients
+    are typically implemented in a browser using a scripting language
+    such as JavaScript.
+
+    Unlike the authorization code grant type, in which the client makes
+    separate requests for authorization and for an access token, the
+    client receives the access token as the result of the authorization
+    request.
+
+    The implicit grant type does not include client authentication, and
+    relies on the presence of the resource owner and the registration of
+    the redirection URI.  Because the access token is encoded into the
+    redirection URI, it may be exposed to the resource owner and other
+    applications residing on the same device::
+
+        +----------+
+        | Resource |
+        |  Owner   |
+        |          |
+        +----------+
+             ^
+             |
+            (B)
+        +----|-----+          Client Identifier     +---------------+
+        |         -+----(A)-- & Redirection URI --->|               |
+        |  User-   |                                | Authorization |
+        |  Agent  -|----(B)-- User authenticates -->|     Server    |
+        |          |                                |               |
+        |          |<---(C)--- Redirection URI ----<|               |
+        |          |          with Access Token     +---------------+
+        |          |            in Fragment
+        |          |                                +---------------+
+        |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
+        |          |          without Fragment      |     Client    |
+        |          |                                |    Resource   |
+        |     (F)  |<---(E)------- Script ---------<|               |
+        |          |                                +---------------+
+        +-|--------+
+          |    |
+         (A)  (G) Access Token
+          |    |
+          ^    v
+        +---------+
+        |         |
+        |  Client |
+        |         |
+        +---------+
+
+   Note: The lines illustrating steps (A) and (B) are broken into two
+   parts as they pass through the user-agent.
+
+   Figure 4: Implicit Grant Flow
+
+   The flow illustrated in Figure 4 includes the following steps:
+
+   (A)  The client initiates the flow by directing the resource owner's
+        user-agent to the authorization endpoint.  The client includes
+        its client identifier, requested scope, local state, and a
+        redirection URI to which the authorization server will send the
+        user-agent back once access is granted (or denied).
+
+   (B)  The authorization server authenticates the resource owner (via
+        the user-agent) and establishes whether the resource owner
+        grants or denies the client's access request.
+
+   (C)  Assuming the resource owner grants access, the authorization
+        server redirects the user-agent back to the client using the
+        redirection URI provided earlier.  The redirection URI includes
+        the access token in the URI fragment.
+
+   (D)  The user-agent follows the redirection instructions by making a
+        request to the web-hosted client resource (which does not
+        include the fragment per [RFC2616]).  The user-agent retains the
+        fragment information locally.
+
+   (E)  The web-hosted client resource returns a web page (typically an
+        HTML document with an embedded script) capable of accessing the
+        full redirection URI including the fragment retained by the
+        user-agent, and extracting the access token (and other
+        parameters) contained in the fragment.
+
+   (F)  The user-agent executes the script provided by the web-hosted
+        client resource locally, which extracts the access token.
+
+   (G)  The user-agent passes the access token to the client.
+
+    See `Section 10.3`_ and `Section 10.16`_ for important security considerations
+    when using the implicit grant.
+
+    .. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2
+    .. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3
+    .. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16
+    """
+
+    def __init__(self, request_validator=None):
+        self.request_validator = request_validator or RequestValidator()
+
+    def create_authorization_response(self, request, token_handler):
+        """Create an authorization response.
+        The client constructs the request URI by adding the following
+        parameters to the query component of the authorization endpoint URI
+        using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
+
+        response_type
+                REQUIRED.  Value MUST be set to "token".
+
+        client_id
+                REQUIRED.  The client identifier as described in `Section 2.2`_.
+
+        redirect_uri
+                OPTIONAL.  As described in `Section 3.1.2`_.
+
+        scope
+                OPTIONAL.  The scope of the access request as described by
+                `Section 3.3`_.
+
+        state
+                RECOMMENDED.  An opaque value used by the client to maintain
+                state between the request and callback.  The authorization
+                server includes this value when redirecting the user-agent back
+                to the client.  The parameter SHOULD be used for preventing
+                cross-site request forgery as described in `Section 10.12`_.
+
+        The authorization server validates the request to ensure that all
+        required parameters are present and valid.  The authorization server
+        MUST verify that the redirection URI to which it will redirect the
+        access token matches a redirection URI registered by the client as
+        described in `Section 3.1.2`_.
+
+        .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+        .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+        .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+        """
+        return self.create_token_response(request, token_handler)
+
+    def create_token_response(self, request, token_handler):
+        """Return token or error embedded in the URI fragment.
+
+        If the resource owner grants the access request, the authorization
+        server issues an access token and delivers it to the client by adding
+        the following parameters to the fragment component of the redirection
+        URI using the "application/x-www-form-urlencoded" format, per
+        `Appendix B`_:
+
+        access_token
+                REQUIRED.  The access token issued by the authorization server.
+
+        token_type
+                REQUIRED.  The type of the token issued as described in
+                `Section 7.1`_.  Value is case insensitive.
+
+        expires_in
+                RECOMMENDED.  The lifetime in seconds of the access token.  For
+                example, the value "3600" denotes that the access token will
+                expire in one hour from the time the response was generated.
+                If omitted, the authorization server SHOULD provide the
+                expiration time via other means or document the default value.
+
+        scope
+                OPTIONAL, if identical to the scope requested by the client;
+                otherwise, REQUIRED.  The scope of the access token as
+                described by `Section 3.3`_.
+
+        state
+                REQUIRED if the "state" parameter was present in the client
+                authorization request.  The exact value received from the
+                client.
+
+        The authorization server MUST NOT issue a refresh token.
+
+        .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+        """
+        try:
+            # request.scopes is only mandated in post auth and both pre and
+            # post auth use validate_authorization_request
+            if not request.scopes:
+                raise ValueError('Scopes must be set on post auth.')
+
+            self.validate_token_request(request)
+
+        # If the request fails due to a missing, invalid, or mismatching
+        # redirection URI, or if the client identifier is missing or invalid,
+        # the authorization server SHOULD inform the resource owner of the
+        # error and MUST NOT automatically redirect the user-agent to the
+        # invalid redirection URI.
+        except errors.FatalClientError as e:
+            log.debug('Fatal client error during validation of %r. %r.',
+                      request, e)
+            raise
+
+        # If the resource owner denies the access request or if the request
+        # fails for reasons other than a missing or invalid redirection URI,
+        # the authorization server informs the client by adding the following
+        # parameters to the fragment component of the redirection URI using the
+        # "application/x-www-form-urlencoded" format, per Appendix B:
+        # http://tools.ietf.org/html/rfc6749#appendix-B
+        except errors.OAuth2Error as e:
+            log.debug('Client error during validation of %r. %r.', request, e)
+            return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples,
+                                                         fragment=True)}, None, 302
+
+        token = token_handler.create_token(request, refresh_token=False)
+        return {'Location': common.add_params_to_uri(request.redirect_uri, token.items(),
+                                                     fragment=True)}, None, 302
+
+    def validate_authorization_request(self, request):
+        return self.validate_token_request(request)
+
+    def validate_token_request(self, request):
+        """Check the token request for normal and fatal errors.
+
+        This method is very similar to validate_authorization_request in
+        the AuthorizationCodeGrant but differ in a few subtle areas.
+
+        A normal error could be a missing response_type parameter or the client
+        attempting to access scope it is not allowed to ask authorization for.
+        Normal errors can safely be included in the redirection URI and
+        sent back to the client.
+
+        Fatal errors occur when the client_id or redirect_uri is invalid or
+        missing. These must be caught by the provider and handled, how this
+        is done is outside of the scope of OAuthLib but showing an error
+        page describing the issue is a good idea.
+        """
+
+        # First check for fatal errors
+
+        # If the request fails due to a missing, invalid, or mismatching
+        # redirection URI, or if the client identifier is missing or invalid,
+        # the authorization server SHOULD inform the resource owner of the
+        # error and MUST NOT automatically redirect the user-agent to the
+        # invalid redirection URI.
+
+        # REQUIRED. The client identifier as described in Section 2.2.
+        # http://tools.ietf.org/html/rfc6749#section-2.2
+        if not request.client_id:
+            raise errors.MissingClientIdError(request=request)
+
+        if not self.request_validator.validate_client_id(request.client_id, request):
+            raise errors.InvalidClientIdError(request=request)
+
+        # OPTIONAL. As described in Section 3.1.2.
+        # http://tools.ietf.org/html/rfc6749#section-3.1.2
+        if request.redirect_uri is not None:
+            request.using_default_redirect_uri = False
+            log.debug('Using provided redirect_uri %s', request.redirect_uri)
+            if not is_absolute_uri(request.redirect_uri):
+                raise errors.InvalidRedirectURIError(request=request)
+
+            # The authorization server MUST verify that the redirection URI
+            # to which it will redirect the access token matches a
+            # redirection URI registered by the client as described in
+            # Section 3.1.2.
+            # http://tools.ietf.org/html/rfc6749#section-3.1.2
+            if not self.request_validator.validate_redirect_uri(
+                    request.client_id, request.redirect_uri, request):
+                raise errors.MismatchingRedirectURIError(request=request)
+        else:
+            request.redirect_uri = self.request_validator.get_default_redirect_uri(
+                request.client_id, request)
+            request.using_default_redirect_uri = True
+            log.debug('Using default redirect_uri %s.', request.redirect_uri)
+            if not request.redirect_uri:
+                raise errors.MissingRedirectURIError(request=request)
+            if not is_absolute_uri(request.redirect_uri):
+                raise errors.InvalidRedirectURIError(request=request)
+
+        # Then check for normal errors.
+
+        # If the resource owner denies the access request or if the request
+        # fails for reasons other than a missing or invalid redirection URI,
+        # the authorization server informs the client by adding the following
+        # parameters to the fragment component of the redirection URI using the
+        # "application/x-www-form-urlencoded" format, per Appendix B.
+        # http://tools.ietf.org/html/rfc6749#appendix-B
+
+        # Note that the correct parameters to be added are automatically
+        # populated through the use of specific exceptions.
+        if request.response_type is None:
+            raise errors.InvalidRequestError(description='Missing response_type parameter.',
+                                             request=request)
+
+        for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
+            if param in request.duplicate_params:
+                raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+
+        # REQUIRED. Value MUST be set to "token".
+        if request.response_type != 'token':
+            raise errors.UnsupportedResponseTypeError(request=request)
+
+        log.debug('Validating use of response_type token for client %r (%r).',
+                  request.client_id, request.client)
+        if not self.request_validator.validate_response_type(request.client_id,
+                                                             request.response_type, request.client, request):
+            log.debug('Client %s is not authorized to use response_type %s.',
+                      request.client_id, request.response_type)
+            raise errors.UnauthorizedClientError(request=request)
+
+        # OPTIONAL. The scope of the access request as described by Section 3.3
+        # http://tools.ietf.org/html/rfc6749#section-3.3
+        self.validate_scopes(request)
+
+        return request.scopes, {
+            'client_id': request.client_id,
+            'redirect_uri': request.redirect_uri,
+            'response_type': request.response_type,
+            'state': request.state,
+            'request': request,
+        }
diff --git a/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py b/oauthlib/oauth2/rfc6749/grant_types/refresh_token.py
new file mode 100644 (file)
index 0000000..0ab10c9
--- /dev/null
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from .base import GrantTypeBase
+from .. import errors, utils
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class RefreshTokenGrant(GrantTypeBase):
+
+    """`Refresh token grant`_
+
+    .. _`Refresh token grant`: http://tools.ietf.org/html/rfc6749#section-6
+    """
+
+    @property
+    def issue_new_refresh_tokens(self):
+        return True
+
+    def __init__(self, request_validator=None, issue_new_refresh_tokens=True):
+        self.request_validator = request_validator or RequestValidator()
+
+    def create_token_response(self, request, token_handler):
+        """Create a new access token from a refresh_token.
+
+        If valid and authorized, the authorization server issues an access
+        token as described in `Section 5.1`_. If the request failed
+        verification or is invalid, the authorization server returns an error
+        response as described in `Section 5.2`_.
+
+        The authorization server MAY issue a new refresh token, in which case
+        the client MUST discard the old refresh token and replace it with the
+        new refresh token. The authorization server MAY revoke the old
+        refresh token after issuing a new refresh token to the client. If a
+        new refresh token is issued, the refresh token scope MUST be
+        identical to that of the refresh token included by the client in the
+        request.
+
+        .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+        .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+        """
+        headers = {
+            'Content-Type': 'application/json',
+            'Cache-Control': 'no-store',
+            'Pragma': 'no-cache',
+        }
+        try:
+            log.debug('Validating refresh token request, %r.', request)
+            self.validate_token_request(request)
+        except errors.OAuth2Error as e:
+            return headers, e.json, e.status_code
+
+        token = token_handler.create_token(request,
+                                           refresh_token=self.issue_new_refresh_tokens)
+        log.debug('Issuing new token to client id %r (%r), %r.',
+                  request.client_id, request.client, token)
+        return headers, json.dumps(token), 200
+
+    def validate_token_request(self, request):
+        # REQUIRED. Value MUST be set to "refresh_token".
+        if request.grant_type != 'refresh_token':
+            raise errors.UnsupportedGrantTypeError(request=request)
+
+        if request.refresh_token is None:
+            raise errors.InvalidRequestError(
+                description='Missing refresh token parameter.',
+                request=request)
+
+        # Because refresh tokens are typically long-lasting credentials used to
+        # request additional access tokens, the refresh token is bound to the
+        # client to which it was issued.  If the client type is confidential or
+        # the client was issued client credentials (or assigned other
+        # authentication requirements), the client MUST authenticate with the
+        # authorization server as described in Section 3.2.1.
+        # http://tools.ietf.org/html/rfc6749#section-3.2.1
+        if self.request_validator.client_authentication_required(request):
+            log.debug('Authenticating client, %r.', request)
+            if not self.request_validator.authenticate_client(request):
+                log.debug('Invalid client (%r), denying access.', request)
+                raise errors.InvalidClientError(request=request)
+        elif not self.request_validator.authenticate_client_id(request.client_id, request):
+            log.debug('Client authentication failed, %r.', request)
+            raise errors.InvalidClientError(request=request)
+
+        # Ensure client is authorized use of this grant type
+        self.validate_grant_type(request)
+
+        # REQUIRED. The refresh token issued to the client.
+        log.debug('Validating refresh token %s for client %r.',
+                  request.refresh_token, request.client)
+        if not self.request_validator.validate_refresh_token(
+                request.refresh_token, request.client, request):
+            log.debug('Invalid refresh token, %s, for client %r.',
+                      request.refresh_token, request.client)
+            raise errors.InvalidGrantError(request=request)
+
+        original_scopes = utils.scope_to_list(
+            self.request_validator.get_original_scopes(
+                request.refresh_token, request))
+
+        if request.scope:
+            request.scopes = utils.scope_to_list(request.scope)
+            if (not all((s in original_scopes for s in request.scopes))
+                and not self.request_validator.is_within_original_scope(
+                    request.scopes, request.refresh_token, request)):
+                log.debug('Refresh token %s lack requested scopes, %r.',
+                          request.refresh_token, request.scopes)
+                raise errors.InvalidScopeError(request=request)
+        else:
+            request.scopes = original_scopes
diff --git a/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py b/oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py
new file mode 100644 (file)
index 0000000..c19e6cf
--- /dev/null
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from .base import GrantTypeBase
+from .. import errors
+from ..request_validator import RequestValidator
+
+log = logging.getLogger(__name__)
+
+
+class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase):
+
+    """`Resource Owner Password Credentials Grant`_
+
+    The resource owner password credentials grant type is suitable in
+    cases where the resource owner has a trust relationship with the
+    client, such as the device operating system or a highly privileged
+    application.  The authorization server should take special care when
+    enabling this grant type and only allow it when other flows are not
+    viable.
+
+    This grant type is suitable for clients capable of obtaining the
+    resource owner's credentials (username and password, typically using
+    an interactive form).  It is also used to migrate existing clients
+    using direct authentication schemes such as HTTP Basic or Digest
+    authentication to OAuth by converting the stored credentials to an
+    access token::
+
+            +----------+
+            | Resource |
+            |  Owner   |
+            |          |
+            +----------+
+                 v
+                 |    Resource Owner
+                (A) Password Credentials
+                 |
+                 v
+            +---------+                                  +---------------+
+            |         |>--(B)---- Resource Owner ------->|               |
+            |         |         Password Credentials     | Authorization |
+            | Client  |                                  |     Server    |
+            |         |<--(C)---- Access Token ---------<|               |
+            |         |    (w/ Optional Refresh Token)   |               |
+            +---------+                                  +---------------+
+
+    Figure 5: Resource Owner Password Credentials Flow
+
+    The flow illustrated in Figure 5 includes the following steps:
+
+    (A)  The resource owner provides the client with its username and
+            password.
+
+    (B)  The client requests an access token from the authorization
+            server's token endpoint by including the credentials received
+            from the resource owner.  When making the request, the client
+            authenticates with the authorization server.
+
+    (C)  The authorization server authenticates the client and validates
+            the resource owner credentials, and if valid, issues an access
+            token.
+
+    .. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3
+    """
+
+    def __init__(self, request_validator=None, refresh_token=True):
+        """
+        If the refresh_token keyword argument is False, do not return
+        a refresh token in the response.
+        """
+        self.request_validator = request_validator or RequestValidator()
+        self.refresh_token = refresh_token
+
+    def create_token_response(self, request, token_handler):
+        """Return token or error in json format.
+
+        If the access token request is valid and authorized, the
+        authorization server issues an access token and optional refresh
+        token as described in `Section 5.1`_.  If the request failed client
+        authentication or is invalid, the authorization server returns an
+        error response as described in `Section 5.2`_.
+
+        .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
+        .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
+        """
+        headers = {
+            'Content-Type': 'application/json',
+            'Cache-Control': 'no-store',
+            'Pragma': 'no-cache',
+        }
+        try:
+            if self.request_validator.client_authentication_required(request):
+                log.debug('Authenticating client, %r.', request)
+                if not self.request_validator.authenticate_client(request):
+                    log.debug('Client authentication failed, %r.', request)
+                    raise errors.InvalidClientError(request=request)
+            elif not self.request_validator.authenticate_client_id(request.client_id, request):
+                log.debug('Client authentication failed, %r.', request)
+                raise errors.InvalidClientError(request=request)
+            log.debug('Validating access token request, %r.', request)
+            self.validate_token_request(request)
+        except errors.OAuth2Error as e:
+            log.debug('Client error in token request, %s.', e)
+            return headers, e.json, e.status_code
+
+        token = token_handler.create_token(request, self.refresh_token)
+        log.debug('Issuing token %r to client id %r (%r) and username %s.',
+                  token, request.client_id, request.client, request.username)
+        return headers, json.dumps(token), 200
+
+    def validate_token_request(self, request):
+        """
+        The client makes a request to the token endpoint by adding the
+        following parameters using the "application/x-www-form-urlencoded"
+        format per Appendix B with a character encoding of UTF-8 in the HTTP
+        request entity-body:
+
+        grant_type
+                REQUIRED.  Value MUST be set to "password".
+
+        username
+                REQUIRED.  The resource owner username.
+
+        password
+                REQUIRED.  The resource owner password.
+
+        scope
+                OPTIONAL.  The scope of the access request as described by
+                `Section 3.3`_.
+
+        If the client type is confidential or the client was issued client
+        credentials (or assigned other authentication requirements), the
+        client MUST authenticate with the authorization server as described
+        in `Section 3.2.1`_.
+
+        The authorization server MUST:
+
+        o  require client authentication for confidential clients or for any
+            client that was issued client credentials (or with other
+            authentication requirements),
+
+        o  authenticate the client if client authentication is included, and
+
+        o  validate the resource owner password credentials using its
+            existing password validation algorithm.
+
+        Since this access token request utilizes the resource owner's
+        password, the authorization server MUST protect the endpoint against
+        brute force attacks (e.g., using rate-limitation or generating
+        alerts).
+
+        .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+        .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1
+        """
+        for param in ('grant_type', 'username', 'password'):
+            if not getattr(request, param):
+                raise errors.InvalidRequestError(
+                    'Request is missing %s parameter.' % param, request=request)
+
+        for param in ('grant_type', 'username', 'password', 'scope'):
+            if param in request.duplicate_params:
+                raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
+
+        # This error should rarely (if ever) occur if requests are routed to
+        # grant type handlers based on the grant_type parameter.
+        if not request.grant_type == 'password':
+            raise errors.UnsupportedGrantTypeError(request=request)
+
+        log.debug('Validating username %s.', request.username)
+        if not self.request_validator.validate_user(request.username,
+                                                    request.password, request.client, request):
+            raise errors.InvalidGrantError(
+                'Invalid credentials given.', request=request)
+        else:
+            if not hasattr(request.client, 'client_id'):
+                raise NotImplementedError(
+                    'Validate user must set the '
+                    'request.client.client_id attribute '
+                    'in authenticate_client.')
+        log.debug('Authorizing access to user %r.', request.user)
+
+        # Ensure client is authorized use of this grant type
+        self.validate_grant_type(request)
+
+        if request.client:
+            request.client_id = request.client_id or request.client.client_id
+        self.validate_scopes(request)
diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py
new file mode 100644 (file)
index 0000000..b46889c
--- /dev/null
@@ -0,0 +1,406 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.parameters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `Section 4`_ of the OAuth 2 RFC.
+
+.. _`Section 4`: http://tools.ietf.org/html/rfc6749#section-4
+"""
+from __future__ import absolute_import, unicode_literals
+
+import json
+import os
+import time
+try:
+    import urlparse
+except ImportError:
+    import urllib.parse as urlparse
+from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
+from oauthlib.signals import scope_changed
+from .errors import raise_from_error, MissingTokenError, MissingTokenTypeError
+from .errors import MismatchingStateError, MissingCodeError
+from .errors import InsecureTransportError
+from .tokens import OAuth2Token
+from .utils import list_to_scope, scope_to_list, is_secure_transport
+
+
+def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
+                      scope=None, state=None, **kwargs):
+    """Prepare the authorization grant request URI.
+
+    The client constructs the request URI by adding the following
+    parameters to the query component of the authorization endpoint URI
+    using the ``application/x-www-form-urlencoded`` format as defined by
+    [`W3C.REC-html401-19991224`_]:
+
+    :param response_type: To indicate which OAuth 2 grant/flow is required,
+                          "code" and "token".
+    :param client_id: The client identifier as described in `Section 2.2`_.
+    :param redirect_uri: The client provided URI to redirect back to after
+                         authorization as described in `Section 3.1.2`_.
+    :param scope: The scope of the access request as described by
+                  `Section 3.3`_.
+
+    :param state: An opaque value used by the client to maintain
+                  state between the request and callback.  The authorization
+                  server includes this value when redirecting the user-agent
+                  back to the client.  The parameter SHOULD be used for
+                  preventing cross-site request forgery as described in
+                  `Section 10.12`_.
+    :param kwargs: Extra arguments to embed in the grant/authorization URL.
+
+    An example of an authorization code grant authorization URL:
+
+    .. code-block:: http
+
+        GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
+            &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
+        Host: server.example.com
+
+    .. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/rfc6749#ref-W3C.REC-html401-19991224
+    .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
+    .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
+    .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+    .. _`section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
+    """
+    if not is_secure_transport(uri):
+        raise InsecureTransportError()
+
+    params = [(('response_type', response_type)),
+              (('client_id', client_id))]
+
+    if redirect_uri:
+        params.append(('redirect_uri', redirect_uri))
+    if scope:
+        params.append(('scope', list_to_scope(scope)))
+    if state:
+        params.append(('state', state))
+
+    for k in kwargs:
+        if kwargs[k]:
+            params.append((unicode_type(k), kwargs[k]))
+
+    return add_params_to_uri(uri, params)
+
+
+def prepare_token_request(grant_type, body='', **kwargs):
+    """Prepare the access token request.
+
+    The client makes a request to the token endpoint by adding the
+    following parameters using the ``application/x-www-form-urlencoded``
+    format in the HTTP request entity-body:
+
+    :param grant_type: To indicate grant type being used, i.e. "password",
+            "authorization_code" or "client_credentials".
+    :param body: Existing request body to embed parameters in.
+    :param code: If using authorization code grant, pass the previously
+                 obtained authorization code as the ``code`` argument.
+    :param redirect_uri: If the "redirect_uri" parameter was included in the
+                         authorization request as described in
+                         `Section 4.1.1`_, and their values MUST be identical.
+    :param kwargs: Extra arguments to embed in the request body.
+
+    An example of an authorization code token request body:
+
+    .. code-block:: http
+
+        grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
+        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
+
+    .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1
+    """
+    params = [('grant_type', grant_type)]
+
+    if 'scope' in kwargs:
+        kwargs['scope'] = list_to_scope(kwargs['scope'])
+
+    for k in kwargs:
+        if kwargs[k]:
+            params.append((unicode_type(k), kwargs[k]))
+
+    return add_params_to_qs(body, params)
+
+
+def prepare_token_revocation_request(url, token, token_type_hint="access_token",
+        callback=None, body='', **kwargs):
+    """Prepare a token revocation request.
+
+    The client constructs the request by including the following parameters
+    using the "application/x-www-form-urlencoded" format in the HTTP request
+    entity-body:
+
+    token   REQUIRED.  The token that the client wants to get revoked.
+
+    token_type_hint  OPTIONAL.  A hint about the type of the token submitted
+    for revocation.  Clients MAY pass this parameter in order to help the
+    authorization server to optimize the token lookup.  If the server is unable
+    to locate the token using the given hint, it MUST extend its search across
+    all of its supported token types.  An authorization server MAY ignore this
+    parameter, particularly if it is able to detect the token type
+    automatically.  This specification defines two such values:
+
+        * access_token: An access token as defined in [RFC6749],
+             `Section 1.4`_
+
+        * refresh_token: A refresh token as defined in [RFC6749],
+             `Section 1.5`_
+
+        Specific implementations, profiles, and extensions of this
+        specification MAY define other values for this parameter using the
+        registry defined in `Section 4.1.2`_.
+
+    .. _`Section 1.4`: http://tools.ietf.org/html/rfc6749#section-1.4
+    .. _`Section 1.5`: http://tools.ietf.org/html/rfc6749#section-1.5
+    .. _`Section 4.1.2`: http://tools.ietf.org/html/rfc7009#section-4.1.2
+
+    """
+    if not is_secure_transport(url):
+        raise InsecureTransportError()
+
+    params = [('token', token)]
+
+    if token_type_hint:
+        params.append(('token_type_hint', token_type_hint))
+
+    for k in kwargs:
+        if kwargs[k]:
+            params.append((unicode_type(k), kwargs[k]))
+
+    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+
+    if callback:
+        params.append(('callback', callback))
+        return add_params_to_uri(url, params), headers, body
+    else:
+        return url, headers, add_params_to_qs(body, params)
+
+
+def parse_authorization_code_response(uri, state=None):
+    """Parse authorization grant response URI into a dict.
+
+    If the resource owner grants the access request, the authorization
+    server issues an authorization code and delivers it to the client by
+    adding the following parameters to the query component of the
+    redirection URI using the ``application/x-www-form-urlencoded`` format:
+
+    **code**
+            REQUIRED.  The authorization code generated by the
+            authorization server.  The authorization code MUST expire
+            shortly after it is issued to mitigate the risk of leaks.  A
+            maximum authorization code lifetime of 10 minutes is
+            RECOMMENDED.  The client MUST NOT use the authorization code
+            more than once.  If an authorization code is used more than
+            once, the authorization server MUST deny the request and SHOULD
+            revoke (when possible) all tokens previously issued based on
+            that authorization code.  The authorization code is bound to
+            the client identifier and redirection URI.
+
+    **state**
+            REQUIRED if the "state" parameter was present in the client
+            authorization request.  The exact value received from the
+            client.
+
+    :param uri: The full redirect URL back to the client.
+    :param state: The state parameter from the authorization request.
+
+    For example, the authorization server redirects the user-agent by
+    sending the following HTTP response:
+
+    .. code-block:: http
+
+        HTTP/1.1 302 Found
+        Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
+                &state=xyz
+
+    """
+    if not is_secure_transport(uri):
+        raise InsecureTransportError()
+
+    query = urlparse.urlparse(uri).query
+    params = dict(urlparse.parse_qsl(query))
+
+    if not 'code' in params:
+        raise MissingCodeError("Missing code parameter in response.")
+
+    if state and params.get('state', None) != state:
+        raise MismatchingStateError()
+
+    return params
+
+
+def parse_implicit_response(uri, state=None, scope=None):
+    """Parse the implicit token response URI into a dict.
+
+    If the resource owner grants the access request, the authorization
+    server issues an access token and delivers it to the client by adding
+    the following parameters to the fragment component of the redirection
+    URI using the ``application/x-www-form-urlencoded`` format:
+
+    **access_token**
+            REQUIRED.  The access token issued by the authorization server.
+
+    **token_type**
+            REQUIRED.  The type of the token issued as described in
+            Section 7.1.  Value is case insensitive.
+
+    **expires_in**
+            RECOMMENDED.  The lifetime in seconds of the access token.  For
+            example, the value "3600" denotes that the access token will
+            expire in one hour from the time the response was generated.
+            If omitted, the authorization server SHOULD provide the
+            expiration time via other means or document the default value.
+
+    **scope**
+            OPTIONAL, if identical to the scope requested by the client,
+            otherwise REQUIRED.  The scope of the access token as described
+            by Section 3.3.
+
+    **state**
+            REQUIRED if the "state" parameter was present in the client
+            authorization request.  The exact value received from the
+            client.
+
+    Similar to the authorization code response, but with a full token provided
+    in the URL fragment:
+
+    .. code-block:: http
+
+        HTTP/1.1 302 Found
+        Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
+                &state=xyz&token_type=example&expires_in=3600
+    """
+    if not is_secure_transport(uri):
+        raise InsecureTransportError()
+
+    fragment = urlparse.urlparse(uri).fragment
+    params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
+
+    if 'scope' in params:
+        params['scope'] = scope_to_list(params['scope'])
+
+    if 'expires_in' in params:
+        params['expires_at'] = time.time() + int(params['expires_in'])
+
+    if state and params.get('state', None) != state:
+        raise ValueError("Mismatching or missing state in params.")
+
+    params = OAuth2Token(params, old_scope=scope)
+    validate_token_parameters(params)
+    return params
+
+
+def parse_token_response(body, scope=None):
+    """Parse the JSON token response body into a dict.
+
+    The authorization server issues an access token and optional refresh
+    token, and constructs the response by adding the following parameters
+    to the entity body of the HTTP response with a 200 (OK) status code:
+
+    access_token
+            REQUIRED.  The access token issued by the authorization server.
+    token_type
+            REQUIRED.  The type of the token issued as described in
+            `Section 7.1`_.  Value is case insensitive.
+    expires_in
+            RECOMMENDED.  The lifetime in seconds of the access token.  For
+            example, the value "3600" denotes that the access token will
+            expire in one hour from the time the response was generated.
+            If omitted, the authorization server SHOULD provide the
+            expiration time via other means or document the default value.
+    refresh_token
+            OPTIONAL.  The refresh token which can be used to obtain new
+            access tokens using the same authorization grant as described
+            in `Section 6`_.
+    scope
+            OPTIONAL, if identical to the scope requested by the client,
+            otherwise REQUIRED.  The scope of the access token as described
+            by `Section 3.3`_.
+
+    The parameters are included in the entity body of the HTTP response
+    using the "application/json" media type as defined by [`RFC4627`_].  The
+    parameters are serialized into a JSON structure by adding each
+    parameter at the highest structure level.  Parameter names and string
+    values are included as JSON strings.  Numerical values are included
+    as JSON numbers.  The order of parameters does not matter and can
+    vary.
+
+    :param body: The full json encoded response body.
+    :param scope: The scope requested during authorization.
+
+    For example:
+
+    .. code-block:: http
+
+        HTTP/1.1 200 OK
+        Content-Type: application/json
+        Cache-Control: no-store
+        Pragma: no-cache
+
+        {
+            "access_token":"2YotnFZFEjr1zCsicMWpAA",
+            "token_type":"example",
+            "expires_in":3600,
+            "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
+            "example_parameter":"example_value"
+        }
+
+    .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+    .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
+    .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
+    .. _`RFC4627`: http://tools.ietf.org/html/rfc4627
+    """
+    try:
+        params = json.loads(body)
+    except ValueError:
+
+        # Fall back to URL-encoded string, to support old implementations,
+        # including (at time of writing) Facebook. See:
+        #   https://github.com/idan/oauthlib/issues/267
+
+        params = dict(urlparse.parse_qsl(body))
+        for key in ('expires_in', 'expires'):
+            if key in params:  # cast a couple things to int
+                params[key] = int(params[key])
+
+    if 'scope' in params:
+        params['scope'] = scope_to_list(params['scope'])
+
+    if 'expires' in params:
+        params['expires_in'] = params.pop('expires')
+
+    if 'expires_in' in params:
+        params['expires_at'] = time.time() + int(params['expires_in'])
+
+    params = OAuth2Token(params, old_scope=scope)
+    validate_token_parameters(params)
+    return params
+
+
+def validate_token_parameters(params):
+    """Ensures token precence, token type, expiration and scope in params."""
+    if 'error' in params:
+        raise_from_error(params.get('error'), params)
+
+    if not 'access_token' in params:
+        raise MissingTokenError(description="Missing access token parameter.")
+
+    if not 'token_type' in params:
+        if os.environ.get('OAUTHLIB_STRICT_TOKEN_TYPE'):
+            raise MissingTokenTypeError()
+
+    # If the issued access token scope is different from the one requested by
+    # the client, the authorization server MUST include the "scope" response
+    # parameter to inform the client of the actual scope granted.
+    # http://tools.ietf.org/html/rfc6749#section-3.3
+    if params.scope_changed:
+        message = 'Scope has changed from "{old}" to "{new}".'.format(
+            old=params.old_scope, new=params.scope,
+        )
+        scope_changed.send(message=message, old=params.old_scopes, new=params.scopes)
+        if not os.environ.get('OAUTHLIB_RELAX_TOKEN_SCOPE', None):
+            w = Warning(message)
+            w.token = params
+            w.old_scope = params.old_scopes
+            w.new_scope = params.scopes
+            raise w
diff --git a/oauthlib/oauth2/rfc6749/request_validator.py b/oauthlib/oauth2/rfc6749/request_validator.py
new file mode 100644 (file)
index 0000000..e622ff1
--- /dev/null
@@ -0,0 +1,460 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.oauth2.rfc6749.grant_types
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+"""
+from __future__ import unicode_literals, absolute_import
+
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class RequestValidator(object):
+
+    def client_authentication_required(self, request, *args, **kwargs):
+        """Determine if client authentication is required for current request.
+
+        According to the rfc6749, client authentication is required in the following cases:
+            - Resource Owner Password Credentials Grant, when Client type is Confidential or when
+              Client was issued client credentials or whenever Client provided client
+              authentication, see `Section 4.3.2`_.
+            - Authorization Code Grant, when Client type is Confidential or when Client was issued
+              client credentials or whenever Client provided client authentication,
+              see `Section 4.1.3`_.
+            - Refresh Token Grant, when Client type is Confidential or when Client was issued
+              client credentials or whenever Client provided client authentication, see
+              `Section 6`_
+
+        :param request: oauthlib.common.Request
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+            - Resource Owner Password Credentials Grant
+            - Refresh Token Grant
+
+        .. _`Section 4.3.2`: http://tools.ietf.org/html/rfc6749#section-4.3.2
+        .. _`Section 4.1.3`: http://tools.ietf.org/html/rfc6749#section-4.1.3
+        .. _`Section 6`: http://tools.ietf.org/html/rfc6749#section-6
+        """
+        return True
+
+    def authenticate_client(self, request, *args, **kwargs):
+        """Authenticate client through means outside the OAuth 2 spec.
+
+        Means of authentication is negotiated beforehand and may for example
+        be `HTTP Basic Authentication Scheme`_ which utilizes the Authorization
+        header.
+
+        Headers may be accesses through request.headers and parameters found in
+        both body and query can be obtained by direct attribute access, i.e.
+        request.client_id for client_id in the URL query.
+
+        OBS! Certain grant types rely on this authentication, possibly with
+        other fallbacks, and for them to recognize this authorization please
+        set the client attribute on the request (request.client). Note that
+        preferably this client object should have a client_id attribute of
+        unicode type (request.client.client_id).
+
+        :param request: oauthlib.common.Request
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+            - Resource Owner Password Credentials Grant (may be disabled)
+            - Client Credentials Grant
+            - Refresh Token Grant
+
+        .. _`HTTP Basic Authentication Scheme`: http://tools.ietf.org/html/rfc1945#section-11.1
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def authenticate_client_id(self, client_id, request, *args, **kwargs):
+        """Ensure client_id belong to a non-confidential client.
+
+        A non-confidential client is one that is not required to authenticate
+        through other means, such as using HTTP Basic.
+
+        Note, while not strictly necessary it can often be very convenient
+        to set request.client to the client object associated with the
+        given client_id.
+
+        :param request: oauthlib.common.Request
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def confirm_redirect_uri(self, client_id, code, redirect_uri, client,
+                             *args, **kwargs):
+        """Ensure client is authorized to redirect to the redirect_uri requested.
+
+        If the client specifies a redirect_uri when obtaining code then
+        that redirect URI must be bound to the code and verified equal
+        in this method.
+
+        All clients should register the absolute URIs of all URIs they intend
+        to redirect to. The registration is outside of the scope of oauthlib.
+
+        :param client_id: Unicode client identifier
+        :param code: Unicode authorization_code.
+        :param redirect_uri: Unicode absolute URI
+        :param client: Client object set by you, see authenticate_client.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant (during token request)
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def get_default_redirect_uri(self, client_id, request, *args, **kwargs):
+        """Get the default redirect URI for the client.
+
+        :param client_id: Unicode client identifier
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: The default redirect URI for the client
+
+        Method is used by:
+            - Authorization Code Grant
+            - Implicit Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def get_default_scopes(self, client_id, request, *args, **kwargs):
+        """Get the default scopes for the client.
+
+        :param client_id: Unicode client identifier
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: List of default scopes
+
+        Method is used by all core grant types:
+            - Authorization Code Grant
+            - Implicit Grant
+            - Resource Owner Password Credentials Grant
+            - Client Credentials grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def get_original_scopes(self, refresh_token, request, *args, **kwargs):
+        """Get the list of scopes associated with the refresh token.
+
+        :param refresh_token: Unicode refresh token
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: List of scopes.
+
+        Method is used by:
+            - Refresh token grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def is_within_original_scope(self, request_scopes, refresh_token, request, *args, **kwargs):
+        """Check if requested scopes are within a scope of the refresh token.
+
+        When access tokens are refreshed the scope of the new token
+        needs to be within the scope of the original token. This is
+        ensured by checking that all requested scopes strings are on
+        the list returned by the get_original_scopes. If this check
+        fails, is_within_original_scope is called. The method can be
+        used in situations where returning all valid scopes from the
+        get_original_scopes is not practical.
+
+        :param request_scopes: A list of scopes that were requested by client
+        :param refresh_token: Unicode refresh_token
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Refresh token grant
+        """
+        return False
+
+    def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
+        """Invalidate an authorization code after use.
+
+        :param client_id: Unicode client identifier
+        :param code: The authorization code grant (request.code).
+        :param request: The HTTP Request (oauthlib.common.Request)
+
+        Method is used by:
+            - Authorization Code Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
+        """Revoke an access or refresh token.
+
+        :param token: The token string.
+        :param token_type_hint: access_token or refresh_token.
+        :param request: The HTTP Request (oauthlib.common.Request)
+
+        Method is used by:
+            - Revocation Endpoint
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def rotate_refresh_token(self, request):
+        """Determine whether to rotate the refresh token. Default, yes.
+
+        When access tokens are refreshed the old refresh token can be kept
+        or replaced with a new one (rotated). Return True to rotate and
+        and False for keeping original.
+
+        :param request: oauthlib.common.Request
+        :rtype: True or False
+
+        Method is used by:
+            - Refresh Token Grant
+        """
+        return True
+
+    def save_authorization_code(self, client_id, code, request, *args, **kwargs):
+        """Persist the authorization_code.
+
+        The code should at minimum be associated with:
+            - a client and it's client_id
+            - the redirect URI used (request.redirect_uri)
+            - whether the redirect URI used is the client default or not
+            - a resource owner / user (request.user)
+            - authorized scopes (request.scopes)
+
+        The authorization code grant dict (code) holds at least the key 'code'::
+
+            {'code': 'sdf345jsdf0934f'}
+
+        :param client_id: Unicode client identifier
+        :param code: A dict of the authorization code grant.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: The default redirect URI for the client
+
+        Method is used by:
+            - Authorization Code Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def save_bearer_token(self, token, request, *args, **kwargs):
+        """Persist the Bearer token.
+
+        The Bearer token should at minimum be associated with:
+            - a client and it's client_id, if available
+            - a resource owner / user (request.user)
+            - authorized scopes (request.scopes)
+            - an expiration time
+            - a refresh token, if issued
+
+        The Bearer token dict may hold a number of items::
+
+            {
+                'token_type': 'Bearer',
+                'access_token': 'askfjh234as9sd8',
+                'expires_in': 3600,
+                'scope': 'string of space separated authorized scopes',
+                'refresh_token': '23sdf876234',  # if issued
+                'state': 'given_by_client',  # if supplied by client
+            }
+
+        Note that while "scope" is a string-separated list of authorized scopes,
+        the original list is still available in request.scopes
+
+        :param client_id: Unicode client identifier
+        :param token: A Bearer token dict
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: The default redirect URI for the client
+
+        Method is used by all core grant types issuing Bearer tokens:
+            - Authorization Code Grant
+            - Implicit Grant
+            - Resource Owner Password Credentials Grant (might not associate a client)
+            - Client Credentials grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_bearer_token(self, token, scopes, request):
+        """Ensure the Bearer token is valid and authorized access to scopes.
+
+        :param token: A string of random characters.
+        :param scopes: A list of scopes associated with the protected resource.
+        :param request: The HTTP Request (oauthlib.common.Request)
+
+        A key to OAuth 2 security and restricting impact of leaked tokens is
+        the short expiration time of tokens, *always ensure the token has not
+        expired!*.
+
+        Two different approaches to scope validation:
+
+            1) all(scopes). The token must be authorized access to all scopes
+                            associated with the resource. For example, the
+                            token has access to ``read-only`` and ``images``,
+                            thus the client can view images but not upload new.
+                            Allows for fine grained access control through
+                            combining various scopes.
+
+            2) any(scopes). The token must be authorized access to one of the
+                            scopes associated with the resource. For example,
+                            token has access to ``read-only-images``.
+                            Allows for fine grained, although arguably less
+                            convenient, access control.
+
+        A powerful way to use scopes would mimic UNIX ACLs and see a scope
+        as a group with certain privileges. For a restful API these might
+        map to HTTP verbs instead of read, write and execute.
+
+        Note, the request.user attribute can be set to the resource owner
+        associated with this token. Similarly the request.client and
+        request.scopes attribute can be set to associated client object
+        and authorized scopes. If you then use a decorator such as the
+        one provided for django these attributes will be made available
+        in all protected views as keyword arguments.
+
+        :param token: Unicode Bearer token
+        :param scopes: List of scopes (defined by you)
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is indirectly used by all core Bearer token issuing grant types:
+            - Authorization Code Grant
+            - Implicit Grant
+            - Resource Owner Password Credentials Grant
+            - Client Credentials Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_client_id(self, client_id, request, *args, **kwargs):
+        """Ensure client_id belong to a valid and active client.
+
+        Note, while not strictly necessary it can often be very convenient
+        to set request.client to the client object associated with the
+        given client_id.
+
+        :param request: oauthlib.common.Request
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+            - Implicit Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_code(self, client_id, code, client, request, *args, **kwargs):
+        """Ensure the authorization_code is valid and assigned to client.
+
+        OBS! The request.user attribute should be set to the resource owner
+        associated with this authorization code. Similarly request.scopes and
+        request.state must also be set.
+
+        :param client_id: Unicode client identifier
+        :param code: Unicode authorization code
+        :param client: Client object set by you, see authenticate_client.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs):
+        """Ensure client is authorized to use the grant_type requested.
+
+        :param client_id: Unicode client identifier
+        :param grant_type: Unicode grant type, i.e. authorization_code, password.
+        :param client: Client object set by you, see authenticate_client.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+            - Resource Owner Password Credentials Grant
+            - Client Credentials Grant
+            - Refresh Token Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
+        """Ensure client is authorized to redirect to the redirect_uri requested.
+
+        All clients should register the absolute URIs of all URIs they intend
+        to redirect to. The registration is outside of the scope of oauthlib.
+
+        :param client_id: Unicode client identifier
+        :param redirect_uri: Unicode absolute URI
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+            - Implicit Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs):
+        """Ensure the Bearer token is valid and authorized access to scopes.
+
+        OBS! The request.user attribute should be set to the resource owner
+        associated with this refresh token.
+
+        :param refresh_token: Unicode refresh token
+        :param client: Client object set by you, see authenticate_client.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant (indirectly by issuing refresh tokens)
+            - Resource Owner Password Credentials Grant (also indirectly)
+            - Refresh Token Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs):
+        """Ensure client is authorized to use the response_type requested.
+
+        :param client_id: Unicode client identifier
+        :param response_type: Unicode response type, i.e. code, token.
+        :param client: Client object set by you, see authenticate_client.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Authorization Code Grant
+            - Implicit Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
+        """Ensure the client is authorized access to requested scopes.
+
+        :param client_id: Unicode client identifier
+        :param scopes: List of scopes (defined by you)
+        :param client: Client object set by you, see authenticate_client.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by all core grant types:
+            - Authorization Code Grant
+            - Implicit Grant
+            - Resource Owner Password Credentials Grant
+            - Client Credentials Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_user(self, username, password, client, request, *args, **kwargs):
+        """Ensure the username and password is valid.
+
+        OBS! The validation should also set the user attribute of the request
+        to a valid resource owner, i.e. request.user = username or similar. If
+        not set you will be unable to associate a token with a user in the
+        persistance method used (commonly, save_bearer_token).
+
+        :param username: Unicode username
+        :param password: Unicode password
+        :param client: Client object set by you, see authenticate_client.
+        :param request: The HTTP Request (oauthlib.common.Request)
+        :rtype: True or False
+
+        Method is used by:
+            - Resource Owner Password Credentials Grant
+        """
+        raise NotImplementedError('Subclasses must implement this method.')
diff --git a/oauthlib/oauth2/rfc6749/tokens.py b/oauthlib/oauth2/rfc6749/tokens.py
new file mode 100644 (file)
index 0000000..3252e90
--- /dev/null
@@ -0,0 +1,294 @@
+"""
+oauthlib.oauth2.rfc6749.tokens
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods for adding two types of access tokens to requests.
+
+- Bearer http://tools.ietf.org/html/rfc6750
+- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+"""
+from __future__ import absolute_import, unicode_literals
+
+from binascii import b2a_base64
+import hashlib
+import hmac
+try:
+    from urlparse import urlparse
+except ImportError:
+    from urllib.parse import urlparse
+
+from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
+from oauthlib import common
+
+from . import utils
+
+
+class OAuth2Token(dict):
+
+    def __init__(self, params, old_scope=None):
+        super(OAuth2Token, self).__init__(params)
+        self._new_scope = None
+        if 'scope' in params:
+            self._new_scope = set(utils.scope_to_list(params['scope']))
+        if old_scope is not None:
+            self._old_scope = set(utils.scope_to_list(old_scope))
+            if self._new_scope is None:
+                # the rfc says that if the scope hasn't changed, it's optional
+                # in params so set the new scope to the old scope
+                self._new_scope = self._old_scope
+        else:
+            self._old_scope = self._new_scope
+
+    @property
+    def scope_changed(self):
+        return self._new_scope != self._old_scope
+
+    @property
+    def old_scope(self):
+        return utils.list_to_scope(self._old_scope)
+
+    @property
+    def old_scopes(self):
+        return list(self._old_scope)
+
+    @property
+    def scope(self):
+        return utils.list_to_scope(self._new_scope)
+
+    @property
+    def scopes(self):
+        return list(self._new_scope)
+
+    @property
+    def missing_scopes(self):
+        return list(self._old_scope - self._new_scope)
+
+    @property
+    def additional_scopes(self):
+        return list(self._new_scope - self._old_scope)
+
+
+def prepare_mac_header(token, uri, key, http_method,
+                       nonce=None,
+                       headers=None,
+                       body=None,
+                       ext='',
+                       hash_algorithm='hmac-sha-1',
+                       issue_time=None,
+                       draft=0):
+    """Add an `MAC Access Authentication`_ signature to headers.
+
+    Unlike OAuth 1, this HMAC signature does not require inclusion of the
+    request payload/body, neither does it use a combination of client_secret
+    and token_secret but rather a mac_key provided together with the access
+    token.
+
+    Currently two algorithms are supported, "hmac-sha-1" and "hmac-sha-256",
+    `extension algorithms`_ are not supported.
+
+    Example MAC Authorization header, linebreaks added for clarity
+
+    Authorization: MAC id="h480djs93hd8",
+                       nonce="1336363200:dj83hs9s",
+                       mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
+
+    .. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
+    .. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
+
+    :param uri: Request URI.
+    :param headers: Request headers as a dictionary.
+    :param http_method: HTTP Request method.
+    :param key: MAC given provided by token endpoint.
+    :param hash_algorithm: HMAC algorithm provided by token endpoint.
+    :param issue_time: Time when the MAC credentials were issued (datetime).
+    :param draft: MAC authentication specification version.
+    :return: headers dictionary with the authorization field added.
+    """
+    http_method = http_method.upper()
+    host, port = utils.host_from_uri(uri)
+
+    if hash_algorithm.lower() == 'hmac-sha-1':
+        h = hashlib.sha1
+    elif hash_algorithm.lower() == 'hmac-sha-256':
+        h = hashlib.sha256
+    else:
+        raise ValueError('unknown hash algorithm')
+
+    if draft == 0:
+        nonce = nonce or '{0}:{1}'.format(utils.generate_age(issue_time),
+                                          common.generate_nonce())
+    else:
+        ts = common.generate_timestamp()
+        nonce = common.generate_nonce()
+
+    sch, net, path, par, query, fra = urlparse(uri)
+
+    if query:
+        request_uri = path + '?' + query
+    else:
+        request_uri = path
+
+    # Hash the body/payload
+    if body is not None and draft == 0:
+        body = body.encode('utf-8')
+        bodyhash = b2a_base64(h(body).digest())[:-1].decode('utf-8')
+    else:
+        bodyhash = ''
+
+    # Create the normalized base string
+    base = []
+    if draft == 0:
+        base.append(nonce)
+    else:
+        base.append(ts)
+        base.append(nonce)
+    base.append(http_method.upper())
+    base.append(request_uri)
+    base.append(host)
+    base.append(port)
+    if draft == 0:
+        base.append(bodyhash)
+    base.append(ext or '')
+    base_string = '\n'.join(base) + '\n'
+
+    # hmac struggles with unicode strings - http://bugs.python.org/issue5285
+    if isinstance(key, unicode_type):
+        key = key.encode('utf-8')
+    sign = hmac.new(key, base_string.encode('utf-8'), h)
+    sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
+
+    header = []
+    header.append('MAC id="%s"' % token)
+    if draft != 0:
+        header.append('ts="%s"' % ts)
+    header.append('nonce="%s"' % nonce)
+    if bodyhash:
+        header.append('bodyhash="%s"' % bodyhash)
+    if ext:
+        header.append('ext="%s"' % ext)
+    header.append('mac="%s"' % sign)
+
+    headers = headers or {}
+    headers['Authorization'] = ', '.join(header)
+    return headers
+
+
+def prepare_bearer_uri(token, uri):
+    """Add a `Bearer Token`_ to the request URI.
+    Not recommended, use only if client can't use authorization header or body.
+
+    http://www.example.com/path?access_token=h480djs93hd8
+
+    .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+    """
+    return add_params_to_uri(uri, [(('access_token', token))])
+
+
+def prepare_bearer_headers(token, headers=None):
+    """Add a `Bearer Token`_ to the request URI.
+    Recommended method of passing bearer tokens.
+
+    Authorization: Bearer h480djs93hd8
+
+    .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+    """
+    headers = headers or {}
+    headers['Authorization'] = 'Bearer %s' % token
+    return headers
+
+
+def prepare_bearer_body(token, body=''):
+    """Add a `Bearer Token`_ to the request body.
+
+    access_token=h480djs93hd8
+
+    .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750
+    """
+    return add_params_to_qs(body, [(('access_token', token))])
+
+
+def random_token_generator(request, refresh_token=False):
+    return common.generate_token()
+
+
+def signed_token_generator(private_pem, **kwargs):
+    def signed_token_generator(request):
+        request.claims = kwargs
+        return common.generate_signed_token(private_pem, request)
+
+    return signed_token_generator
+
+
+class TokenBase(object):
+
+    def __call__(self, request, refresh_token=False):
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def validate_request(self, request):
+        raise NotImplementedError('Subclasses must implement this method.')
+
+    def estimate_type(self, request):
+        raise NotImplementedError('Subclasses must implement this method.')
+
+
+class BearerToken(TokenBase):
+
+    def __init__(self, request_validator=None, token_generator=None,
+                 expires_in=None, refresh_token_generator=None):
+        self.request_validator = request_validator
+        self.token_generator = token_generator or random_token_generator
+        self.refresh_token_generator = (
+            refresh_token_generator or self.token_generator
+        )
+        self.expires_in = expires_in or 3600
+
+    def create_token(self, request, refresh_token=False):
+        """Create a BearerToken, by default without refresh token."""
+
+        if callable(self.expires_in):
+            expires_in = self.expires_in(request)
+        else:
+            expires_in = self.expires_in
+
+        request.expires_in = expires_in
+
+        token = {
+            'access_token': self.token_generator(request),
+            'expires_in': expires_in,
+            'token_type': 'Bearer',
+        }
+
+        if request.scopes is not None:
+            token['scope'] = ' '.join(request.scopes)
+
+        if request.state is not None:
+            token['state'] = request.state
+
+        if refresh_token:
+            if (request.refresh_token and
+                    not self.request_validator.rotate_refresh_token(request)):
+                token['refresh_token'] = request.refresh_token
+            else:
+                token['refresh_token'] = self.refresh_token_generator(request)
+
+        token.update(request.extra_credentials or {})
+        token = OAuth2Token(token)
+        self.request_validator.save_bearer_token(token, request)
+        return token
+
+    def validate_request(self, request):
+        token = None
+        if 'Authorization' in request.headers:
+            token = request.headers.get('Authorization')[7:]
+        else:
+            token = request.access_token
+        return self.request_validator.validate_bearer_token(
+            token, request.scopes, request)
+
+    def estimate_type(self, request):
+        if request.headers.get('Authorization', '').startswith('Bearer'):
+            return 9
+        elif request.access_token is not None:
+            return 5
+        else:
+            return 0
diff --git a/oauthlib/oauth2/rfc6749/utils.py b/oauthlib/oauth2/rfc6749/utils.py
new file mode 100644 (file)
index 0000000..6a8e24b
--- /dev/null
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+"""
+oauthlib.utils
+~~~~~~~~~~~~~~
+
+This module contains utility methods used by various parts of the OAuth 2 spec.
+"""
+from __future__ import absolute_import, unicode_literals
+
+import os
+import datetime
+try:
+    from urllib import quote
+except ImportError:
+    from urllib.parse import quote
+try:
+    from urlparse import urlparse
+except ImportError:
+    from urllib.parse import urlparse
+from oauthlib.common import unicode_type, urldecode
+
+
+def list_to_scope(scope):
+    """Convert a list of scopes to a space separated string."""
+    if isinstance(scope, unicode_type) or scope is None:
+        return scope
+    elif isinstance(scope, (tuple, list)):
+        return " ".join([unicode_type(s) for s in scope])
+    elif isinstance(scope, set):
+        return list_to_scope(list(scope))
+    else:
+        raise ValueError("Invalid scope, must be string or list.")
+
+
+def scope_to_list(scope):
+    """Convert a space separated string to a list of scopes."""
+    if isinstance(scope, list):
+        return [unicode_type(s) for s in scope]
+    if isinstance(scope, set):
+        scope_to_list(list(scope))
+    elif scope is None:
+        return None
+    else:
+        return scope.strip().split(" ")
+
+
+def params_from_uri(uri):
+    params = dict(urldecode(urlparse(uri).query))
+    if 'scope' in params:
+        params['scope'] = scope_to_list(params['scope'])
+    return params
+
+
+def host_from_uri(uri):
+    """Extract hostname and port from URI.
+
+    Will use default port for HTTP and HTTPS if none is present in the URI.
+    """
+    default_ports = {
+        'HTTP': '80',
+        'HTTPS': '443',
+    }
+
+    sch, netloc, path, par, query, fra = urlparse(uri)
+    if ':' in netloc:
+        netloc, port = netloc.split(':', 1)
+    else:
+        port = default_ports.get(sch.upper())
+
+    return netloc, port
+
+
+def escape(u):
+    """Escape a string in an OAuth-compatible fashion.
+
+    TODO: verify whether this can in fact be used for OAuth 2
+
+    """
+    if not isinstance(u, unicode_type):
+        raise ValueError('Only unicode objects are escapable.')
+    return quote(u.encode('utf-8'), safe=b'~')
+
+
+def generate_age(issue_time):
+    """Generate a age parameter for MAC authentication draft 00."""
+    td = datetime.datetime.now() - issue_time
+    age = (td.microseconds + (td.seconds + td.days * 24 * 3600)
+           * 10 ** 6) / 10 ** 6
+    return unicode_type(age)
+
+
+def is_secure_transport(uri):
+    """Check if the uri is over ssl."""
+    if os.environ.get('OAUTHLIB_INSECURE_TRANSPORT'):
+        return True
+    return uri.lower().startswith('https://')
diff --git a/oauthlib/signals.py b/oauthlib/signals.py
new file mode 100644 (file)
index 0000000..2f86650
--- /dev/null
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+"""
+    Implements signals based on blinker if available, otherwise
+    falls silently back to a noop. Shamelessly stolen from flask.signals:
+    https://github.com/mitsuhiko/flask/blob/master/flask/signals.py
+"""
+signals_available = False
+try:
+    from blinker import Namespace
+    signals_available = True
+except ImportError:
+    class Namespace(object):
+        def signal(self, name, doc=None):
+            return _FakeSignal(name, doc)
+
+    class _FakeSignal(object):
+        """If blinker is unavailable, create a fake class with the same
+        interface that allows sending of signals but will fail with an
+        error on anything else.  Instead of doing anything on send, it
+        will just ignore the arguments and do nothing instead.
+        """
+
+        def __init__(self, name, doc=None):
+            self.name = name
+            self.__doc__ = doc
+        def _fail(self, *args, **kwargs):
+            raise RuntimeError('signalling support is unavailable '
+                               'because the blinker library is '
+                               'not installed.')
+        send = lambda *a, **kw: None
+        connect = disconnect = has_receivers_for = receivers_for = \
+            temporarily_connected_to = connected_to = _fail
+        del _fail
+
+# The namespace for code signals.  If you are not oauthlib code, do
+# not put signals in here.  Create your own namespace instead.
+_signals = Namespace()
+
+
+# Core signals.
+scope_changed = _signals.signal('scope-changed')
diff --git a/oauthlib/uri_validate.py b/oauthlib/uri_validate.py
new file mode 100644 (file)
index 0000000..e553f32
--- /dev/null
@@ -0,0 +1,215 @@
+"""
+Regex for URIs
+
+These regex are directly derived from the collected ABNF in RFC3986
+(except for DIGIT, ALPHA and HEXDIG, defined by RFC2234).
+
+They should be processed with re.VERBOSE.
+
+Thanks Mark Nottingham for this code - https://gist.github.com/138549
+"""
+from __future__ import unicode_literals
+import re
+
+# basics
+
+DIGIT = r"[\x30-\x39]"
+
+ALPHA = r"[\x41-\x5A\x61-\x7A]"
+
+HEXDIG = r"[\x30-\x39A-Fa-f]"
+
+#   pct-encoded   = "%" HEXDIG HEXDIG
+pct_encoded = r" %% %(HEXDIG)s %(HEXDIG)s" % locals()
+
+#   unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
+unreserved = r"(?: %(ALPHA)s | %(DIGIT)s | \- | \. | _ | ~ )" % locals()
+
+# gen-delims    = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+gen_delims = r"(?: : | / | \? | \# | \[ | \] | @ )"
+
+#   sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
+#                 / "*" / "+" / "," / ";" / "="
+sub_delims = r"""(?: ! | \$ | & | ' | \( | \) |
+                     \* | \+ | , | ; | = )"""
+
+#   pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
+pchar = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | : | @ )" % locals(
+)
+
+#   reserved      = gen-delims / sub-delims
+reserved = r"(?: %(gen_delims)s | %(sub_delims)s )" % locals()
+
+
+# scheme
+
+#   scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+scheme = r"%(ALPHA)s (?: %(ALPHA)s | %(DIGIT)s | \+ | \- | \. )*" % locals()
+
+
+# authority
+
+#   dec-octet     = DIGIT                 ; 0-9
+#                 / %x31-39 DIGIT         ; 10-99
+#                 / "1" 2DIGIT            ; 100-199
+#                 / "2" %x30-34 DIGIT     ; 200-249
+#                 / "25" %x30-35          ; 250-255
+dec_octet = r"""(?: %(DIGIT)s |
+                    [\x31-\x39] %(DIGIT)s |
+                    1 %(DIGIT)s{2} |
+                    2 [\x30-\x34] %(DIGIT)s |
+                    25 [\x30-\x35]
+                )
+""" % locals()
+
+#  IPv4address   = dec-octet "." dec-octet "." dec-octet "." dec-octet
+IPv4address = r"%(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s \. %(dec_octet)s" % locals(
+)
+
+#  h16           = 1*4HEXDIG
+h16 = r"(?: %(HEXDIG)s ){1,4}" % locals()
+
+#  ls32          = ( h16 ":" h16 ) / IPv4address
+ls32 = r"(?: (?: %(h16)s : %(h16)s ) | %(IPv4address)s )" % locals()
+
+#   IPv6address   =                            6( h16 ":" ) ls32
+#                 /                       "::" 5( h16 ":" ) ls32
+#                 / [               h16 ] "::" 4( h16 ":" ) ls32
+#                 / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
+#                 / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
+#                 / [ *3( h16 ":" ) h16 ] "::"    h16 ":"   ls32
+#                 / [ *4( h16 ":" ) h16 ] "::"              ls32
+#                 / [ *5( h16 ":" ) h16 ] "::"              h16
+#                 / [ *6( h16 ":" ) h16 ] "::"
+IPv6address = r"""(?:                                  (?: %(h16)s : ){6} %(ls32)s |
+                                                    :: (?: %(h16)s : ){5} %(ls32)s |
+                                            %(h16)s :: (?: %(h16)s : ){4} %(ls32)s |
+                         (?: %(h16)s : )    %(h16)s :: (?: %(h16)s : ){3} %(ls32)s |
+                         (?: %(h16)s : ){2} %(h16)s :: (?: %(h16)s : ){2} %(ls32)s |
+                         (?: %(h16)s : ){3} %(h16)s ::     %(h16)s :      %(ls32)s |
+                         (?: %(h16)s : ){4} %(h16)s ::                    %(ls32)s |
+                         (?: %(h16)s : ){5} %(h16)s ::                    %(h16)s  |
+                         (?: %(h16)s : ){6} %(h16)s ::
+                  )
+""" % locals()
+
+#   IPvFuture     = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
+IPvFuture = r"v %(HEXDIG)s+ \. (?: %(unreserved)s | %(sub_delims)s | : )+" % locals()
+
+#   IP-literal    = "[" ( IPv6address / IPvFuture  ) "]"
+IP_literal = r"\[ (?: %(IPv6address)s | %(IPvFuture)s ) \]" % locals()
+
+#   reg-name      = *( unreserved / pct-encoded / sub-delims )
+reg_name = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s )*" % locals()
+
+#   userinfo      = *( unreserved / pct-encoded / sub-delims / ":" )
+userinfo = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | : )" % locals(
+)
+
+#   host          = IP-literal / IPv4address / reg-name
+host = r"(?: %(IP_literal)s | %(IPv4address)s | %(reg_name)s )" % locals()
+
+#   port          = *DIGIT
+port = r"(?: %(DIGIT)s )*" % locals()
+
+#   authority     = [ userinfo "@" ] host [ ":" port ]
+authority = r"(?: %(userinfo)s @)? %(host)s (?: : %(port)s)?" % locals()
+
+# Path
+
+#   segment       = *pchar
+segment = r"%(pchar)s*" % locals()
+
+#   segment-nz    = 1*pchar
+segment_nz = r"%(pchar)s+" % locals()
+
+#   segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
+#                 ; non-zero-length segment without any colon ":"
+segment_nz_nc = r"(?: %(unreserved)s | %(pct_encoded)s | %(sub_delims)s | @ )+" % locals()
+
+#   path-abempty  = *( "/" segment )
+path_abempty = r"(?: / %(segment)s )*" % locals()
+
+#   path-absolute = "/" [ segment-nz *( "/" segment ) ]
+path_absolute = r"/ (?: %(segment_nz)s (?: / %(segment)s )* )?" % locals()
+
+#   path-noscheme = segment-nz-nc *( "/" segment )
+path_noscheme = r"%(segment_nz_nc)s (?: / %(segment)s )*" % locals()
+
+#   path-rootless = segment-nz *( "/" segment )
+path_rootless = r"%(segment_nz)s (?: / %(segment)s )*" % locals()
+
+#   path-empty    = 0<pchar>
+path_empty = r""  # FIXME
+
+#   path          = path-abempty    ; begins with "/" or is empty
+#                 / path-absolute   ; begins with "/" but not "//"
+#                 / path-noscheme   ; begins with a non-colon segment
+#                 / path-rootless   ; begins with a segment
+#                 / path-empty      ; zero characters
+path = r"""(?: %(path_abempty)s |
+               %(path_absolute)s |
+               %(path_noscheme)s |
+               %(path_rootless)s |
+               %(path_empty)s
+            )
+""" % locals()
+
+### Query and Fragment
+
+#   query         = *( pchar / "/" / "?" )
+query = r"(?: %(pchar)s | / | \? )*" % locals()
+
+#   fragment      = *( pchar / "/" / "?" )
+fragment = r"(?: %(pchar)s | / | \? )*" % locals()
+
+# URIs
+
+#   hier-part     = "//" authority path-abempty
+#                 / path-absolute
+#                 / path-rootless
+#                 / path-empty
+hier_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
+                    %(path_absolute)s |
+                    %(path_rootless)s |
+                    %(path_empty)s
+                )
+""" % locals()
+
+#   relative-part = "//" authority path-abempty
+#                 / path-absolute
+#                 / path-noscheme
+#                 / path-empty
+relative_part = r"""(?: (?: // %(authority)s %(path_abempty)s ) |
+                        %(path_absolute)s |
+                        %(path_noscheme)s |
+                        %(path_empty)s
+                    )
+""" % locals()
+
+# relative-ref  = relative-part [ "?" query ] [ "#" fragment ]
+relative_ref = r"%(relative_part)s (?: \? %(query)s)? (?: \# %(fragment)s)?" % locals(
+)
+
+# URI           = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? (?: \# %(fragment)s )? )$" % locals(
+)
+
+#   URI-reference = URI / relative-ref
+URI_reference = r"^(?: %(URI)s | %(relative_ref)s )$" % locals()
+
+#   absolute-URI  = scheme ":" hier-part [ "?" query ]
+absolute_URI = r"^(?: %(scheme)s : %(hier_part)s (?: \? %(query)s )? )$" % locals(
+)
+
+
+def is_uri(uri):
+    return re.match(URI, uri, re.VERBOSE)
+
+
+def is_uri_reference(uri):
+    return re.match(URI_reference, uri, re.VERBOSE)
+
+
+def is_absolute_uri(uri):
+    return re.match(absolute_URI, uri, re.VERBOSE)
diff --git a/requests_oauthlib/__init__.py b/requests_oauthlib/__init__.py
new file mode 100644 (file)
index 0000000..89f668b
--- /dev/null
@@ -0,0 +1,22 @@
+from .oauth1_auth import OAuth1
+from .oauth1_session import OAuth1Session
+from .oauth2_auth import OAuth2
+from .oauth2_session import OAuth2Session, TokenUpdated
+
+__version__ = '0.4.2'
+
+import requests
+if requests.__version__ < '2.0.0':
+    msg = ('You are using requests version %s, which is older than '
+           'requests-oauthlib expects, please upgrade to 2.0.0 or later.')
+    raise Warning(msg % requests.__version__)
+
+import logging
+try:  # Python 2.7+
+    from logging import NullHandler
+except ImportError:
+   class NullHandler(logging.Handler):
+       def emit(self, record):
+           pass
+
+logging.getLogger('requests_oauthlib').addHandler(NullHandler())
diff --git a/requests_oauthlib/compliance_fixes/__init__.py b/requests_oauthlib/compliance_fixes/__init__.py
new file mode 100644 (file)
index 0000000..30cdd10
--- /dev/null
@@ -0,0 +1,5 @@
+from __future__ import absolute_import
+
+from .facebook import facebook_compliance_fix
+from .linkedin import linkedin_compliance_fix
+from .weibo import weibo_compliance_fix
diff --git a/requests_oauthlib/compliance_fixes/douban.py b/requests_oauthlib/compliance_fixes/douban.py
new file mode 100644 (file)
index 0000000..2e45b3b
--- /dev/null
@@ -0,0 +1,18 @@
+import json
+
+from oauthlib.common import to_unicode
+
+
+def douban_compliance_fix(session):
+
+    def fix_token_type(r):
+        token = json.loads(r.text)
+        token.setdefault('token_type', 'Bearer')
+        fixed_token = json.dumps(token)
+        r._content = to_unicode(fixed_token).encode('utf-8')
+        return r
+
+    session._client_default_token_placement = 'query'
+    session.register_compliance_hook('access_token_response', fix_token_type)
+
+    return session
diff --git a/requests_oauthlib/compliance_fixes/facebook.py b/requests_oauthlib/compliance_fixes/facebook.py
new file mode 100644 (file)
index 0000000..07181c3
--- /dev/null
@@ -0,0 +1,33 @@
+from json import dumps
+try:
+    from urlparse import parse_qsl
+except ImportError:
+    from urllib.parse import parse_qsl
+
+from oauthlib.common import to_unicode
+
+
+def facebook_compliance_fix(session):
+
+    def _compliance_fix(r):
+        # if Facebook claims to be sending us json, let's trust them.
+        if 'application/json' in r.headers.get('content-type', {}):
+            return r
+
+        # Facebook returns a content-type of text/plain when sending their
+        # x-www-form-urlencoded responses, along with a 200. If not, let's
+        # assume we're getting JSON and bail on the fix.
+        if 'text/plain' in r.headers.get('content-type', {}) and r.status_code == 200:
+            token = dict(parse_qsl(r.text, keep_blank_values=True))
+        else:
+            return r
+
+        expires = token.get('expires')
+        if expires is not None:
+            token['expires_in'] = expires
+        token['token_type'] = 'Bearer'
+        r._content = to_unicode(dumps(token)).encode('UTF-8')
+        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..c6e4d68
--- /dev/null
@@ -0,0 +1,24 @@
+from json import loads, dumps
+
+from oauthlib.common import add_params_to_uri, to_unicode
+
+
+def linkedin_compliance_fix(session):
+
+    def _missing_token_type(r):
+        token = loads(r.text)
+        token['token_type'] = 'Bearer'
+        r._content = to_unicode(dumps(token)).encode('UTF-8')
+        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/compliance_fixes/weibo.py b/requests_oauthlib/compliance_fixes/weibo.py
new file mode 100644 (file)
index 0000000..28aca32
--- /dev/null
@@ -0,0 +1,17 @@
+from json import loads, dumps
+
+from oauthlib.common import to_unicode
+
+
+def weibo_compliance_fix(session):
+
+    def _missing_token_type(r):
+        token = loads(r.text)
+        token['token_type'] = 'Bearer'
+        r._content = to_unicode(dumps(token)).encode('UTF-8')
+        return r
+
+    session._client.default_token_placement = 'query'
+    session.register_compliance_hook('access_token_response',
+                                     _missing_token_type)
+    return session
diff --git a/requests_oauthlib/oauth1_auth.py b/requests_oauthlib/oauth1_auth.py
new file mode 100644 (file)
index 0000000..303ecd2
--- /dev/null
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import logging
+
+from oauthlib.common import extract_params
+from oauthlib.oauth1 import Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER
+from oauthlib.oauth1 import SIGNATURE_TYPE_BODY
+from requests.compat import is_py3
+from requests.utils import to_native_string
+
+CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
+CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
+
+if is_py3:
+    unicode = str
+
+log = logging.getLogger(__name__)
+
+# 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)"""
+
+    client_class = Client
+
+    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',
+            client_class=None,
+            force_include_body=False,
+            **kwargs):
+
+        try:
+            signature_type = signature_type.upper()
+        except AttributeError:
+            pass
+
+        client_class = client_class or self.client_class
+
+        self.force_include_body = force_include_body
+
+        self.client = client_class(client_key, client_secret, resource_owner_key,
+            resource_owner_secret, callback_uri, signature_method,
+            signature_type, rsa_key, verifier, decoding=decoding, **kwargs)
+
+    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.
+        log.debug('Signing request %s using client %s', r, self.client)
+
+        content_type = r.headers.get('Content-Type', '')
+        if (not content_type and extract_params(r.body)
+                or self.client.signature_type == SIGNATURE_TYPE_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)
+
+        log.debug('Including body in call to sign: %s',
+                  is_form_encoded or self.force_include_body)
+
+        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)
+        elif self.force_include_body:
+            # To allow custom clients to work on non form encoded bodies.
+            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_string(r.url)
+        log.debug('Updated url: %s', r.url)
+        log.debug('Updated headers: %s', headers)
+        log.debug('Updated body: %r', r.body)
+        return r
diff --git a/requests_oauthlib/oauth1_session.py b/requests_oauthlib/oauth1_session.py
new file mode 100644 (file)
index 0000000..3d129ac
--- /dev/null
@@ -0,0 +1,371 @@
+from __future__ import unicode_literals
+
+try:
+    from urlparse import urlparse
+except ImportError:
+    from urllib.parse import urlparse
+
+import logging
+
+from oauthlib.common import add_params_to_uri
+from oauthlib.common import urldecode as _urldecode
+from oauthlib.oauth1 import (
+    SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_TYPE_AUTH_HEADER
+)
+import requests
+
+from . import OAuth1
+
+import sys
+if sys.version > "3":
+    unicode = str
+
+
+log = logging.getLogger(__name__)
+
+
+def urldecode(body):
+    """Parse query or json to python dictionary"""
+    try:
+        return _urldecode(body)
+    except:
+        import json
+        return json.loads(body)
+
+
+class TokenRequestDenied(ValueError):
+
+    def __init__(self, message, status_code):
+        super(TokenRequestDenied, self).__init__(message)
+        self.status_code = status_code
+
+
+class TokenMissing(ValueError):
+    def __init__(self, message, response):
+        super(TokenMissing, self).__init__(message)
+        self.response = response
+
+
+class VerifierMissing(ValueError):
+    pass
+
+
+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 its 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,
+            client_class=None,
+            force_include_body=False,
+            **kwargs):
+        """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.
+        :param client_class: A subclass of `oauthlib.oauth1.Client` to use with
+                             `requests_oauthlib.OAuth1` instead of the default
+        :param force_include_body: Always include the request body in the
+                                   signature creation.
+        :param **kwargs: Additional keyword arguments passed to `OAuth1`
+        """
+        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,
+                client_class=client_class,
+                force_include_body=force_include_body,
+                **kwargs)
+        self.auth = self._client
+
+    @property
+    def authorized(self):
+        """Boolean that indicates whether this session has an OAuth token
+        or not. If `self.authorized` is True, you can reasonably expect
+        OAuth-protected requests to the resource to succeed. If
+        `self.authorized` is False, you need the user to go through the OAuth
+        authentication dance before OAuth-protected requests to the resource
+        will succeed.
+        """
+        if self._client.client.signature_method == SIGNATURE_RSA:
+            # RSA only uses resource_owner_key
+            return bool(self._client.client.resource_owner_key)
+        else:
+            # other methods of authentication use all three pieces
+            return (
+                bool(self._client.client.client_secret) and
+                bool(self._client.client.resource_owner_key) and
+                bool(self._client.client.resource_owner_secret)
+            )
+
+    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
+        log.debug('Adding parameters %s to url %s', kwargs, url)
+        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)
+        log.debug('Resetting callback_uri and realm (not needed in next phase).')
+        self._client.client.callback_uri = None
+        self._client.client.realm = None
+        return token
+
+    def fetch_access_token(self, url, verifier=None):
+        """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 verifier:
+            self._client.client.verifier = verifier
+        if not getattr(self._client.client, 'verifier', None):
+            raise VerifierMissing('No client verifier has been set.')
+        token = self._fetch_token(url)
+        log.debug('Resetting verifier attribute, should not be used anymore.')
+        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',
+        }
+        """
+        log.debug('Parsing token from query part of url %s', url)
+        token = dict(urldecode(urlparse(url).query))
+        log.debug('Updating internal client token attribute.')
+        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 TokenMissing(
+                'Response does not contain a token: {resp}'.format(resp=token),
+                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):
+        log.debug('Fetching token from %s using client %s', url, self._client.client)
+        r = self.post(url)
+
+        if r.status_code >= 400:
+            error = "Token request failed with code %s, response was '%s'."
+            raise TokenRequestDenied(error % (r.status_code, r.text), r.status_code)
+
+        log.debug('Decoding token from response "%s"', r.text)
+        try:
+            token = dict(urldecode(r.text))
+        except ValueError as e:
+            error = ("Unable to decode token from token response. "
+                     "This is commonly caused by an unsuccessful request where"
+                     " a non urlencoded error message is returned. "
+                     "The decoding error was %s""" % e)
+            raise ValueError(error)
+
+        log.debug('Obtained token %s', token)
+        log.debug('Updating internal client attributes from token data.')
+        self._populate_attributes(token)
+        return token
+
+    def rebuild_auth(self, prepared_request, response):
+        """
+        When being redirected we should always strip Authorization
+        header, since nonce may not be reused as per OAuth spec.
+        """
+        if 'Authorization' in prepared_request.headers:
+            # If we get redirected to a new host, we should strip out
+            # any authentication headers.
+            prepared_request.headers.pop('Authorization', True)
+            prepared_request.prepare_auth(self.auth)
+        return
diff --git a/requests_oauthlib/oauth2_auth.py b/requests_oauthlib/oauth2_auth.py
new file mode 100644 (file)
index 0000000..42366e7
--- /dev/null
@@ -0,0 +1,35 @@
+from __future__ import unicode_literals
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+from oauthlib.oauth2 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..245cb8a
--- /dev/null
@@ -0,0 +1,317 @@
+from __future__ import unicode_literals
+
+import logging
+
+from oauthlib.common import generate_token, urldecode
+from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
+from oauthlib.oauth2 import TokenExpiredError, is_secure_transport
+import requests
+
+log = logging.getLogger(__name__)
+
+
+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, 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 = state
+        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
+
+    @property
+    def authorized(self):
+        """Boolean that indicates whether this session has an OAuth token
+        or not. If `self.authorized` is True, you can reasonably expect
+        OAuth-protected requests to the resource to succeed. If
+        `self.authorized` is False, you need the user to go through the OAuth
+        authentication dance before OAuth-protected requests to the resource
+        will succeed.
+        """
+        return bool(self._client.access_token)
+
+    def authorization_url(self, url, state=None, **kwargs):
+        """Form an authorization URL.
+
+        :param url: Authorization endpoint url, must be HTTPS.
+        :param state: An optional state string for CSRF protection. If not
+                      given it will be generated for you.
+        :param kwargs: Extra parameters to include.
+        :return: authorization_url, state
+        """
+        state = state or 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, method='POST',
+            timeout=None, headers=None, verify=True, **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 method: The HTTP method used to make the request. Defaults
+                       to POST, but may also be GET. Other methods should
+                       be added as needed.
+        :param headers: Dict to default request headers with.
+        :param timeout: Timeout of the request in seconds.
+        :param verify: Verify SSL certificate.
+        :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)
+
+        headers = headers or {
+            'Accept': 'application/json',
+            'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
+        }
+        if method.upper() == 'POST':
+            r = self.post(token_url, data=dict(urldecode(body)),
+                timeout=timeout, headers=headers, auth=auth,
+                verify=verify)
+            log.debug('Prepared fetch token request body %s', body)
+        elif method.upper() == 'GET':
+            # if method is not 'POST', switch body to querystring and GET
+            r = self.get(token_url, params=dict(urldecode(body)),
+                timeout=timeout, headers=headers, auth=auth,
+                verify=verify)
+            log.debug('Prepared fetch token request querystring %s', body)
+        else:
+            raise ValueError('The method kwarg must be POST or GET.')
+
+        log.debug('Request to fetch token completed with status %s.',
+                  r.status_code)
+        log.debug('Request headers were %s', r.request.headers)
+        log.debug('Request body was %s', r.request.body)
+        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)
+
+        r.raise_for_status()
+
+        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,
+                      timeout=None, verify=True, **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 timeout: Timeout of the request in seconds.
+        :param verify: Verify SSL certificate.
+        :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,
+                      timeout=timeout, verify=verify)
+        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)
index ff0005fa5ee72ec80a0486914a03a0fbb969b0b5..b691e3491272a128992e8e8a7198016f5bb0ae24 100644 (file)
@@ -5,7 +5,7 @@
 """
 Tweepy Twitter API library
 """
 """
 Tweepy Twitter API library
 """
-__version__ = '2.3'
+__version__ = '3.3.0'
 __author__ = 'Joshua Roesslein'
 __license__ = 'MIT'
 
 __author__ = 'Joshua Roesslein'
 __license__ = 'MIT'
 
@@ -13,7 +13,7 @@ from tweepy.models import Status, User, DirectMessage, Friendship, SavedSearch,
 from tweepy.error import TweepError
 from tweepy.api import API
 from tweepy.cache import Cache, MemoryCache, FileCache
 from tweepy.error import TweepError
 from tweepy.api import API
 from tweepy.cache import Cache, MemoryCache, FileCache
-from tweepy.auth import OAuthHandler
+from tweepy.auth import OAuthHandler, AppAuthHandler
 from tweepy.streaming import Stream, StreamListener
 from tweepy.cursor import Cursor
 
 from tweepy.streaming import Stream, StreamListener
 from tweepy.cursor import Cursor
 
@@ -21,7 +21,5 @@ from tweepy.cursor import Cursor
 api = API()
 
 def debug(enable=True, level=1):
 api = API()
 
 def debug(enable=True, level=1):
-
-    import httplib
-    httplib.HTTPConnection.debuglevel = level
-
+    from six.moves.http_client import HTTPConnection
+    HTTPConnection.debuglevel = level
index 1d530790290e0e84ccc629d306f9537b3dba2bca..4744275829945ed6e0e52f1befe5b3d091d4cc2a 100644 (file)
@@ -2,12 +2,16 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
+from __future__ import print_function
+
 import os
 import mimetypes
 
 import os
 import mimetypes
 
+import six
+
 from tweepy.binder import bind_api
 from tweepy.error import TweepError
 from tweepy.binder import bind_api
 from tweepy.error import TweepError
-from tweepy.parsers import ModelParser
+from tweepy.parsers import ModelParser, Parser
 from tweepy.utils import list_to_csv
 
 
 from tweepy.utils import list_to_csv
 
 
@@ -15,698 +19,1269 @@ class API(object):
     """Twitter API"""
 
     def __init__(self, auth_handler=None,
     """Twitter API"""
 
     def __init__(self, auth_handler=None,
-            host='api.twitter.com', search_host='search.twitter.com',
-             cache=None, secure=True, api_root='/1.1', search_root='',
-            retry_count=0, retry_delay=0, retry_errors=None, timeout=60,
-            parser=None, compression=False):
+                 host='api.twitter.com', search_host='search.twitter.com',
+                 upload_host='upload.twitter.com', cache=None, api_root='/1.1',
+                 search_root='', upload_root='/1.1', retry_count=0,
+                 retry_delay=0, retry_errors=None, timeout=60, parser=None,
+                 compression=False, wait_on_rate_limit=False,
+                 wait_on_rate_limit_notify=False, proxy=''):
+        """ Api instance Constructor
+
+        :param auth_handler:
+        :param host:  url of the server of the rest api, default:'api.twitter.com'
+        :param search_host: url of the search server, default:'search.twitter.com'
+        :param upload_host: url of the upload server, default:'upload.twitter.com'
+        :param cache: Cache to query if a GET method is used, default:None
+        :param api_root: suffix of the api version, default:'/1.1'
+        :param search_root: suffix of the search version, default:''
+        :param upload_root: suffix of the upload version, default:'/1.1'
+        :param retry_count: number of allowed retries, default:0
+        :param retry_delay: delay in second between retries, default:0
+        :param retry_errors: default:None
+        :param timeout: delay before to consider the request as timed out in seconds, default:60
+        :param parser: ModelParser instance to parse the responses, default:None
+        :param compression: If the response is compressed, default:False
+        :param wait_on_rate_limit: If the api wait when it hits the rate limit, default:False
+        :param wait_on_rate_limit_notify: If the api print a notification when the rate limit is hit, default:False
+        :param proxy: Url to use as proxy during the HTTP request, default:''
+
+        :raise TypeError: If the given parser is not a ModelParser instance.
+        """
         self.auth = auth_handler
         self.host = host
         self.search_host = search_host
         self.auth = auth_handler
         self.host = host
         self.search_host = search_host
+        self.upload_host = upload_host
         self.api_root = api_root
         self.search_root = search_root
         self.api_root = api_root
         self.search_root = search_root
+        self.upload_root = upload_root
         self.cache = cache
         self.cache = cache
-        self.secure = secure
         self.compression = compression
         self.retry_count = retry_count
         self.retry_delay = retry_delay
         self.retry_errors = retry_errors
         self.timeout = timeout
         self.compression = compression
         self.retry_count = retry_count
         self.retry_delay = retry_delay
         self.retry_errors = retry_errors
         self.timeout = timeout
+        self.wait_on_rate_limit = wait_on_rate_limit
+        self.wait_on_rate_limit_notify = wait_on_rate_limit_notify
         self.parser = parser or ModelParser()
         self.parser = parser or ModelParser()
+        self.proxy = {}
+        if proxy:
+            self.proxy['https'] = proxy
+
+        # Attempt to explain more clearly the parser argument requirements
+        # https://github.com/tweepy/tweepy/issues/421
+        #
+        parser_type = Parser
+        if not isinstance(self.parser, parser_type):
+            raise TypeError(
+                '"parser" argument has to be an instance of "{required}".'
+                ' It is currently a {actual}.'.format(
+                    required=parser_type.__name__,
+                    actual=type(self.parser)
+                )
+            )
+
+    @property
+    def home_timeline(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/home_timeline
+            :allowed_param:'since_id', 'max_id', 'count'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/home_timeline.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['since_id', 'max_id', 'count'],
+            require_auth=True
+        )
+
+    def statuses_lookup(self, id_, include_entities=None,
+                        trim_user=None, map_=None):
+        return self._statuses_lookup(list_to_csv(id_), include_entities,
+                                     trim_user, map_)
+
+    @property
+    def _statuses_lookup(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/lookup
+            :allowed_param:'id', 'include_entities', 'trim_user', 'map'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/lookup.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['id', 'include_entities', 'trim_user', 'map'],
+            require_auth=True
+        )
+
+    @property
+    def user_timeline(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/user_timeline
+            :allowed_param:'id', 'user_id', 'screen_name', 'since_id'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/user_timeline.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['id', 'user_id', 'screen_name', 'since_id',
+                           'max_id', 'count', 'include_rts']
+        )
+
+    @property
+    def mentions_timeline(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/mentions_timeline
+            :allowed_param:'since_id', 'max_id', 'count'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/mentions_timeline.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['since_id', 'max_id', 'count'],
+            require_auth=True
+        )
+
+    @property
+    def related_results(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/get/related_results/show/%3id.format
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/related_results/show/{id}.json',
+            payload_type='relation', payload_list=True,
+            allowed_param=['id'],
+            require_auth=False
+        )
+
+    @property
+    def retweets_of_me(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me
+            :allowed_param:'since_id', 'max_id', 'count'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/retweets_of_me.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['since_id', 'max_id', 'count'],
+            require_auth=True
+        )
+
+    @property
+    def get_status(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/show/%3Aid
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/show.json',
+            payload_type='status',
+            allowed_param=['id']
+        )
+
+    def update_status(self, media_ids=None, *args, **kwargs):
+        """ :reference: https://dev.twitter.com/rest/reference/post/statuses/update
+            :allowed_param:'status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id', 'display_coordinates', 'media_ids'
+        """
+        post_data = {}
+        if media_ids is not None:
+            post_data["media_ids"] = list_to_csv(media_ids)
+        
+        return bind_api(
+            api=self,
+            path='/statuses/update.json',
+            method='POST',
+            payload_type='status',
+            allowed_param=['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id', 'display_coordinates'],
+            require_auth=True
+        )(post_data=post_data, *args, **kwargs)
+
+    def media_upload(self, filename, *args, **kwargs):
+        """ :reference: https://dev.twitter.com/rest/reference/post/media/upload
+            :allowed_param:
+        """
+        f = kwargs.pop('file', None)
+        headers, post_data = API._pack_image(filename, 3072, form_field='media', f=f)
+        kwargs.update({'headers': headers, 'post_data': post_data})
+
+        return bind_api(
+            api=self,
+            path='/media/upload.json',
+            method='POST',
+            payload_type='media',
+            allowed_param=[],
+            require_auth=True,
+            upload_api=True
+        )(*args, **kwargs)
 
 
-    """ statuses/home_timeline """
-    home_timeline = bind_api(
-        path = '/statuses/home_timeline.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['since_id', 'max_id', 'count'],
-        require_auth = True
-    )
-
-    """ statuses/user_timeline """
-    user_timeline = bind_api(
-        path = '/statuses/user_timeline.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['id', 'user_id', 'screen_name', 'since_id',
-                          'max_id', 'count', 'include_rts']
-    )
-
-    """ statuses/mentions """
-    mentions_timeline = bind_api(
-        path = '/statuses/mentions_timeline.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['since_id', 'max_id', 'count'],
-        require_auth = True
-    )
-
-    """/related_results/show/:id.format"""
-    related_results = bind_api(
-        path = '/related_results/show/{id}.json',
-        payload_type = 'relation', payload_list = True,
-        allowed_param = ['id'],
-        require_auth = False
-    )
-
-    """ statuses/retweets_of_me """
-    retweets_of_me = bind_api(
-        path = '/statuses/retweets_of_me.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['since_id', 'max_id', 'count'],
-        require_auth = True
-    )
-
-    """ statuses/show """
-    get_status = bind_api(
-        path = '/statuses/show.json',
-        payload_type = 'status',
-        allowed_param = ['id']
-    )
-
-    """ statuses/update """
-    update_status = bind_api(
-        path = '/statuses/update.json',
-        method = 'POST',
-        payload_type = 'status',
-        allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'],
-        require_auth = True
-    )
-
-    """ statuses/update_with_media """
     def update_with_media(self, filename, *args, **kwargs):
     def update_with_media(self, filename, *args, **kwargs):
-        headers, post_data = API._pack_image(filename, 3072, form_field='media[]')
+        """ :reference: https://dev.twitter.com/rest/reference/post/statuses/update_with_media
+            :allowed_param:'status', 'possibly_sensitive', 'in_reply_to_status_id', 'lat', 'long', 'place_id', 'display_coordinates'
+        """
+        f = kwargs.pop('file', None)
+        headers, post_data = API._pack_image(filename, 3072, form_field='media[]', f=f)
         kwargs.update({'headers': headers, 'post_data': post_data})
 
         return bind_api(
         kwargs.update({'headers': headers, 'post_data': post_data})
 
         return bind_api(
+            api=self,
             path='/statuses/update_with_media.json',
             path='/statuses/update_with_media.json',
-            method = 'POST',
+            method='POST',
             payload_type='status',
             payload_type='status',
-            allowed_param = [
+            allowed_param=[
                 'status', 'possibly_sensitive', 'in_reply_to_status_id', 'lat', 'long',
                 'place_id', 'display_coordinates'
             ],
             require_auth=True
                 'status', 'possibly_sensitive', 'in_reply_to_status_id', 'lat', 'long',
                 'place_id', 'display_coordinates'
             ],
             require_auth=True
-        )(self, *args, **kwargs)
-
-    """ statuses/destroy """
-    destroy_status = bind_api(
-        path = '/statuses/destroy/{id}.json',
-        method = 'POST',
-        payload_type = 'status',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    """ statuses/retweet """
-    retweet = bind_api(
-        path = '/statuses/retweet/{id}.json',
-        method = 'POST',
-        payload_type = 'status',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    """ statuses/retweets """
-    retweets = bind_api(
-        path = '/statuses/retweets/{id}.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['id', 'count'],
-        require_auth = True
-    )
-
-    retweeters = bind_api(
-        path = '/statuses/retweeters/ids.json',
-        payload_type = 'ids',
-        allowed_param = ['id', 'cursor', 'stringify_ids']
-    )
-
-    """ users/show """
-    get_user = bind_api(
-        path = '/users/show.json',
-        payload_type = 'user',
-        allowed_param = ['id', 'user_id', 'screen_name']
-    )
-
-    ''' statuses/oembed '''
-    get_oembed = bind_api(
-        path = '/statuses/oembed.json',
-        payload_type = 'json',
-        allowed_param = ['id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang']
-    )
-
-    """ Perform bulk look up of users from user ID or screenname """
-    def lookup_users(self, user_ids=None, screen_names=None):
-        return self._lookup_users(list_to_csv(user_ids), list_to_csv(screen_names))
-
-    _lookup_users = bind_api(
-        path = '/users/lookup.json',
-        payload_type = 'user', payload_list = True,
-        allowed_param = ['user_id', 'screen_name'],
-    )
-
-    """ Get the authenticated user """
+        )(*args, **kwargs)
+
+    @property
+    def destroy_status(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/statuses/destroy/%3Aid
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/destroy/{id}.json',
+            method='POST',
+            payload_type='status',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def retweet(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/statuses/retweet/%3Aid
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/retweet/{id}.json',
+            method='POST',
+            payload_type='status',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def retweets(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweets/%3Aid
+            :allowed_param:'id', 'count'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/retweets/{id}.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['id', 'count'],
+            require_auth=True
+        )
+
+    @property
+    def retweeters(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/retweeters/ids
+            :allowed_param:'id', 'cursor', 'stringify_ids
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/retweeters/ids.json',
+            payload_type='ids',
+            allowed_param=['id', 'cursor', 'stringify_ids']
+        )
+
+    @property
+    def get_user(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/users/show
+            :allowed_param:'id', 'user_id', 'screen_name'
+        """
+        return bind_api(
+            api=self,
+            path='/users/show.json',
+            payload_type='user',
+            allowed_param=['id', 'user_id', 'screen_name']
+        )
+
+    @property
+    def get_oembed(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/statuses/oembed
+            :allowed_param:'id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang'
+        """
+        return bind_api(
+            api=self,
+            path='/statuses/oembed.json',
+            payload_type='json',
+            allowed_param=['id', 'url', 'maxwidth', 'hide_media', 'omit_script', 'align', 'related', 'lang']
+        )
+
+    def lookup_users(self, user_ids=None, screen_names=None, include_entities=None):
+        """ Perform bulk look up of users from user ID or screenname """
+        post_data = {}
+        if include_entities is not None:
+            include_entities = 'true' if include_entities else 'false'
+            post_data['include_entities'] = include_entities
+        if user_ids:
+            post_data['user_id'] = list_to_csv(user_ids)
+        if screen_names:
+            post_data['screen_name'] = list_to_csv(screen_names)
+
+        return self._lookup_users(post_data=post_data)
+
+    @property
+    def _lookup_users(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/users/lookup
+            allowed_param='user_id', 'screen_name', 'include_entities'
+        """
+        return bind_api(
+            api=self,
+            path='/users/lookup.json',
+            payload_type='user', payload_list=True,
+            method='POST',
+        )
+
     def me(self):
     def me(self):
+        """ Get the authenticated user """
         return self.get_user(screen_name=self.auth.get_username())
 
         return self.get_user(screen_name=self.auth.get_username())
 
-    """ users/search """
-    search_users = bind_api(
-        path = '/users/search.json',
-        payload_type = 'user', payload_list = True,
-        require_auth = True,
-        allowed_param = ['q', 'count', 'page']
-    )
-
-    """ users/suggestions/:slug """
-    suggested_users = bind_api(
-        path = '/users/suggestions/{slug}.json',
-        payload_type = 'user', payload_list = True,
-        require_auth = True,
-        allowed_param = ['slug', 'lang']
-    )
-
-    """ users/suggestions """
-    suggested_categories = bind_api(
-        path = '/users/suggestions.json',
-        payload_type = 'category', payload_list = True,
-        allowed_param = ['lang'],
-        require_auth = True
-    )
-
-    """ users/suggestions/:slug/members """
-    suggested_users_tweets = bind_api(
-        path = '/users/suggestions/{slug}/members.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['slug'],
-        require_auth = True
-    )
-
-    """ direct_messages """
-    direct_messages = bind_api(
-        path = '/direct_messages.json',
-        payload_type = 'direct_message', payload_list = True,
-        allowed_param = ['since_id', 'max_id', 'count'],
-        require_auth = True
-    )
-
-    """ direct_messages/show """
-    get_direct_message = bind_api(
-        path = '/direct_messages/show/{id}.json',
-        payload_type = 'direct_message',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    """ direct_messages/sent """
-    sent_direct_messages = bind_api(
-        path = '/direct_messages/sent.json',
-        payload_type = 'direct_message', payload_list = True,
-        allowed_param = ['since_id', 'max_id', 'count', 'page'],
-        require_auth = True
-    )
-
-    """ direct_messages/new """
-    send_direct_message = bind_api(
-        path = '/direct_messages/new.json',
-        method = 'POST',
-        payload_type = 'direct_message',
-        allowed_param = ['user', 'screen_name', 'user_id', 'text'],
-        require_auth = True
-    )
-
-    """ direct_messages/destroy """
-    destroy_direct_message = bind_api(
-        path = '/direct_messages/destroy.json',
-        method = 'DELETE',
-        payload_type = 'direct_message',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    """ friendships/create """
-    create_friendship = bind_api(
-        path = '/friendships/create.json',
-        method = 'POST',
-        payload_type = 'user',
-        allowed_param = ['id', 'user_id', 'screen_name', 'follow'],
-        require_auth = True
-    )
-
-    """ friendships/destroy """
-    destroy_friendship = bind_api(
-        path = '/friendships/destroy.json',
-        method = 'DELETE',
-        payload_type = 'user',
-        allowed_param = ['id', 'user_id', 'screen_name'],
-        require_auth = True
-    )
-
-    """ friendships/show """
-    show_friendship = bind_api(
-        path = '/friendships/show.json',
-        payload_type = 'friendship',
-        allowed_param = ['source_id', 'source_screen_name',
-                          'target_id', 'target_screen_name']
-    )
-
-    """ Perform bulk look up of friendships from user ID or screenname """
+    @property
+    def search_users(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/users/search
+            :allowed_param:'q', 'count', 'page'
+        """
+        return bind_api(
+            api=self,
+            path='/users/search.json',
+            payload_type='user', payload_list=True,
+            require_auth=True,
+            allowed_param=['q', 'count', 'page']
+        )
+
+    @property
+    def suggested_users(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions/%3Aslug
+            :allowed_param:'slug', 'lang'
+        """
+        return bind_api(
+            api=self,
+            path='/users/suggestions/{slug}.json',
+            payload_type='user', payload_list=True,
+            require_auth=True,
+            allowed_param=['slug', 'lang']
+        )
+
+    @property
+    def suggested_categories(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions
+            :allowed_param:'lang'
+        """
+        return bind_api(
+            api=self,
+            path='/users/suggestions.json',
+            payload_type='category', payload_list=True,
+            allowed_param=['lang'],
+            require_auth=True
+        )
+
+    @property
+    def suggested_users_tweets(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/users/suggestions/%3Aslug/members
+            :allowed_param:'slug'
+        """
+        return bind_api(
+            api=self,
+            path='/users/suggestions/{slug}/members.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['slug'],
+            require_auth=True
+        )
+
+    @property
+    def direct_messages(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages
+            :allowed_param:'since_id', 'max_id', 'count'
+        """
+        return bind_api(
+            api=self,
+            path='/direct_messages.json',
+            payload_type='direct_message', payload_list=True,
+            allowed_param=['since_id', 'max_id', 'count'],
+            require_auth=True
+        )
+
+    @property
+    def get_direct_message(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages/show
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/direct_messages/show/{id}.json',
+            payload_type='direct_message',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def sent_direct_messages(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/direct_messages/sent
+            :allowed_param:'since_id', 'max_id', 'count', 'page'
+        """
+        return bind_api(
+            api=self,
+            path='/direct_messages/sent.json',
+            payload_type='direct_message', payload_list=True,
+            allowed_param=['since_id', 'max_id', 'count', 'page'],
+            require_auth=True
+        )
+
+    @property
+    def send_direct_message(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/direct_messages/new
+            :allowed_param:'user', 'screen_name', 'user_id', 'text'
+        """
+        return bind_api(
+            api=self,
+            path='/direct_messages/new.json',
+            method='POST',
+            payload_type='direct_message',
+            allowed_param=['user', 'screen_name', 'user_id', 'text'],
+            require_auth=True
+        )
+
+    @property
+    def destroy_direct_message(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/direct_messages/destroy
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/direct_messages/destroy.json',
+            method='POST',
+            payload_type='direct_message',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def create_friendship(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/friendships/create
+            :allowed_param:'id', 'user_id', 'screen_name', 'follow'
+        """
+        return bind_api(
+            api=self,
+            path='/friendships/create.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['id', 'user_id', 'screen_name', 'follow'],
+            require_auth=True
+        )
+
+    @property
+    def destroy_friendship(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/friendships/destroy
+            :allowed_param:'id', 'user_id', 'screen_name'
+        """
+        return bind_api(
+            api=self,
+            path='/friendships/destroy.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['id', 'user_id', 'screen_name'],
+            require_auth=True
+        )
+
+    @property
+    def show_friendship(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/friendships/show
+            :allowed_param:'source_id', 'source_screen_name'
+        """
+        return bind_api(
+            api=self,
+            path='/friendships/show.json',
+            payload_type='friendship',
+            allowed_param=['source_id', 'source_screen_name',
+                           'target_id', 'target_screen_name']
+        )
+
     def lookup_friendships(self, user_ids=None, screen_names=None):
     def lookup_friendships(self, user_ids=None, screen_names=None):
+        """ Perform bulk look up of friendships from user ID or screenname """
         return self._lookup_friendships(list_to_csv(user_ids), list_to_csv(screen_names))
 
         return self._lookup_friendships(list_to_csv(user_ids), list_to_csv(screen_names))
 
-    _lookup_friendships = bind_api(
-        path = '/friendships/lookup.json',
-        payload_type = 'relationship', payload_list = True,
-        allowed_param = ['user_id', 'screen_name'],
-        require_auth = True
-    )
-
-
-    """ friends/ids """
-    friends_ids = bind_api(
-        path = '/friends/ids.json',
-        payload_type = 'ids',
-        allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
-    )
-
-    """ friends/list """
-    friends = bind_api(
-        path = '/friends/list.json',
-        payload_type = 'user', payload_list = True,
-        allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
-    )
-
-    """ friendships/incoming """
-    friendships_incoming = bind_api(
-        path = '/friendships/incoming.json',
-        payload_type = 'ids',
-        allowed_param = ['cursor']
-    )
-
-    """ friendships/outgoing"""
-    friendships_outgoing = bind_api(
-        path = '/friendships/outgoing.json',
-        payload_type = 'ids',
-        allowed_param = ['cursor']
-    )
-
-    """ followers/ids """
-    followers_ids = bind_api(
-        path = '/followers/ids.json',
-        payload_type = 'ids',
-        allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
-    )
-
-    """ followers/list """
-    followers = bind_api(
-        path = '/followers/list.json',
-        payload_type = 'user', payload_list = True,
-        allowed_param = ['id', 'user_id', 'screen_name', 'cursor', 'count',
-            'skip_status', 'include_user_entities']
-    )
-
-    """ account/verify_credentials """
+    @property
+    def _lookup_friendships(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/friendships/lookup
+            :allowed_param:'user_id', 'screen_name'
+        """
+        return bind_api(
+            api=self,
+            path='/friendships/lookup.json',
+            payload_type='relationship', payload_list=True,
+            allowed_param=['user_id', 'screen_name'],
+            require_auth=True
+        )
+
+    @property
+    def friends_ids(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/friends/ids
+            :allowed_param:'id', 'user_id', 'screen_name', 'cursor'
+        """
+        return bind_api(
+            api=self,
+            path='/friends/ids.json',
+            payload_type='ids',
+            allowed_param=['id', 'user_id', 'screen_name', 'cursor']
+        )
+
+    @property
+    def friends(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/friends/list
+            :allowed_param:'id', 'user_id', 'screen_name', 'cursor'
+        """
+        return bind_api(
+            api=self,
+            path='/friends/list.json',
+            payload_type='user', payload_list=True,
+            allowed_param=['id', 'user_id', 'screen_name', 'cursor']
+        )
+
+    @property
+    def friendships_incoming(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/friendships/incoming
+            :allowed_param:'cursor'
+        """
+        return bind_api(
+            api=self,
+            path='/friendships/incoming.json',
+            payload_type='ids',
+            allowed_param=['cursor']
+        )
+
+    @property
+    def friendships_outgoing(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/friendships/outgoing
+            :allowed_param:'cursor'
+        """
+        return bind_api(
+            api=self,
+            path='/friendships/outgoing.json',
+            payload_type='ids',
+            allowed_param=['cursor']
+        )
+
+    @property
+    def followers_ids(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/followers/ids
+            :allowed_param:'id', 'user_id', 'screen_name', 'cursor', 'count'
+        """
+        return bind_api(
+            api=self,
+            path='/followers/ids.json',
+            payload_type='ids',
+            allowed_param=['id', 'user_id', 'screen_name', 'cursor', 'count']
+        )
+
+    @property
+    def followers(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/followers/list
+            :allowed_param:'id', 'user_id', 'screen_name', 'cursor', 'count', 'skip_status', 'include_user_entities'
+        """
+        return bind_api(
+            api=self,
+            path='/followers/list.json',
+            payload_type='user', payload_list=True,
+            allowed_param=['id', 'user_id', 'screen_name', 'cursor', 'count',
+                           'skip_status', 'include_user_entities']
+        )
+
     def verify_credentials(self, **kargs):
     def verify_credentials(self, **kargs):
+        """ :reference: https://dev.twitter.com/rest/reference/get/account/verify_credentials
+            :allowed_param:'include_entities', 'skip_status'
+        """
         try:
             return bind_api(
         try:
             return bind_api(
-                path = '/account/verify_credentials.json',
-                payload_type = 'user',
-                require_auth = True,
-                allowed_param = ['include_entities', 'skip_status'],
-            )(self, **kargs)
+                api=self,
+                path='/account/verify_credentials.json',
+                payload_type='user',
+                require_auth=True,
+                allowed_param=['include_entities', 'skip_status'],
+            )(**kargs)
         except TweepError as e:
             if e.response and e.response.status == 401:
                 return False
             raise
 
         except TweepError as e:
             if e.response and e.response.status == 401:
                 return False
             raise
 
-    """ account/rate_limit_status """
-    rate_limit_status = bind_api(
-        path = '/application/rate_limit_status.json',
-        payload_type = 'json',
-        allowed_param = ['resources'],
-        use_cache = False
-    )
-
-    """ account/update_delivery_device """
-    set_delivery_device = bind_api(
-        path = '/account/update_delivery_device.json',
-        method = 'POST',
-        allowed_param = ['device'],
-        payload_type = 'user',
-        require_auth = True
-    )
-
-    """ account/update_profile_colors """
-    update_profile_colors = bind_api(
-        path = '/account/update_profile_colors.json',
-        method = 'POST',
-        payload_type = 'user',
-        allowed_param = ['profile_background_color', 'profile_text_color',
-                          'profile_link_color', 'profile_sidebar_fill_color',
-                          'profile_sidebar_border_color'],
-        require_auth = True
-    )
-
-    """ account/update_profile_image """
-    def update_profile_image(self, filename):
-        headers, post_data = API._pack_image(filename, 700)
-        return bind_api(
-            path = '/account/update_profile_image.json',
-            method = 'POST',
-            payload_type = 'user',
-            require_auth = True
-        )(self, post_data=post_data, headers=headers)
+    @property
+    def rate_limit_status(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/application/rate_limit_status
+            :allowed_param:'resources'
+        """
+        return bind_api(
+            api=self,
+            path='/application/rate_limit_status.json',
+            payload_type='json',
+            allowed_param=['resources'],
+            use_cache=False
+        )
+
+    @property
+    def set_delivery_device(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/account/update_delivery_device
+            :allowed_param:'device'
+        """
+        return bind_api(
+            api=self,
+            path='/account/update_delivery_device.json',
+            method='POST',
+            allowed_param=['device'],
+            payload_type='user',
+            require_auth=True
+        )
+
+    @property
+    def update_profile_colors(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors
+            :allowed_param:'profile_background_color', 'profile_text_color',
+             'profile_link_color', 'profile_sidebar_fill_color',
+             'profile_sidebar_border_color'],
+        """
+        return bind_api(
+            api=self,
+            path='/account/update_profile_colors.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['profile_background_color', 'profile_text_color',
+                           'profile_link_color', 'profile_sidebar_fill_color',
+                           'profile_sidebar_border_color'],
+            require_auth=True
+        )
 
 
-    """ account/update_profile_background_image """
-    def update_profile_background_image(self, filename, *args, **kargs):
-        headers, post_data = API._pack_image(filename, 800)
-        bind_api(
-            path = '/account/update_profile_background_image.json',
-            method = 'POST',
-            payload_type = 'user',
-            allowed_param = ['tile'],
-            require_auth = True
+    def update_profile_image(self, filename, file_=None):
+        """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_image
+            :allowed_param:'include_entities', 'skip_status'
+        """
+        headers, post_data = API._pack_image(filename, 700, f=file_)
+        return bind_api(
+            api=self,
+            path='/account/update_profile_image.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['include_entities', 'skip_status'],
+            require_auth=True
         )(self, post_data=post_data, headers=headers)
 
         )(self, post_data=post_data, headers=headers)
 
-    """ account/update_profile_banner """
-    def update_profile_banner(self, filename, *args, **kargs):
-        headers, post_data = API._pack_image(filename, 700, form_field="banner")
+    def update_profile_background_image(self, filename, **kargs):
+        """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_background_image
+            :allowed_param:'tile', 'include_entities', 'skip_status', 'use'
+        """
+        f = kargs.pop('file', None)
+        headers, post_data = API._pack_image(filename, 800, f=f)
         bind_api(
         bind_api(
-            path = '/account/update_profile_banner.json',
-            method = 'POST',
-            allowed_param = ['width', 'height', 'offset_left', 'offset_right'],
-            require_auth = True
-        )(self, post_data=post_data, headers=headers)
+            api=self,
+            path='/account/update_profile_background_image.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['tile', 'include_entities', 'skip_status', 'use'],
+            require_auth=True
+        )(post_data=post_data, headers=headers)
+
+    def update_profile_banner(self, filename, **kargs):
+        """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile_banner
+            :allowed_param:'width', 'height', 'offset_left', 'offset_right'
+        """
+        f = kargs.pop('file', None)
+        headers, post_data = API._pack_image(filename, 700, form_field="banner", f=f)
+        bind_api(
+            api=self,
+            path='/account/update_profile_banner.json',
+            method='POST',
+            allowed_param=['width', 'height', 'offset_left', 'offset_right'],
+            require_auth=True
+        )(post_data=post_data, headers=headers)
+
+    @property
+    def update_profile(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/account/update_profile
+            :allowed_param:'name', 'url', 'location', 'description'
+        """
+        return bind_api(
+            api=self,
+            path='/account/update_profile.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['name', 'url', 'location', 'description'],
+            require_auth=True
+        )
+
+    @property
+    def favorites(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/favorites/list
+            :allowed_param:'screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id'
+        """
+        return bind_api(
+            api=self,
+            path='/favorites/list.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id']
+        )
+
+    @property
+    def create_favorite(self):
+        """ :reference:https://dev.twitter.com/rest/reference/post/favorites/create
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/favorites/create.json',
+            method='POST',
+            payload_type='status',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def destroy_favorite(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/favorites/destroy
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/favorites/destroy.json',
+            method='POST',
+            payload_type='status',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def create_block(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/blocks/create
+            :allowed_param:'id', 'user_id', 'screen_name'
+        """
+        return bind_api(
+            api=self,
+            path='/blocks/create.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['id', 'user_id', 'screen_name'],
+            require_auth=True
+        )
+
+    @property
+    def destroy_block(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/blocks/destroy
+            :allowed_param:'id', 'user_id', 'screen_name'
+        """
+        return bind_api(
+            api=self,
+            path='/blocks/destroy.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['id', 'user_id', 'screen_name'],
+            require_auth=True
+        )
+
+    @property
+    def blocks(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/blocks/list
+            :allowed_param:'cursor'
+        """
+        return bind_api(
+            api=self,
+            path='/blocks/list.json',
+            payload_type='user', payload_list=True,
+            allowed_param=['cursor'],
+            require_auth=True
+        )
 
 
+    @property
+    def blocks_ids(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/blocks/ids """
+        return bind_api(
+            api=self,
+            path='/blocks/ids.json',
+            payload_type='json',
+            require_auth=True
+        )
+
+    @property
+    def report_spam(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/users/report_spam
+            :allowed_param:'user_id', 'screen_name'
+        """
+        return bind_api(
+            api=self,
+            path='/users/report_spam.json',
+            method='POST',
+            payload_type='user',
+            allowed_param=['user_id', 'screen_name'],
+            require_auth=True
+        )
+
+    @property
+    def saved_searches(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/saved_searches/show/%3Aid """
+        return bind_api(
+            api=self,
+            path='/saved_searches/list.json',
+            payload_type='saved_search', payload_list=True,
+            require_auth=True
+        )
 
 
-    """ account/update_profile """
-    update_profile = bind_api(
-        path = '/account/update_profile.json',
-        method = 'POST',
-        payload_type = 'user',
-        allowed_param = ['name', 'url', 'location', 'description'],
-        require_auth = True
-    )
-
-    """ favorites """
-    favorites = bind_api(
-        path = '/favorites/list.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['screen_name', 'user_id', 'max_id', 'count', 'since_id', 'max_id']
-    )
-
-    """ favorites/create """
-    create_favorite = bind_api(
-        path = '/favorites/create.json',
-        method = 'POST',
-        payload_type = 'status',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    """ favorites/destroy """
-    destroy_favorite = bind_api(
-        path = '/favorites/destroy.json',
-        method = 'POST',
-        payload_type = 'status',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    """ blocks/create """
-    create_block = bind_api(
-        path = '/blocks/create.json',
-        method = 'POST',
-        payload_type = 'user',
-        allowed_param = ['id', 'user_id', 'screen_name'],
-        require_auth = True
-    )
-
-    """ blocks/destroy """
-    destroy_block = bind_api(
-        path = '/blocks/destroy.json',
-        method = 'DELETE',
-        payload_type = 'user',
-        allowed_param = ['id', 'user_id', 'screen_name'],
-        require_auth = True
-    )
-
-    """ blocks/blocking """
-    blocks = bind_api(
-        path = '/blocks/list.json',
-        payload_type = 'user', payload_list = True,
-        allowed_param = ['cursor'],
-        require_auth = True
-    )
-
-    """ blocks/blocking/ids """
-    blocks_ids = bind_api(
-        path = '/blocks/ids.json',
-        payload_type = 'json',
-        require_auth = True
-    )
-
-    """ report_spam """
-    report_spam = bind_api(
-        path = '/users/report_spam.json',
-        method = 'POST',
-        payload_type = 'user',
-        allowed_param = ['user_id', 'screen_name'],
-        require_auth = True
-    )
-
-    """ saved_searches """
-    saved_searches = bind_api(
-        path = '/saved_searches/list.json',
-        payload_type = 'saved_search', payload_list = True,
-        require_auth = True
-    )
-
-    """ saved_searches/show """
-    get_saved_search = bind_api(
-        path = '/saved_searches/show/{id}.json',
-        payload_type = 'saved_search',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    """ saved_searches/create """
-    create_saved_search = bind_api(
-        path = '/saved_searches/create.json',
-        method = 'POST',
-        payload_type = 'saved_search',
-        allowed_param = ['query'],
-        require_auth = True
-    )
-
-    """ saved_searches/destroy """
-    destroy_saved_search = bind_api(
-        path = '/saved_searches/destroy/{id}.json',
-        method = 'POST',
-        payload_type = 'saved_search',
-        allowed_param = ['id'],
-        require_auth = True
-    )
-
-    create_list = bind_api(
-        path = '/lists/create.json',
-        method = 'POST',
-        payload_type = 'list',
-        allowed_param = ['name', 'mode', 'description'],
-        require_auth = True
-    )
-
-    destroy_list = bind_api(
-        path = '/lists/destroy.json',
-        method = 'POST',
-        payload_type = 'list',
-        allowed_param = ['owner_screen_name', 'owner_id', 'list_id', 'slug'],
-        require_auth = True
-    )
-
-    update_list = bind_api(
-        path = '/lists/update.json',
-        method = 'POST',
-        payload_type = 'list',
-        allowed_param = ['list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'],
-        require_auth = True
-    )
-
-    lists_all = bind_api(
-        path = '/lists/list.json',
-        payload_type = 'list', payload_list = True,
-        allowed_param = ['screen_name', 'user_id'],
-        require_auth = True
-    )
-
-    lists_memberships = bind_api(
-        path = '/lists/memberships.json',
-        payload_type = 'list', payload_list = True,
-        allowed_param = ['screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'],
-        require_auth = True
-    )
-
-    lists_subscriptions = bind_api(
-        path = '/lists/subscriptions.json',
-        payload_type = 'list', payload_list = True,
-        allowed_param = ['screen_name', 'user_id', 'cursor'],
-        require_auth = True
-    )
-
-    list_timeline = bind_api(
-        path = '/lists/statuses.json',
-        payload_type = 'status', payload_list = True,
-        allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'since_id', 'max_id', 'count', 'include_rts']
-    )
-
-    get_list = bind_api(
-        path = '/lists/show.json',
-        payload_type = 'list',
-        allowed_param = ['owner_screen_name', 'owner_id', 'slug', 'list_id']
-    )
-
-    add_list_member = bind_api(
-        path = '/lists/members/create.json',
-        method = 'POST',
-        payload_type = 'list',
-        allowed_param = ['screen_name', 'user_id', 'owner_screen_name', 'owner_id', 'slug', 'list_id'],
-        require_auth = True
-    )
-
-    remove_list_member = bind_api(
-        path = '/lists/members/destroy.json',
-        method = 'POST',
-        payload_type = 'list',
-        allowed_param = ['screen_name', 'user_id', 'owner_screen_name', 'owner_id', 'slug', 'list_id'],
-        require_auth = True
-    )
-
-    list_members = bind_api(
-        path = '/lists/members.json',
-        payload_type = 'user', payload_list = True,
-        allowed_param = ['owner_screen_name', 'slug', 'list_id', 'owner_id', 'cursor']
-    )
-
-    show_list_member = bind_api(
-        path = '/lists/members/show.json',
-        payload_type = 'user',
-        allowed_param = ['list_id', 'slug', 'user_id', 'screen_name', 'owner_screen_name', 'owner_id']
-    )
-
-    subscribe_list = bind_api(
-        path = '/lists/subscribers/create.json',
-        method = 'POST',
-        payload_type = 'list',
-        allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id'],
-        require_auth = True
-    )
-
-    unsubscribe_list = bind_api(
-        path = '/lists/subscribers/destroy.json',
-        method = 'POST',
-        payload_type = 'list',
-        allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id'],
-        require_auth = True
-    )
-
-    list_subscribers = bind_api(
-        path = '/lists/subscribers.json',
-        payload_type = 'user', payload_list = True,
-        allowed_param = ['owner_screen_name', 'slug', 'owner_id', 'list_id', 'cursor']
-    )
-
-    show_list_subscriber = bind_api(
-        path = '/lists/subscribers/show.json',
-        payload_type = 'user',
-        allowed_param = ['owner_screen_name', 'slug', 'screen_name', 'owner_id', 'list_id', 'user_id']
-    )
-
-    """ trends/available """
-    trends_available = bind_api(
-        path = '/trends/available.json',
-        payload_type = 'json'
-    )
-
-    trends_place = bind_api(
-        path = '/trends/place.json',
-        payload_type = 'json',
-        allowed_param = ['id', 'exclude']
-    )
-
-    trends_closest = bind_api(
-        path = '/trends/closest.json',
-        payload_type = 'json',
-        allowed_param = ['lat', 'long']
-    )
-
-    """ search """
-    search = bind_api(
-        path = '/search/tweets.json',
-        payload_type = 'search_results',
-        allowed_param = ['q', 'lang', 'locale', 'since_id', 'geocode', 'max_id', 'since', 'until', 'result_type', 'count', 'include_entities', 'from', 'to', 'source']
-    )
-
-    """ trends/daily """
-    trends_daily = bind_api(
-        path = '/trends/daily.json',
-        payload_type = 'json',
-        allowed_param = ['date', 'exclude']
-    )
-
-    """ trends/weekly """
-    trends_weekly = bind_api(
-        path = '/trends/weekly.json',
-        payload_type = 'json',
-        allowed_param = ['date', 'exclude']
-    )
-
-    """ geo/reverse_geocode """
-    reverse_geocode = bind_api(
-        path = '/geo/reverse_geocode.json',
-        payload_type = 'place', payload_list = True,
-        allowed_param = ['lat', 'long', 'accuracy', 'granularity', 'max_results']
-    )
-
-    """ geo/id """
-    geo_id = bind_api(
-        path = '/geo/id/{id}.json',
-        payload_type = 'place',
-        allowed_param = ['id']
-    )
-
-    """ geo/search """
-    geo_search = bind_api(
-        path = '/geo/search.json',
-        payload_type = 'place', payload_list = True,
-        allowed_param = ['lat', 'long', 'query', 'ip', 'granularity', 'accuracy', 'max_results', 'contained_within']
-    )
-
-    """ geo/similar_places """
-    geo_similar_places = bind_api(
-        path = '/geo/similar_places.json',
-        payload_type = 'place', payload_list = True,
-        allowed_param = ['lat', 'long', 'name', 'contained_within']
-    )
-
-    """ help/languages.json """
-    supported_languages = bind_api(
-        path = '/help/languages.json',
-        payload_type = 'json',
-        require_auth = True
-    )
-
-    """ help/configuration """
-    configuration = bind_api(
-        path = '/help/configuration.json',
-        payload_type = 'json',
-        require_auth = True
-    )
+    @property
+    def get_saved_search(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/saved_searches/show/%3Aid
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/saved_searches/show/{id}.json',
+            payload_type='saved_search',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def create_saved_search(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/saved_searches/create
+            :allowed_param:'query'
+        """
+        return bind_api(
+            api=self,
+            path='/saved_searches/create.json',
+            method='POST',
+            payload_type='saved_search',
+            allowed_param=['query'],
+            require_auth=True
+        )
+
+    @property
+    def destroy_saved_search(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/saved_searches/destroy/%3Aid
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/saved_searches/destroy/{id}.json',
+            method='POST',
+            payload_type='saved_search',
+            allowed_param=['id'],
+            require_auth=True
+        )
+
+    @property
+    def create_list(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/lists/create
+            :allowed_param:'name', 'mode', 'description'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/create.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['name', 'mode', 'description'],
+            require_auth=True
+        )
+
+    @property
+    def destroy_list(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/lists/destroy
+            :allowed_param:'owner_screen_name', 'owner_id', 'list_id', 'slug'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/destroy.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['owner_screen_name', 'owner_id', 'list_id', 'slug'],
+            require_auth=True
+        )
+
+    @property
+    def update_list(self):
+        """ :reference: https://dev.twitter.com/rest/reference/post/lists/update
+            :allowed_param: list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/update.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['list_id', 'slug', 'name', 'mode', 'description', 'owner_screen_name', 'owner_id'],
+            require_auth=True
+        )
+
+    @property
+    def lists_all(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/lists/list
+            :allowed_param:'screen_name', 'user_id'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/list.json',
+            payload_type='list', payload_list=True,
+            allowed_param=['screen_name', 'user_id'],
+            require_auth=True
+        )
+
+    @property
+    def lists_memberships(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/lists/memberships
+            :allowed_param:'screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/memberships.json',
+            payload_type='list', payload_list=True,
+            allowed_param=['screen_name', 'user_id', 'filter_to_owned_lists', 'cursor'],
+            require_auth=True
+        )
+
+    @property
+    def lists_subscriptions(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/lists/subscriptions
+            :allowed_param:'screen_name', 'user_id', 'cursor'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/subscriptions.json',
+            payload_type='list', payload_list=True,
+            allowed_param=['screen_name', 'user_id', 'cursor'],
+            require_auth=True
+        )
+
+    @property
+    def list_timeline(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/statuses
+            :allowed_param:'owner_screen_name', 'slug', 'owner_id', 'list_id',
+             'since_id', 'max_id', 'count', 'include_rts
+        """
+        return bind_api(
+            api=self,
+            path='/lists/statuses.json',
+            payload_type='status', payload_list=True,
+            allowed_param=['owner_screen_name', 'slug', 'owner_id',
+                           'list_id', 'since_id', 'max_id', 'count',
+                           'include_rts']
+        )
+
+    @property
+    def get_list(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/lists/show
+            :allowed_param:'owner_screen_name', 'owner_id', 'slug', 'list_id'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/show.json',
+            payload_type='list',
+            allowed_param=['owner_screen_name', 'owner_id', 'slug', 'list_id']
+        )
+
+    @property
+    def add_list_member(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/create
+            :allowed_param:'screen_name', 'user_id', 'owner_screen_name',
+             'owner_id', 'slug', 'list_id'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/members/create.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['screen_name', 'user_id', 'owner_screen_name',
+                           'owner_id', 'slug', 'list_id'],
+            require_auth=True
+        )
+
+    @property
+    def remove_list_member(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy
+            :allowed_param:'screen_name', 'user_id', 'owner_screen_name',
+             'owner_id', 'slug', 'list_id'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/members/destroy.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['screen_name', 'user_id', 'owner_screen_name',
+                           'owner_id', 'slug', 'list_id'],
+            require_auth=True
+        )
+
+    def add_list_members(self, screen_name=None, user_id=None, slug=None,
+                         list_id=None, owner_id=None, owner_screen_name=None):
+        """ Perform bulk add of list members from user ID or screenname """
+        return self._add_list_members(list_to_csv(screen_name),
+                                      list_to_csv(user_id),
+                                      slug, list_id, owner_id,
+                                      owner_screen_name)
+
+    @property
+    def _add_list_members(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all
+            :allowed_param:'screen_name', 'user_id', 'slug', 'lit_id',
+            'owner_id', 'owner_screen_name'
+
+        """
+        return bind_api(
+            api=self,
+            path='/lists/members/create_all.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['screen_name', 'user_id', 'slug', 'lit_id',
+                           'owner_id', 'owner_screen_name'],
+            require_auth=True
+        )
+
+    def remove_list_members(self, screen_name=None, user_id=None, slug=None,
+                            list_id=None, owner_id=None, owner_screen_name=None):
+        """ Perform bulk remove of list members from user ID or screenname """
+        return self._remove_list_members(list_to_csv(screen_name),
+                                         list_to_csv(user_id),
+                                         slug, list_id, owner_id,
+                                         owner_screen_name)
+
+    @property
+    def _remove_list_members(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all
+            :allowed_param:'screen_name', 'user_id', 'slug', 'lit_id',
+            'owner_id', 'owner_screen_name'
+
+        """
+        return bind_api(
+            api=self,
+            path='/lists/members/destroy_all.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['screen_name', 'user_id', 'slug', 'lit_id',
+                           'owner_id', 'owner_screen_name'],
+            require_auth=True
+        )
+
+    @property
+    def list_members(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/members
+            :allowed_param:'owner_screen_name', 'slug', 'list_id',
+             'owner_id', 'cursor
+        """
+        return bind_api(
+            api=self,
+            path='/lists/members.json',
+            payload_type='user', payload_list=True,
+            allowed_param=['owner_screen_name', 'slug', 'list_id',
+                           'owner_id', 'cursor']
+        )
+
+    @property
+    def show_list_member(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/members/show
+            :allowed_param:'list_id', 'slug', 'user_id', 'screen_name',
+             'owner_screen_name', 'owner_id
+        """
+        return bind_api(
+            api=self,
+            path='/lists/members/show.json',
+            payload_type='user',
+            allowed_param=['list_id', 'slug', 'user_id', 'screen_name',
+                           'owner_screen_name', 'owner_id']
+        )
+
+    @property
+    def subscribe_list(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create
+            :allowed_param:'owner_screen_name', 'slug', 'owner_id',
+            'list_id'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/subscribers/create.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['owner_screen_name', 'slug', 'owner_id',
+                           'list_id'],
+            require_auth=True
+        )
+
+    @property
+    def unsubscribe_list(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy
+            :allowed_param:'owner_screen_name', 'slug', 'owner_id',
+            'list_id'
+        """
+        return bind_api(
+            api=self,
+            path='/lists/subscribers/destroy.json',
+            method='POST',
+            payload_type='list',
+            allowed_param=['owner_screen_name', 'slug', 'owner_id',
+                           'list_id'],
+            require_auth=True
+        )
+
+    @property
+    def list_subscribers(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers
+            :allowed_param:'owner_screen_name', 'slug', 'owner_id',
+             'list_id', 'cursor
+        """
+        return bind_api(
+            api=self,
+            path='/lists/subscribers.json',
+            payload_type='user', payload_list=True,
+            allowed_param=['owner_screen_name', 'slug', 'owner_id',
+                           'list_id', 'cursor']
+        )
+
+    @property
+    def show_list_subscriber(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers/show
+            :allowed_param:'owner_screen_name', 'slug', 'screen_name',
+             'owner_id', 'list_id', 'user_id
+        """
+        return bind_api(
+            api=self,
+            path='/lists/subscribers/show.json',
+            payload_type='user',
+            allowed_param=['owner_screen_name', 'slug', 'screen_name',
+                           'owner_id', 'list_id', 'user_id']
+        )
+
+    @property
+    def trends_available(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/trends/available """
+        return bind_api(
+            api=self,
+            path='/trends/available.json',
+            payload_type='json'
+        )
+
+    @property
+    def trends_place(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/trends/place
+            :allowed_param:'id', 'exclude'
+        """
+        return bind_api(
+            api=self,
+            path='/trends/place.json',
+            payload_type='json',
+            allowed_param=['id', 'exclude']
+        )
+
+    @property
+    def trends_closest(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/trends/closest
+            :allowed_param:'lat', 'long'
+        """
+        return bind_api(
+            api=self,
+            path='/trends/closest.json',
+            payload_type='json',
+            allowed_param=['lat', 'long']
+        )
+
+    @property
+    def search(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/search/tweets
+            :allowed_param:'q', 'lang', 'locale', 'since_id', 'geocode',
+             'max_id', 'since', 'until', 'result_type', 'count',
+              'include_entities', 'from', 'to', 'source']
+        """
+        return bind_api(
+            api=self,
+            path='/search/tweets.json',
+            payload_type='search_results',
+            allowed_param=['q', 'lang', 'locale', 'since_id', 'geocode',
+                           'max_id', 'since', 'until', 'result_type',
+                           'count', 'include_entities', 'from',
+                           'to', 'source']
+        )
+
+    @property
+    def reverse_geocode(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/geo/reverse_geocode
+            :allowed_param:'lat', 'long', 'accuracy', 'granularity', 'max_results'
+        """
+        return bind_api(
+            api=self,
+            path='/geo/reverse_geocode.json',
+            payload_type='place', payload_list=True,
+            allowed_param=['lat', 'long', 'accuracy', 'granularity',
+                           'max_results']
+        )
+
+    @property
+    def geo_id(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/geo/id/%3Aplace_id
+            :allowed_param:'id'
+        """
+        return bind_api(
+            api=self,
+            path='/geo/id/{id}.json',
+            payload_type='place',
+            allowed_param=['id']
+        )
+
+    @property
+    def geo_search(self):
+        """ :reference: https://dev.twitter.com/docs/api/1.1/get/geo/search
+            :allowed_param:'lat', 'long', 'query', 'ip', 'granularity',
+             'accuracy', 'max_results', 'contained_within
+
+        """
+        return bind_api(
+            api=self,
+            path='/geo/search.json',
+            payload_type='place', payload_list=True,
+            allowed_param=['lat', 'long', 'query', 'ip', 'granularity',
+                           'accuracy', 'max_results', 'contained_within']
+        )
+
+    @property
+    def geo_similar_places(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/geo/similar_places
+            :allowed_param:'lat', 'long', 'name', 'contained_within'
+        """
+        return bind_api(
+            api=self,
+            path='/geo/similar_places.json',
+            payload_type='place', payload_list=True,
+            allowed_param=['lat', 'long', 'name', 'contained_within']
+        )
+
+    @property
+    def supported_languages(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/help/languages """
+        return bind_api(
+            api=self,
+            path='/help/languages.json',
+            payload_type='json',
+            require_auth=True
+        )
+
+    @property
+    def configuration(self):
+        """ :reference: https://dev.twitter.com/rest/reference/get/help/configuration """
+        return bind_api(
+            api=self,
+            path='/help/configuration.json',
+            payload_type='json',
+            require_auth=True
+        )
 
     """ Internal use only """
 
     """ Internal use only """
+
     @staticmethod
     @staticmethod
-    def _pack_image(filename, max_size, form_field="image"):
+    def _pack_image(filename, max_size, form_field="image", f=None):
         """Pack image from file into multipart-formdata post body"""
         # image must be less than 700kb in size
         """Pack image from file into multipart-formdata post body"""
         # image must be less than 700kb in size
-        try:
-            if os.path.getsize(filename) > (max_size * 1024):
-                raise TweepError('File is too big, must be less than 700kb.')
-        except os.error:
-            raise TweepError('Unable to access file')
+        if f is None:
+            try:
+                if os.path.getsize(filename) > (max_size * 1024):
+                    raise TweepError('File is too big, must be less than %skb.' % max_size)
+            except os.error as e:
+                raise TweepError('Unable to access file: %s' % e.strerror)
+
+            # build the mulitpart-formdata body
+            fp = open(filename, 'rb')
+        else:
+            f.seek(0, 2)  # Seek to end of file
+            if f.tell() > (max_size * 1024):
+                raise TweepError('File is too big, must be less than %skb.' % max_size)
+            f.seek(0)  # Reset to beginning of file
+            fp = f
 
         # image must be gif, jpeg, or png
         file_type = mimetypes.guess_type(filename)
 
         # image must be gif, jpeg, or png
         file_type = mimetypes.guess_type(filename)
@@ -716,19 +1291,22 @@ class API(object):
         if file_type not in ['image/gif', 'image/jpeg', 'image/png']:
             raise TweepError('Invalid file type for image: %s' % file_type)
 
         if file_type not in ['image/gif', 'image/jpeg', 'image/png']:
             raise TweepError('Invalid file type for image: %s' % file_type)
 
-        # build the mulitpart-formdata body
-        fp = open(filename, 'rb')
-        BOUNDARY = 'Tw3ePy'
-        body = []
-        body.append('--' + BOUNDARY)
-        body.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (form_field, filename))
-        body.append('Content-Type: %s' % file_type)
-        body.append('')
+        if isinstance(filename, six.text_type):
+            filename = filename.encode("utf-8")
+
+        BOUNDARY = b'Tw3ePy'
+        body = list()
+        body.append(b'--' + BOUNDARY)
+        body.append('Content-Disposition: form-data; name="{0}";'
+                    ' filename="{1}"'.format(form_field, filename)
+                    .encode('utf-8'))
+        body.append('Content-Type: {0}'.format(file_type).encode('utf-8'))
+        body.append(b'')
         body.append(fp.read())
         body.append(fp.read())
-        body.append('--' + BOUNDARY + '--')
-        body.append('')
+        body.append(b'--' + BOUNDARY + b'--')
+        body.append(b'')
         fp.close()
         fp.close()
-        body = '\r\n'.join(body)
+        body = b'\r\n'.join(body)
 
         # build headers
         headers = {
 
         # build headers
         headers = {
@@ -737,4 +1315,3 @@ class API(object):
         }
 
         return headers, body
         }
 
         return headers, body
-
index 86c443091afc5e35c77caa801c8854288a72693d..b15434bcdbbb65b0962d103336209efd0927bdc4 100644 (file)
@@ -1,13 +1,18 @@
-# Tweepy
-# Copyright 2009-2010 Joshua Roesslein
-# See LICENSE for details.
+from __future__ import print_function
 
 
-from urllib2 import Request, urlopen
-import base64
+import six
+import logging
 
 
-from tweepy import oauth
 from tweepy.error import TweepError
 from tweepy.api import API
 from tweepy.error import TweepError
 from tweepy.api import API
+import requests
+from requests_oauthlib import OAuth1Session, OAuth1
+from requests.auth import AuthBase
+from six.moves.urllib.parse import parse_qs
+
+WARNING_MESSAGE = """Warning! Due to a Twitter API bug, signin_with_twitter
+and access_type don't always play nice together. Details
+https://dev.twitter.com/discussions/21281"""
 
 
 class AuthHandler(object):
 
 
 class AuthHandler(object):
@@ -23,76 +28,64 @@ class AuthHandler(object):
 
 class OAuthHandler(AuthHandler):
     """OAuth authentication handler"""
 
 class OAuthHandler(AuthHandler):
     """OAuth authentication handler"""
-
     OAUTH_HOST = 'api.twitter.com'
     OAUTH_ROOT = '/oauth/'
 
     OAUTH_HOST = 'api.twitter.com'
     OAUTH_ROOT = '/oauth/'
 
-    def __init__(self, consumer_key, consumer_secret, callback=None, secure=True):
-        if type(consumer_key) == unicode:
-            consumer_key = bytes(consumer_key)
+    def __init__(self, consumer_key, consumer_secret, callback=None):
+        if type(consumer_key) == six.text_type:
+            consumer_key = consumer_key.encode('ascii')
 
 
-        if type(consumer_secret) == unicode:
-            consumer_secret = bytes(consumer_secret)
+        if type(consumer_secret) == six.text_type:
+            consumer_secret = consumer_secret.encode('ascii')
 
 
-        self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
-        self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1()
-        self.request_token = None
+        self.consumer_key = consumer_key
+        self.consumer_secret = consumer_secret
         self.access_token = None
         self.access_token = None
+        self.access_token_secret = None
         self.callback = callback
         self.username = None
         self.callback = callback
         self.username = None
-        self.secure = secure
+        self.oauth = OAuth1Session(consumer_key,
+                                   client_secret=consumer_secret,
+                                   callback_uri=self.callback)
 
 
-    def _get_oauth_url(self, endpoint, secure=True):
-        if self.secure or secure:
-            prefix = 'https://'
-        else:
-            prefix = 'http://'
+    def _get_oauth_url(self, endpoint):
+        return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
 
 
-        return prefix + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
+    def apply_auth(self):
+        return OAuth1(self.consumer_key,
+                      client_secret=self.consumer_secret,
+                      resource_owner_key=self.access_token,
+                      resource_owner_secret=self.access_token_secret,
+                      decoding=None)
 
 
-    def apply_auth(self, url, method, headers, parameters):
-        request = oauth.OAuthRequest.from_consumer_and_token(
-            self._consumer, http_url=url, http_method=method,
-            token=self.access_token, parameters=parameters
-        )
-        request.sign_request(self._sigmethod, self._consumer, self.access_token)
-        headers.update(request.to_header())
-
-    def _get_request_token(self):
+    def _get_request_token(self, access_type=None):
         try:
             url = self._get_oauth_url('request_token')
         try:
             url = self._get_oauth_url('request_token')
-            request = oauth.OAuthRequest.from_consumer_and_token(
-                self._consumer, http_url=url, callback=self.callback
-            )
-            request.sign_request(self._sigmethod, self._consumer, None)
-            resp = urlopen(Request(url, headers=request.to_header()))
-            return oauth.OAuthToken.from_string(resp.read())
+            if access_type:
+                url += '?x_auth_access_type=%s' % access_type
+            return self.oauth.fetch_request_token(url)
         except Exception as e:
             raise TweepError(e)
 
         except Exception as e:
             raise TweepError(e)
 
-    def set_request_token(self, key, secret):
-        self.request_token = oauth.OAuthToken(key, secret)
-
     def set_access_token(self, key, secret):
     def set_access_token(self, key, secret):
-        self.access_token = oauth.OAuthToken(key, secret)
+        self.access_token = key
+        self.access_token_secret = secret
 
 
-    def get_authorization_url(self, signin_with_twitter=False):
+    def get_authorization_url(self,
+                              signin_with_twitter=False,
+                              access_type=None):
         """Get the authorization URL to redirect the user"""
         try:
         """Get the authorization URL to redirect the user"""
         try:
-            # get the request token
-            self.request_token = self._get_request_token()
-
-            # build auth request and return as url
             if signin_with_twitter:
                 url = self._get_oauth_url('authenticate')
             if signin_with_twitter:
                 url = self._get_oauth_url('authenticate')
+                if access_type:
+                    logging.warning(WARNING_MESSAGE)
             else:
                 url = self._get_oauth_url('authorize')
             else:
                 url = self._get_oauth_url('authorize')
-            request = oauth.OAuthRequest.from_token_and_callback(
-                token=self.request_token, http_url=url
-            )
-
-            return request.to_url()
+            self.request_token = self._get_request_token(access_type=access_type)
+            return self.oauth.authorization_url(url)
         except Exception as e:
         except Exception as e:
+            raise
             raise TweepError(e)
 
     def get_access_token(self, verifier=None):
             raise TweepError(e)
 
     def get_access_token(self, verifier=None):
@@ -102,19 +95,15 @@ class OAuthHandler(AuthHandler):
         """
         try:
             url = self._get_oauth_url('access_token')
         """
         try:
             url = self._get_oauth_url('access_token')
-
-            # build request
-            request = oauth.OAuthRequest.from_consumer_and_token(
-                self._consumer,
-                token=self.request_token, http_url=url,
-                verifier=str(verifier)
-            )
-            request.sign_request(self._sigmethod, self._consumer, self.request_token)
-
-            # send request
-            resp = urlopen(Request(url, headers=request.to_header()))
-            self.access_token = oauth.OAuthToken.from_string(resp.read())
-            return self.access_token
+            self.oauth = OAuth1Session(self.consumer_key,
+                                       client_secret=self.consumer_secret,
+                                       resource_owner_key=self.request_token['oauth_token'],
+                                       resource_owner_secret=self.request_token['oauth_token_secret'],
+                                       verifier=verifier, callback_uri=self.callback)
+            resp = self.oauth.fetch_access_token(url)
+            self.access_token = resp['oauth_token']
+            self.access_token_secret = resp['oauth_token_secret']
+            return self.access_token, self.access_token_secret
         except Exception as e:
             raise TweepError(e)
 
         except Exception as e:
             raise TweepError(e)
 
@@ -126,21 +115,18 @@ class OAuthHandler(AuthHandler):
         and request activation of xAuth for it.
         """
         try:
         and request activation of xAuth for it.
         """
         try:
-            url = self._get_oauth_url('access_token', secure=True) # must use HTTPS
-            request = oauth.OAuthRequest.from_consumer_and_token(
-                oauth_consumer=self._consumer,
-                http_method='POST', http_url=url,
-                parameters = {
-                    'x_auth_mode': 'client_auth',
-                    'x_auth_username': username,
-                    'x_auth_password': password
-                }
-            )
-            request.sign_request(self._sigmethod, self._consumer, None)
-
-            resp = urlopen(Request(url, data=request.to_postdata()))
-            self.access_token = oauth.OAuthToken.from_string(resp.read())
-            return self.access_token
+            url = self._get_oauth_url('access_token')
+            oauth = OAuth1(self.consumer_key,
+                           client_secret=self.consumer_secret)
+            r = requests.post(url=url,
+                              auth=oauth,
+                              headers={'x_auth_mode': 'client_auth',
+                                       'x_auth_username': username,
+                                       'x_auth_password': password})
+
+            print(r.content)
+            credentials = parse_qs(r.content)
+            return credentials.get('oauth_token')[0], credentials.get('oauth_token_secret')[0]
         except Exception as e:
             raise TweepError(e)
 
         except Exception as e:
             raise TweepError(e)
 
@@ -151,6 +137,44 @@ class OAuthHandler(AuthHandler):
             if user:
                 self.username = user.screen_name
             else:
             if user:
                 self.username = user.screen_name
             else:
-                raise TweepError("Unable to get username, invalid oauth token!")
+                raise TweepError('Unable to get username,'
+                                 ' invalid oauth token!')
         return self.username
 
         return self.username
 
+
+class OAuth2Bearer(AuthBase):
+    def __init__(self, bearer_token):
+        self.bearer_token = bearer_token
+
+    def __call__(self, request):
+        request.headers['Authorization'] = 'Bearer ' + self.bearer_token
+        return request
+
+
+class AppAuthHandler(AuthHandler):
+    """Application-only authentication handler"""
+
+    OAUTH_HOST = 'api.twitter.com'
+    OAUTH_ROOT = '/oauth2/'
+
+    def __init__(self, consumer_key, consumer_secret):
+        self.consumer_key = consumer_key
+        self.consumer_secret = consumer_secret
+        self._bearer_token = ''
+
+        resp = requests.post(self._get_oauth_url('token'),
+                             auth=(self.consumer_key,
+                                   self.consumer_secret),
+                             data={'grant_type': 'client_credentials'})
+        data = resp.json()
+        if data.get('token_type') != 'bearer':
+            raise TweepError('Expected token_type to equal "bearer", '
+                             'but got %s instead' % data.get('token_type'))
+
+        self._bearer_token = data['access_token']
+
+    def _get_oauth_url(self, endpoint):
+        return 'https://' + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
+
+    def apply_auth(self):
+        return OAuth2Bearer(self._bearer_token)
index 1f8c61960a2c9a14f8b75395428e50bf8fe577b9..2ac614647d2d9bba28c187ce97a825cf6588416b 100644 (file)
@@ -2,24 +2,30 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
-import httplib
-import urllib
+from __future__ import print_function
+
 import time
 import re
 import time
 import re
-from StringIO import StringIO
-import gzip
+
+from six.moves.urllib.parse import quote
+import requests
+
+import logging
 
 from tweepy.error import TweepError
 from tweepy.utils import convert_to_utf8_str
 from tweepy.models import Model
 
 
 from tweepy.error import TweepError
 from tweepy.utils import convert_to_utf8_str
 from tweepy.models import Model
 
+
 re_path_template = re.compile('{\w+}')
 
 re_path_template = re.compile('{\w+}')
 
+log = logging.getLogger('tweepy.binder')
 
 def bind_api(**config):
 
     class APIMethod(object):
 
 
 def bind_api(**config):
 
     class APIMethod(object):
 
+        api = config['api']
         path = config['path']
         payload_type = config.get('payload_type', None)
         payload_list = config.get('payload_list', False)
         path = config['path']
         payload_type = config.get('payload_type', None)
         payload_list = config.get('payload_list', False)
@@ -27,38 +33,47 @@ def bind_api(**config):
         method = config.get('method', 'GET')
         require_auth = config.get('require_auth', False)
         search_api = config.get('search_api', False)
         method = config.get('method', 'GET')
         require_auth = config.get('require_auth', False)
         search_api = config.get('search_api', False)
+        upload_api = config.get('upload_api', False)
         use_cache = config.get('use_cache', True)
         use_cache = config.get('use_cache', True)
+        session = requests.Session()
 
 
-        def __init__(self, api, args, kargs):
+        def __init__(self, args, kwargs):
+            api = self.api
             # If authentication is required and no credentials
             # are provided, throw an error.
             if self.require_auth and not api.auth:
                 raise TweepError('Authentication required!')
 
             # If authentication is required and no credentials
             # are provided, throw an error.
             if self.require_auth and not api.auth:
                 raise TweepError('Authentication required!')
 
-            self.api = api
-            self.post_data = kargs.pop('post_data', None)
-            self.retry_count = kargs.pop('retry_count', api.retry_count)
-            self.retry_delay = kargs.pop('retry_delay', api.retry_delay)
-            self.retry_errors = kargs.pop('retry_errors', api.retry_errors)
-            self.headers = kargs.pop('headers', {})
-            self.build_parameters(args, kargs)
+            self.post_data = kwargs.pop('post_data', None)
+            self.retry_count = kwargs.pop('retry_count',
+                                          api.retry_count)
+            self.retry_delay = kwargs.pop('retry_delay',
+                                          api.retry_delay)
+            self.retry_errors = kwargs.pop('retry_errors',
+                                           api.retry_errors)
+            self.wait_on_rate_limit = kwargs.pop('wait_on_rate_limit',
+                                                 api.wait_on_rate_limit)
+            self.wait_on_rate_limit_notify = kwargs.pop('wait_on_rate_limit_notify',
+                                                        api.wait_on_rate_limit_notify)
+            self.parser = kwargs.pop('parser', api.parser)
+            self.session.headers = kwargs.pop('headers', {})
+            self.build_parameters(args, kwargs)
 
             # Pick correct URL root to use
             if self.search_api:
                 self.api_root = api.search_root
 
             # Pick correct URL root to use
             if self.search_api:
                 self.api_root = api.search_root
+            elif self.upload_api:
+                self.api_root = api.upload_root
             else:
                 self.api_root = api.api_root
 
             # Perform any path variable substitution
             self.build_path()
 
             else:
                 self.api_root = api.api_root
 
             # Perform any path variable substitution
             self.build_path()
 
-            if api.secure:
-                self.scheme = 'https://'
-            else:
-                self.scheme = 'http://'
-
             if self.search_api:
                 self.host = api.search_host
             if self.search_api:
                 self.host = api.search_host
+            elif self.upload_api:
+                self.host = api.upload_host
             else:
                 self.host = api.host
 
             else:
                 self.host = api.host
 
@@ -66,40 +81,44 @@ def bind_api(**config):
             # or older where Host is set including the 443 port.
             # This causes Twitter to issue 301 redirect.
             # See Issue https://github.com/tweepy/tweepy/issues/12
             # or older where Host is set including the 443 port.
             # This causes Twitter to issue 301 redirect.
             # See Issue https://github.com/tweepy/tweepy/issues/12
-            self.headers['Host'] = self.host
+            self.session.headers['Host'] = self.host
+            # Monitoring rate limits
+            self._remaining_calls = None
+            self._reset_time = None
 
 
-        def build_parameters(self, args, kargs):
-            self.parameters = {}
+        def build_parameters(self, args, kwargs):
+            self.session.params = {}
             for idx, arg in enumerate(args):
                 if arg is None:
                     continue
             for idx, arg in enumerate(args):
                 if arg is None:
                     continue
-
                 try:
                 try:
-                    self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
+                    self.session.params[self.allowed_param[idx]] = convert_to_utf8_str(arg)
                 except IndexError:
                     raise TweepError('Too many parameters supplied!')
 
                 except IndexError:
                     raise TweepError('Too many parameters supplied!')
 
-            for k, arg in kargs.items():
+            for k, arg in kwargs.items():
                 if arg is None:
                     continue
                 if arg is None:
                     continue
-                if k in self.parameters:
+                if k in self.session.params:
                     raise TweepError('Multiple values for parameter %s supplied!' % k)
 
                     raise TweepError('Multiple values for parameter %s supplied!' % k)
 
-                self.parameters[k] = convert_to_utf8_str(arg)
+                self.session.params[k] = convert_to_utf8_str(arg)
+
+            log.info("PARAMS: %r", self.session.params)
 
         def build_path(self):
             for variable in re_path_template.findall(self.path):
                 name = variable.strip('{}')
 
 
         def build_path(self):
             for variable in re_path_template.findall(self.path):
                 name = variable.strip('{}')
 
-                if name == 'user' and 'user' not in self.parameters and self.api.auth:
+                if name == 'user' and 'user' not in self.session.params and self.api.auth:
                     # No 'user' parameter provided, fetch it from Auth instead.
                     value = self.api.auth.get_username()
                 else:
                     try:
                     # No 'user' parameter provided, fetch it from Auth instead.
                     value = self.api.auth.get_username()
                 else:
                     try:
-                        value = urllib.quote(self.parameters[name])
+                        value = quote(self.session.params[name])
                     except KeyError:
                         raise TweepError('No parameter value found for path variable: %s' % name)
                     except KeyError:
                         raise TweepError('No parameter value found for path variable: %s' % name)
-                    del self.parameters[name]
+                    del self.session.params[name]
 
                 self.path = self.path.replace(variable, value)
 
 
                 self.path = self.path.replace(variable, value)
 
@@ -108,8 +127,7 @@ def bind_api(**config):
 
             # Build the request URL
             url = self.api_root + self.path
 
             # Build the request URL
             url = self.api_root + self.path
-            if len(self.parameters):
-                url = '%s?%s' % (url, urllib.urlencode(self.parameters))
+            full_url = 'https://' + self.host + url
 
             # Query the cache if one is available
             # and this request uses a GET method.
 
             # Query the cache if one is available
             # and this request uses a GET method.
@@ -132,60 +150,80 @@ def bind_api(**config):
             # or maximum number of retries is reached.
             retries_performed = 0
             while retries_performed < self.retry_count + 1:
             # or maximum number of retries is reached.
             retries_performed = 0
             while retries_performed < self.retry_count + 1:
-                # Open connection
-                if self.api.secure:
-                    conn = httplib.HTTPSConnection(self.host, timeout=self.api.timeout)
-                else:
-                    conn = httplib.HTTPConnection(self.host, timeout=self.api.timeout)
+                # handle running out of api calls
+                if self.wait_on_rate_limit:
+                    if self._reset_time is not None:
+                        if self._remaining_calls is not None:
+                            if self._remaining_calls < 1:
+                                sleep_time = self._reset_time - int(time.time())
+                                if sleep_time > 0:
+                                    if self.wait_on_rate_limit_notify:
+                                        print("Rate limit reached. Sleeping for:", sleep_time)
+                                    time.sleep(sleep_time + 5)  # sleep for few extra sec
+
+                # if self.wait_on_rate_limit and self._reset_time is not None and \
+                #                 self._remaining_calls is not None and self._remaining_calls < 1:
+                #     sleep_time = self._reset_time - int(time.time())
+                #     if sleep_time > 0:
+                #         if self.wait_on_rate_limit_notify:
+                #             print("Rate limit reached. Sleeping for: " + str(sleep_time))
+                #         time.sleep(sleep_time + 5)  # sleep for few extra sec
 
                 # Apply authentication
                 if self.api.auth:
 
                 # Apply authentication
                 if self.api.auth:
-                    self.api.auth.apply_auth(
-                            self.scheme + self.host + url,
-                            self.method, self.headers, self.parameters
-                    )
+                    auth = self.api.auth.apply_auth()
 
                 # Request compression if configured
                 if self.api.compression:
 
                 # Request compression if configured
                 if self.api.compression:
-                    self.headers['Accept-encoding'] = 'gzip'
+                    self.session.headers['Accept-encoding'] = 'gzip'
 
                 # Execute request
                 try:
 
                 # Execute request
                 try:
-                    conn.request(self.method, url, headers=self.headers, body=self.post_data)
-                    resp = conn.getresponse()
+                    resp = self.session.request(self.method,
+                                                full_url,
+                                                data=self.post_data,
+                                                timeout=self.api.timeout,
+                                                auth=auth,
+                                                proxies=self.api.proxy)
                 except Exception as e:
                     raise TweepError('Failed to send request: %s' % e)
                 except Exception as e:
                     raise TweepError('Failed to send request: %s' % e)
-
+                rem_calls = resp.headers.get('x-rate-limit-remaining')
+                if rem_calls is not None:
+                    self._remaining_calls = int(rem_calls)
+                elif isinstance(self._remaining_calls, int):
+                    self._remaining_calls -= 1
+                reset_time = resp.headers.get('x-rate-limit-reset')
+                if reset_time is not None:
+                    self._reset_time = int(reset_time)
+                if self.wait_on_rate_limit and self._remaining_calls == 0 and (
+                        # if ran out of calls before waiting switching retry last call
+                        resp.status_code == 429 or resp.status_code == 420):
+                    continue
+                retry_delay = self.retry_delay
                 # Exit request loop if non-retry error code
                 # Exit request loop if non-retry error code
-                if self.retry_errors:
-                    if resp.status not in self.retry_errors: break
-                else:
-                    if resp.status == 200: break
+                if resp.status_code == 200:
+                    break
+                elif (resp.status_code == 429 or resp.status_code == 420) and self.wait_on_rate_limit:
+                    if 'retry-after' in resp.headers:
+                        retry_delay = float(resp.headers['retry-after'])
+                elif self.retry_errors and resp.status_code not in self.retry_errors:
+                    break
 
                 # Sleep before retrying request again
 
                 # Sleep before retrying request again
-                time.sleep(self.retry_delay)
+                time.sleep(retry_delay)
                 retries_performed += 1
 
             # If an error was returned, throw an exception
             self.api.last_response = resp
                 retries_performed += 1
 
             # If an error was returned, throw an exception
             self.api.last_response = resp
-            if resp.status and not 200 <= resp.status < 300:
+            if resp.status_code and not 200 <= resp.status_code < 300:
                 try:
                 try:
-                    error_msg = self.api.parser.parse_error(resp.read())
+                    error_msg = self.parser.parse_error(resp.text)
                 except Exception:
                 except Exception:
-                    error_msg = "Twitter error response: status code = %s" % resp.status
+                    error_msg = "Twitter error response: status code = %s" % resp.status_code
                 raise TweepError(error_msg, resp)
 
             # Parse the response payload
                 raise TweepError(error_msg, resp)
 
             # Parse the response payload
-            body = resp.read()
-            if resp.getheader('Content-Encoding', '') == 'gzip':
-                try:
-                    zipper = gzip.GzipFile(fileobj=StringIO(body))
-                    body = zipper.read()
-                except Exception as e:
-                    raise TweepError('Failed to decompress data: %s' % e)
-            result = self.api.parser.parse(self, body)
-
-            conn.close()
+            result = self.parser.parse(self, resp.text)
 
             # Store result into cache if one is available.
             if self.use_cache and self.api.cache and self.method == 'GET' and result:
 
             # Store result into cache if one is available.
             if self.use_cache and self.api.cache and self.method == 'GET' and result:
@@ -193,21 +231,20 @@ def bind_api(**config):
 
             return result
 
 
             return result
 
-
-    def _call(api, *args, **kargs):
-
-        method = APIMethod(api, args, kargs)
-        return method.execute()
-
+    def _call(*args, **kwargs):
+        method = APIMethod(args, kwargs)
+        if kwargs.get('create'):
+            return method
+        else:
+            return method.execute()
 
     # Set pagination mode
     if 'cursor' in APIMethod.allowed_param:
         _call.pagination_mode = 'cursor'
 
     # Set pagination mode
     if 'cursor' in APIMethod.allowed_param:
         _call.pagination_mode = 'cursor'
-    elif 'max_id' in APIMethod.allowed_param and \
-         'since_id' in APIMethod.allowed_param:
-        _call.pagination_mode = 'id'
+    elif 'max_id' in APIMethod.allowed_param:
+        if 'since_id' in APIMethod.allowed_param:
+            _call.pagination_mode = 'id'
     elif 'page' in APIMethod.allowed_param:
         _call.pagination_mode = 'page'
 
     return _call
     elif 'page' in APIMethod.allowed_param:
         _call.pagination_mode = 'page'
 
     return _call
-
index a50a349891e9a2f5f7a63f45039183131e109c0f..1d6cb562f945abed175fbffd352d627bd8664df2 100644 (file)
@@ -2,6 +2,8 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
+from __future__ import print_function
+
 import time
 import datetime
 import threading
 import time
 import datetime
 import threading
@@ -119,7 +121,7 @@ class MemoryCache(Cache):
     def cleanup(self):
         self.lock.acquire()
         try:
     def cleanup(self):
         self.lock.acquire()
         try:
-            for k, v in self._entries.items():
+            for k, v in dict(self._entries).items():
                 if self._is_expired(v, self.timeout):
                     del self._entries[k]
         finally:
                 if self._is_expired(v, self.timeout):
                     del self._entries[k]
         finally:
@@ -161,7 +163,7 @@ class FileCache(Cache):
 
     def _get_path(self, key):
         md5 = hashlib.md5()
 
     def _get_path(self, key):
         md5 = hashlib.md5()
-        md5.update(key)
+        md5.update(key.encode('utf-8'))
         return os.path.join(self.cache_dir, md5.hexdigest())
 
     def _lock_file_dummy(self, path, exclusive=True):
         return os.path.join(self.cache_dir, md5.hexdigest())
 
     def _lock_file_dummy(self, path, exclusive=True):
@@ -236,10 +238,11 @@ class FileCache(Cache):
             # check if value is expired
             if timeout is None:
                 timeout = self.timeout
             # check if value is expired
             if timeout is None:
                 timeout = self.timeout
-            if timeout > 0 and (time.time() - created_time) >= timeout:
-                # expired! delete from cache
-                value = None
-                self._delete_file(path)
+            if timeout > 0:
+                if (time.time() - created_time) >= timeout:
+                    # expired! delete from cache
+                    value = None
+                    self._delete_file(path)
 
             # unlock and return result
             self._unlock_file(f_lock)
 
             # unlock and return result
             self._unlock_file(f_lock)
@@ -267,6 +270,7 @@ class FileCache(Cache):
                 continue
             self._delete_file(os.path.join(self.cache_dir, entry))
 
                 continue
             self._delete_file(os.path.join(self.cache_dir, entry))
 
+
 class MemCacheCache(Cache):
     """Cache interface"""
 
 class MemCacheCache(Cache):
     """Cache interface"""
 
@@ -288,7 +292,8 @@ class MemCacheCache(Cache):
     def get(self, key, timeout=None):
         """Get cached entry if exists and not expired
             key: which entry to get
     def get(self, key, timeout=None):
         """Get cached entry if exists and not expired
             key: which entry to get
-            timeout: override timeout with this value [optional]. DOES NOT WORK HERE
+            timeout: override timeout with this value [optional].
+            DOES NOT WORK HERE
         """
         return self.client.get(key)
 
         """
         return self.client.get(key)
 
@@ -304,10 +309,14 @@ class MemCacheCache(Cache):
         """Delete all cached entries. NO-OP"""
         raise NotImplementedError
 
         """Delete all cached entries. NO-OP"""
         raise NotImplementedError
 
+
 class RedisCache(Cache):
 class RedisCache(Cache):
-    '''Cache running in a redis server'''
+    """Cache running in a redis server"""
 
 
-    def __init__(self, client, timeout=60, keys_container = 'tweepy:keys', pre_identifier = 'tweepy:'):
+    def __init__(self, client,
+                 timeout=60,
+                 keys_container='tweepy:keys',
+                 pre_identifier='tweepy:'):
         Cache.__init__(self, timeout)
         self.client = client
         self.keys_container = keys_container
         Cache.__init__(self, timeout)
         self.client = client
         self.keys_container = keys_container
@@ -318,8 +327,9 @@ class RedisCache(Cache):
         return timeout > 0 and (time.time() - entry[0]) >= timeout
 
     def store(self, key, value):
         return timeout > 0 and (time.time() - entry[0]) >= timeout
 
     def store(self, key, value):
-        '''Store the key, value pair in our redis server'''
-        # Prepend tweepy to our key, this makes it easier to identify tweepy keys in our redis server
+        """Store the key, value pair in our redis server"""
+        # Prepend tweepy to our key,
+        # this makes it easier to identify tweepy keys in our redis server
         key = self.pre_identifier + key
         # Get a pipe (to execute several redis commands in one step)
         pipe = self.client.pipeline()
         key = self.pre_identifier + key
         # Get a pipe (to execute several redis commands in one step)
         pipe = self.client.pipeline()
@@ -333,7 +343,7 @@ class RedisCache(Cache):
         pipe.execute()
 
     def get(self, key, timeout=None):
         pipe.execute()
 
     def get(self, key, timeout=None):
-        '''Given a key, returns an element from the redis table'''
+        """Given a key, returns an element from the redis table"""
         key = self.pre_identifier + key
         # Check to see if we have this key
         unpickled_entry = self.client.get(key)
         key = self.pre_identifier + key
         # Check to see if we have this key
         unpickled_entry = self.client.get(key)
@@ -356,19 +366,20 @@ class RedisCache(Cache):
         return entry[1]
 
     def count(self):
         return entry[1]
 
     def count(self):
-        '''Note: This is not very efficient, since it retreives all the keys from the redis
-        server to know how many keys we have'''
+        """Note: This is not very efficient,
+        since it retreives all the keys from the redis
+        server to know how many keys we have"""
         return len(self.client.smembers(self.keys_container))
 
     def delete_entry(self, key):
         return len(self.client.smembers(self.keys_container))
 
     def delete_entry(self, key):
-        '''Delete an object from the redis table'''
+        """Delete an object from the redis table"""
         pipe = self.client.pipeline()
         pipe.srem(self.keys_container, key)
         pipe.delete(key)
         pipe.execute()
 
     def cleanup(self):
         pipe = self.client.pipeline()
         pipe.srem(self.keys_container, key)
         pipe.delete(key)
         pipe.execute()
 
     def cleanup(self):
-        '''Cleanup all the expired keys'''
+        """Cleanup all the expired keys"""
         keys = self.client.smembers(self.keys_container)
         for key in keys:
             entry = self.client.get(key)
         keys = self.client.smembers(self.keys_container)
         for key in keys:
             entry = self.client.get(key)
@@ -378,7 +389,7 @@ class RedisCache(Cache):
                     self.delete_entry(key)
 
     def flush(self):
                     self.delete_entry(key)
 
     def flush(self):
-        '''Delete all entries from the cache'''
+        """Delete all entries from the cache"""
         keys = self.client.smembers(self.keys_container)
         for key in keys:
             self.delete_entry(key)
         keys = self.client.smembers(self.keys_container)
         for key in keys:
             self.delete_entry(key)
index 4c06f17a9f2fab3af9885f831c17c2c7fde1152a..1d63f4979deb67137bc6e1dc3e2b8ef667e468fa 100644 (file)
@@ -2,7 +2,11 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
+from __future__ import print_function
+
 from tweepy.error import TweepError
 from tweepy.error import TweepError
+from tweepy.parsers import ModelParser, RawParser
+
 
 class Cursor(object):
     """Pagination helper class"""
 
 class Cursor(object):
     """Pagination helper class"""
@@ -32,6 +36,7 @@ class Cursor(object):
         i.limit = limit
         return i
 
         i.limit = limit
         return i
 
+
 class BaseIterator(object):
 
     def __init__(self, method, args, kargs):
 class BaseIterator(object):
 
     def __init__(self, method, args, kargs):
@@ -40,6 +45,9 @@ class BaseIterator(object):
         self.kargs = kargs
         self.limit = 0
 
         self.kargs = kargs
         self.limit = 0
 
+    def __next__(self):
+        return self.next()
+
     def next(self):
         raise NotImplementedError
 
     def next(self):
         raise NotImplementedError
 
@@ -49,6 +57,7 @@ class BaseIterator(object):
     def __iter__(self):
         return self
 
     def __iter__(self):
         return self
 
+
 class CursorIterator(BaseIterator):
 
     def __init__(self, method, args, kargs):
 class CursorIterator(BaseIterator):
 
     def __init__(self, method, args, kargs):
@@ -56,67 +65,96 @@ class CursorIterator(BaseIterator):
         start_cursor = kargs.pop('cursor', None)
         self.next_cursor = start_cursor or -1
         self.prev_cursor = start_cursor or 0
         start_cursor = kargs.pop('cursor', None)
         self.next_cursor = start_cursor or -1
         self.prev_cursor = start_cursor or 0
-        self.count = 0
+        self.num_tweets = 0
 
     def next(self):
 
     def next(self):
-        if self.next_cursor == 0 or (self.limit and self.count == self.limit):
+        if self.next_cursor == 0 or (self.limit and self.num_tweets == self.limit):
             raise StopIteration
             raise StopIteration
-        data, cursors = self.method(
-                cursor=self.next_cursor, *self.args, **self.kargs
-        )
+        data, cursors = self.method(cursor=self.next_cursor,
+                                    *self.args,
+                                    **self.kargs)
         self.prev_cursor, self.next_cursor = cursors
         if len(data) == 0:
             raise StopIteration
         self.prev_cursor, self.next_cursor = cursors
         if len(data) == 0:
             raise StopIteration
-        self.count += 1
+        self.num_tweets += 1
         return data
 
     def prev(self):
         if self.prev_cursor == 0:
             raise TweepError('Can not page back more, at first page')
         return data
 
     def prev(self):
         if self.prev_cursor == 0:
             raise TweepError('Can not page back more, at first page')
-        data, self.next_cursor, self.prev_cursor = self.method(
-                cursor=self.prev_cursor, *self.args, **self.kargs
-        )
-        self.count -= 1
+        data, self.next_cursor, self.prev_cursor = self.method(cursor=self.prev_cursor,
+                                                               *self.args,
+                                                               **self.kargs)
+        self.num_tweets -= 1
         return data
 
         return data
 
+
 class IdIterator(BaseIterator):
 
     def __init__(self, method, args, kargs):
         BaseIterator.__init__(self, method, args, kargs)
 class IdIterator(BaseIterator):
 
     def __init__(self, method, args, kargs):
         BaseIterator.__init__(self, method, args, kargs)
-        self.max_id = kargs.get('max_id')
-        self.since_id = kargs.get('since_id')
-        self.count = 0
+        self.max_id = kargs.pop('max_id', None)
+        self.num_tweets = 0
+        self.results = []
+        self.model_results = []
+        self.index = 0
 
     def next(self):
         """Fetch a set of items with IDs less than current set."""
 
     def next(self):
         """Fetch a set of items with IDs less than current set."""
-        if self.limit and self.limit == self.count:
+        if self.limit and self.limit == self.num_tweets:
             raise StopIteration
 
             raise StopIteration
 
-        # max_id is inclusive so decrement by one
-        # to avoid requesting duplicate items.
-        max_id = self.since_id - 1 if self.max_id else None
-        data = self.method(max_id = max_id, *self.args, **self.kargs)
-        if len(data) == 0:
+        if self.index >= len(self.results) - 1:
+            data = self.method(max_id=self.max_id, parser=RawParser(), *self.args, **self.kargs)
+
+            if hasattr(self.method, '__self__'):
+                old_parser = self.method.__self__.parser
+                # Hack for models which expect ModelParser to be set
+                self.method.__self__.parser = ModelParser()
+
+            # This is a special invocation that returns the underlying
+            # APIMethod class
+            model = ModelParser().parse(self.method(create=True), data)
+            if hasattr(self.method, '__self__'):
+                self.method.__self__.parser = old_parser
+                result = self.method.__self__.parser.parse(self.method(create=True), data)
+            else:
+                result = model
+
+            if len(self.results) != 0:
+                self.index += 1
+            self.results.append(result)
+            self.model_results.append(model)
+        else:
+            self.index += 1
+            result = self.results[self.index]
+            model = self.model_results[self.index]
+
+        if len(result) == 0:
             raise StopIteration
             raise StopIteration
-        self.max_id = data.max_id
-        self.since_id = data.since_id
-        self.count += 1
-        return data
+        # TODO: Make this not dependant on the parser making max_id and
+        # since_id available
+        self.max_id = model.max_id
+        self.num_tweets += 1
+        return result
 
     def prev(self):
         """Fetch a set of items with IDs greater than current set."""
 
     def prev(self):
         """Fetch a set of items with IDs greater than current set."""
-        if self.limit and self.limit == self.count:
+        if self.limit and self.limit == self.num_tweets:
             raise StopIteration
 
             raise StopIteration
 
-        since_id = self.max_id
-        data = self.method(since_id = since_id, *self.args, **self.kargs)
-        if len(data) == 0:
+        self.index -= 1
+        if self.index < 0:
+            # There's no way to fetch a set of tweets directly 'above' the
+            # current set
             raise StopIteration
             raise StopIteration
-        self.max_id = data.max_id
-        self.since_id = data.since_id
-        self.count += 1
+
+        data = self.results[self.index]
+        self.max_id = self.model_results[self.index].max_id
+        self.num_tweets += 1
         return data
 
         return data
 
+
 class PageIterator(BaseIterator):
 
     def __init__(self, method, args, kargs):
 class PageIterator(BaseIterator):
 
     def __init__(self, method, args, kargs):
@@ -124,18 +162,23 @@ class PageIterator(BaseIterator):
         self.current_page = 0
 
     def next(self):
         self.current_page = 0
 
     def next(self):
-        self.current_page += 1
+        if self.limit > 0:
+            if self.current_page > self.limit:
+                raise StopIteration
+
         items = self.method(page=self.current_page, *self.args, **self.kargs)
         items = self.method(page=self.current_page, *self.args, **self.kargs)
-        if len(items) == 0 or (self.limit > 0 and self.current_page > self.limit):
+        if len(items) == 0:
             raise StopIteration
             raise StopIteration
+        self.current_page += 1
         return items
 
     def prev(self):
         return items
 
     def prev(self):
-        if (self.current_page == 1):
+        if self.current_page == 1:
             raise TweepError('Can not page back more, at first page')
         self.current_page -= 1
         return self.method(page=self.current_page, *self.args, **self.kargs)
 
             raise TweepError('Can not page back more, at first page')
         self.current_page -= 1
         return self.method(page=self.current_page, *self.args, **self.kargs)
 
+
 class ItemIterator(BaseIterator):
 
     def __init__(self, page_iterator):
 class ItemIterator(BaseIterator):
 
     def __init__(self, page_iterator):
@@ -143,17 +186,18 @@ class ItemIterator(BaseIterator):
         self.limit = 0
         self.current_page = None
         self.page_index = -1
         self.limit = 0
         self.current_page = None
         self.page_index = -1
-        self.count = 0
+        self.num_tweets = 0
 
     def next(self):
 
     def next(self):
-        if self.limit > 0 and self.count == self.limit:
-            raise StopIteration
+        if self.limit > 0:
+            if self.num_tweets == self.limit:
+                raise StopIteration
         if self.current_page is None or self.page_index == len(self.current_page) - 1:
             # Reached end of current page, get the next page...
             self.current_page = self.page_iterator.next()
             self.page_index = -1
         self.page_index += 1
         if self.current_page is None or self.page_index == len(self.current_page) - 1:
             # Reached end of current page, get the next page...
             self.current_page = self.page_iterator.next()
             self.page_index = -1
         self.page_index += 1
-        self.count += 1
+        self.num_tweets += 1
         return self.current_page[self.page_index]
 
     def prev(self):
         return self.current_page[self.page_index]
 
     def prev(self):
@@ -166,6 +210,5 @@ class ItemIterator(BaseIterator):
             if self.page_index == 0:
                 raise TweepError('No more items')
         self.page_index -= 1
             if self.page_index == 0:
                 raise TweepError('No more items')
         self.page_index -= 1
-        self.count -= 1
+        self.num_tweets -= 1
         return self.current_page[self.page_index]
         return self.current_page[self.page_index]
-
index 753e2fe676cf2f581a9fe032a71d9c4d638cc471..1c47a5a2c3724fb74c6a56157c990c41856f9b53 100644 (file)
@@ -2,14 +2,18 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
+from __future__ import print_function
+
+import six
+
+
 class TweepError(Exception):
     """Tweepy exception"""
 
     def __init__(self, reason, response=None):
 class TweepError(Exception):
     """Tweepy exception"""
 
     def __init__(self, reason, response=None):
-        self.reason = unicode(reason)
+        self.reason = six.text_type(reason)
         self.response = response
         Exception.__init__(self, reason)
 
     def __str__(self):
         return self.reason
         self.response = response
         Exception.__init__(self, reason)
 
     def __str__(self):
         return self.reason
-
index 9b70070887bbb6fa48ea03d15f161cac4f513c3e..09ed3eb026a2afd412755e3f32bf47aabc538839 100644 (file)
@@ -2,7 +2,8 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
-from tweepy.error import TweepError
+from __future__ import absolute_import, print_function
+
 from tweepy.utils import parse_datetime, parse_html_value, parse_a_href
 
 
 from tweepy.utils import parse_datetime, parse_html_value, parse_a_href
 
 
@@ -18,18 +19,21 @@ class ResultSet(list):
         if self._max_id:
             return self._max_id
         ids = self.ids()
         if self._max_id:
             return self._max_id
         ids = self.ids()
-        return max(ids) if ids else None
+        # Max_id is always set to the *smallest* id, minus one, in the set
+        return (min(ids) - 1) if ids else None
 
     @property
     def since_id(self):
         if self._since_id:
             return self._since_id
         ids = self.ids()
 
     @property
     def since_id(self):
         if self._since_id:
             return self._since_id
         ids = self.ids()
-        return min(ids) if ids else None
+        # Since_id is always set to the *greatest* id in the set
+        return max(ids) if ids else None
 
     def ids(self):
         return [item.id for item in self if hasattr(item, 'id')]
 
 
     def ids(self):
         return [item.id for item in self if hasattr(item, 'id')]
 
+
 class Model(object):
 
     def __init__(self, api=None):
 class Model(object):
 
     def __init__(self, api=None):
@@ -51,7 +55,10 @@ class Model(object):
 
     @classmethod
     def parse_list(cls, api, json_list):
 
     @classmethod
     def parse_list(cls, api, json_list):
-        """Parse a list of JSON objects into a result set of model instances."""
+        """
+            Parse a list of JSON objects into
+            a result set of model instances.
+        """
         results = ResultSet()
         for obj in json_list:
             if obj:
         results = ResultSet()
         for obj in json_list:
             if obj:
@@ -59,7 +66,7 @@ class Model(object):
         return results
 
     def __repr__(self):
         return results
 
     def __repr__(self):
-        state = ['%s=%s' % (k, repr(v)) for (k,v) in vars(self).items()]
+        state = ['%s=%s' % (k, repr(v)) for (k, v) in vars(self).items()]
         return '%s(%s)' % (self.__class__.__name__, ', '.join(state))
 
 
         return '%s(%s)' % (self.__class__.__name__, ', '.join(state))
 
 
@@ -68,12 +75,13 @@ class Status(Model):
     @classmethod
     def parse(cls, api, json):
         status = cls(api)
     @classmethod
     def parse(cls, api, json):
         status = cls(api)
-        for k, v in json.items():
-            
-            # Hack by guyrt to fix unicode issues, which we do *not* want to teach.
-            if isinstance(v, basestring):
-                v = v.encode('ascii', errors='ignore')
 
 
+        # I'm not proud. Blame billg.
+        json['text'] = str(json['text'].encode('ascii', 'ignore'))[2:-1]
+        json['user']['screen_name'] = str(json['user']['screen_name'].encode('ascii', 'ignore'))[2:-1]
+
+        setattr(status, '_json', json)
+        for k, v in json.items():
             if k == 'user':
                 user_model = getattr(api.parser.model_factory, 'user') if api else User
                 user = user_model.parse(api, v)
             if k == 'user':
                 user_model = getattr(api.parser.model_factory, 'user') if api else User
                 user = user_model.parse(api, v)
@@ -111,12 +119,27 @@ class Status(Model):
     def favorite(self):
         return self._api.create_favorite(self.id)
 
     def favorite(self):
         return self._api.create_favorite(self.id)
 
+    def __eq__(self, other):
+        if isinstance(other, Status):
+            return self.id == other.id
+
+        return NotImplemented
+
+    def __ne__(self, other):
+        result = self == other
+
+        if result is NotImplemented:
+            return result
+
+        return not result
+
 
 class User(Model):
 
     @classmethod
     def parse(cls, api, json):
         user = cls(api)
 
 class User(Model):
 
     @classmethod
     def parse(cls, api, json):
         user = cls(api)
+        setattr(user, '_json', json)
         for k, v in json.items():
             if k == 'created_at':
                 setattr(user, k, parse_datetime(v))
         for k, v in json.items():
             if k == 'created_at':
                 setattr(user, k, parse_datetime(v))
@@ -162,16 +185,24 @@ class User(Model):
         self.following = False
 
     def lists_memberships(self, *args, **kargs):
         self.following = False
 
     def lists_memberships(self, *args, **kargs):
-        return self._api.lists_memberships(user=self.screen_name, *args, **kargs)
+        return self._api.lists_memberships(user=self.screen_name,
+                                           *args,
+                                           **kargs)
 
     def lists_subscriptions(self, *args, **kargs):
 
     def lists_subscriptions(self, *args, **kargs):
-        return self._api.lists_subscriptions(user=self.screen_name, *args, **kargs)
+        return self._api.lists_subscriptions(user=self.screen_name,
+                                             *args,
+                                             **kargs)
 
     def lists(self, *args, **kargs):
 
     def lists(self, *args, **kargs):
-        return self._api.lists_all(user=self.screen_name, *args, **kargs)
+        return self._api.lists_all(user=self.screen_name,
+                                   *args,
+                                   **kargs)
 
     def followers_ids(self, *args, **kargs):
 
     def followers_ids(self, *args, **kargs):
-        return self._api.followers_ids(user_id=self.id, *args, **kargs)
+        return self._api.followers_ids(user_id=self.id,
+                                       *args,
+                                       **kargs)
 
 
 class DirectMessage(Model):
 
 
 class DirectMessage(Model):
@@ -242,15 +273,17 @@ class SearchResults(ResultSet):
     @classmethod
     def parse(cls, api, json):
         metadata = json['search_metadata']
     @classmethod
     def parse(cls, api, json):
         metadata = json['search_metadata']
-        results = SearchResults(metadata.get('max_id'), metadata.get('since_id'))
+        results = SearchResults()
         results.refresh_url = metadata.get('refresh_url')
         results.completed_in = metadata.get('completed_in')
         results.query = metadata.get('query')
         results.count = metadata.get('count')
         results.next_results = metadata.get('next_results')
 
         results.refresh_url = metadata.get('refresh_url')
         results.completed_in = metadata.get('completed_in')
         results.query = metadata.get('query')
         results.count = metadata.get('count')
         results.next_results = metadata.get('next_results')
 
+        status_model = getattr(api.parser.model_factory, 'status') if api else Status
+
         for status in json['statuses']:
         for status in json['statuses']:
-            results.append(Status.parse(api, status))
+            results.append(status_model.parse(api, status))
         return results
 
 
         return results
 
 
@@ -259,7 +292,7 @@ class List(Model):
     @classmethod
     def parse(cls, api, json):
         lst = List(api)
     @classmethod
     def parse(cls, api, json):
         lst = List(api)
-        for k,v in json.items():
+        for k, v in json.items():
             if k == 'user':
                 setattr(lst, k, User.parse(api, v))
             elif k == 'created_at':
             if k == 'user':
                 setattr(lst, k, User.parse(api, v))
             elif k == 'created_at':
@@ -284,7 +317,9 @@ class List(Model):
         return self._api.destroy_list(self.slug)
 
     def timeline(self, **kargs):
         return self._api.destroy_list(self.slug)
 
     def timeline(self, **kargs):
-        return self._api.list_timeline(self.user.screen_name, self.slug, **kargs)
+        return self._api.list_timeline(self.user.screen_name,
+                                       self.slug,
+                                       **kargs)
 
     def add_member(self, id):
         return self._api.add_list_member(self.slug, id)
 
     def add_member(self, id):
         return self._api.add_list_member(self.slug, id)
@@ -293,10 +328,14 @@ class List(Model):
         return self._api.remove_list_member(self.slug, id)
 
     def members(self, **kargs):
         return self._api.remove_list_member(self.slug, id)
 
     def members(self, **kargs):
-        return self._api.list_members(self.user.screen_name, self.slug, **kargs)
+        return self._api.list_members(self.user.screen_name,
+                                      self.slug,
+                                      **kargs)
 
     def is_member(self, id):
 
     def is_member(self, id):
-        return self._api.is_list_member(self.user.screen_name, self.slug, id)
+        return self._api.is_list_member(self.user.screen_name,
+                                        self.slug,
+                                        id)
 
     def subscribe(self):
         return self._api.subscribe_list(self.user.screen_name, self.slug)
 
     def subscribe(self):
         return self._api.subscribe_list(self.user.screen_name, self.slug)
@@ -305,16 +344,21 @@ class List(Model):
         return self._api.unsubscribe_list(self.user.screen_name, self.slug)
 
     def subscribers(self, **kargs):
         return self._api.unsubscribe_list(self.user.screen_name, self.slug)
 
     def subscribers(self, **kargs):
-        return self._api.list_subscribers(self.user.screen_name, self.slug, **kargs)
+        return self._api.list_subscribers(self.user.screen_name,
+                                          self.slug,
+                                          **kargs)
 
     def is_subscribed(self, id):
 
     def is_subscribed(self, id):
-        return self._api.is_subscribed_list(self.user.screen_name, self.slug, id)
+        return self._api.is_subscribed_list(self.user.screen_name,
+                                            self.slug,
+                                            id)
+
 
 class Relation(Model):
     @classmethod
     def parse(cls, api, json):
         result = cls(api)
 
 class Relation(Model):
     @classmethod
     def parse(cls, api, json):
         result = cls(api)
-        for k,v in json.items():
+        for k, v in json.items():
             if k == 'value' and json['kind'] in ['Tweet', 'LookedupStatus']:
                 setattr(result, k, Status.parse(api, v))
             elif k == 'results':
             if k == 'value' and json['kind'] in ['Tweet', 'LookedupStatus']:
                 setattr(result, k, Status.parse(api, v))
             elif k == 'results':
@@ -323,11 +367,12 @@ class Relation(Model):
                 setattr(result, k, v)
         return result
 
                 setattr(result, k, v)
         return result
 
+
 class Relationship(Model):
     @classmethod
     def parse(cls, api, json):
         result = cls(api)
 class Relationship(Model):
     @classmethod
     def parse(cls, api, json):
         result = cls(api)
-        for k,v in json.items():
+        for k, v in json.items():
             if k == 'connections':
                 setattr(result, 'is_following', 'following' in v)
                 setattr(result, 'is_followed_by', 'followed_by' in v)
             if k == 'connections':
                 setattr(result, 'is_following', 'following' in v)
                 setattr(result, 'is_followed_by', 'followed_by' in v)
@@ -335,6 +380,7 @@ class Relationship(Model):
                 setattr(result, k, v)
         return result
 
                 setattr(result, k, v)
         return result
 
+
 class JSONModel(Model):
 
     @classmethod
 class JSONModel(Model):
 
     @classmethod
@@ -416,6 +462,17 @@ class Place(Model):
             results.append(cls.parse(api, obj))
         return results
 
             results.append(cls.parse(api, obj))
         return results
 
+
+class Media(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        media = cls(api)
+        for k, v in json.items():
+            setattr(media, k, v)
+        return media
+
+
 class ModelFactory(object):
     """
     Used by parsers for creating instances
 class ModelFactory(object):
     """
     Used by parsers for creating instances
@@ -433,9 +490,9 @@ class ModelFactory(object):
     list = List
     relation = Relation
     relationship = Relationship
     list = List
     relation = Relation
     relationship = Relationship
+    media = Media
 
     json = JSONModel
     ids = IDModel
     place = Place
     bounding_box = BoundingBox
 
     json = JSONModel
     ids = IDModel
     place = Place
     bounding_box = BoundingBox
-
diff --git a/tweepy/oauth.py b/tweepy/oauth.py
deleted file mode 100644 (file)
index 286de18..0000000
+++ /dev/null
@@ -1,655 +0,0 @@
-"""
-The MIT License
-
-Copyright (c) 2007 Leah Culver
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import cgi
-import urllib
-import time
-import random
-import urlparse
-import hmac
-import binascii
-
-
-VERSION = '1.0' # Hi Blaine!
-HTTP_METHOD = 'GET'
-SIGNATURE_METHOD = 'PLAINTEXT'
-
-
-class OAuthError(RuntimeError):
-    """Generic exception class."""
-    def __init__(self, message='OAuth error occured.'):
-        self.message = message
-
-def build_authenticate_header(realm=''):
-    """Optional WWW-Authenticate header (401 error)"""
-    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-def escape(s):
-    """Escape a URL including any /."""
-    return urllib.quote(s, safe='~')
-
-def _utf8_str(s):
-    """Convert unicode to utf-8."""
-    if isinstance(s, unicode):
-        return s.encode("utf-8")
-    else:
-        return str(s)
-
-def generate_timestamp():
-    """Get seconds since epoch (UTC)."""
-    return int(time.time())
-
-def generate_nonce(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-def generate_verifier(length=8):
-    """Generate pseudorandom number."""
-    return ''.join([str(random.randint(0, 9)) for i in range(length)])
-
-
-class OAuthConsumer(object):
-    """Consumer of OAuth authentication.
-
-    OAuthConsumer is a data type that represents the identity of the Consumer
-    via its shared secret with the Service Provider.
-
-    """
-    key = None
-    secret = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-
-class OAuthToken(object):
-    """OAuthToken is a data type that represents an End User via either an access
-    or request token.
-    
-    key -- the token
-    secret -- the token secret
-
-    """
-    key = None
-    secret = None
-    callback = None
-    callback_confirmed = None
-    verifier = None
-
-    def __init__(self, key, secret):
-        self.key = key
-        self.secret = secret
-
-    def set_callback(self, callback):
-        self.callback = callback
-        self.callback_confirmed = 'true'
-
-    def set_verifier(self, verifier=None):
-        if verifier is not None:
-            self.verifier = verifier
-        else:
-            self.verifier = generate_verifier()
-
-    def get_callback_url(self):
-        if self.callback and self.verifier:
-            # Append the oauth_verifier.
-            parts = urlparse.urlparse(self.callback)
-            scheme, netloc, path, params, query, fragment = parts[:6]
-            if query:
-                query = '%s&oauth_verifier=%s' % (query, self.verifier)
-            else:
-                query = 'oauth_verifier=%s' % self.verifier
-            return urlparse.urlunparse((scheme, netloc, path, params,
-                query, fragment))
-        return self.callback
-
-    def to_string(self):
-        data = {
-            'oauth_token': self.key,
-            'oauth_token_secret': self.secret,
-        }
-        if self.callback_confirmed is not None:
-            data['oauth_callback_confirmed'] = self.callback_confirmed
-        return urllib.urlencode(data)
-    def from_string(s):
-        """ Returns a token from something like:
-        oauth_token_secret=xxx&oauth_token=xxx
-        """
-        params = cgi.parse_qs(s, keep_blank_values=False)
-        key = params['oauth_token'][0]
-        secret = params['oauth_token_secret'][0]
-        token = OAuthToken(key, secret)
-        try:
-            token.callback_confirmed = params['oauth_callback_confirmed'][0]
-        except KeyError:
-            pass # 1.0, no callback confirmed.
-        return token
-    from_string = staticmethod(from_string)
-
-    def __str__(self):
-        return self.to_string()
-
-
-class OAuthRequest(object):
-    """OAuthRequest represents the request and can be serialized.
-
-    OAuth parameters:
-        - oauth_consumer_key 
-        - oauth_token
-        - oauth_signature_method
-        - oauth_signature 
-        - oauth_timestamp 
-        - oauth_nonce
-        - oauth_version
-        - oauth_verifier
-        ... any additional parameters, as defined by the Service Provider.
-    """
-    parameters = None # OAuth parameters.
-    http_method = HTTP_METHOD
-    http_url = None
-    version = VERSION
-
-    def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
-        self.http_method = http_method
-        self.http_url = http_url
-        self.parameters = parameters or {}
-
-    def set_parameter(self, parameter, value):
-        self.parameters[parameter] = value
-
-    def get_parameter(self, parameter):
-        try:
-            return self.parameters[parameter]
-        except:
-            raise OAuthError('Parameter not found: %s' % parameter)
-
-    def _get_timestamp_nonce(self):
-        return self.get_parameter('oauth_timestamp'), self.get_parameter(
-            'oauth_nonce')
-
-    def get_nonoauth_parameters(self):
-        """Get any non-OAuth parameters."""
-        parameters = {}
-        for k, v in self.parameters.iteritems():
-            # Ignore oauth parameters.
-            if k.find('oauth_') < 0:
-                parameters[k] = v
-        return parameters
-
-    def to_header(self, realm=''):
-        """Serialize as a header for an HTTPAuth request."""
-        auth_header = 'OAuth realm="%s"' % realm
-        # Add the oauth parameters.
-        if self.parameters:
-            for k, v in self.parameters.iteritems():
-                if k[:6] == 'oauth_':
-                    auth_header += ', %s="%s"' % (k, escape(str(v)))
-        return {'Authorization': auth_header}
-
-    def to_postdata(self):
-        """Serialize as post data for a POST request."""
-        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
-            for k, v in self.parameters.iteritems()])
-
-    def to_url(self):
-        """Serialize as a URL for a GET request."""
-        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
-
-    def get_normalized_parameters(self):
-        """Return a string that contains the parameters that must be signed."""
-        params = self.parameters
-        try:
-            # Exclude the signature if it exists.
-            del params['oauth_signature']
-        except:
-            pass
-        # Escape key values before sorting.
-        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
-            for k,v in params.items()]
-        # Sort lexicographically, first after key, then after value.
-        key_values.sort()
-        # Combine key value pairs into a string.
-        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
-
-    def get_normalized_http_method(self):
-        """Uppercases the http method."""
-        return self.http_method.upper()
-
-    def get_normalized_http_url(self):
-        """Parses the URL and rebuilds it to be scheme://host/path."""
-        parts = urlparse.urlparse(self.http_url)
-        scheme, netloc, path = parts[:3]
-        # Exclude default port numbers.
-        if scheme == 'http' and netloc[-3:] == ':80':
-            netloc = netloc[:-3]
-        elif scheme == 'https' and netloc[-4:] == ':443':
-            netloc = netloc[:-4]
-        return '%s://%s%s' % (scheme, netloc, path)
-
-    def sign_request(self, signature_method, consumer, token):
-        """Set the signature parameter to the result of build_signature."""
-        # Set the signature method.
-        self.set_parameter('oauth_signature_method',
-            signature_method.get_name())
-        # Set the signature.
-        self.set_parameter('oauth_signature',
-            self.build_signature(signature_method, consumer, token))
-
-    def build_signature(self, signature_method, consumer, token):
-        """Calls the build signature method within the signature method."""
-        return signature_method.build_signature(self, consumer, token)
-
-    def from_request(http_method, http_url, headers=None, parameters=None,
-            query_string=None):
-        """Combines multiple parameter sources."""
-        if parameters is None:
-            parameters = {}
-
-        # Headers
-        if headers and 'Authorization' in headers:
-            auth_header = headers['Authorization']
-            # Check that the authorization header is OAuth.
-            if auth_header[:6] == 'OAuth ':
-                auth_header = auth_header[6:]
-                try:
-                    # Get the parameters from the header.
-                    header_params = OAuthRequest._split_header(auth_header)
-                    parameters.update(header_params)
-                except:
-                    raise OAuthError('Unable to parse OAuth parameters from '
-                        'Authorization header.')
-
-        # GET or POST query string.
-        if query_string:
-            query_params = OAuthRequest._split_url_string(query_string)
-            parameters.update(query_params)
-
-        # URL parameters.
-        param_str = urlparse.urlparse(http_url)[4] # query
-        url_params = OAuthRequest._split_url_string(param_str)
-        parameters.update(url_params)
-
-        if parameters:
-            return OAuthRequest(http_method, http_url, parameters)
-
-        return None
-    from_request = staticmethod(from_request)
-
-    def from_consumer_and_token(oauth_consumer, token=None,
-            callback=None, verifier=None, http_method=HTTP_METHOD,
-            http_url=None, parameters=None):
-        if not parameters:
-            parameters = {}
-
-        defaults = {
-            'oauth_consumer_key': oauth_consumer.key,
-            'oauth_timestamp': generate_timestamp(),
-            'oauth_nonce': generate_nonce(),
-            'oauth_version': OAuthRequest.version,
-        }
-
-        defaults.update(parameters)
-        parameters = defaults
-
-        if token:
-            parameters['oauth_token'] = token.key
-            if token.callback:
-                parameters['oauth_callback'] = token.callback
-            # 1.0a support for verifier.
-            if verifier:
-                parameters['oauth_verifier'] = verifier
-        elif callback:
-            # 1.0a support for callback in the request token request.
-            parameters['oauth_callback'] = callback
-
-        return OAuthRequest(http_method, http_url, parameters)
-    from_consumer_and_token = staticmethod(from_consumer_and_token)
-
-    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
-            http_url=None, parameters=None):
-        if not parameters:
-            parameters = {}
-
-        parameters['oauth_token'] = token.key
-
-        if callback:
-            parameters['oauth_callback'] = callback
-
-        return OAuthRequest(http_method, http_url, parameters)
-    from_token_and_callback = staticmethod(from_token_and_callback)
-
-    def _split_header(header):
-        """Turn Authorization: header into parameters."""
-        params = {}
-        parts = header.split(',')
-        for param in parts:
-            # Ignore realm parameter.
-            if param.find('realm') > -1:
-                continue
-            # Remove whitespace.
-            param = param.strip()
-            # Split key-value.
-            param_parts = param.split('=', 1)
-            # Remove quotes and unescape the value.
-            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
-        return params
-    _split_header = staticmethod(_split_header)
-
-    def _split_url_string(param_str):
-        """Turn URL string into parameters."""
-        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
-        for k, v in parameters.iteritems():
-            parameters[k] = urllib.unquote(v[0])
-        return parameters
-    _split_url_string = staticmethod(_split_url_string)
-
-class OAuthServer(object):
-    """A worker to check the validity of a request against a data store."""
-    timestamp_threshold = 300 # In seconds, five minutes.
-    version = VERSION
-    signature_methods = None
-    data_store = None
-
-    def __init__(self, data_store=None, signature_methods=None):
-        self.data_store = data_store
-        self.signature_methods = signature_methods or {}
-
-    def set_data_store(self, data_store):
-        self.data_store = data_store
-
-    def get_data_store(self):
-        return self.data_store
-
-    def add_signature_method(self, signature_method):
-        self.signature_methods[signature_method.get_name()] = signature_method
-        return self.signature_methods
-
-    def fetch_request_token(self, oauth_request):
-        """Processes a request_token request and returns the
-        request token on success.
-        """
-        try:
-            # Get the request token for authorization.
-            token = self._get_token(oauth_request, 'request')
-        except OAuthError:
-            # No token required for the initial token request.
-            version = self._get_version(oauth_request)
-            consumer = self._get_consumer(oauth_request)
-            try:
-                callback = self.get_callback(oauth_request)
-            except OAuthError:
-                callback = None # 1.0, no callback specified.
-            self._check_signature(oauth_request, consumer, None)
-            # Fetch a new token.
-            token = self.data_store.fetch_request_token(consumer, callback)
-        return token
-
-    def fetch_access_token(self, oauth_request):
-        """Processes an access_token request and returns the
-        access token on success.
-        """
-        version = self._get_version(oauth_request)
-        consumer = self._get_consumer(oauth_request)
-        try:
-            verifier = self._get_verifier(oauth_request)
-        except OAuthError:
-            verifier = None
-        # Get the request token.
-        token = self._get_token(oauth_request, 'request')
-        self._check_signature(oauth_request, consumer, token)
-        new_token = self.data_store.fetch_access_token(consumer, token, verifier)
-        return new_token
-
-    def verify_request(self, oauth_request):
-        """Verifies an api call and checks all the parameters."""
-        # -> consumer and token
-        version = self._get_version(oauth_request)
-        consumer = self._get_consumer(oauth_request)
-        # Get the access token.
-        token = self._get_token(oauth_request, 'access')
-        self._check_signature(oauth_request, consumer, token)
-        parameters = oauth_request.get_nonoauth_parameters()
-        return consumer, token, parameters
-
-    def authorize_token(self, token, user):
-        """Authorize a request token."""
-        return self.data_store.authorize_request_token(token, user)
-
-    def get_callback(self, oauth_request):
-        """Get the callback URL."""
-        return oauth_request.get_parameter('oauth_callback')
-    def build_authenticate_header(self, realm=''):
-        """Optional support for the authenticate header."""
-        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-
-    def _get_version(self, oauth_request):
-        """Verify the correct version request for this server."""
-        try:
-            version = oauth_request.get_parameter('oauth_version')
-        except:
-            version = VERSION
-        if version and version != self.version:
-            raise OAuthError('OAuth version %s not supported.' % str(version))
-        return version
-
-    def _get_signature_method(self, oauth_request):
-        """Figure out the signature with some defaults."""
-        try:
-            signature_method = oauth_request.get_parameter(
-                'oauth_signature_method')
-        except:
-            signature_method = SIGNATURE_METHOD
-        try:
-            # Get the signature method object.
-            signature_method = self.signature_methods[signature_method]
-        except:
-            signature_method_names = ', '.join(self.signature_methods.keys())
-            raise OAuthError('Signature method %s not supported try one of the '
-                'following: %s' % (signature_method, signature_method_names))
-
-        return signature_method
-
-    def _get_consumer(self, oauth_request):
-        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
-        consumer = self.data_store.lookup_consumer(consumer_key)
-        if not consumer:
-            raise OAuthError('Invalid consumer.')
-        return consumer
-
-    def _get_token(self, oauth_request, token_type='access'):
-        """Try to find the token for the provided request token key."""
-        token_field = oauth_request.get_parameter('oauth_token')
-        token = self.data_store.lookup_token(token_type, token_field)
-        if not token:
-            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
-        return token
-    
-    def _get_verifier(self, oauth_request):
-        return oauth_request.get_parameter('oauth_verifier')
-
-    def _check_signature(self, oauth_request, consumer, token):
-        timestamp, nonce = oauth_request._get_timestamp_nonce()
-        self._check_timestamp(timestamp)
-        self._check_nonce(consumer, token, nonce)
-        signature_method = self._get_signature_method(oauth_request)
-        try:
-            signature = oauth_request.get_parameter('oauth_signature')
-        except:
-            raise OAuthError('Missing signature.')
-        # Validate the signature.
-        valid_sig = signature_method.check_signature(oauth_request, consumer,
-            token, signature)
-        if not valid_sig:
-            key, base = signature_method.build_signature_base_string(
-                oauth_request, consumer, token)
-            raise OAuthError('Invalid signature. Expected signature base '
-                'string: %s' % base)
-        built = signature_method.build_signature(oauth_request, consumer, token)
-
-    def _check_timestamp(self, timestamp):
-        """Verify that timestamp is recentish."""
-        timestamp = int(timestamp)
-        now = int(time.time())
-        lapsed = abs(now - timestamp)
-        if lapsed > self.timestamp_threshold:
-            raise OAuthError('Expired timestamp: given %d and now %s has a '
-                'greater difference than threshold %d' %
-                (timestamp, now, self.timestamp_threshold))
-
-    def _check_nonce(self, consumer, token, nonce):
-        """Verify that the nonce is uniqueish."""
-        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
-        if nonce:
-            raise OAuthError('Nonce already used: %s' % str(nonce))
-
-
-class OAuthClient(object):
-    """OAuthClient is a worker to attempt to execute a request."""
-    consumer = None
-    token = None
-
-    def __init__(self, oauth_consumer, oauth_token):
-        self.consumer = oauth_consumer
-        self.token = oauth_token
-
-    def get_consumer(self):
-        return self.consumer
-
-    def get_token(self):
-        return self.token
-
-    def fetch_request_token(self, oauth_request):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_access_token(self, oauth_request):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def access_resource(self, oauth_request):
-        """-> Some protected resource."""
-        raise NotImplementedError
-
-
-class OAuthDataStore(object):
-    """A database abstraction used to lookup consumers and tokens."""
-
-    def lookup_consumer(self, key):
-        """-> OAuthConsumer."""
-        raise NotImplementedError
-
-    def lookup_token(self, oauth_consumer, token_type, token_token):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_request_token(self, oauth_consumer, oauth_callback):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-    def authorize_request_token(self, oauth_token, user):
-        """-> OAuthToken."""
-        raise NotImplementedError
-
-
-class OAuthSignatureMethod(object):
-    """A strategy class that implements a signature method."""
-    def get_name(self):
-        """-> str."""
-        raise NotImplementedError
-
-    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
-        """-> str key, str raw."""
-        raise NotImplementedError
-
-    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
-        """-> str."""
-        raise NotImplementedError
-
-    def check_signature(self, oauth_request, consumer, token, signature):
-        built = self.build_signature(oauth_request, consumer, token)
-        return built == signature
-
-
-class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
-
-    def get_name(self):
-        return 'HMAC-SHA1'
-        
-    def build_signature_base_string(self, oauth_request, consumer, token):
-        sig = (
-            escape(oauth_request.get_normalized_http_method()),
-            escape(oauth_request.get_normalized_http_url()),
-            escape(oauth_request.get_normalized_parameters()),
-        )
-
-        key = '%s&' % escape(consumer.secret)
-        if token:
-            key += escape(token.secret)
-        raw = '&'.join(sig)
-        return key, raw
-
-    def build_signature(self, oauth_request, consumer, token):
-        """Builds the base signature string."""
-        key, raw = self.build_signature_base_string(oauth_request, consumer,
-            token)
-
-        # HMAC object.
-        try:
-            import hashlib # 2.5
-            hashed = hmac.new(key, raw, hashlib.sha1)
-        except:
-            import sha # Deprecated
-            hashed = hmac.new(key, raw, sha)
-
-        # Calculate the digest base 64.
-        return binascii.b2a_base64(hashed.digest())[:-1]
-
-
-class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
-
-    def get_name(self):
-        return 'PLAINTEXT'
-
-    def build_signature_base_string(self, oauth_request, consumer, token):
-        """Concatenates the consumer key and secret."""
-        sig = '%s&' % escape(consumer.secret)
-        if token:
-            sig = sig + escape(token.secret)
-        return sig, sig
-
-    def build_signature(self, oauth_request, consumer, token):
-        key, raw = self.build_signature_base_string(oauth_request, consumer,
-            token)
-        return key
\ No newline at end of file
index 31e002204565e3484e3c959164ff5b16804b0a48..bccb0322c5257b64ad978d4e1fb1cd0584a63fcb 100644 (file)
@@ -2,6 +2,8 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
+from __future__ import print_function
+
 from tweepy.models import ModelFactory
 from tweepy.utils import import_simplejson
 from tweepy.error import TweepError
 from tweepy.models import ModelFactory
 from tweepy.utils import import_simplejson
 from tweepy.error import TweepError
@@ -51,10 +53,12 @@ class JSONParser(Parser):
         except Exception as e:
             raise TweepError('Failed to parse JSON payload: %s' % e)
 
         except Exception as e:
             raise TweepError('Failed to parse JSON payload: %s' % e)
 
-        needsCursors = method.parameters.has_key('cursor')
-        if needsCursors and isinstance(json, dict) and 'previous_cursor' in json and 'next_cursor' in json:
-            cursors = json['previous_cursor'], json['next_cursor']
-            return json, cursors
+        needs_cursors = 'cursor' in method.session.params
+        if needs_cursors and isinstance(json, dict):
+            if 'previous_cursor' in json:
+                if 'next_cursor' in json:
+                    cursors = json['previous_cursor'], json['next_cursor']
+                    return json, cursors
         else:
             return json
 
         else:
             return json
 
@@ -74,10 +78,12 @@ class ModelParser(JSONParser):
 
     def parse(self, method, payload):
         try:
 
     def parse(self, method, payload):
         try:
-            if method.payload_type is None: return
+            if method.payload_type is None:
+                return
             model = getattr(self.model_factory, method.payload_type)
         except AttributeError:
             model = getattr(self.model_factory, method.payload_type)
         except AttributeError:
-            raise TweepError('No model for this payload type: %s' % method.payload_type)
+            raise TweepError('No model for this payload type: '
+                             '%s' % method.payload_type)
 
         json = JSONParser.parse(self, method, payload)
         if isinstance(json, tuple):
 
         json = JSONParser.parse(self, method, payload)
         if isinstance(json, tuple):
@@ -94,4 +100,3 @@ class ModelParser(JSONParser):
             return result, cursors
         else:
             return result
             return result, cursors
         else:
             return result
-
index be233ffe0ef158bf7fa732976b2abfe6c18285f9..faf42eacfa3724e73d2c1cbead877e53dba00deb 100644 (file)
@@ -2,18 +2,25 @@
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2009-2010 Joshua Roesslein
 # See LICENSE for details.
 
+# Appengine users: https://developers.google.com/appengine/docs/python/sockets/#making_httplib_use_sockets
+
+from __future__ import absolute_import, print_function
+
 import logging
 import logging
-import httplib
-from socket import timeout
+import requests
+from requests.exceptions import Timeout
 from threading import Thread
 from time import sleep
 from threading import Thread
 from time import sleep
+
+import six
+
 import ssl
 
 from tweepy.models import Status
 from tweepy.api import API
 from tweepy.error import TweepError
 
 import ssl
 
 from tweepy.models import Status
 from tweepy.api import API
 from tweepy.error import TweepError
 
-from tweepy.utils import import_simplejson, urlencode_noplus
+from tweepy.utils import import_simplejson
 json = import_simplejson()
 
 STREAM_VERSION = '1.1'
 json = import_simplejson()
 
 STREAM_VERSION = '1.1'
@@ -57,15 +64,25 @@ class StreamListener(object):
             status = Status.parse(self.api, data)
             if self.on_direct_message(status) is False:
                 return False
             status = Status.parse(self.api, data)
             if self.on_direct_message(status) is False:
                 return False
+        elif 'friends' in data:
+            if self.on_friends(data['friends']) is False:
+                return False
         elif 'limit' in data:
             if self.on_limit(data['limit']['track']) is False:
                 return False
         elif 'disconnect' in data:
             if self.on_disconnect(data['disconnect']) is False:
                 return False
         elif 'limit' in data:
             if self.on_limit(data['limit']['track']) is False:
                 return False
         elif 'disconnect' in data:
             if self.on_disconnect(data['disconnect']) is False:
                 return False
+        elif 'warning' in data:
+            if self.on_warning(data['warning']) is False:
+                return False
         else:
             logging.error("Unknown message type: " + str(raw_data))
 
         else:
             logging.error("Unknown message type: " + str(raw_data))
 
+    def keep_alive(self):
+        """Called when a keep-alive arrived"""
+        return
+
     def on_status(self, status):
         """Called when a new status arrives"""
         return
     def on_status(self, status):
         """Called when a new status arrives"""
         return
@@ -86,8 +103,15 @@ class StreamListener(object):
         """Called when a new direct message arrives"""
         return
 
         """Called when a new direct message arrives"""
         return
 
+    def on_friends(self, friends):
+        """Called when a friends list arrives.
+
+        friends is a list that contains user_id
+        """
+        return
+
     def on_limit(self, track):
     def on_limit(self, track):
-        """Called when a limitation notice arrvies"""
+        """Called when a limitation notice arrives"""
         return
 
     def on_error(self, status_code):
         return
 
     def on_error(self, status_code):
@@ -106,6 +130,51 @@ class StreamListener(object):
         """
         return
 
         """
         return
 
+    def on_warning(self, notice):
+        """Called when a disconnection warning message arrives"""
+        return
+
+
+class ReadBuffer(object):
+    """Buffer data from the response in a smarter way than httplib/requests can.
+
+    Tweets are roughly in the 2-12kb range, averaging around 3kb.
+    Requests/urllib3/httplib/socket all use socket.read, which blocks
+    until enough data is returned. On some systems (eg google appengine), socket
+    reads are quite slow. To combat this latency we can read big chunks,
+    but the blocking part means we won't get results until enough tweets
+    have arrived. That may not be a big deal for high throughput systems.
+    For low throughput systems we don't want to sacrafice latency, so we
+    use small chunks so it can read the length and the tweet in 2 read calls.
+    """
+
+    def __init__(self, stream, chunk_size):
+        self._stream = stream
+        self._buffer = u""
+        self._chunk_size = chunk_size
+
+    def read_len(self, length):
+        while not self._stream.closed:
+            if len(self._buffer) >= length:
+                return self._pop(length)
+            read_len = max(self._chunk_size, length - len(self._buffer))
+            self._buffer += self._stream.read(read_len).decode("ascii")
+
+    def read_line(self, sep='\n'):
+        start = 0
+        while not self._stream.closed:
+            loc = self._buffer.find(sep, start)
+            if loc >= 0:
+                return self._pop(loc + len(sep))
+            else:
+                start = len(self._buffer)
+            self._buffer += self._stream.read(self._chunk_size).decode("ascii")
+
+    def _pop(self, length):
+        r = self._buffer[:length]
+        self._buffer = self._buffer[length:]
+        return r
+
 
 class Stream(object):
 
 
 class Stream(object):
 
@@ -117,82 +186,99 @@ class Stream(object):
         self.running = False
         self.timeout = options.get("timeout", 300.0)
         self.retry_count = options.get("retry_count")
         self.running = False
         self.timeout = options.get("timeout", 300.0)
         self.retry_count = options.get("retry_count")
-        # values according to https://dev.twitter.com/docs/streaming-apis/connecting#Reconnecting
+        # values according to
+        # https://dev.twitter.com/docs/streaming-apis/connecting#Reconnecting
         self.retry_time_start = options.get("retry_time", 5.0)
         self.retry_420_start = options.get("retry_420", 60.0)
         self.retry_time_cap = options.get("retry_time_cap", 320.0)
         self.snooze_time_step = options.get("snooze_time", 0.25)
         self.snooze_time_cap = options.get("snooze_time_cap", 16)
         self.retry_time_start = options.get("retry_time", 5.0)
         self.retry_420_start = options.get("retry_420", 60.0)
         self.retry_time_cap = options.get("retry_time_cap", 320.0)
         self.snooze_time_step = options.get("snooze_time", 0.25)
         self.snooze_time_cap = options.get("snooze_time_cap", 16)
-        self.buffer_size = options.get("buffer_size",  1500)
-        if options.get("secure", True):
-            self.scheme = "https"
-        else:
-            self.scheme = "http"
+
+        # The default socket.read size. Default to less than half the size of
+        # a tweet so that it reads tweets with the minimal latency of 2 reads
+        # per tweet. Values higher than ~1kb will increase latency by waiting
+        # for more data to arrive but may also increase throughput by doing
+        # fewer socket read calls.
+        self.chunk_size = options.get("chunk_size",  512)
+
+        self.verify = options.get("verify", True)
 
         self.api = API()
         self.headers = options.get("headers") or {}
 
         self.api = API()
         self.headers = options.get("headers") or {}
-        self.parameters = None
+        self.new_session()
         self.body = None
         self.retry_time = self.retry_time_start
         self.snooze_time = self.snooze_time_step
 
         self.body = None
         self.retry_time = self.retry_time_start
         self.snooze_time = self.snooze_time_step
 
+    def new_session(self):
+        self.session = requests.Session()
+        self.session.headers = self.headers
+        self.session.params = None
+
     def _run(self):
         # Authenticate
     def _run(self):
         # Authenticate
-        url = "%s://%s%s" % (self.scheme, self.host, self.url)
+        url = "https://%s%s" % (self.host, self.url)
 
         # Connect and process the stream
         error_counter = 0
 
         # Connect and process the stream
         error_counter = 0
-        conn = None
+        resp = None
         exception = None
         while self.running:
         exception = None
         while self.running:
-            if self.retry_count is not None and error_counter > self.retry_count:
-                # quit if error count greater than retry count
-                break
+            if self.retry_count is not None:
+                if error_counter > self.retry_count:
+                    # quit if error count greater than retry count
+                    break
             try:
             try:
-                if self.scheme == "http":
-                    conn = httplib.HTTPConnection(self.host, timeout=self.timeout)
-                else:
-                    conn = httplib.HTTPSConnection(self.host, timeout=self.timeout)
-                self.auth.apply_auth(url, 'POST', self.headers, self.parameters)
-                conn.connect()
-                conn.request('POST', self.url, self.body, headers=self.headers)
-                resp = conn.getresponse()
-                if resp.status != 200:
-                    if self.listener.on_error(resp.status) is False:
+                auth = self.auth.apply_auth()
+                resp = self.session.request('POST',
+                                            url,
+                                            data=self.body,
+                                            timeout=self.timeout,
+                                            stream=True,
+                                            auth=auth,
+                                            verify=self.verify)
+                if resp.status_code != 200:
+                    if self.listener.on_error(resp.status_code) is False:
                         break
                     error_counter += 1
                         break
                     error_counter += 1
-                    if resp.status == 420:
-                        self.retry_time = max(self.retry_420_start, self.retry_time)
+                    if resp.status_code == 420:
+                        self.retry_time = max(self.retry_420_start,
+                                              self.retry_time)
                     sleep(self.retry_time)
                     sleep(self.retry_time)
-                    self.retry_time = min(self.retry_time * 2, self.retry_time_cap)
+                    self.retry_time = min(self.retry_time * 2,
+                                          self.retry_time_cap)
                 else:
                     error_counter = 0
                     self.retry_time = self.retry_time_start
                     self.snooze_time = self.snooze_time_step
                     self.listener.on_connect()
                     self._read_loop(resp)
                 else:
                     error_counter = 0
                     self.retry_time = self.retry_time_start
                     self.snooze_time = self.snooze_time_step
                     self.listener.on_connect()
                     self._read_loop(resp)
-            except (timeout, ssl.SSLError) as exc:
+            except (Timeout, ssl.SSLError) as exc:
+                # This is still necessary, as a SSLError can actually be
+                # thrown when using Requests
                 # If it's not time out treat it like any other exception
                 # If it's not time out treat it like any other exception
-                if isinstance(exc, ssl.SSLError) and not (exc.args and 'timed out' in str(exc.args[0])):
-                    exception = exc
-                    break
-
-                if self.listener.on_timeout() == False:
+                if isinstance(exc, ssl.SSLError):
+                    if not (exc.args and 'timed out' in str(exc.args[0])):
+                        exception = exc
+                        break
+                if self.listener.on_timeout() is False:
                     break
                 if self.running is False:
                     break
                     break
                 if self.running is False:
                     break
-                conn.close()
                 sleep(self.snooze_time)
                 self.snooze_time = min(self.snooze_time + self.snooze_time_step,
                                        self.snooze_time_cap)
                 sleep(self.snooze_time)
                 self.snooze_time = min(self.snooze_time + self.snooze_time_step,
                                        self.snooze_time_cap)
-            except Exception as exception:
-                # any other exception is fatal, so kill loop
-                break
+            # except Exception as exc:
+            #     exception = exc
+            #     # any other exception is fatal, so kill loop
+            #     break
 
         # cleanup
         self.running = False
 
         # cleanup
         self.running = False
-        if conn:
-            conn.close()
+        if resp:
+            resp.close()
+
+        self.new_session()
 
         if exception:
             # call a handler first so that the exception can be logged.
 
         if exception:
             # call a handler first so that the exception can be logged.
@@ -204,34 +290,58 @@ class Stream(object):
             self.running = False
 
     def _read_loop(self, resp):
             self.running = False
 
     def _read_loop(self, resp):
+        buf = ReadBuffer(resp.raw, self.chunk_size)
+
+        while self.running and not resp.raw.closed:
+            length = 0
+            while not resp.raw.closed:
+                line = buf.read_line().strip()
+                if not line:
+                    self.listener.keep_alive()  # keep-alive new lines are expected
+                elif line.isdigit():
+                    length = int(line)
+                    break
+                else:
+                    raise TweepError('Expecting length, unexpected value found')
 
 
-        while self.running and not resp.isclosed():
-
-            # Note: keep-alive newlines might be inserted before each length value.
-            # read until we get a digit...
-            c = '\n'
-            while c == '\n' and self.running and not resp.isclosed():
-                c = resp.read(1)
-            delimited_string = c
-
-            # read rest of delimiter length..
-            d = ''
-            while d != '\n' and self.running and not resp.isclosed():
-                d = resp.read(1)
-                delimited_string += d
-
-            # read the next twitter status object
-            if delimited_string.strip().isdigit():
-                next_status_obj = resp.read( int(delimited_string) )
+            next_status_obj = buf.read_len(length)
+            if self.running:
                 self._data(next_status_obj)
 
                 self._data(next_status_obj)
 
-        if resp.isclosed():
+            # # Note: keep-alive newlines might be inserted before each length value.
+            # # read until we get a digit...
+            # c = b'\n'
+            # for c in resp.iter_content(decode_unicode=True):
+            #     if c == b'\n':
+            #         continue
+            #     break
+            #
+            # delimited_string = c
+            #
+            # # read rest of delimiter length..
+            # d = b''
+            # for d in resp.iter_content(decode_unicode=True):
+            #     if d != b'\n':
+            #         delimited_string += d
+            #         continue
+            #     break
+            #
+            # # read the next twitter status object
+            # if delimited_string.decode('utf-8').strip().isdigit():
+            #     status_id = int(delimited_string)
+            #     next_status_obj = resp.raw.read(status_id)
+            #     if self.running:
+            #         self._data(next_status_obj.decode('utf-8'))
+
+
+        if resp.raw.closed:
             self.on_closed(resp)
 
     def _start(self, async):
         self.running = True
         if async:
             self.on_closed(resp)
 
     def _start(self, async):
         self.running = True
         if async:
-            Thread(target=self._run).start()
+            self._thread = Thread(target=self._run)
+            self._thread.start()
         else:
             self._run()
 
         else:
             self._run()
 
@@ -239,81 +349,101 @@ class Stream(object):
         """ Called when the response has been closed by Twitter """
         pass
 
         """ Called when the response has been closed by Twitter """
         pass
 
-    def userstream(self, stall_warnings=False, _with=None, replies=None,
-            track=None, locations=None, async=False, encoding='utf8'):
-        self.parameters = {'delimited': 'length'}
+    def userstream(self,
+                   stall_warnings=False,
+                   _with=None,
+                   replies=None,
+                   track=None,
+                   locations=None,
+                   async=False,
+                   encoding='utf8'):
+        self.session.params = {'delimited': 'length'}
         if self.running:
             raise TweepError('Stream object already connected!')
         if self.running:
             raise TweepError('Stream object already connected!')
-        self.url = '/%s/user.json?delimited=length' % STREAM_VERSION
-        self.host='userstream.twitter.com'
+        self.url = '/%s/user.json' % STREAM_VERSION
+        self.host = 'userstream.twitter.com'
         if stall_warnings:
         if stall_warnings:
-            self.parameters['stall_warnings'] = stall_warnings
+            self.session.params['stall_warnings'] = stall_warnings
         if _with:
         if _with:
-            self.parameters['with'] = _with
+            self.session.params['with'] = _with
         if replies:
         if replies:
-            self.parameters['replies'] = replies
+            self.session.params['replies'] = replies
         if locations and len(locations) > 0:
         if locations and len(locations) > 0:
-            assert len(locations) % 4 == 0
-            self.parameters['locations'] = ','.join(['%.2f' % l for l in locations])
+            if len(locations) % 4 != 0:
+                raise TweepError("Wrong number of locations points, "
+                                 "it has to be a multiple of 4")
+            self.session.params['locations'] = ','.join(['%.2f' % l for l in locations])
         if track:
         if track:
-            encoded_track = [s.encode(encoding) for s in track]
-            self.parameters['track'] = ','.join(encoded_track)
-        self.body = urlencode_noplus(self.parameters)
+            self.session.params['track'] = u','.join(track).encode(encoding)
+
         self._start(async)
 
     def firehose(self, count=None, async=False):
         self._start(async)
 
     def firehose(self, count=None, async=False):
-        self.parameters = {'delimited': 'length'}
+        self.session.params = {'delimited': 'length'}
         if self.running:
             raise TweepError('Stream object already connected!')
         if self.running:
             raise TweepError('Stream object already connected!')
-        self.url = '/%s/statuses/firehose.json?delimited=length' % STREAM_VERSION
+        self.url = '/%s/statuses/firehose.json' % STREAM_VERSION
         if count:
             self.url += '&count=%s' % count
         self._start(async)
 
     def retweet(self, async=False):
         if count:
             self.url += '&count=%s' % count
         self._start(async)
 
     def retweet(self, async=False):
-        self.parameters = {'delimited': 'length'}
+        self.session.params = {'delimited': 'length'}
         if self.running:
             raise TweepError('Stream object already connected!')
         if self.running:
             raise TweepError('Stream object already connected!')
-        self.url = '/%s/statuses/retweet.json?delimited=length' % STREAM_VERSION
+        self.url = '/%s/statuses/retweet.json' % STREAM_VERSION
         self._start(async)
 
         self._start(async)
 
-    def sample(self, count=None, async=False):
-        self.parameters = {'delimited': 'length'}
+    def sample(self, async=False, languages=None):
+        self.session.params = {'delimited': 'length'}
         if self.running:
             raise TweepError('Stream object already connected!')
         if self.running:
             raise TweepError('Stream object already connected!')
-        self.url = '/%s/statuses/sample.json?delimited=length' % STREAM_VERSION
-        if count:
-            self.url += '&count=%s' % count
+        self.url = '/%s/statuses/sample.json' % STREAM_VERSION
+        if languages:
+            self.session.params['language'] = ','.join(map(str, languages))
         self._start(async)
 
     def filter(self, follow=None, track=None, async=False, locations=None,
         self._start(async)
 
     def filter(self, follow=None, track=None, async=False, locations=None,
-               count=None, stall_warnings=False, languages=None, encoding='utf8'):
-        self.parameters = {}
-        self.headers['Content-type'] = "application/x-www-form-urlencoded"
+               stall_warnings=False, languages=None, encoding='utf8'):
+        self.body = {}
+        self.session.headers['Content-type'] = "application/x-www-form-urlencoded"
         if self.running:
             raise TweepError('Stream object already connected!')
         if self.running:
             raise TweepError('Stream object already connected!')
-        self.url = '/%s/statuses/filter.json?delimited=length' % STREAM_VERSION
+        self.url = '/%s/statuses/filter.json' % STREAM_VERSION
         if follow:
         if follow:
-            encoded_follow = [s.encode(encoding) for s in follow]
-            self.parameters['follow'] = ','.join(encoded_follow)
+            self.body['follow'] = u','.join(follow).encode(encoding)
         if track:
         if track:
-            encoded_track = [s.encode(encoding) for s in track]
-            self.parameters['track'] = ','.join(encoded_track)
+            self.body['track'] = u','.join(track).encode(encoding)
         if locations and len(locations) > 0:
         if locations and len(locations) > 0:
-            assert len(locations) % 4 == 0
-            self.parameters['locations'] = ','.join(['%.4f' % l for l in locations])
-        if count:
-            self.parameters['count'] = count
+            if len(locations) % 4 != 0:
+                raise TweepError("Wrong number of locations points, "
+                                 "it has to be a multiple of 4")
+            self.body['locations'] = u','.join(['%.4f' % l for l in locations])
         if stall_warnings:
         if stall_warnings:
-            self.parameters['stall_warnings'] = stall_warnings
+            self.body['stall_warnings'] = stall_warnings
         if languages:
         if languages:
-            self.parameters['language'] = ','.join(map(str, languages))
-        self.body = urlencode_noplus(self.parameters)
-        self.parameters['delimited'] = 'length'
+            self.body['language'] = u','.join(map(str, languages))
+        self.session.params = {'delimited': 'length'}
+        self.host = 'stream.twitter.com'
+        self._start(async)
+
+    def sitestream(self, follow, stall_warnings=False,
+                   with_='user', replies=False, async=False):
+        self.body = {}
+        if self.running:
+            raise TweepError('Stream object already connected!')
+        self.url = '/%s/site.json' % STREAM_VERSION
+        self.body['follow'] = u','.join(map(six.text_type, follow))
+        self.body['delimited'] = 'length'
+        if stall_warnings:
+            self.body['stall_warnings'] = stall_warnings
+        if with_:
+            self.body['with'] = with_
+        if replies:
+            self.body['replies'] = replies
         self._start(async)
 
     def disconnect(self):
         if self.running is False:
             return
         self.running = False
         self._start(async)
 
     def disconnect(self):
         if self.running is False:
             return
         self.running = False
-
index e5d2a5ed917e41223881c24b69c330cd96f7a67a..36d340251986e029b11479140c4db6cfa40ea07f 100644 (file)
@@ -2,11 +2,13 @@
 # Copyright 2010 Joshua Roesslein
 # See LICENSE for details.
 
 # Copyright 2010 Joshua Roesslein
 # See LICENSE for details.
 
+from __future__ import print_function
+
 from datetime import datetime
 from datetime import datetime
-import time
-import re
-import locale
-from urllib import quote
+
+import six
+from six.moves.urllib.parse import quote
+
 from email.utils import parsedate
 
 
 from email.utils import parsedate
 
 
@@ -28,14 +30,13 @@ def parse_a_href(atag):
 
 def convert_to_utf8_str(arg):
     # written by Michael Norton (http://docondev.blogspot.com/)
 
 def convert_to_utf8_str(arg):
     # written by Michael Norton (http://docondev.blogspot.com/)
-    if isinstance(arg, unicode):
+    if isinstance(arg, six.text_type):
         arg = arg.encode('utf-8')
         arg = arg.encode('utf-8')
-    elif not isinstance(arg, str):
-        arg = str(arg)
+    elif not isinstance(arg, bytes):
+        arg = six.text_type(arg).encode('utf-8')
     return arg
 
 
     return arg
 
 
-
 def import_simplejson():
     try:
         import simplejson as json
 def import_simplejson():
     try:
         import simplejson as json
@@ -44,16 +45,14 @@ def import_simplejson():
             import json  # Python 2.6+
         except ImportError:
             try:
             import json  # Python 2.6+
         except ImportError:
             try:
-                from django.utils import simplejson as json  # Google App Engine
+                # Google App Engine
+                from django.utils import simplejson as json
             except ImportError:
                 raise ImportError("Can't load a json library")
 
     return json
 
             except ImportError:
                 raise ImportError("Can't load a json library")
 
     return json
 
+
 def list_to_csv(item_list):
     if item_list:
         return ','.join([str(i) for i in item_list])
 def list_to_csv(item_list):
     if item_list:
         return ','.join([str(i) for i in item_list])
-
-def urlencode_noplus(query):
-    return '&'.join(['%s=%s' % (quote(str(k), ''), quote(str(v), '')) \
-        for k, v in query.iteritems()])
index 734a59f9f16da4c3d6b78f0691cf266283bd809c..2ed14032c52549f93e80ee6aa284ddd6e7d5312a 100644 (file)
@@ -5,7 +5,7 @@ from twitter_authentication import CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN,
 auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
 auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
 
 auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
 auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
 
-api = tweepy.API(auth, parser=tweepy.parsers.RawParser)
+api = tweepy.API(auth, parser=tweepy.parsers.RawParser())
 
 @classmethod                    
 def parse(cls, api, raw):
 
 @classmethod                    
 def parse(cls, api, raw):
@@ -18,10 +18,10 @@ tweepy.models.Status.parse = parse
 
 class StreamListener(tweepy.StreamListener):
     def on_status(self, tweet):
 
 class StreamListener(tweepy.StreamListener):
     def on_status(self, tweet):
-        print tweet.json
+        print(tweet.json)
 
     def on_error(self, status_code):
 
     def on_error(self, status_code):
-        print 'Error: ' + repr(status_code)
+        print('Error: ' + repr(status_code))
         return False
 
 l = StreamListener()
         return False
 
 l = StreamListener()
index 06f7be07d3046b523e9ff79b789aef17cfacbb00..87c548c0093d61c9435b0957f5a54f7307a3fe27 100644 (file)
@@ -8,10 +8,10 @@ api = tweepy.API(auth)
 
 class StreamListener(tweepy.StreamListener):
     def on_status(self, tweet):
 
 class StreamListener(tweepy.StreamListener):
     def on_status(self, tweet):
-        print tweet.user.screen_name + "\t" + tweet.text
+        print(tweet.author.screen_name + "\t" + tweet.text)
 
     def on_error(self, status_code):
 
     def on_error(self, status_code):
-        print 'Error: ' + repr(status_code)
+        print('Error: ' + repr(status_code))
         return False
 
 l = StreamListener()
         return False
 
 l = StreamListener()
index ce905ead89cc63880f680b6e17101ccbe830a201..8e1eee1c44f5e32f59eb194fd224fb1be3e65890 100644 (file)
@@ -8,10 +8,10 @@ api = tweepy.API(auth)
 
 class StreamListener(tweepy.StreamListener):
     def on_status(self, tweet):
 
 class StreamListener(tweepy.StreamListener):
     def on_status(self, tweet):
-        print tweet.user.screen_name + "\t" + tweet.text
+        print(tweet.author.screen_name + "\t" + tweet.text)
 
     def on_error(self, status_code):
 
     def on_error(self, status_code):
-        print 'Error: ' + repr(status_code)
+        print( 'Error: ' + repr(status_code))
         return False
 
 l = StreamListener()
         return False
 
 l = StreamListener()
index cee5967f12fc26fb1b4bb479017188258ed5b29c..c1ec98fc81dacd04942db5ae7dc88e5fcd660965 100644 (file)
@@ -9,4 +9,4 @@ api = tweepy.API(auth)
 public_tweets = api.home_timeline(count=100)
 
 for tweet in public_tweets:
 public_tweets = api.home_timeline(count=100)
 
 for tweet in public_tweets:
-    print tweet.text
+    print(tweet.text)
index b00b0f76f93925aaa0c2e83870a9d77c185a7f32..96a86fc764fede2a122059576f27c229faee1d51 100644 (file)
@@ -8,10 +8,10 @@ api = tweepy.API(auth)
 
 user = api.get_user('makoshark')
 
 
 user = api.get_user('makoshark')
 
-print user.screen_name + " has " + str(user.followers_count) + " followers."
+print(user.screen_name + " has " + str(user.followers_count) + " followers.")
 
 
-print "They include these 100 people:"
+print("They include these 100 people:")
 
 for friend in user.friends(count=100):
 
 for friend in user.friends(count=100):
-   print friend.screen_name
+   print(friend.screen_name)
 
 
index 897b7b322c7d484b5ea301ebf32eacd2b22d9ba8..d9b7ab7e844d99085a076056921a8c72387821a3 100644 (file)
@@ -9,4 +9,4 @@ api = tweepy.API(auth)
 public_tweets = api.search("data science", count=20)
 
 for tweet in public_tweets:
 public_tweets = api.search("data science", count=20)
 
 for tweet in public_tweets:
-    print tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text
+    print(tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text)
index 26d402778f77a96f3d6bbbe8cc1bb342904204e1..2bf35a64440283c206065c098e884d689c0cf346 100644 (file)
@@ -13,5 +13,5 @@ output_file = codecs.open("MY_DATA.tsv", "w", "utf-8")
 public_tweets = api.search("data science", count=10)
 
 for tweet in public_tweets:
 public_tweets = api.search("data science", count=10)
 
 for tweet in public_tweets:
-    print >>output_file, tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text
+    print(tweet.user.screen_name + "\t" + str(tweet.created_at) + "\t" + tweet.text, file=output_file)
 
 

Benjamin Mako Hill || Want to submit a patch?