Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(122)

Side by Side Diff: third_party/oauth2client/appengine.py

Issue 1085893002: Upgrade 3rd packages (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: rebase Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 # Copyright (C) 2010 Google Inc. 1 # Copyright 2014 Google Inc. All rights reserved.
2 # 2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); 3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License. 4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at 5 # You may obtain a copy of the License at
6 # 6 #
7 # http://www.apache.org/licenses/LICENSE-2.0 7 # http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 # Unless required by applicable law or agreed to in writing, software 9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, 10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and 12 # See the License for the specific language governing permissions and
13 # limitations under the License. 13 # limitations under the License.
14 14
15 """Utilities for Google App Engine 15 """Utilities for Google App Engine
16 16
17 Utilities for making it easier to use OAuth 2.0 on Google App Engine. 17 Utilities for making it easier to use OAuth 2.0 on Google App Engine.
18 """ 18 """
19 19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' 20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 21
22 import base64
23 import cgi 22 import cgi
24 import httplib2 23 import json
25 import logging 24 import logging
26 import os 25 import os
27 import pickle 26 import pickle
28 import threading 27 import threading
29 import time 28
29 import httplib2
30 30
31 from google.appengine.api import app_identity 31 from google.appengine.api import app_identity
32 from google.appengine.api import memcache 32 from google.appengine.api import memcache
33 from google.appengine.api import users 33 from google.appengine.api import users
34 from google.appengine.ext import db 34 from google.appengine.ext import db
35 from google.appengine.ext import webapp 35 from google.appengine.ext import webapp
36 from google.appengine.ext.webapp.util import login_required 36 from google.appengine.ext.webapp.util import login_required
37 from google.appengine.ext.webapp.util import run_wsgi_app 37 from google.appengine.ext.webapp.util import run_wsgi_app
38 from oauth2client import GOOGLE_AUTH_URI 38 from oauth2client import GOOGLE_AUTH_URI
39 from oauth2client import GOOGLE_REVOKE_URI 39 from oauth2client import GOOGLE_REVOKE_URI
40 from oauth2client import GOOGLE_TOKEN_URI 40 from oauth2client import GOOGLE_TOKEN_URI
41 from oauth2client import clientsecrets 41 from oauth2client import clientsecrets
42 from oauth2client import util 42 from oauth2client import util
43 from oauth2client import xsrfutil 43 from oauth2client import xsrfutil
44 from oauth2client.anyjson import simplejson
45 from oauth2client.client import AccessTokenRefreshError 44 from oauth2client.client import AccessTokenRefreshError
46 from oauth2client.client import AssertionCredentials 45 from oauth2client.client import AssertionCredentials
47 from oauth2client.client import Credentials 46 from oauth2client.client import Credentials
48 from oauth2client.client import Flow 47 from oauth2client.client import Flow
49 from oauth2client.client import OAuth2WebServerFlow 48 from oauth2client.client import OAuth2WebServerFlow
50 from oauth2client.client import Storage 49 from oauth2client.client import Storage
51 50
52 # TODO(dhermes): Resolve import issue. 51 # TODO(dhermes): Resolve import issue.
53 # This is a temporary fix for a Google internal issue. 52 # This is a temporary fix for a Google internal issue.
54 try: 53 try:
(...skipping 97 matching lines...) Expand 10 before | Expand all | Expand 10 after
152 generate and refresh its own access tokens. 151 generate and refresh its own access tokens.
153 """ 152 """
154 153
155 @util.positional(2) 154 @util.positional(2)
156 def __init__(self, scope, **kwargs): 155 def __init__(self, scope, **kwargs):
157 """Constructor for AppAssertionCredentials 156 """Constructor for AppAssertionCredentials
158 157
159 Args: 158 Args:
160 scope: string or iterable of strings, scope(s) of the credentials being 159 scope: string or iterable of strings, scope(s) of the credentials being
161 requested. 160 requested.
161 **kwargs: optional keyword args, including:
162 service_account_id: service account id of the application. If None or
163 unspecified, the default service account for the app is used.
162 """ 164 """
163 self.scope = util.scopes_to_string(scope) 165 self.scope = util.scopes_to_string(scope)
166 self._kwargs = kwargs
167 self.service_account_id = kwargs.get('service_account_id', None)
164 168
165 # Assertion type is no longer used, but still in the parent class signature. 169 # Assertion type is no longer used, but still in the parent class signature.
166 super(AppAssertionCredentials, self).__init__(None) 170 super(AppAssertionCredentials, self).__init__(None)
167 171
168 @classmethod 172 @classmethod
169 def from_json(cls, json): 173 def from_json(cls, json_data):
170 data = simplejson.loads(json) 174 data = json.loads(json_data)
171 return AppAssertionCredentials(data['scope']) 175 return AppAssertionCredentials(data['scope'])
172 176
173 def _refresh(self, http_request): 177 def _refresh(self, http_request):
174 """Refreshes the access_token. 178 """Refreshes the access_token.
175 179
176 Since the underlying App Engine app_identity implementation does its own 180 Since the underlying App Engine app_identity implementation does its own
177 caching we can skip all the storage hoops and just to a refresh using the 181 caching we can skip all the storage hoops and just to a refresh using the
178 API. 182 API.
179 183
180 Args: 184 Args:
181 http_request: callable, a callable that matches the method signature of 185 http_request: callable, a callable that matches the method signature of
182 httplib2.Http.request, used to make the refresh request. 186 httplib2.Http.request, used to make the refresh request.
183 187
184 Raises: 188 Raises:
185 AccessTokenRefreshError: When the refresh fails. 189 AccessTokenRefreshError: When the refresh fails.
186 """ 190 """
187 try: 191 try:
188 scopes = self.scope.split() 192 scopes = self.scope.split()
189 (token, _) = app_identity.get_access_token(scopes) 193 (token, _) = app_identity.get_access_token(
190 except app_identity.Error, e: 194 scopes, service_account_id=self.service_account_id)
195 except app_identity.Error as e:
191 raise AccessTokenRefreshError(str(e)) 196 raise AccessTokenRefreshError(str(e))
192 self.access_token = token 197 self.access_token = token
193 198
199 @property
200 def serialization_data(self):
201 raise NotImplementedError('Cannot serialize credentials for AppEngine.')
202
203 def create_scoped_required(self):
204 return not self.scope
205
206 def create_scoped(self, scopes):
207 return AppAssertionCredentials(scopes, **self._kwargs)
208
194 209
195 class FlowProperty(db.Property): 210 class FlowProperty(db.Property):
196 """App Engine datastore Property for Flow. 211 """App Engine datastore Property for Flow.
197 212
198 Utility property that allows easy storage and retrieval of an 213 Utility property that allows easy storage and retrieval of an
199 oauth2client.Flow""" 214 oauth2client.Flow"""
200 215
201 # Tell what the user type is. 216 # Tell what the user type is.
202 data_type = Flow 217 data_type = Flow
203 218
(...skipping 223 matching lines...) Expand 10 before | Expand all | Expand 10 after
427 442
428 Attempts to delete using the key_name stored on the object, whether or not 443 Attempts to delete using the key_name stored on the object, whether or not
429 the given key is in the datastore. 444 the given key is in the datastore.
430 """ 445 """
431 if self._is_ndb(): 446 if self._is_ndb():
432 ndb.Key(self._model, self._key_name).delete() 447 ndb.Key(self._model, self._key_name).delete()
433 else: 448 else:
434 entity_key = db.Key.from_path(self._model.kind(), self._key_name) 449 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
435 db.delete(entity_key) 450 db.delete(entity_key)
436 451
452 @db.non_transactional(allow_existing=True)
437 def locked_get(self): 453 def locked_get(self):
438 """Retrieve Credential from datastore. 454 """Retrieve Credential from datastore.
439 455
440 Returns: 456 Returns:
441 oauth2client.Credentials 457 oauth2client.Credentials
442 """ 458 """
443 credentials = None 459 credentials = None
444 if self._cache: 460 if self._cache:
445 json = self._cache.get(self._key_name) 461 json = self._cache.get(self._key_name)
446 if json: 462 if json:
447 credentials = Credentials.new_from_json(json) 463 credentials = Credentials.new_from_json(json)
448 if credentials is None: 464 if credentials is None:
449 entity = self._get_entity() 465 entity = self._get_entity()
450 if entity is not None: 466 if entity is not None:
451 credentials = getattr(entity, self._property_name) 467 credentials = getattr(entity, self._property_name)
452 if self._cache: 468 if self._cache:
453 self._cache.set(self._key_name, credentials.to_json()) 469 self._cache.set(self._key_name, credentials.to_json())
454 470
455 if credentials and hasattr(credentials, 'set_store'): 471 if credentials and hasattr(credentials, 'set_store'):
456 credentials.set_store(self) 472 credentials.set_store(self)
457 return credentials 473 return credentials
458 474
475 @db.non_transactional(allow_existing=True)
459 def locked_put(self, credentials): 476 def locked_put(self, credentials):
460 """Write a Credentials to the datastore. 477 """Write a Credentials to the datastore.
461 478
462 Args: 479 Args:
463 credentials: Credentials, the credentials to store. 480 credentials: Credentials, the credentials to store.
464 """ 481 """
465 entity = self._model.get_or_insert(self._key_name) 482 entity = self._model.get_or_insert(self._key_name)
466 setattr(entity, self._property_name, credentials) 483 setattr(entity, self._property_name, credentials)
467 entity.put() 484 entity.put()
468 if self._cache: 485 if self._cache:
469 self._cache.set(self._key_name, credentials.to_json()) 486 self._cache.set(self._key_name, credentials.to_json())
470 487
488 @db.non_transactional(allow_existing=True)
471 def locked_delete(self): 489 def locked_delete(self):
472 """Delete Credential from datastore.""" 490 """Delete Credential from datastore."""
473 491
474 if self._cache: 492 if self._cache:
475 self._cache.delete(self._key_name) 493 self._cache.delete(self._key_name)
476 494
477 self._delete_entity() 495 self._delete_entity()
478 496
479 497
480 class CredentialsModel(db.Model): 498 class CredentialsModel(db.Model):
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
546 564
547 return uri 565 return uri
548 566
549 567
550 class OAuth2Decorator(object): 568 class OAuth2Decorator(object):
551 """Utility for making OAuth 2.0 easier. 569 """Utility for making OAuth 2.0 easier.
552 570
553 Instantiate and then use with oauth_required or oauth_aware 571 Instantiate and then use with oauth_required or oauth_aware
554 as decorators on webapp.RequestHandler methods. 572 as decorators on webapp.RequestHandler methods.
555 573
556 Example: 574 ::
557 575
558 decorator = OAuth2Decorator( 576 decorator = OAuth2Decorator(
559 client_id='837...ent.com', 577 client_id='837...ent.com',
560 client_secret='Qh...wwI', 578 client_secret='Qh...wwI',
561 scope='https://www.googleapis.com/auth/plus') 579 scope='https://www.googleapis.com/auth/plus')
562 580
563
564 class MainHandler(webapp.RequestHandler): 581 class MainHandler(webapp.RequestHandler):
565
566 @decorator.oauth_required 582 @decorator.oauth_required
567 def get(self): 583 def get(self):
568 http = decorator.http() 584 http = decorator.http()
569 # http is authorized with the user's Credentials and can be used 585 # http is authorized with the user's Credentials and can be used
570 # in API calls 586 # in API calls
571 587
572 """ 588 """
573 589
574 def set_credentials(self, credentials): 590 def set_credentials(self, credentials):
575 self._tls.credentials = credentials 591 self._tls.credentials = credentials
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
643 _storage_class: "Protected" keyword argument not typically provided to 659 _storage_class: "Protected" keyword argument not typically provided to
644 this constructor. A storage class to aid in storing a Credentials object 660 this constructor. A storage class to aid in storing a Credentials object
645 for a user in the datastore. Defaults to StorageByKeyName. 661 for a user in the datastore. Defaults to StorageByKeyName.
646 _credentials_class: "Protected" keyword argument not typically provided to 662 _credentials_class: "Protected" keyword argument not typically provided to
647 this constructor. A db or ndb Model class to hold credentials. Defaults 663 this constructor. A db or ndb Model class to hold credentials. Defaults
648 to CredentialsModel. 664 to CredentialsModel.
649 _credentials_property_name: "Protected" keyword argument not typically 665 _credentials_property_name: "Protected" keyword argument not typically
650 provided to this constructor. A string indicating the name of the field 666 provided to this constructor. A string indicating the name of the field
651 on the _credentials_class where a Credentials object will be stored. 667 on the _credentials_class where a Credentials object will be stored.
652 Defaults to 'credentials'. 668 Defaults to 'credentials'.
653 **kwargs: dict, Keyword arguments are be passed along as kwargs to the 669 **kwargs: dict, Keyword arguments are passed along as kwargs to
654 OAuth2WebServerFlow constructor. 670 the OAuth2WebServerFlow constructor.
671
655 """ 672 """
656 self._tls = threading.local() 673 self._tls = threading.local()
657 self.flow = None 674 self.flow = None
658 self.credentials = None 675 self.credentials = None
659 self._client_id = client_id 676 self._client_id = client_id
660 self._client_secret = client_secret 677 self._client_secret = client_secret
661 self._scope = util.scopes_to_string(scope) 678 self._scope = util.scopes_to_string(scope)
662 self._auth_uri = auth_uri 679 self._auth_uri = auth_uri
663 self._token_uri = token_uri 680 self._token_uri = token_uri
664 self._revoke_uri = revoke_uri 681 self._revoke_uri = revoke_uri
(...skipping 126 matching lines...) Expand 10 before | Expand all | Expand 10 after
791 808
792 def authorize_url(self): 809 def authorize_url(self):
793 """Returns the URL to start the OAuth dance. 810 """Returns the URL to start the OAuth dance.
794 811
795 Must only be called from with a webapp.RequestHandler subclassed method 812 Must only be called from with a webapp.RequestHandler subclassed method
796 that had been decorated with either @oauth_required or @oauth_aware. 813 that had been decorated with either @oauth_required or @oauth_aware.
797 """ 814 """
798 url = self.flow.step1_get_authorize_url() 815 url = self.flow.step1_get_authorize_url()
799 return str(url) 816 return str(url)
800 817
801 def http(self): 818 def http(self, *args, **kwargs):
802 """Returns an authorized http instance. 819 """Returns an authorized http instance.
803 820
804 Must only be called from within an @oauth_required decorated method, or 821 Must only be called from within an @oauth_required decorated method, or
805 from within an @oauth_aware decorated method where has_credentials() 822 from within an @oauth_aware decorated method where has_credentials()
806 returns True. 823 returns True.
824
825 Args:
826 *args: Positional arguments passed to httplib2.Http constructor.
827 **kwargs: Positional arguments passed to httplib2.Http constructor.
807 """ 828 """
808 return self.credentials.authorize(httplib2.Http()) 829 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
809 830
810 @property 831 @property
811 def callback_path(self): 832 def callback_path(self):
812 """The absolute path where the callback will occur. 833 """The absolute path where the callback will occur.
813 834
814 Note this is the absolute path, not the absolute URI, that will be 835 Note this is the absolute path, not the absolute URI, that will be
815 calculated by the decorator at runtime. See callback_handler() for how this 836 calculated by the decorator at runtime. See callback_handler() for how this
816 should be used. 837 should be used.
817 838
818 Returns: 839 Returns:
819 The callback path as a string. 840 The callback path as a string.
820 """ 841 """
821 return self._callback_path 842 return self._callback_path
822 843
823 844
824 def callback_handler(self): 845 def callback_handler(self):
825 """RequestHandler for the OAuth 2.0 redirect callback. 846 """RequestHandler for the OAuth 2.0 redirect callback.
826 847
827 Usage: 848 Usage::
849
828 app = webapp.WSGIApplication([ 850 app = webapp.WSGIApplication([
829 ('/index', MyIndexHandler), 851 ('/index', MyIndexHandler),
830 ..., 852 ...,
831 (decorator.callback_path, decorator.callback_handler()) 853 (decorator.callback_path, decorator.callback_handler())
832 ]) 854 ])
833 855
834 Returns: 856 Returns:
835 A webapp.RequestHandler that handles the redirect back from the 857 A webapp.RequestHandler that handles the redirect back from the
836 server during the OAuth 2.0 dance. 858 server during the OAuth 2.0 dance.
837 """ 859 """
(...skipping 13 matching lines...) Expand all
851 user = users.get_current_user() 873 user = users.get_current_user()
852 decorator._create_flow(self) 874 decorator._create_flow(self)
853 credentials = decorator.flow.step2_exchange(self.request.params) 875 credentials = decorator.flow.step2_exchange(self.request.params)
854 decorator._storage_class( 876 decorator._storage_class(
855 decorator._credentials_class, None, 877 decorator._credentials_class, None,
856 decorator._credentials_property_name, user=user).put(credentials) 878 decorator._credentials_property_name, user=user).put(credentials)
857 redirect_uri = _parse_state_value(str(self.request.get('state')), 879 redirect_uri = _parse_state_value(str(self.request.get('state')),
858 user) 880 user)
859 881
860 if decorator._token_response_param and credentials.token_response: 882 if decorator._token_response_param and credentials.token_response:
861 resp_json = simplejson.dumps(credentials.token_response) 883 resp_json = json.dumps(credentials.token_response)
862 redirect_uri = util._add_query_parameter( 884 redirect_uri = util._add_query_parameter(
863 redirect_uri, decorator._token_response_param, resp_json) 885 redirect_uri, decorator._token_response_param, resp_json)
864 886
865 self.redirect(redirect_uri) 887 self.redirect(redirect_uri)
866 888
867 return OAuth2Handler 889 return OAuth2Handler
868 890
869 def callback_application(self): 891 def callback_application(self):
870 """WSGI application for handling the OAuth 2.0 redirect callback. 892 """WSGI application for handling the OAuth 2.0 redirect callback.
871 893
872 If you need finer grained control use `callback_handler` which returns just 894 If you need finer grained control use `callback_handler` which returns just
873 the webapp.RequestHandler. 895 the webapp.RequestHandler.
874 896
875 Returns: 897 Returns:
876 A webapp.WSGIApplication that handles the redirect back from the 898 A webapp.WSGIApplication that handles the redirect back from the
877 server during the OAuth 2.0 dance. 899 server during the OAuth 2.0 dance.
878 """ 900 """
879 return webapp.WSGIApplication([ 901 return webapp.WSGIApplication([
880 (self.callback_path, self.callback_handler()) 902 (self.callback_path, self.callback_handler())
881 ]) 903 ])
882 904
883 905
884 class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): 906 class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
885 """An OAuth2Decorator that builds from a clientsecrets file. 907 """An OAuth2Decorator that builds from a clientsecrets file.
886 908
887 Uses a clientsecrets file as the source for all the information when 909 Uses a clientsecrets file as the source for all the information when
888 constructing an OAuth2Decorator. 910 constructing an OAuth2Decorator.
889 911
890 Example: 912 ::
891 913
892 decorator = OAuth2DecoratorFromClientSecrets( 914 decorator = OAuth2DecoratorFromClientSecrets(
893 os.path.join(os.path.dirname(__file__), 'client_secrets.json') 915 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
894 scope='https://www.googleapis.com/auth/plus') 916 scope='https://www.googleapis.com/auth/plus')
895 917
896
897 class MainHandler(webapp.RequestHandler): 918 class MainHandler(webapp.RequestHandler):
898
899 @decorator.oauth_required 919 @decorator.oauth_required
900 def get(self): 920 def get(self):
901 http = decorator.http() 921 http = decorator.http()
902 # http is authorized with the user's Credentials and can be used 922 # http is authorized with the user's Credentials and can be used
903 # in API calls 923 # in API calls
924
904 """ 925 """
905 926
906 @util.positional(3) 927 @util.positional(3)
907 def __init__(self, filename, scope, message=None, cache=None): 928 def __init__(self, filename, scope, message=None, cache=None, **kwargs):
908 """Constructor 929 """Constructor
909 930
910 Args: 931 Args:
911 filename: string, File name of client secrets. 932 filename: string, File name of client secrets.
912 scope: string or iterable of strings, scope(s) of the credentials being 933 scope: string or iterable of strings, scope(s) of the credentials being
913 requested. 934 requested.
914 message: string, A friendly string to display to the user if the 935 message: string, A friendly string to display to the user if the
915 clientsecrets file is missing or invalid. The message may contain HTML 936 clientsecrets file is missing or invalid. The message may contain HTML
916 and will be presented on the web interface for any method that uses the 937 and will be presented on the web interface for any method that uses the
917 decorator. 938 decorator.
918 cache: An optional cache service client that implements get() and set() 939 cache: An optional cache service client that implements get() and set()
919 methods. See clientsecrets.loadfile() for details. 940 methods. See clientsecrets.loadfile() for details.
941 **kwargs: dict, Keyword arguments are passed along as kwargs to
942 the OAuth2WebServerFlow constructor.
920 """ 943 """
921 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 944 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
922 if client_type not in [ 945 if client_type not in [
923 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 946 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
924 raise InvalidClientSecretsError( 947 raise InvalidClientSecretsError(
925 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') 948 "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
926 constructor_kwargs = { 949 constructor_kwargs = dict(kwargs)
927 'auth_uri': client_info['auth_uri'], 950 constructor_kwargs.update({
928 'token_uri': client_info['token_uri'], 951 'auth_uri': client_info['auth_uri'],
929 'message': message, 952 'token_uri': client_info['token_uri'],
930 } 953 'message': message,
954 })
931 revoke_uri = client_info.get('revoke_uri') 955 revoke_uri = client_info.get('revoke_uri')
932 if revoke_uri is not None: 956 if revoke_uri is not None:
933 constructor_kwargs['revoke_uri'] = revoke_uri 957 constructor_kwargs['revoke_uri'] = revoke_uri
934 super(OAuth2DecoratorFromClientSecrets, self).__init__( 958 super(OAuth2DecoratorFromClientSecrets, self).__init__(
935 client_info['client_id'], client_info['client_secret'], 959 client_info['client_id'], client_info['client_secret'],
936 scope, **constructor_kwargs) 960 scope, **constructor_kwargs)
937 if message is not None: 961 if message is not None:
938 self._message = message 962 self._message = message
939 else: 963 else:
940 self._message = 'Please configure your application for OAuth 2.0.' 964 self._message = 'Please configure your application for OAuth 2.0.'
(...skipping 12 matching lines...) Expand all
953 clientsecrets file is missing or invalid. The message may contain HTML and 977 clientsecrets file is missing or invalid. The message may contain HTML and
954 will be presented on the web interface for any method that uses the 978 will be presented on the web interface for any method that uses the
955 decorator. 979 decorator.
956 cache: An optional cache service client that implements get() and set() 980 cache: An optional cache service client that implements get() and set()
957 methods. See clientsecrets.loadfile() for details. 981 methods. See clientsecrets.loadfile() for details.
958 982
959 Returns: An OAuth2Decorator 983 Returns: An OAuth2Decorator
960 984
961 """ 985 """
962 return OAuth2DecoratorFromClientSecrets(filename, scope, 986 return OAuth2DecoratorFromClientSecrets(filename, scope,
963 message=message, cache=cache) 987 message=message, cache=cache)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698