Loading of client_secrets JSON file backed by a cache.

Contributed by crhyme.

Reviwed in http://codereview.appspot.com/6349087/.
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()