diff --git a/apiclient/discovery.py b/apiclient/discovery.py index 5f3c267..2a924a2 100644 --- a/apiclient/discovery.py +++ b/apiclient/discovery.py @@ -59,11 +59,12 @@ DEFAULT_METHOD_DOC = 'A description of how to use this function' STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp', 'userip', 'strict'] -RESERVED_WORDS = [ 'and', 'assert', 'break', 'class', 'continue', 'def', 'del', +RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while' ] + def _fix_method_name(name): if name in RESERVED_WORDS: return name + '_' @@ -242,10 +243,10 @@ def _cast(value, schema_type): return str(value) MULTIPLIERS = { - "KB": 2**10, - "MB": 2**20, - "GB": 2**30, - "TB": 2**40, + "KB": 2 ** 10, + "MB": 2 ** 20, + "GB": 2 ** 30, + "TB": 2 ** 40, } def _media_size_to_long(maxSize): @@ -255,7 +256,7 @@ def _media_size_to_long(maxSize): units = maxSize[-2:].upper() multiplier = MULTIPLIERS.get(units, 0) if multiplier: - return int(maxSize[:-2])*multiplier + return int(maxSize[:-2]) * multiplier else: return int(maxSize) diff --git a/apiclient/model.py b/apiclient/model.py index 7f51d29..b8271f9 100644 --- a/apiclient/model.py +++ b/apiclient/model.py @@ -222,7 +222,8 @@ class BaseModel(Model): _abstract() def deserialize(self, content): - """Perform the actual deserialization from response string to Python object. + """Perform the actual deserialization from response string to Python + object. Args: content: string, the body of the HTTP response @@ -285,8 +286,8 @@ class ProtocolBufferModel(BaseModel): de-serialized using the given protocol buffer class. Args: - protocol_buffer: The protocol buffer class used to de-serialize a response - from the API. + protocol_buffer: The protocol buffer class used to de-serialize a + response from the API. """ self._protocol_buffer = protocol_buffer diff --git a/apiclient/oauth.py b/apiclient/oauth.py index 18877b0..11eb680 100644 --- a/apiclient/oauth.py +++ b/apiclient/oauth.py @@ -377,7 +377,6 @@ class TwoLeggedOAuthCredentials(Credentials): return http - class FlowThreeLegged(Flow): """Does the Three Legged Dance for OAuth 1.0a. """ diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py index 64fd3ac..2811069 100644 --- a/oauth2client/appengine.py +++ b/oauth2client/appengine.py @@ -95,6 +95,16 @@ class AppAssertionCredentials(AssertionCredentials): None, token_uri) + @classmethod + def from_json(cls, json): + data = simplejson.loads(json) + retval = AccessTokenCredentials( + data['scope'], + data['audience'], + data['assertion_type'], + data['token_uri']) + return retval + def _generate_assertion(self): header = { 'typ': 'JWT', @@ -165,17 +175,28 @@ class CredentialsProperty(db.Property): def get_value_for_datastore(self, model_instance): cred = super(CredentialsProperty, self).get_value_for_datastore(model_instance) - return db.Blob(pickle.dumps(cred)) + if cred is None: + cred = '' + else: + cred = cred.to_json() + return db.Blob(cred) # For reading from datastore. def make_value_from_datastore(self, value): if value is None: return None - return pickle.loads(value) + if len(value) == 0: + return None + credentials = None + try: + credentials = Credentials.new_from_json(value) + except ValueError: + credentials = pickle.loads(value) + return credentials def validate(self, value): if value is not None and not isinstance(value, Credentials): - raise BadValueError('Property %s must be convertible ' + raise db.BadValueError('Property %s must be convertible ' 'to an Credentials instance (%s)' % (self.name, value)) return super(CredentialsProperty, self).validate(value) @@ -215,15 +236,15 @@ class StorageByKeyName(Storage): oauth2client.Credentials """ if self._cache: - credential = self._cache.get(self._key_name) - if credential: - return pickle.loads(credential) + json = self._cache.get(self._key_name) + if json: + return Credentials.new_from_json(json) entity = self._model.get_or_insert(self._key_name) credential = getattr(entity, self._property_name) if credential and hasattr(credential, 'set_store'): credential.set_store(self) if self._cache: - self._cache.set(self._key_name, pickle.dumps(credentials)) + self._cache.set(self._key_name, credentials.to_json()) return credential @@ -237,7 +258,7 @@ class StorageByKeyName(Storage): setattr(entity, self._property_name, credentials) entity.put() if self._cache: - self._cache.set(self._key_name, pickle.dumps(credentials)) + self._cache.set(self._key_name, credentials.to_json()) class CredentialsModel(db.Model): diff --git a/oauth2client/client.py b/oauth2client/client.py index 52f6fb3..2b97d4d 100644 --- a/oauth2client/client.py +++ b/oauth2client/client.py @@ -43,6 +43,9 @@ except ImportError: logger = logging.getLogger(__name__) +# Expiry is stored in RFC3339 UTC format +EXPIRY_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" + class Error(Exception): """Base error for this module.""" @@ -71,10 +74,15 @@ def _abstract(): class Credentials(object): """Base class for all Credentials objects. - Subclasses must define an authorize() method - that applies the credentials to an HTTP transport. + Subclasses must define an authorize() method that applies the credentials to + an HTTP transport. + + Subclasses must also specify a classmethod named 'from_json' that takes a JSON + string as input and returns an instaniated Crentials object. """ + NON_SERIALIZED_MEMBERS = ['store'] + def authorize(self, http): """Take an httplib2.Http instance (or equivalent) and authorizes it for the set of credentials, usually by @@ -84,6 +92,58 @@ class Credentials(object): """ _abstract() + def _to_json(self, strip): + """Utility function for creating a JSON representation of an instance of Credentials. + + Args: + strip: array, An array of names of members to not include in the JSON. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + t = type(self) + d = copy.copy(self.__dict__) + for member in strip: + del d[member] + if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): + d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) + # Add in information we will need later to reconsistitue this instance. + d['_class'] = t.__name__ + d['_module'] = t.__module__ + return simplejson.dumps(d) + + def to_json(self): + """Creating a JSON representation of an instance of Credentials. + + Returns: + string, a JSON representation of this instance, suitable to pass to + from_json(). + """ + return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) + + @classmethod + def new_from_json(cls, s): + """Utility class method to instantiate a Credentials subclass from a JSON + representation produced by to_json(). + + Args: + s: string, JSON from to_json(). + + Returns: + An instance of the subclass of Credentials that was serialized with + to_json(). + """ + data = simplejson.loads(s) + # Find and call the right classmethod from_json() to restore the object. + module = data['_module'] + m = __import__(module) + for sub_module in module.split('.')[1:]: + m = getattr(m, sub_module) + kls = getattr(m, data['_class']) + from_json = getattr(kls, 'from_json') + return from_json(s) + class Flow(object): """Base class for all Flow objects.""" @@ -206,6 +266,36 @@ class OAuth2Credentials(Credentials): # refreshed. self.invalid = False + def to_json(self): + return self._to_json(Credentials.NON_SERIALIZED_MEMBERS) + + @classmethod + def from_json(cls, s): + """Instantiate a Credentials object from a JSON description of it. The JSON + should have been produced by calling .to_json() on the object. + + Args: + data: dict, A deserialized JSON object. + + Returns: + An instance of a Credentials subclass. + """ + data = simplejson.loads(s) + if 'token_expiry' in data and not isinstance(data['token_expiry'], + datetime.datetime): + data['token_expiry'] = datetime.datetime.strptime( + data['token_expiry'], EXPIRY_FORMAT) + retval = OAuth2Credentials( + data['access_token'], + data['client_id'], + data['client_secret'], + data['refresh_token'], + data['token_expiry'], + data['token_uri'], + data['user_agent']) + retval.invalid = data['invalid'] + return retval + @property def access_token_expired(self): """True if the credential is expired or invalid. @@ -218,7 +308,7 @@ class OAuth2Credentials(Credentials): if not self.token_expiry: return False - now = datetime.datetime.now() + now = datetime.datetime.utcnow() if now >= self.token_expiry: logger.info('access_token is expired. Now: %s, token_expiry: %s', now, self.token_expiry) @@ -318,7 +408,7 @@ class OAuth2Credentials(Credentials): self.refresh_token = d.get('refresh_token', self.refresh_token) if 'expires_in' in d: self.token_expiry = datetime.timedelta( - seconds=int(d['expires_in'])) + datetime.datetime.now() + seconds=int(d['expires_in'])) + datetime.datetime.utcnow() else: self.token_expiry = None if self.store: @@ -446,6 +536,15 @@ class AccessTokenCredentials(OAuth2Credentials): None, user_agent) + + @classmethod + def from_json(cls, s): + data = simplejson.loads(s) + retval = AccessTokenCredentials( + data['access_token'], + data['user_agent']) + return retval + def _refresh(self, http_request): raise AccessTokenCredentialsError( "The access_token is expired or invalid and can't be refreshed.") @@ -601,7 +700,7 @@ class OAuth2WebServerFlow(Flow): refresh_token = d.get('refresh_token', None) token_expiry = None if 'expires_in' in d: - token_expiry = datetime.datetime.now() + datetime.timedelta( + token_expiry = datetime.datetime.utcnow() + datetime.timedelta( seconds=int(d['expires_in'])) logger.info('Successfully retrieved access token: %s' % content) diff --git a/oauth2client/file.py b/oauth2client/file.py index b7f9c7d..89140b8 100644 --- a/oauth2client/file.py +++ b/oauth2client/file.py @@ -23,7 +23,20 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import pickle import threading + +try: # pragma: no cover + import simplejson +except ImportError: # pragma: no cover + try: + # Try to import from django, should work on App Engine + from django.utils import simplejson + except ImportError: + # Should work for Python2.6 and higher. + import json as simplejson + + from client import Storage as BaseStorage +from client import Credentials class Storage(BaseStorage): @@ -40,25 +53,40 @@ class Storage(BaseStorage): oauth2client.client.Credentials """ self._lock.acquire() + credentials = None try: f = open(self._filename, 'r') - credentials = pickle.loads(f.read()) + content = f.read() f.close() + except IOError: + self._lock.release() + return credentials + + # First try reading as JSON, and if that fails fall back to pickle. + try: + credentials = Credentials.new_from_json(content) credentials.set_store(self) - except: - credentials = None - self._lock.release() + except ValueError: + # TODO(jcgregorio) On a future release remove this path to finally remove + # all pickle support. + try: + credentials = pickle.loads(content) + credentials.set_store(self) + except: + pass + finally: + self._lock.release() return credentials def put(self, credentials): - """Write a pickled Credentials to file. + """Write Credentials to file. Args: credentials: Credentials, the credentials to store. """ self._lock.acquire() f = open(self._filename, 'w') - f.write(pickle.dumps(credentials)) + f.write(credentials.to_json()) f.close() self._lock.release() diff --git a/oauth2client/multistore_file.py b/oauth2client/multistore_file.py index 8841194..e3e3f6d 100644 --- a/oauth2client/multistore_file.py +++ b/oauth2client/multistore_file.py @@ -21,7 +21,9 @@ The format of the stored data is like so: 'userAgent': '', 'scope': '' }, - 'credential': '' + 'credential': { + # JSON serialized Credentials. + } } ] } @@ -47,6 +49,7 @@ except ImportError: # pragma: no cover import json as simplejson from client import Storage as BaseStorage +from client import Credentials logger = logging.getLogger(__name__) @@ -295,7 +298,8 @@ class _MultiStore(object): user_agent = raw_key['userAgent'] scope = raw_key['scope'] key = (client_id, user_agent, scope) - credential = pickle.loads(base64.b64decode(cred_entry['credential'])) + credential = None + credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential'])) return (key, credential) def _write(self): @@ -312,7 +316,7 @@ class _MultiStore(object): 'userAgent': cred_key[1], 'scope': cred_key[2] } - raw_cred = base64.b64encode(pickle.dumps(cred)) + raw_cred = simplejson.loads(cred.to_json()) raw_creds.append({'key': raw_key, 'credential': raw_cred}) self._locked_json_write(raw_data) @@ -330,6 +334,7 @@ class _MultiStore(object): The credential specified or None if not present """ key = (client_id, user_agent, scope) + return self._data.get(key, None) def _update_credential(self, cred, scope): diff --git a/oauth2client/tools.py b/oauth2client/tools.py index dc779b4..574a747 100644 --- a/oauth2client/tools.py +++ b/oauth2client/tools.py @@ -129,12 +129,16 @@ def run(flow, storage): print '--noauth_local_webserver.' print + code = None if FLAGS.auth_local_webserver: httpd.handle_request() if 'error' in httpd.query_params: sys.exit('Authentication request was rejected.') if 'code' in httpd.query_params: code = httpd.query_params['code'] + else: + print 'Failed to find "code" in the query parameters of the redirect.' + sys.exit('Try running with --noauth_local_webserver.') else: code = raw_input('Enter verification code: ').strip() diff --git a/samples/appengine_with_decorator2/main.py b/samples/appengine_with_decorator2/main.py index 03963f4..4cd2401 100644 --- a/samples/appengine_with_decorator2/main.py +++ b/samples/appengine_with_decorator2/main.py @@ -43,8 +43,7 @@ from google.appengine.ext.webapp.util import run_wsgi_app decorator = OAuth2Decorator( client_id='837647042410-75ifgipj95q4agpm0cs452mg7i2pn17c.apps.googleusercontent.com', client_secret='QhxYsjM__u4vy5N0DXUFRwwI', - scope='https://www.googleapis.com/auth/buzz', - user_agent='my-sample-app/1.0') + scope='https://www.googleapis.com/auth/buzz') http = httplib2.Http(memcache) service = build("buzz", "v1", http=http) diff --git a/samples/buzz/buzz.py b/samples/buzz/buzz.py index 2fb4e85..8906048 100644 --- a/samples/buzz/buzz.py +++ b/samples/buzz/buzz.py @@ -88,6 +88,7 @@ def main(argv): # Credentials will get written back to a file. storage = Storage('buzz.dat') credentials = storage.get() + if credentials is None or credentials.invalid: credentials = run(FLOW, storage) @@ -112,31 +113,10 @@ def main(argv): activitylist = activities.list_next(activitylist).execute() print "Retrieved the next two activities" - # Add a new activity - new_activity_body = { - 'title': 'Testing insert', - 'object': { - 'content': - u'Just a short note to show that insert is working. ☄', - 'type': 'note'} - } - activity = activities.insert(userId='@me', body=new_activity_body).execute() - print "Added a new activity" - - activitylist = activities.list( - max_results='2', scope='@self', userId='@me').execute() - - # Add a comment to that activity - comment_body = { - "content": "This is a comment" - } - item = activitylist['items'][0] - comment = service.comments().insert( - userId=item['actor']['id'], postId=item['id'], body=comment_body - ).execute() - print 'Added a comment to the new activity' - pprint.pprint(comment) - + # List the number of followers + followers = service.people().list( + userId='@me', groupId='@followers').execute(http) + print 'Hello, you have %s followers!' % followers['totalResults'] except AccessTokenRefreshError: print ("The credentials have been revoked or expired, please re-run" diff --git a/samples/moderator/moderator.py b/samples/moderator/moderator.py index b7da058..e92ef1b 100644 --- a/samples/moderator/moderator.py +++ b/samples/moderator/moderator.py @@ -140,8 +140,6 @@ def main(argv): body=vote_body) print "Voted on the submission" - - except AccessTokenRefreshError: print ("The credentials have been revoked or expired, please re-run" "the application to re-authorize") diff --git a/samples/prediction/prediction.py b/samples/prediction/prediction.py index 3ffa974..1227019 100644 --- a/samples/prediction/prediction.py +++ b/samples/prediction/prediction.py @@ -109,7 +109,7 @@ def main(argv): # Start training on a data set train = service.training() - body = {'id' : FLAGS.object_name} + body = {'id': FLAGS.object_name} start = train.insert(body=body).execute() print 'Started training' diff --git a/samples/src/moderator.py b/samples/src/moderator.py index 1ab9ea8..e27b811 100644 --- a/samples/src/moderator.py +++ b/samples/src/moderator.py @@ -43,4 +43,3 @@ submissionId=submission['id']['submissionId'], body=vote_body) print "Voted on the submission" - diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py index de11810..14beb97 100644 --- a/tests/test_oauth2client.py +++ b/tests/test_oauth2client.py @@ -22,6 +22,7 @@ Unit tests for oauth2client. __author__ = 'jcgregorio@google.com (Joe Gregorio)' +import datetime import httplib2 import unittest import urlparse @@ -31,6 +32,16 @@ try: except ImportError: from cgi import parse_qs +try: # pragma: no cover + import simplejson +except ImportError: # pragma: no cover + try: + # Try to import from django, should work on App Engine + from django.utils import simplejson + except ImportError: + # Should work for Python2.6 and higher. + import json as simplejson + from apiclient.http import HttpMockSequence from oauth2client.client import AccessTokenCredentials from oauth2client.client import AccessTokenCredentialsError @@ -48,7 +59,7 @@ class OAuth2CredentialsTests(unittest.TestCase): client_id = "some_client_id" client_secret = "cOuDdkfjxxnv+" refresh_token = "1/0/a.df219fjls0" - token_expiry = "ignored" + token_expiry = datetime.datetime.utcnow() token_uri = "https://www.google.com/accounts/o8/oauth2/token" user_agent = "refresh_checker/1.0" self.credentials = OAuth2Credentials( @@ -86,6 +97,12 @@ class OAuth2CredentialsTests(unittest.TestCase): resp, content = http.request("http://example.com") self.assertEqual(400, resp.status) + def test_to_from_json(self): + json = self.credentials.to_json() + instance = OAuth2Credentials.from_json(json) + self.assertEquals(type(instance), OAuth2Credentials) + self.assertEquals(self.credentials.__dict__, instance.__dict__) + class AccessTokenCredentialsTests(unittest.TestCase): diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py index f9b5094..9362220 100644 --- a/tests/test_oauth2client_appengine.py +++ b/tests/test_oauth2client_appengine.py @@ -173,7 +173,7 @@ class DecoratorTests(unittest.TestCase): self.assertEqual('code', q['response_type'][0]) self.assertEqual(False, self.decorator.has_credentials()) - # Now simulate the callback to /oauth2callback + # Now simulate the callback to /oauth2callback. response = self.app.get('/oauth2callback', { 'code': 'foo_access_code', 'state': 'foo_path', @@ -181,7 +181,7 @@ class DecoratorTests(unittest.TestCase): self.assertEqual('http://localhost/foo_path', response.headers['Location']) self.assertEqual(None, self.decorator.credentials) - # Now requesting the decorated path should work + # Now requesting the decorated path should work. response = self.app.get('/foo_path') self.assertEqual('200 OK', response.status) self.assertEqual(True, self.decorator.has_credentials()) @@ -190,18 +190,18 @@ class DecoratorTests(unittest.TestCase): self.assertEqual('foo_access_token', self.decorator.credentials.access_token) - # Invalidate the stored Credentials + # Invalidate the stored Credentials. self.decorator.credentials.invalid = True self.decorator.credentials.store.put(self.decorator.credentials) - # Invalid Credentials should start the OAuth dance again + # Invalid Credentials should start the OAuth dance again. response = self.app.get('/foo_path') self.assertTrue(response.status.startswith('302')) q = parse_qs(response.headers['Location'].split('?', 1)[1]) self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0]) def test_aware(self): - # An initial request to an oauth_aware decorated path should not redirect + # An initial request to an oauth_aware decorated path should not redirect. response = self.app.get('/bar_path') self.assertEqual('Hello World!', response.body) self.assertEqual('200 OK', response.status) @@ -214,7 +214,7 @@ class DecoratorTests(unittest.TestCase): self.assertEqual('http://localhost/bar_path', q['state'][0]) self.assertEqual('code', q['response_type'][0]) - # Now simulate the callback to /oauth2callback + # Now simulate the callback to /oauth2callback. url = self.decorator.authorize_url() response = self.app.get('/oauth2callback', { 'code': 'foo_access_code', @@ -223,7 +223,7 @@ class DecoratorTests(unittest.TestCase): self.assertEqual('http://localhost/bar_path', response.headers['Location']) self.assertEqual(False, self.decorator.has_credentials()) - # Now requesting the decorated path will have credentials + # Now requesting the decorated path will have credentials. response = self.app.get('/bar_path') self.assertEqual('200 OK', response.status) self.assertEqual('Hello World!', response.body) diff --git a/tests/test_oauth2client_file.py b/tests/test_oauth2client_file.py new file mode 100644 index 0000000..05deaa0 --- /dev/null +++ b/tests/test_oauth2client_file.py @@ -0,0 +1,157 @@ +#!/usr/bin/python2.4 +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Oauth2client.file tests + +Unit tests for oauth2client.file +""" + +__author__ = 'jcgregorio@google.com (Joe Gregorio)' + +import os +import pickle +import unittest +import datetime + + +try: # pragma: no cover + import simplejson +except ImportError: # pragma: no cover + try: + # Try to import from django, should work on App Engine + from django.utils import simplejson + except ImportError: + # Should work for Python2.6 and higher. + import json as simplejson + + +from oauth2client.client import OAuth2Credentials +from oauth2client.client import AccessTokenCredentials +from oauth2client.client import AssertionCredentials +from oauth2client.file import Storage +from oauth2client import multistore_file + + +FILENAME = os.path.join(os.path.dirname(__file__), 'test_file_storage.data') + + +class OAuth2ClientFileTests(unittest.TestCase): + + def tearDown(self): + try: + os.unlink(FILENAME) + except OSError: + pass + + def setUp(self): + try: + os.unlink(FILENAME) + except OSError: + pass + + def test_non_existent_file_storage(self): + s = Storage(FILENAME) + credentials = s.get() + self.assertEquals(None, credentials) + + def test_pickle_and_json_interop(self): + # Write a file with a pickled OAuth2Credentials. + access_token = 'foo' + client_id = 'some_client_id' + client_secret = 'cOuDdkfjxxnv+' + refresh_token = '1/0/a.df219fjls0' + token_expiry = datetime.datetime.utcnow() + token_uri = 'https://www.google.com/accounts/o8/oauth2/token' + user_agent = 'refresh_checker/1.0' + + credentials = OAuth2Credentials( + access_token, client_id, client_secret, + refresh_token, token_expiry, token_uri, + user_agent) + + f = open(FILENAME, 'w') + pickle.dump(credentials, f) + f.close() + + # Storage should be able to read that object. + # TODO(jcgregorio) This should fail once pickle support is removed. + s = Storage(FILENAME) + credentials = s.get() + self.assertNotEquals(None, credentials) + self.assertEquals('foo', credentials.access_token) + + # Now write it back out and confirm it has been rewritten as JSON + s.put(credentials) + f = file(FILENAME) + data = simplejson.load(f) + f.close() + + self.assertEquals(data['access_token'], 'foo') + self.assertEquals(data['_class'], 'OAuth2Credentials') + self.assertEquals(data['_module'], 'oauth2client.client') + + def test_access_token_credentials(self): + access_token = 'foo' + user_agent = 'refresh_checker/1.0' + + credentials = AccessTokenCredentials(access_token, user_agent) + + s = Storage(FILENAME) + credentials = s.put(credentials) + credentials = s.get() + + self.assertNotEquals(None, credentials) + self.assertEquals('foo', credentials.access_token) + + def test_multistore_non_existent_file(self): + store = multistore_file.get_credential_storage( + FILENAME, + 'some_client_id', + 'user-agent/1.0', + 'some-scope') + + credentials = store.get() + self.assertEquals(None, credentials) + + def test_multistore_file(self): + access_token = 'foo' + client_secret = 'cOuDdkfjxxnv+' + refresh_token = '1/0/a.df219fjls0' + token_expiry = datetime.datetime.utcnow() + token_uri = 'https://www.google.com/accounts/o8/oauth2/token' + user_agent = 'refresh_checker/1.0' + client_id = 'some_client_id' + + credentials = OAuth2Credentials( + access_token, client_id, client_secret, + refresh_token, token_expiry, token_uri, + user_agent) + + store = multistore_file.get_credential_storage( + FILENAME, + credentials.client_id, + credentials.user_agent, + 'some-scope') + + store.put(credentials) + credentials = store.get() + + self.assertNotEquals(None, credentials) + self.assertEquals('foo', credentials.access_token) + +if __name__ == '__main__': + unittest.main()