Adding credentials_from_code and credentials_from_clientsecrets_and_code.

Reviewed in http://codereview.appspot.com/6188069/.
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 3376806..5d92b86 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -881,6 +881,78 @@
 
   return simplejson.loads(_urlsafe_b64decode(segments[1]))
 
+def credentials_from_code(client_id, client_secret, scope, code,
+                        redirect_uri = 'postmessage',
+                        http=None, user_agent=None,
+                        token_uri='https://accounts.google.com/o/oauth2/token'):
+  """Exchanges an authorization code for an OAuth2Credentials object.
+
+  Args:
+    client_id: string, client identifier.
+    client_secret: string, client secret.
+    scope: string or list of strings, scope(s) to request.
+    code: string, An authroization code, most likely passed down from
+      the client
+    redirect_uri: string, this is generally set to 'postmessage' to match the
+      redirect_uri that the client specified
+    http: httplib2.Http, optional http instance to use to do the fetch
+    token_uri: string, URI for token endpoint. For convenience
+      defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+  Returns:
+    An OAuth2Credentials object.
+
+  Raises:
+    FlowExchangeError if the authorization code cannot be exchanged for an
+     access token
+  """
+  flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
+                             'https://accounts.google.com/o/oauth2/auth',
+                             token_uri)
+
+  # We primarily make this call to set up the redirect_uri in the flow object
+  uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri)
+  credentials = flow.step2_exchange(code, http)
+  return credentials
+
+
+def credentials_from_clientsecrets_and_code(filename, scope, code,
+                                            message = None,
+                                            redirect_uri = 'postmessage',
+                                            http=None):
+  """Returns OAuth2Credentials from a clientsecrets file and an auth code.
+
+  Will create the right kind of Flow based on the contents of the clientsecrets
+  file or will raise InvalidClientSecretsError for unknown types of Flows.
+
+  Args:
+    filename: string, File name of clientsecrets.
+    scope: string or list of strings, scope(s) to request.
+    code: string, An authroization code, most likely passed down from
+      the client
+    message: string, A friendly string to display to the user if the
+      clientsecrets file is missing or invalid. If message is provided then
+      sys.exit will be called in the case of an error. If message in not
+      provided then clientsecrets.InvalidClientSecretsError will be raised.
+    redirect_uri: string, this is generally set to 'postmessage' to match the
+      redirect_uri that the client specified
+    http: httplib2.Http, optional http instance to use to do the fetch
+
+  Returns:
+    An OAuth2Credentials object.
+
+  Raises:
+    FlowExchangeError if the authorization code cannot be exchanged for an
+     access token
+    UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
+    clientsecrets.InvalidClientSecretsError if the clientsecrets file is
+      invalid.
+  """
+  flow = flow_from_clientsecrets(filename, scope, message)
+  # We primarily make this call to set up the redirect_uri in the flow object
+  uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri)
+  credentials = flow.step2_exchange(code, http)
+  return credentials
+
 
 class OAuth2WebServerFlow(Flow):
   """Does the Web Server Flow for OAuth 2.0.
diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py
index 95ca066..cb1b4b3 100644
--- a/tests/test_oauth2client.py
+++ b/tests/test_oauth2client.py
@@ -25,6 +25,7 @@
 import base64
 import datetime
 import httplib2
+import os
 import unittest
 import urlparse
 
@@ -47,6 +48,13 @@
 from oauth2client.client import OOB_CALLBACK_URN
 from oauth2client.client import VerifyJwtTokenError
 from oauth2client.client import _extract_id_token
+from oauth2client.client import credentials_from_code
+from oauth2client.client import credentials_from_clientsecrets_and_code
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+
+def datafile(filename):
+  return os.path.join(DATA_DIR, filename)
 
 
 class CredentialsTests(unittest.TestCase):
@@ -296,6 +304,66 @@
     credentials = self.flow.step2_exchange('some random code', http)
     self.assertEqual(credentials.id_token, body)
 
+class CredentialsFromCodeTests(unittest.TestCase):
+  def setUp(self):
+    self.client_id = 'client_id_abc'
+    self.client_secret = 'secret_use_code'
+    self.scope = 'foo'
+    self.code = '12345abcde'
+    self.redirect_uri = 'postmessage'
+
+  def test_exchange_code_for_token(self):
+    http = HttpMockSequence([
+      ({'status': '200'},
+      """{ "access_token":"asdfghjkl",
+       "expires_in":3600 }"""),
+    ])
+    credentials = credentials_from_code(self.client_id, self.client_secret,
+                                    self.scope, self.code, self.redirect_uri,
+                                    http)
+    self.assertEquals(credentials.access_token, 'asdfghjkl')
+    self.assertNotEqual(None, credentials.token_expiry)
+
+  def test_exchange_code_for_token_fail(self):
+    http = HttpMockSequence([
+      ({'status': '400'}, '{"error":"invalid_request"}'),
+      ])
+
+    try:
+      credentials = credentials_from_code(self.client_id, self.client_secret,
+                                      self.scope, self.code, self.redirect_uri,
+                                      http)
+      self.fail("should raise exception if exchange doesn't get 200")
+    except FlowExchangeError:
+      pass
+
+
+  def test_exchange_code_and_file_for_token(self):
+    http = HttpMockSequence([
+      ({'status': '200'},
+      """{ "access_token":"asdfghjkl",
+       "expires_in":3600 }"""),
+    ])
+    credentials = credentials_from_clientsecrets_and_code(
+                            datafile('client_secrets.json'), self.scope,
+                            self.code, http=http)
+    self.assertEquals(credentials.access_token, 'asdfghjkl')
+    self.assertNotEqual(None, credentials.token_expiry)
+
+  def test_exchange_code_and_file_for_token_fail(self):
+    http = HttpMockSequence([
+      ({'status': '400'}, '{"error":"invalid_request"}'),
+      ])
+
+    try:
+      credentials = credentials_from_clientsecrets_and_code(
+                            datafile('client_secrets.json'), self.scope,
+                            self.code, http=http)
+      self.fail("should raise exception if exchange doesn't get 200")
+    except FlowExchangeError:
+      pass
+
+
 
 class MemoryCacheTests(unittest.TestCase):