Loading of client_secrets JSON file backed by a cache.
Contributed by crhyme.
Reviwed in http://codereview.appspot.com/6349087/.
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index ca2ff15..12ba04d 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -446,7 +446,7 @@
# in API calls
"""
- def __init__(self, filename, scope, message=None):
+ def __init__(self, filename, scope, message=None, cache=None):
"""Constructor
Args:
@@ -457,9 +457,11 @@
clientsecrets file is missing or invalid. The message may contain HTML and
will be presented on the web interface for any method that uses the
decorator.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
"""
try:
- client_type, client_info = clientsecrets.loadfile(filename)
+ client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
super(OAuth2DecoratorFromClientSecrets,
@@ -478,7 +480,8 @@
self._message = "Please configure your application for OAuth 2.0"
-def oauth2decorator_from_clientsecrets(filename, scope, message=None):
+def oauth2decorator_from_clientsecrets(filename, scope,
+ message=None, cache=None):
"""Creates an OAuth2Decorator populated from a clientsecrets file.
Args:
@@ -489,11 +492,14 @@
clientsecrets file is missing or invalid. The message may contain HTML and
will be presented on the web interface for any method that uses the
decorator.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
Returns: An OAuth2Decorator
"""
- return OAuth2DecoratorFromClientSecrets(filename, scope, message)
+ return OAuth2DecoratorFromClientSecrets(filename, scope,
+ message=message, cache=cache)
class OAuth2Handler(webapp.RequestHandler):
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 5ccaccd..e701f02 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -955,7 +955,8 @@
def credentials_from_clientsecrets_and_code(filename, scope, code,
message = None,
redirect_uri = 'postmessage',
- http=None):
+ http=None,
+ cache=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
@@ -973,6 +974,8 @@
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
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
Returns:
An OAuth2Credentials object.
@@ -984,7 +987,7 @@
clientsecrets.InvalidClientSecretsError if the clientsecrets file is
invalid.
"""
- flow = flow_from_clientsecrets(filename, scope, message)
+ flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache)
# 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)
@@ -1130,7 +1133,7 @@
error_msg = 'Invalid response: %s.' % str(resp.status)
raise FlowExchangeError(error_msg)
-def flow_from_clientsecrets(filename, scope, message=None):
+def flow_from_clientsecrets(filename, scope, message=None, cache=None):
"""Create a Flow from a clientsecrets file.
Will create the right kind of Flow based on the contents of the clientsecrets
@@ -1143,6 +1146,8 @@
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.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
Returns:
A Flow object.
@@ -1153,7 +1158,7 @@
invalid.
"""
try:
- client_type, client_info = clientsecrets.loadfile(filename)
+ client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
return OAuth2WebServerFlow(
client_info['client_id'],
diff --git a/oauth2client/clientsecrets.py b/oauth2client/clientsecrets.py
index 1327a2a..428c5ec 100644
--- a/oauth2client/clientsecrets.py
+++ b/oauth2client/clientsecrets.py
@@ -93,7 +93,7 @@
return _validate_clientsecrets(obj)
-def loadfile(filename):
+def _loadfile(filename):
try:
fp = file(filename, 'r')
try:
@@ -103,3 +103,48 @@
except IOError:
raise InvalidClientSecretsError('File not found: "%s"' % filename)
return _validate_clientsecrets(obj)
+
+
+def loadfile(filename, cache=None):
+ """Loading of client_secrets JSON file, optionally backed by a cache.
+
+ Typical cache storage would be App Engine memcache service,
+ but you can pass in any other cache client that implements
+ these methods:
+ - get(key, namespace=ns)
+ - set(key, value, namespace=ns)
+
+ Usage:
+ # without caching
+ client_type, client_info = loadfile('secrets.json')
+ # using App Engine memcache service
+ from google.appengine.api import memcache
+ client_type, client_info = loadfile('secrets.json', cache=memcache)
+
+ Args:
+ filename: string, Path to a client_secrets.json file on a filesystem.
+ cache: An optional cache service client that implements get() and set()
+ methods. If not specified, the file is always being loaded from
+ a filesystem.
+
+ Raises:
+ InvalidClientSecretsError: In case of a validation error or some
+ I/O failure. Can happen only on cache miss.
+
+ Returns:
+ (client_type, client_info) tuple, as _loadfile() normally would.
+ JSON contents is validated only during first load. Cache hits are not
+ validated.
+ """
+ _SECRET_NAMESPACE = 'oauth2client:secrets#ns'
+
+ if not cache:
+ return _loadfile(filename)
+
+ obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
+ if obj is None:
+ client_type, client_info = _loadfile(filename)
+ obj = { client_type: client_info }
+ cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
+
+ return obj.iteritems().next()