/gdata/service.py
Python | 1564 lines | 1504 code | 13 blank | 47 comment | 27 complexity | 93110a26726421630535b6fd6a2236fe MD5 | raw file
Large files files are truncated, but you can click here to view the full file
1#!/usr/bin/python 2# 3# Copyright (C) 2006,2008 Google Inc. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17 18"""GDataService provides CRUD ops. and programmatic login for GData services. 19 20 Error: A base exception class for all exceptions in the gdata_client 21 module. 22 23 CaptchaRequired: This exception is thrown when a login attempt results in a 24 captcha challenge from the ClientLogin service. When this 25 exception is thrown, the captcha_token and captcha_url are 26 set to the values provided in the server's response. 27 28 BadAuthentication: Raised when a login attempt is made with an incorrect 29 username or password. 30 31 NotAuthenticated: Raised if an operation requiring authentication is called 32 before a user has authenticated. 33 34 NonAuthSubToken: Raised if a method to modify an AuthSub token is used when 35 the user is either not authenticated or is authenticated 36 through another authentication mechanism. 37 38 NonOAuthToken: Raised if a method to modify an OAuth token is used when the 39 user is either not authenticated or is authenticated through 40 another authentication mechanism. 41 42 RequestError: Raised if a CRUD request returned a non-success code. 43 44 UnexpectedReturnType: Raised if the response from the server was not of the 45 desired type. For example, this would be raised if the 46 server sent a feed when the client requested an entry. 47 48 GDataService: Encapsulates user credentials needed to perform insert, update 49 and delete operations with the GData API. An instance can 50 perform user authentication, query, insertion, deletion, and 51 update. 52 53 Query: Eases query URI creation by allowing URI parameters to be set as 54 dictionary attributes. For example a query with a feed of 55 '/base/feeds/snippets' and ['bq'] set to 'digital camera' will 56 produce '/base/feeds/snippets?bq=digital+camera' when .ToUri() is 57 called on it. 58""" 59 60 61__author__ = 'api.jscudder (Jeffrey Scudder)' 62 63import re 64import urllib 65import urlparse 66try: 67 from xml.etree import cElementTree as ElementTree 68except ImportError: 69 try: 70 import cElementTree as ElementTree 71 except ImportError: 72 try: 73 from xml.etree import ElementTree 74 except ImportError: 75 from elementtree import ElementTree 76import atom.service 77import gdata 78import atom 79import atom.http_interface 80import atom.token_store 81import gdata.auth 82import gdata.gauth 83 84 85AUTH_SERVER_HOST = 'https://www.google.com' 86 87 88# When requesting an AuthSub token, it is often helpful to track the scope 89# which is being requested. One way to accomplish this is to add a URL 90# parameter to the 'next' URL which contains the requested scope. This 91# constant is the default name (AKA key) for the URL parameter. 92SCOPE_URL_PARAM_NAME = 'authsub_token_scope' 93# When requesting an OAuth access token or authorization of an existing OAuth 94# request token, it is often helpful to track the scope(s) which is/are being 95# requested. One way to accomplish this is to add a URL parameter to the 96# 'callback' URL which contains the requested scope. This constant is the 97# default name (AKA key) for the URL parameter. 98OAUTH_SCOPE_URL_PARAM_NAME = 'oauth_token_scope' 99# Maps the service names used in ClientLogin to scope URLs. 100CLIENT_LOGIN_SCOPES = gdata.gauth.AUTH_SCOPES 101# Default parameters for GDataService.GetWithRetries method 102DEFAULT_NUM_RETRIES = 3 103DEFAULT_DELAY = 1 104DEFAULT_BACKOFF = 2 105 106 107def lookup_scopes(service_name): 108 """Finds the scope URLs for the desired service. 109 110 In some cases, an unknown service may be used, and in those cases this 111 function will return None. 112 """ 113 if service_name in CLIENT_LOGIN_SCOPES: 114 return CLIENT_LOGIN_SCOPES[service_name] 115 return None 116 117 118# Module level variable specifies which module should be used by GDataService 119# objects to make HttpRequests. This setting can be overridden on each 120# instance of GDataService. 121# This module level variable is deprecated. Reassign the http_client member 122# of a GDataService object instead. 123http_request_handler = atom.service 124 125 126class Error(Exception): 127 pass 128 129 130class CaptchaRequired(Error): 131 pass 132 133 134class BadAuthentication(Error): 135 pass 136 137 138class NotAuthenticated(Error): 139 pass 140 141 142class NonAuthSubToken(Error): 143 pass 144 145 146class NonOAuthToken(Error): 147 pass 148 149 150class RequestError(Error): 151 pass 152 153 154class UnexpectedReturnType(Error): 155 pass 156 157 158class BadAuthenticationServiceURL(Error): 159 pass 160 161 162class FetchingOAuthRequestTokenFailed(RequestError): 163 pass 164 165 166class TokenUpgradeFailed(RequestError): 167 pass 168 169 170class RevokingOAuthTokenFailed(RequestError): 171 pass 172 173 174class AuthorizationRequired(Error): 175 pass 176 177 178class TokenHadNoScope(Error): 179 pass 180 181 182class RanOutOfTries(Error): 183 pass 184 185 186class GDataService(atom.service.AtomService): 187 """Contains elements needed for GData login and CRUD request headers. 188 189 Maintains additional headers (tokens for example) needed for the GData 190 services to allow a user to perform inserts, updates, and deletes. 191 """ 192 # The hander member is deprecated, use http_client instead. 193 handler = None 194 # The auth_token member is deprecated, use the token_store instead. 195 auth_token = None 196 # The tokens dict is deprecated in favor of the token_store. 197 tokens = None 198 199 def __init__(self, email=None, password=None, account_type='HOSTED_OR_GOOGLE', 200 service=None, auth_service_url=None, source=None, server=None, 201 additional_headers=None, handler=None, tokens=None, 202 http_client=None, token_store=None): 203 """Creates an object of type GDataService. 204 205 Args: 206 email: string (optional) The user's email address, used for 207 authentication. 208 password: string (optional) The user's password. 209 account_type: string (optional) The type of account to use. Use 210 'GOOGLE' for regular Google accounts or 'HOSTED' for Google 211 Apps accounts, or 'HOSTED_OR_GOOGLE' to try finding a HOSTED 212 account first and, if it doesn't exist, try finding a regular 213 GOOGLE account. Default value: 'HOSTED_OR_GOOGLE'. 214 service: string (optional) The desired service for which credentials 215 will be obtained. 216 auth_service_url: string (optional) User-defined auth token request URL 217 allows users to explicitly specify where to send auth token requests. 218 source: string (optional) The name of the user's application. 219 server: string (optional) The name of the server to which a connection 220 will be opened. Default value: 'base.google.com'. 221 additional_headers: dictionary (optional) Any additional headers which 222 should be included with CRUD operations. 223 handler: module (optional) This parameter is deprecated and has been 224 replaced by http_client. 225 tokens: This parameter is deprecated, calls should be made to 226 token_store instead. 227 http_client: An object responsible for making HTTP requests using a 228 request method. If none is provided, a new instance of 229 atom.http.ProxiedHttpClient will be used. 230 token_store: Keeps a collection of authorization tokens which can be 231 applied to requests for a specific URLs. Critical methods are 232 find_token based on a URL (atom.url.Url or a string), add_token, 233 and remove_token. 234 """ 235 atom.service.AtomService.__init__(self, http_client=http_client, 236 token_store=token_store) 237 self.email = email 238 self.password = password 239 self.account_type = account_type 240 self.service = service 241 self.auth_service_url = auth_service_url 242 self.server = server 243 self.additional_headers = additional_headers or {} 244 self._oauth_input_params = None 245 self.__SetSource(source) 246 self.__captcha_token = None 247 self.__captcha_url = None 248 self.__gsessionid = None 249 250 if http_request_handler.__name__ == 'gdata.urlfetch': 251 import gdata.alt.appengine 252 self.http_client = gdata.alt.appengine.AppEngineHttpClient() 253 254 def _SetSessionId(self, session_id): 255 """Used in unit tests to simulate a 302 which sets a gsessionid.""" 256 self.__gsessionid = session_id 257 258 # Define properties for GDataService 259 def _SetAuthSubToken(self, auth_token, scopes=None): 260 """Deprecated, use SetAuthSubToken instead.""" 261 self.SetAuthSubToken(auth_token, scopes=scopes) 262 263 def __SetAuthSubToken(self, auth_token, scopes=None): 264 """Deprecated, use SetAuthSubToken instead.""" 265 self._SetAuthSubToken(auth_token, scopes=scopes) 266 267 def _GetAuthToken(self): 268 """Returns the auth token used for authenticating requests. 269 270 Returns: 271 string 272 """ 273 current_scopes = lookup_scopes(self.service) 274 if current_scopes: 275 token = self.token_store.find_token(current_scopes[0]) 276 if hasattr(token, 'auth_header'): 277 return token.auth_header 278 return None 279 280 def _GetCaptchaToken(self): 281 """Returns a captcha token if the most recent login attempt generated one. 282 283 The captcha token is only set if the Programmatic Login attempt failed 284 because the Google service issued a captcha challenge. 285 286 Returns: 287 string 288 """ 289 return self.__captcha_token 290 291 def __GetCaptchaToken(self): 292 return self._GetCaptchaToken() 293 294 captcha_token = property(__GetCaptchaToken, 295 doc="""Get the captcha token for a login request.""") 296 297 def _GetCaptchaURL(self): 298 """Returns the URL of the captcha image if a login attempt generated one. 299 300 The captcha URL is only set if the Programmatic Login attempt failed 301 because the Google service issued a captcha challenge. 302 303 Returns: 304 string 305 """ 306 return self.__captcha_url 307 308 def __GetCaptchaURL(self): 309 return self._GetCaptchaURL() 310 311 captcha_url = property(__GetCaptchaURL, 312 doc="""Get the captcha URL for a login request.""") 313 314 def GetGeneratorFromLinkFinder(self, link_finder, func, 315 num_retries=DEFAULT_NUM_RETRIES, 316 delay=DEFAULT_DELAY, 317 backoff=DEFAULT_BACKOFF): 318 """returns a generator for pagination""" 319 yield link_finder 320 next = link_finder.GetNextLink() 321 while next is not None: 322 next_feed = func(str(self.GetWithRetries( 323 next.href, num_retries=num_retries, delay=delay, backoff=backoff))) 324 yield next_feed 325 next = next_feed.GetNextLink() 326 327 def _GetElementGeneratorFromLinkFinder(self, link_finder, func, 328 num_retries=DEFAULT_NUM_RETRIES, 329 delay=DEFAULT_DELAY, 330 backoff=DEFAULT_BACKOFF): 331 for element in self.GetGeneratorFromLinkFinder(link_finder, func, 332 num_retries=num_retries, 333 delay=delay, 334 backoff=backoff).entry: 335 yield element 336 337 def GetOAuthInputParameters(self): 338 return self._oauth_input_params 339 340 def SetOAuthInputParameters(self, signature_method, consumer_key, 341 consumer_secret=None, rsa_key=None, 342 two_legged_oauth=False, requestor_id=None): 343 """Sets parameters required for using OAuth authentication mechanism. 344 345 NOTE: Though consumer_secret and rsa_key are optional, either of the two 346 is required depending on the value of the signature_method. 347 348 Args: 349 signature_method: class which provides implementation for strategy class 350 oauth.oauth.OAuthSignatureMethod. Signature method to be used for 351 signing each request. Valid implementations are provided as the 352 constants defined by gdata.auth.OAuthSignatureMethod. Currently 353 they are gdata.auth.OAuthSignatureMethod.RSA_SHA1 and 354 gdata.auth.OAuthSignatureMethod.HMAC_SHA1 355 consumer_key: string Domain identifying third_party web application. 356 consumer_secret: string (optional) Secret generated during registration. 357 Required only for HMAC_SHA1 signature method. 358 rsa_key: string (optional) Private key required for RSA_SHA1 signature 359 method. 360 two_legged_oauth: boolean (optional) Enables two-legged OAuth process. 361 requestor_id: string (optional) User email adress to make requests on 362 their behalf. This parameter should only be set when two_legged_oauth 363 is True. 364 """ 365 self._oauth_input_params = gdata.auth.OAuthInputParams( 366 signature_method, consumer_key, consumer_secret=consumer_secret, 367 rsa_key=rsa_key, requestor_id=requestor_id) 368 if two_legged_oauth: 369 oauth_token = gdata.auth.OAuthToken( 370 oauth_input_params=self._oauth_input_params) 371 self.SetOAuthToken(oauth_token) 372 373 def FetchOAuthRequestToken(self, scopes=None, extra_parameters=None, 374 request_url='%s/accounts/OAuthGetRequestToken' % \ 375 AUTH_SERVER_HOST, oauth_callback=None): 376 """Fetches and sets the OAuth request token and returns it. 377 378 Args: 379 scopes: string or list of string base URL(s) of the service(s) to be 380 accessed. If None, then this method tries to determine the 381 scope(s) from the current service. 382 extra_parameters: dict (optional) key-value pairs as any additional 383 parameters to be included in the URL and signature while making a 384 request for fetching an OAuth request token. All the OAuth parameters 385 are added by default. But if provided through this argument, any 386 default parameters will be overwritten. For e.g. a default parameter 387 oauth_version 1.0 can be overwritten if 388 extra_parameters = {'oauth_version': '2.0'} 389 request_url: Request token URL. The default is 390 'https://www.google.com/accounts/OAuthGetRequestToken'. 391 oauth_callback: str (optional) If set, it is assume the client is using 392 the OAuth v1.0a protocol where the callback url is sent in the 393 request token step. If the oauth_callback is also set in 394 extra_params, this value will override that one. 395 396 Returns: 397 The fetched request token as a gdata.auth.OAuthToken object. 398 399 Raises: 400 FetchingOAuthRequestTokenFailed if the server responded to the request 401 with an error. 402 """ 403 if scopes is None: 404 scopes = lookup_scopes(self.service) 405 if not isinstance(scopes, (list, tuple)): 406 scopes = [scopes,] 407 if oauth_callback: 408 if extra_parameters is not None: 409 extra_parameters['oauth_callback'] = oauth_callback 410 else: 411 extra_parameters = {'oauth_callback': oauth_callback} 412 request_token_url = gdata.auth.GenerateOAuthRequestTokenUrl( 413 self._oauth_input_params, scopes, 414 request_token_url=request_url, 415 extra_parameters=extra_parameters) 416 response = self.http_client.request('GET', str(request_token_url)) 417 if response.status == 200: 418 token = gdata.auth.OAuthToken() 419 token.set_token_string(response.read()) 420 token.scopes = scopes 421 token.oauth_input_params = self._oauth_input_params 422 self.SetOAuthToken(token) 423 return token 424 error = { 425 'status': response.status, 426 'reason': 'Non 200 response on fetch request token', 427 'body': response.read() 428 } 429 raise FetchingOAuthRequestTokenFailed(error) 430 431 def SetOAuthToken(self, oauth_token): 432 """Attempts to set the current token and add it to the token store. 433 434 The oauth_token can be any OAuth token i.e. unauthorized request token, 435 authorized request token or access token. 436 This method also attempts to add the token to the token store. 437 Use this method any time you want the current token to point to the 438 oauth_token passed. For e.g. call this method with the request token 439 you receive from FetchOAuthRequestToken. 440 441 Args: 442 request_token: gdata.auth.OAuthToken OAuth request token. 443 """ 444 if self.auto_set_current_token: 445 self.current_token = oauth_token 446 if self.auto_store_tokens: 447 self.token_store.add_token(oauth_token) 448 449 def GenerateOAuthAuthorizationURL( 450 self, request_token=None, callback_url=None, extra_params=None, 451 include_scopes_in_callback=False, 452 scopes_param_prefix=OAUTH_SCOPE_URL_PARAM_NAME, 453 request_url='%s/accounts/OAuthAuthorizeToken' % AUTH_SERVER_HOST): 454 """Generates URL at which user will login to authorize the request token. 455 456 Args: 457 request_token: gdata.auth.OAuthToken (optional) OAuth request token. 458 If not specified, then the current token will be used if it is of 459 type <gdata.auth.OAuthToken>, else it is found by looking in the 460 token_store by looking for a token for the current scope. 461 callback_url: string (optional) The URL user will be sent to after 462 logging in and granting access. 463 extra_params: dict (optional) Additional parameters to be sent. 464 include_scopes_in_callback: Boolean (default=False) if set to True, and 465 if 'callback_url' is present, the 'callback_url' will be modified to 466 include the scope(s) from the request token as a URL parameter. The 467 key for the 'callback' URL's scope parameter will be 468 OAUTH_SCOPE_URL_PARAM_NAME. The benefit of including the scope URL as 469 a parameter to the 'callback' URL, is that the page which receives 470 the OAuth token will be able to tell which URLs the token grants 471 access to. 472 scopes_param_prefix: string (default='oauth_token_scope') The URL 473 parameter key which maps to the list of valid scopes for the token. 474 This URL parameter will be included in the callback URL along with 475 the scopes of the token as value if include_scopes_in_callback=True. 476 request_url: Authorization URL. The default is 477 'https://www.google.com/accounts/OAuthAuthorizeToken'. 478 Returns: 479 A string URL at which the user is required to login. 480 481 Raises: 482 NonOAuthToken if the user's request token is not an OAuth token or if a 483 request token was not available. 484 """ 485 if request_token and not isinstance(request_token, gdata.auth.OAuthToken): 486 raise NonOAuthToken 487 if not request_token: 488 if isinstance(self.current_token, gdata.auth.OAuthToken): 489 request_token = self.current_token 490 else: 491 current_scopes = lookup_scopes(self.service) 492 if current_scopes: 493 token = self.token_store.find_token(current_scopes[0]) 494 if isinstance(token, gdata.auth.OAuthToken): 495 request_token = token 496 if not request_token: 497 raise NonOAuthToken 498 return str(gdata.auth.GenerateOAuthAuthorizationUrl( 499 request_token, 500 authorization_url=request_url, 501 callback_url=callback_url, extra_params=extra_params, 502 include_scopes_in_callback=include_scopes_in_callback, 503 scopes_param_prefix=scopes_param_prefix)) 504 505 def UpgradeToOAuthAccessToken(self, authorized_request_token=None, 506 request_url='%s/accounts/OAuthGetAccessToken' \ 507 % AUTH_SERVER_HOST, oauth_version='1.0', 508 oauth_verifier=None): 509 """Upgrades the authorized request token to an access token and returns it 510 511 Args: 512 authorized_request_token: gdata.auth.OAuthToken (optional) OAuth request 513 token. If not specified, then the current token will be used if it is 514 of type <gdata.auth.OAuthToken>, else it is found by looking in the 515 token_store by looking for a token for the current scope. 516 request_url: Access token URL. The default is 517 'https://www.google.com/accounts/OAuthGetAccessToken'. 518 oauth_version: str (default='1.0') oauth_version parameter. All other 519 'oauth_' parameters are added by default. This parameter too, is 520 added by default but here you can override it's value. 521 oauth_verifier: str (optional) If present, it is assumed that the client 522 will use the OAuth v1.0a protocol which includes passing the 523 oauth_verifier (as returned by the SP) in the access token step. 524 525 Returns: 526 Access token 527 528 Raises: 529 NonOAuthToken if the user's authorized request token is not an OAuth 530 token or if an authorized request token was not available. 531 TokenUpgradeFailed if the server responded to the request with an 532 error. 533 """ 534 if (authorized_request_token and 535 not isinstance(authorized_request_token, gdata.auth.OAuthToken)): 536 raise NonOAuthToken 537 if not authorized_request_token: 538 if isinstance(self.current_token, gdata.auth.OAuthToken): 539 authorized_request_token = self.current_token 540 else: 541 current_scopes = lookup_scopes(self.service) 542 if current_scopes: 543 token = self.token_store.find_token(current_scopes[0]) 544 if isinstance(token, gdata.auth.OAuthToken): 545 authorized_request_token = token 546 if not authorized_request_token: 547 raise NonOAuthToken 548 access_token_url = gdata.auth.GenerateOAuthAccessTokenUrl( 549 authorized_request_token, 550 self._oauth_input_params, 551 access_token_url=request_url, 552 oauth_version=oauth_version, 553 oauth_verifier=oauth_verifier) 554 response = self.http_client.request('GET', str(access_token_url)) 555 if response.status == 200: 556 token = gdata.auth.OAuthTokenFromHttpBody(response.read()) 557 token.scopes = authorized_request_token.scopes 558 token.oauth_input_params = authorized_request_token.oauth_input_params 559 self.SetOAuthToken(token) 560 return token 561 else: 562 raise TokenUpgradeFailed({'status': response.status, 563 'reason': 'Non 200 response on upgrade', 564 'body': response.read()}) 565 566 def RevokeOAuthToken(self, request_url='%s/accounts/AuthSubRevokeToken' % \ 567 AUTH_SERVER_HOST): 568 """Revokes an existing OAuth token. 569 570 request_url: Token revoke URL. The default is 571 'https://www.google.com/accounts/AuthSubRevokeToken'. 572 Raises: 573 NonOAuthToken if the user's auth token is not an OAuth token. 574 RevokingOAuthTokenFailed if request for revoking an OAuth token failed. 575 """ 576 scopes = lookup_scopes(self.service) 577 token = self.token_store.find_token(scopes[0]) 578 if not isinstance(token, gdata.auth.OAuthToken): 579 raise NonOAuthToken 580 581 response = token.perform_request(self.http_client, 'GET', request_url, 582 headers={'Content-Type':'application/x-www-form-urlencoded'}) 583 if response.status == 200: 584 self.token_store.remove_token(token) 585 else: 586 raise RevokingOAuthTokenFailed 587 588 def GetAuthSubToken(self): 589 """Returns the AuthSub token as a string. 590 591 If the token is an gdta.auth.AuthSubToken, the Authorization Label 592 ("AuthSub token") is removed. 593 594 This method examines the current_token to see if it is an AuthSubToken 595 or SecureAuthSubToken. If not, it searches the token_store for a token 596 which matches the current scope. 597 598 The current scope is determined by the service name string member. 599 600 Returns: 601 If the current_token is set to an AuthSubToken/SecureAuthSubToken, 602 return the token string. If there is no current_token, a token string 603 for a token which matches the service object's default scope is returned. 604 If there are no tokens valid for the scope, returns None. 605 """ 606 if isinstance(self.current_token, gdata.auth.AuthSubToken): 607 return self.current_token.get_token_string() 608 current_scopes = lookup_scopes(self.service) 609 if current_scopes: 610 token = self.token_store.find_token(current_scopes[0]) 611 if isinstance(token, gdata.auth.AuthSubToken): 612 return token.get_token_string() 613 else: 614 token = self.token_store.find_token(atom.token_store.SCOPE_ALL) 615 if isinstance(token, gdata.auth.ClientLoginToken): 616 return token.get_token_string() 617 return None 618 619 def SetAuthSubToken(self, token, scopes=None, rsa_key=None): 620 """Sets the token sent in requests to an AuthSub token. 621 622 Sets the current_token and attempts to add the token to the token_store. 623 624 Only use this method if you have received a token from the AuthSub 625 service. The auth token is set automatically when UpgradeToSessionToken() 626 is used. See documentation for Google AuthSub here: 627 http://code.google.com/apis/accounts/AuthForWebApps.html 628 629 Args: 630 token: gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken or string 631 The token returned by the AuthSub service. If the token is an 632 AuthSubToken or SecureAuthSubToken, the scope information stored in 633 the token is used. If the token is a string, the scopes parameter is 634 used to determine the valid scopes. 635 scopes: list of URLs for which the token is valid. This is only used 636 if the token parameter is a string. 637 rsa_key: string (optional) Private key required for RSA_SHA1 signature 638 method. This parameter is necessary if the token is a string 639 representing a secure token. 640 """ 641 if not isinstance(token, gdata.auth.AuthSubToken): 642 token_string = token 643 if rsa_key: 644 token = gdata.auth.SecureAuthSubToken(rsa_key) 645 else: 646 token = gdata.auth.AuthSubToken() 647 648 token.set_token_string(token_string) 649 650 # If no scopes were set for the token, use the scopes passed in, or 651 # try to determine the scopes based on the current service name. If 652 # all else fails, set the token to match all requests. 653 if not token.scopes: 654 if scopes is None: 655 scopes = lookup_scopes(self.service) 656 if scopes is None: 657 scopes = [atom.token_store.SCOPE_ALL] 658 token.scopes = scopes 659 if self.auto_set_current_token: 660 self.current_token = token 661 if self.auto_store_tokens: 662 self.token_store.add_token(token) 663 664 def GetClientLoginToken(self): 665 """Returns the token string for the current token or a token matching the 666 service scope. 667 668 If the current_token is a ClientLoginToken, the token string for 669 the current token is returned. If the current_token is not set, this method 670 searches for a token in the token_store which is valid for the service 671 object's current scope. 672 673 The current scope is determined by the service name string member. 674 The token string is the end of the Authorization header, it doesn not 675 include the ClientLogin label. 676 """ 677 if isinstance(self.current_token, gdata.auth.ClientLoginToken): 678 return self.current_token.get_token_string() 679 current_scopes = lookup_scopes(self.service) 680 if current_scopes: 681 token = self.token_store.find_token(current_scopes[0]) 682 if isinstance(token, gdata.auth.ClientLoginToken): 683 return token.get_token_string() 684 else: 685 token = self.token_store.find_token(atom.token_store.SCOPE_ALL) 686 if isinstance(token, gdata.auth.ClientLoginToken): 687 return token.get_token_string() 688 return None 689 690 def SetClientLoginToken(self, token, scopes=None): 691 """Sets the token sent in requests to a ClientLogin token. 692 693 This method sets the current_token to a new ClientLoginToken and it 694 also attempts to add the ClientLoginToken to the token_store. 695 696 Only use this method if you have received a token from the ClientLogin 697 service. The auth_token is set automatically when ProgrammaticLogin() 698 is used. See documentation for Google ClientLogin here: 699 http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html 700 701 Args: 702 token: string or instance of a ClientLoginToken. 703 """ 704 if not isinstance(token, gdata.auth.ClientLoginToken): 705 token_string = token 706 token = gdata.auth.ClientLoginToken() 707 token.set_token_string(token_string) 708 709 if not token.scopes: 710 if scopes is None: 711 scopes = lookup_scopes(self.service) 712 if scopes is None: 713 scopes = [atom.token_store.SCOPE_ALL] 714 token.scopes = scopes 715 if self.auto_set_current_token: 716 self.current_token = token 717 if self.auto_store_tokens: 718 self.token_store.add_token(token) 719 720 # Private methods to create the source property. 721 def __GetSource(self): 722 return self.__source 723 724 def __SetSource(self, new_source): 725 self.__source = new_source 726 # Update the UserAgent header to include the new application name. 727 self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % ( 728 self.__source,) 729 730 source = property(__GetSource, __SetSource, 731 doc="""The source is the name of the application making the request. 732 It should be in the form company_id-app_name-app_version""") 733 734 # Authentication operations 735 736 def ProgrammaticLogin(self, captcha_token=None, captcha_response=None): 737 """Authenticates the user and sets the GData Auth token. 738 739 Login retreives a temporary auth token which must be used with all 740 requests to GData services. The auth token is stored in the GData client 741 object. 742 743 Login is also used to respond to a captcha challenge. If the user's login 744 attempt failed with a CaptchaRequired error, the user can respond by 745 calling Login with the captcha token and the answer to the challenge. 746 747 Args: 748 captcha_token: string (optional) The identifier for the captcha challenge 749 which was presented to the user. 750 captcha_response: string (optional) The user's answer to the captch 751 challenge. 752 753 Raises: 754 CaptchaRequired if the login service will require a captcha response 755 BadAuthentication if the login service rejected the username or password 756 Error if the login service responded with a 403 different from the above 757 """ 758 request_body = gdata.auth.generate_client_login_request_body(self.email, 759 self.password, self.service, self.source, self.account_type, 760 captcha_token, captcha_response) 761 762 # If the user has defined their own authentication service URL, 763 # send the ClientLogin requests to this URL: 764 if not self.auth_service_url: 765 auth_request_url = AUTH_SERVER_HOST + '/accounts/ClientLogin' 766 else: 767 auth_request_url = self.auth_service_url 768 769 auth_response = self.http_client.request('POST', auth_request_url, 770 data=request_body, 771 headers={'Content-Type':'application/x-www-form-urlencoded'}) 772 response_body = auth_response.read() 773 774 if auth_response.status == 200: 775 # TODO: insert the token into the token_store directly. 776 self.SetClientLoginToken( 777 gdata.auth.get_client_login_token(response_body)) 778 self.__captcha_token = None 779 self.__captcha_url = None 780 781 elif auth_response.status == 403: 782 # Examine each line to find the error type and the captcha token and 783 # captch URL if they are present. 784 captcha_parameters = gdata.auth.get_captcha_challenge(response_body, 785 captcha_base_url='%s/accounts/' % AUTH_SERVER_HOST) 786 if captcha_parameters: 787 self.__captcha_token = captcha_parameters['token'] 788 self.__captcha_url = captcha_parameters['url'] 789 raise CaptchaRequired, 'Captcha Required' 790 elif response_body.splitlines()[0] == 'Error=BadAuthentication': 791 self.__captcha_token = None 792 self.__captcha_url = None 793 raise BadAuthentication, 'Incorrect username or password' 794 else: 795 self.__captcha_token = None 796 self.__captcha_url = None 797 raise Error, 'Server responded with a 403 code' 798 elif auth_response.status == 302: 799 self.__captcha_token = None 800 self.__captcha_url = None 801 # Google tries to redirect all bad URLs back to 802 # http://www.google.<locale>. If a redirect 803 # attempt is made, assume the user has supplied an incorrect authentication URL 804 raise BadAuthenticationServiceURL, 'Server responded with a 302 code.' 805 806 def ClientLogin(self, username, password, account_type=None, service=None, 807 auth_service_url=None, source=None, captcha_token=None, 808 captcha_response=None): 809 """Convenience method for authenticating using ProgrammaticLogin. 810 811 Sets values for email, password, and other optional members. 812 813 Args: 814 username: 815 password: 816 account_type: string (optional) 817 service: string (optional) 818 auth_service_url: string (optional) 819 captcha_token: string (optional) 820 captcha_response: string (optional) 821 """ 822 self.email = username 823 self.password = password 824 825 if account_type: 826 self.account_type = account_type 827 if service: 828 self.service = service 829 if source: 830 self.source = source 831 if auth_service_url: 832 self.auth_service_url = auth_service_url 833 834 self.ProgrammaticLogin(captcha_token, captcha_response) 835 836 def GenerateAuthSubURL(self, next, scope, secure=False, session=True, 837 domain='default'): 838 """Generate a URL at which the user will login and be redirected back. 839 840 Users enter their credentials on a Google login page and a token is sent 841 to the URL specified in next. See documentation for AuthSub login at: 842 http://code.google.com/apis/accounts/docs/AuthSub.html 843 844 Args: 845 next: string The URL user will be sent to after logging in. 846 scope: string or list of strings. The URLs of the services to be 847 accessed. 848 secure: boolean (optional) Determines whether or not the issued token 849 is a secure token. 850 session: boolean (optional) Determines whether or not the issued token 851 can be upgraded to a session token. 852 """ 853 if not isinstance(scope, (list, tuple)): 854 scope = (scope,) 855 return gdata.auth.generate_auth_sub_url(next, scope, secure=secure, 856 session=session, 857 request_url='%s/accounts/AuthSubRequest' % AUTH_SERVER_HOST, 858 domain=domain) 859 860 def UpgradeToSessionToken(self, token=None): 861 """Upgrades a single use AuthSub token to a session token. 862 863 Args: 864 token: A gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken 865 (optional) which is good for a single use but can be upgraded 866 to a session token. If no token is passed in, the token 867 is found by looking in the token_store by looking for a token 868 for the current scope. 869 870 Raises: 871 NonAuthSubToken if the user's auth token is not an AuthSub token 872 TokenUpgradeFailed if the server responded to the request with an 873 error. 874 """ 875 if token is None: 876 scopes = lookup_scopes(self.service) 877 if scopes: 878 token = self.token_store.find_token(scopes[0]) 879 else: 880 token = self.token_store.find_token(atom.token_store.SCOPE_ALL) 881 if not isinstance(token, gdata.auth.AuthSubToken): 882 raise NonAuthSubToken 883 884 self.SetAuthSubToken(self.upgrade_to_session_token(token)) 885 886 def upgrade_to_session_token(self, token): 887 """Upgrades a single use AuthSub token to a session token. 888 889 Args: 890 token: A gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken 891 which is good for a single use but can be upgraded to a 892 session token. 893 894 Returns: 895 The upgraded token as a gdata.auth.AuthSubToken object. 896 897 Raises: 898 TokenUpgradeFailed if the server responded to the request with an 899 error. 900 """ 901 response = token.perform_request(self.http_client, 'GET', 902 AUTH_SERVER_HOST + '/accounts/AuthSubSessionToken', 903 headers={'Content-Type':'application/x-www-form-urlencoded'}) 904 response_body = response.read() 905 if response.status == 200: 906 token.set_token_string( 907 gdata.auth.token_from_http_body(response_body)) 908 return token 909 else: 910 raise TokenUpgradeFailed({'status': response.status, 911 'reason': 'Non 200 response on upgrade', 912 'body': response_body}) 913 914 def RevokeAuthSubToken(self): 915 """Revokes an existing AuthSub token. 916 917 Raises: 918 NonAuthSubToken if the user's auth token is not an AuthSub token 919 """ 920 scopes = lookup_scopes(self.service) 921 token = self.token_store.find_token(scopes[0]) 922 if not isinstance(token, gdata.auth.AuthSubToken): 923 raise NonAuthSubToken 924 925 response = token.perform_request(self.http_client, 'GET', 926 AUTH_SERVER_HOST + '/accounts/AuthSubRevokeToken', 927 headers={'Content-Type':'application/x-www-form-urlencoded'}) 928 if response.status == 200: 929 self.token_store.remove_token(token) 930 931 def AuthSubTokenInfo(self): 932 """Fetches the AuthSub token's metadata from the server. 933 934 Raises: 935 NonAuthSubToken if the user's auth token is not an AuthSub token 936 """ 937 scopes = lookup_scopes(self.service) 938 token = self.token_store.find_token(scopes[0]) 939 if not isinstance(token, gdata.auth.AuthSubToken): 940 raise NonAuthSubToken 941 942 response = token.perform_request(self.http_client, 'GET', 943 AUTH_SERVER_HOST + '/accounts/AuthSubTokenInfo', 944 headers={'Content-Type':'application/x-www-form-urlencoded'}) 945 result_body = response.read() 946 if response.status == 200: 947 return result_body 948 else: 949 raise RequestError, {'status': response.status, 950 'body': result_body} 951 952 def GetWithRetries(self, uri, extra_headers=None, redirects_remaining=4, 953 encoding='UTF-8', converter=None, num_retries=DEFAULT_NUM_RETRIES, 954 delay=DEFAULT_DELAY, backoff=DEFAULT_BACKOFF, logger=None): 955 """This is a wrapper method for Get with retring capability. 956 957 To avoid various errors while retrieving bulk entities by retring 958 specified times. 959 960 Note this method relies on the time module and so may not be usable 961 by default in Python2.2. 962 963 Args: 964 num_retries: integer The retry count. 965 delay: integer The initial delay for retring. 966 backoff: integer how much the delay should lengthen after each failure. 967 logger: an object which has a debug(str) method to receive logging 968 messages. Recommended that you pass in the logging module. 969 Raises: 970 ValueError if any of the parameters has an invalid value. 971 RanOutOfTries on failure after number of retries. 972 """ 973 # Moved import for time module inside this method since time is not a 974 # default module in Python2.2. This method will not be usable in 975 # Python2.2. 976 import time 977 if backoff <= 1: 978 raise ValueError("backoff must be greater than 1") 979 num_retries = int(num_retries) 980 981 if num_retries < 0: 982 raise ValueError("num_retries must be 0 or greater") 983 984 if delay <= 0: 985 raise ValueError("delay must be greater than 0") 986 987 # Let's start 988 mtries, mdelay = num_retries, delay 989 while mtries > 0: 990 if mtries != num_retries: 991 if logger: 992 logger.debug("Retrying...") 993 try: 994 rv = self.Get(uri, extra_headers=extra_headers, 995 redirects_remaining=redirects_remaining, 996 encoding=encoding, converter=converter) 997 except (SystemExit, RequestError): 998 # Allow these errors 999 raise 1000 except Exception, e: 1001 if logger: 1002 logger.debug(e) 1003 mtries -= 1 1004 time.sleep(mdelay) 1005 mdelay *= backoff 1006 else: 1007 # This is the right path. 1008 if logger: 1009 logger.debug("Succeeeded...") 1010 return rv 1011 raise RanOutOfTries('Ran out of tries.') 1012 1013 # CRUD operations 1014 def Get(self, uri, extra_headers=None, redirects_remaining=4, 1015 encoding='UTF-8', converter=None): 1016 """Query the GData API with the given URI 1017 1018 The uri is the portion of the URI after the server value 1019 (ex: www.google.com). 1020 1021 To perform a query against Google Base, set the server to 1022 'base.google.com' and set the uri to '/base/feeds/...', where ... is 1023 your query. For example, to find snippets for all digital cameras uri 1024 should be set to: '/base/feeds/snippets?bq=digital+camera' 1025 1026 Args: 1027 uri: string The query in the form of a URI. Example: 1028 '/base/feeds/snippets?bq=digital+camera'. 1029 extra_headers: dictionary (optional) Extra HTTP headers to be included 1030 in the GET request. These headers are in addition to 1031 those stored in the client's additional_headers property. 1032 The client automatically sets the Content-Type and 1033 Authorization headers. 1034 redirects_remaining: int (optional) Tracks the number of additional 1035 redirects this method will allow. If the service object receives 1036 a redirect and remaining is 0, it will not follow the redirect. 1037 This was added to avoid infinite redirect loops. 1038 encoding: string (optional) The character encoding for the server's 1039 response. Default is UTF-8 1040 converter: func (optional) A function which will transform 1041 the server's results before it is returned. Example: use 1042 GDataFeedFromString to parse the server response as if it 1043 were a GDataFeed. 1044 1045 Returns: 1046 If there is no ResultsTransformer specified in the call, a GDataFeed 1047 or GDataEntry depending on which is sent from the server. If the 1048 response is niether a feed or entry and there is no ResultsTransformer, 1049 return a string. If there is a ResultsTransformer, the returned value 1050 will be that of the ResultsTransformer function. 1051 """ 1052 1053 if extra_headers is None: 1054 extra_headers = {} 1055 1056 if self.__gsessionid is not None: 1057 if uri.find('gsessionid=') < 0: 1058 if uri.find('?') > -1: 1059 uri += '&gsessionid=%s' % (self.__gsessionid,) 1060 else: 1061 uri += '?gsessionid=%s' % (self.__gsessionid,) 1062 1063 server_response = self.request('GET', uri, 1064 headers=extra_headers) 1065 result_body = server_response.read() 1066 1067 if server_response.status == 200: 1068 if converter: 1069 return converter(result_body) 1070 # There was no ResultsTransformer specified, so try to convert the 1071 # server's response into a GDataFeed. 1072 feed = gdata.GDataFeedFromString(result_body) 1073 if not feed: 1074 # If conversion to a GDataFeed failed, try to convert the server's 1075 # response to a GDataEntry. 1076 entry = gdata.GDataEntryFromString(result_body) 1077 if not entry: 1078 # The server's response wasn't a feed, or an entry, so return the 1079 # response body as a string. 1080 return result_body 1081 return entry 1082 return feed 1083 elif server_response.status == 302: 1084 if redirects_remaining > 0: 1085 location = (server_response.getheader('Location') 1086 or server_response.getheader('location')) 1087 if location is not None: 1088 m = re.compile('[\?\&]gsessionid=(\w*)').search(location) 1089 if m is not None: 1090 self.__gsessionid = m.group(1) 1091 return GDataService.Get(self, location, extra_headers, redirects_remaining - 1, 1092 encoding=encoding, converter=converter) 1093 else: 1094 raise RequestError, {'status': server_response.status, 1095 'reason': '302 received without Location header', 1096 'body': result_body} 1097 else: 1098 raise RequestError, {'status': server_response.status, 1099 'reason': 'Redirect received, but redirects_remaining <= 0', 1100 'body': result_body} 1101 else: 1102 raise RequestError, {'status': server_response.status, 1103 'reason': server_response.reason, 'body': result_body} 1104 1105 def GetMedia(self, uri, extra_headers=None): 1106 """Returns a MediaSource containing media and its metadata from the given 1107 URI string. 1108 """ 1109 response_handle = self.request('GET', uri, 1110 headers=extra_headers) 1111 return gdata.MediaSource(response_handle, response_handle.getheader( 1112 'Content-Type'), 1113 response_handle.getheader('Content-Length')) 1114 1115 def GetEntry(self, uri, extra_headers=None): 1116 """Query the GData API with the given URI and receive an Entry. 1117 1118 See also documentation for gdata.service.Get 1119 1120 Args: 1121 uri: string The query in the form of a URI. Example: 1122 '/base/feeds/snippets?bq=digital+camera'. 1123 extra_headers: dictionary (optional) Extra HTTP headers to be included 1124 in the GET request. These headers are in addition to 1125 those stored in the client's additional_headers property. 1126 The client automatically sets the Content-Type and 1127 Authorization headers. 1128 1129 Returns: 1130 A GDataEntry built from the XML in the server's response. 1131 """ 1132 1133 result = GDataService.Get(self, uri, extra_headers, 1134 converter=atom.EntryFromString) 1135 if isinstance(result, atom.Entry): 1136 return result 1137 else: 1138 raise UnexpectedReturnType, 'Server did not send an entry' 1139 1140 def GetFeed(self, uri, extra_headers=None, 1141 converter=gdata.GDataFeedFromString): 1142 """Query the GData API with the given URI and receive a Feed. 1143 1144 See also documentation for gdata.service.Get 1145 1146 Args: 1147 uri: string The query in the form of a URI. Example: 1148 '/base/feeds/snippets?bq=digital+camera'. 1149 extra_headers: dictionary (optional) Extra HTTP headers to be included 1150 in the GET request. These headers are in addition to 1151 those stored in the client's additional_headers property. 1152 The client automatically sets the Content-Type and 1153 Authorization headers. 1154 1155 Returns: 1156 A GDataFeed built from the XML in the server's response. 1157 """ 1158 1159 result = GDataService.Get(self, uri, extra_headers, converter=converter) 1160 if isinstance(result, atom.Feed): 1161 return result 1162 else: 1163 raise UnexpectedReturnType, 'Server did not send a feed' 1164 1165 def GetNext(self, feed): 1166 """Requests the next 'page' of results in the feed. 1167 1168 This method uses the feed's next link to request an additional feed 1169 and uses the class of the feed to convert the results of the GET request. 1170 1171 Args: 1172 feed: atom.Feed or a subclass. The feed should contain a next link and 1173 the type of the feed will be applied to the results from the 1174 server. The new feed which is returned will be of the same class 1175 as this feed which was passed in. 1176 1177 Returns: 1178 A new feed representing the next set of results in the server's feed. 1179 The type of this feed will match that of the feed argument. 1180 """ 1181 next_link = feed.GetNextLink() 1182 # Create a closure which will convert an XML string to the class of 1183 # the feed object passed in. 1184 def ConvertToFeedClass(xml_string): 1185 return atom.CreateClassFromXMLString(feed.__class__, xml_string) 1186 # Make a GET request on the next link and use the above closure for the 1187 # converted which processes the XML string from the server. 1188 if next_link and next_link.href: 1189 return GDataService.Get(self, next_link.href, 1190 converter=ConvertToFeedClass) 1191 else: 1192 return None 1193 1194 def Post(self, data, uri, extra_headers=None, url_params=None, 1195 escape_params=True, redirects_remaining=4, media_source=None, 1196 converter=None): 1197 """Insert or update data into a GData service at the given URI. 1198 1199 Args: 1200 data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry The 1201 XML to be sent to the uri. 1202 uri: string The location (feed) to which the data should be inserted. 1203 Example: '/base/feeds/items'. 1204 extra_headers: dict (optional) HTTP headers which are to be included. 1205 The client automatically sets the Content-Type, 1206 Authorization, and Content-Length headers. 1207 url_params: dict (optional) Additional URL parameters to be included 1208 in the URI. These are translated into query arguments 1209 in the form '&dict_key=value&...'. 1210 Example: {'max-results': '250'} becomes &max-results=250 1211 escape_params: boolean (optional) If false, the calling code has already 1212 ensured that the query will form a valid URL (all 1213 reserved characters have been escaped). If true, this 1214 method will escape the query and any URL parameters 1215 provided. 1216 media_source: MediaSource (optional) Container for the media to be sent 1217 along with the entry, if provided. 1218 converter: func (optional) A function which will be executed on the 1219 server's response. Often this is a function like 1220 GDataEntryFromString which will parse the body of the server's 1221 response and return a GDataEntry. 1222 1223 Returns: 1224 If the post succeeded, this method will return a GDataFeed, GDataEntry, 1225 or the results of running converter on the server's result body (if 1226 converter was specified). 1227 """ 1228 return GDataService.PostOrPut(self, 'POST', data, uri, 1229 extra_headers=extra_headers, url_params=url_params, 1230 escape_params=escape_params, redirects_remaining=redirects_remaining, 1231 media_source=media_sour…
Large files files are truncated, but you can click here to view the full file