| #!/usr/bin/python |
| # |
| # Copyright (C) 2006,2008 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. |
| |
| |
| """GDataService provides CRUD ops. and programmatic login for GData services. |
| |
| Error: A base exception class for all exceptions in the gdata_client |
| module. |
| |
| CaptchaRequired: This exception is thrown when a login attempt results in a |
| captcha challenge from the ClientLogin service. When this |
| exception is thrown, the captcha_token and captcha_url are |
| set to the values provided in the server's response. |
| |
| BadAuthentication: Raised when a login attempt is made with an incorrect |
| username or password. |
| |
| NotAuthenticated: Raised if an operation requiring authentication is called |
| before a user has authenticated. |
| |
| NonAuthSubToken: Raised if a method to modify an AuthSub token is used when |
| the user is either not authenticated or is authenticated |
| through another authentication mechanism. |
| |
| NonOAuthToken: Raised if a method to modify an OAuth token is used when the |
| user is either not authenticated or is authenticated through |
| another authentication mechanism. |
| |
| RequestError: Raised if a CRUD request returned a non-success code. |
| |
| UnexpectedReturnType: Raised if the response from the server was not of the |
| desired type. For example, this would be raised if the |
| server sent a feed when the client requested an entry. |
| |
| GDataService: Encapsulates user credentials needed to perform insert, update |
| and delete operations with the GData API. An instance can |
| perform user authentication, query, insertion, deletion, and |
| update. |
| |
| Query: Eases query URI creation by allowing URI parameters to be set as |
| dictionary attributes. For example a query with a feed of |
| '/base/feeds/snippets' and ['bq'] set to 'digital camera' will |
| produce '/base/feeds/snippets?bq=digital+camera' when .ToUri() is |
| called on it. |
| """ |
| |
| |
| __author__ = 'api.jscudder (Jeffrey Scudder)' |
| |
| import re |
| import urllib |
| import urlparse |
| try: |
| from xml.etree import cElementTree as ElementTree |
| except ImportError: |
| try: |
| import cElementTree as ElementTree |
| except ImportError: |
| try: |
| from xml.etree import ElementTree |
| except ImportError: |
| from elementtree import ElementTree |
| import atom.service |
| import gdata |
| import atom |
| import atom.http_interface |
| import atom.token_store |
| import gdata.auth |
| import gdata.gauth |
| |
| |
| AUTH_SERVER_HOST = 'https://www.google.com' |
| |
| |
| # When requesting an AuthSub token, it is often helpful to track the scope |
| # which is being requested. One way to accomplish this is to add a URL |
| # parameter to the 'next' URL which contains the requested scope. This |
| # constant is the default name (AKA key) for the URL parameter. |
| SCOPE_URL_PARAM_NAME = 'authsub_token_scope' |
| # When requesting an OAuth access token or authorization of an existing OAuth |
| # request token, it is often helpful to track the scope(s) which is/are being |
| # requested. One way to accomplish this is to add a URL parameter to the |
| # 'callback' URL which contains the requested scope. This constant is the |
| # default name (AKA key) for the URL parameter. |
| OAUTH_SCOPE_URL_PARAM_NAME = 'oauth_token_scope' |
| # Maps the service names used in ClientLogin to scope URLs. |
| CLIENT_LOGIN_SCOPES = gdata.gauth.AUTH_SCOPES |
| # Default parameters for GDataService.GetWithRetries method |
| DEFAULT_NUM_RETRIES = 3 |
| DEFAULT_DELAY = 1 |
| DEFAULT_BACKOFF = 2 |
| |
| |
| def lookup_scopes(service_name): |
| """Finds the scope URLs for the desired service. |
| |
| In some cases, an unknown service may be used, and in those cases this |
| function will return None. |
| """ |
| if service_name in CLIENT_LOGIN_SCOPES: |
| return CLIENT_LOGIN_SCOPES[service_name] |
| return None |
| |
| |
| # Module level variable specifies which module should be used by GDataService |
| # objects to make HttpRequests. This setting can be overridden on each |
| # instance of GDataService. |
| # This module level variable is deprecated. Reassign the http_client member |
| # of a GDataService object instead. |
| http_request_handler = atom.service |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class CaptchaRequired(Error): |
| pass |
| |
| |
| class BadAuthentication(Error): |
| pass |
| |
| |
| class NotAuthenticated(Error): |
| pass |
| |
| |
| class NonAuthSubToken(Error): |
| pass |
| |
| |
| class NonOAuthToken(Error): |
| pass |
| |
| |
| class RequestError(Error): |
| pass |
| |
| |
| class UnexpectedReturnType(Error): |
| pass |
| |
| |
| class BadAuthenticationServiceURL(Error): |
| pass |
| |
| |
| class FetchingOAuthRequestTokenFailed(RequestError): |
| pass |
| |
| |
| class TokenUpgradeFailed(RequestError): |
| pass |
| |
| |
| class RevokingOAuthTokenFailed(RequestError): |
| pass |
| |
| |
| class AuthorizationRequired(Error): |
| pass |
| |
| |
| class TokenHadNoScope(Error): |
| pass |
| |
| |
| class RanOutOfTries(Error): |
| pass |
| |
| |
| class GDataService(atom.service.AtomService): |
| """Contains elements needed for GData login and CRUD request headers. |
| |
| Maintains additional headers (tokens for example) needed for the GData |
| services to allow a user to perform inserts, updates, and deletes. |
| """ |
| # The hander member is deprecated, use http_client instead. |
| handler = None |
| # The auth_token member is deprecated, use the token_store instead. |
| auth_token = None |
| # The tokens dict is deprecated in favor of the token_store. |
| tokens = None |
| |
| def __init__(self, email=None, password=None, account_type='HOSTED_OR_GOOGLE', |
| service=None, auth_service_url=None, source=None, server=None, |
| additional_headers=None, handler=None, tokens=None, |
| http_client=None, token_store=None): |
| """Creates an object of type GDataService. |
| |
| Args: |
| email: string (optional) The user's email address, used for |
| authentication. |
| password: string (optional) The user's password. |
| account_type: string (optional) The type of account to use. Use |
| 'GOOGLE' for regular Google accounts or 'HOSTED' for Google |
| Apps accounts, or 'HOSTED_OR_GOOGLE' to try finding a HOSTED |
| account first and, if it doesn't exist, try finding a regular |
| GOOGLE account. Default value: 'HOSTED_OR_GOOGLE'. |
| service: string (optional) The desired service for which credentials |
| will be obtained. |
| auth_service_url: string (optional) User-defined auth token request URL |
| allows users to explicitly specify where to send auth token requests. |
| source: string (optional) The name of the user's application. |
| server: string (optional) The name of the server to which a connection |
| will be opened. Default value: 'base.google.com'. |
| additional_headers: dictionary (optional) Any additional headers which |
| should be included with CRUD operations. |
| handler: module (optional) This parameter is deprecated and has been |
| replaced by http_client. |
| tokens: This parameter is deprecated, calls should be made to |
| token_store instead. |
| http_client: An object responsible for making HTTP requests using a |
| request method. If none is provided, a new instance of |
| atom.http.ProxiedHttpClient will be used. |
| token_store: Keeps a collection of authorization tokens which can be |
| applied to requests for a specific URLs. Critical methods are |
| find_token based on a URL (atom.url.Url or a string), add_token, |
| and remove_token. |
| """ |
| atom.service.AtomService.__init__(self, http_client=http_client, |
| token_store=token_store) |
| self.email = email |
| self.password = password |
| self.account_type = account_type |
| self.service = service |
| self.auth_service_url = auth_service_url |
| self.server = server |
| self.additional_headers = additional_headers or {} |
| self._oauth_input_params = None |
| self.__SetSource(source) |
| self.__captcha_token = None |
| self.__captcha_url = None |
| self.__gsessionid = None |
| |
| if http_request_handler.__name__ == 'gdata.urlfetch': |
| import gdata.alt.appengine |
| self.http_client = gdata.alt.appengine.AppEngineHttpClient() |
| |
| def _SetSessionId(self, session_id): |
| """Used in unit tests to simulate a 302 which sets a gsessionid.""" |
| self.__gsessionid = session_id |
| |
| # Define properties for GDataService |
| def _SetAuthSubToken(self, auth_token, scopes=None): |
| """Deprecated, use SetAuthSubToken instead.""" |
| self.SetAuthSubToken(auth_token, scopes=scopes) |
| |
| def __SetAuthSubToken(self, auth_token, scopes=None): |
| """Deprecated, use SetAuthSubToken instead.""" |
| self._SetAuthSubToken(auth_token, scopes=scopes) |
| |
| def _GetAuthToken(self): |
| """Returns the auth token used for authenticating requests. |
| |
| Returns: |
| string |
| """ |
| current_scopes = lookup_scopes(self.service) |
| if current_scopes: |
| token = self.token_store.find_token(current_scopes[0]) |
| if hasattr(token, 'auth_header'): |
| return token.auth_header |
| return None |
| |
| def _GetCaptchaToken(self): |
| """Returns a captcha token if the most recent login attempt generated one. |
| |
| The captcha token is only set if the Programmatic Login attempt failed |
| because the Google service issued a captcha challenge. |
| |
| Returns: |
| string |
| """ |
| return self.__captcha_token |
| |
| def __GetCaptchaToken(self): |
| return self._GetCaptchaToken() |
| |
| captcha_token = property(__GetCaptchaToken, |
| doc="""Get the captcha token for a login request.""") |
| |
| def _GetCaptchaURL(self): |
| """Returns the URL of the captcha image if a login attempt generated one. |
| |
| The captcha URL is only set if the Programmatic Login attempt failed |
| because the Google service issued a captcha challenge. |
| |
| Returns: |
| string |
| """ |
| return self.__captcha_url |
| |
| def __GetCaptchaURL(self): |
| return self._GetCaptchaURL() |
| |
| captcha_url = property(__GetCaptchaURL, |
| doc="""Get the captcha URL for a login request.""") |
| |
| def GetGeneratorFromLinkFinder(self, link_finder, func, |
| num_retries=DEFAULT_NUM_RETRIES, |
| delay=DEFAULT_DELAY, |
| backoff=DEFAULT_BACKOFF): |
| """returns a generator for pagination""" |
| yield link_finder |
| next = link_finder.GetNextLink() |
| while next is not None: |
| next_feed = func(str(self.GetWithRetries( |
| next.href, num_retries=num_retries, delay=delay, backoff=backoff))) |
| yield next_feed |
| next = next_feed.GetNextLink() |
| |
| def _GetElementGeneratorFromLinkFinder(self, link_finder, func, |
| num_retries=DEFAULT_NUM_RETRIES, |
| delay=DEFAULT_DELAY, |
| backoff=DEFAULT_BACKOFF): |
| for element in self.GetGeneratorFromLinkFinder(link_finder, func, |
| num_retries=num_retries, |
| delay=delay, |
| backoff=backoff).entry: |
| yield element |
| |
| def GetOAuthInputParameters(self): |
| return self._oauth_input_params |
| |
| def SetOAuthInputParameters(self, signature_method, consumer_key, |
| consumer_secret=None, rsa_key=None, |
| two_legged_oauth=False, requestor_id=None): |
| """Sets parameters required for using OAuth authentication mechanism. |
| |
| NOTE: Though consumer_secret and rsa_key are optional, either of the two |
| is required depending on the value of the signature_method. |
| |
| Args: |
| signature_method: class which provides implementation for strategy class |
| oauth.oauth.OAuthSignatureMethod. Signature method to be used for |
| signing each request. Valid implementations are provided as the |
| constants defined by gdata.auth.OAuthSignatureMethod. Currently |
| they are gdata.auth.OAuthSignatureMethod.RSA_SHA1 and |
| gdata.auth.OAuthSignatureMethod.HMAC_SHA1 |
| consumer_key: string Domain identifying third_party web application. |
| consumer_secret: string (optional) Secret generated during registration. |
| Required only for HMAC_SHA1 signature method. |
| rsa_key: string (optional) Private key required for RSA_SHA1 signature |
| method. |
| two_legged_oauth: boolean (optional) Enables two-legged OAuth process. |
| requestor_id: string (optional) User email adress to make requests on |
| their behalf. This parameter should only be set when two_legged_oauth |
| is True. |
| """ |
| self._oauth_input_params = gdata.auth.OAuthInputParams( |
| signature_method, consumer_key, consumer_secret=consumer_secret, |
| rsa_key=rsa_key, requestor_id=requestor_id) |
| if two_legged_oauth: |
| oauth_token = gdata.auth.OAuthToken( |
| oauth_input_params=self._oauth_input_params) |
| self.SetOAuthToken(oauth_token) |
| |
| def FetchOAuthRequestToken(self, scopes=None, extra_parameters=None, |
| request_url='%s/accounts/OAuthGetRequestToken' % \ |
| AUTH_SERVER_HOST, oauth_callback=None): |
| """Fetches and sets the OAuth request token and returns it. |
| |
| Args: |
| scopes: string or list of string base URL(s) of the service(s) to be |
| accessed. If None, then this method tries to determine the |
| scope(s) from the current service. |
| extra_parameters: dict (optional) key-value pairs as any additional |
| parameters to be included in the URL and signature while making a |
| request for fetching an OAuth request token. All the OAuth parameters |
| are added by default. But if provided through this argument, any |
| default parameters will be overwritten. For e.g. a default parameter |
| oauth_version 1.0 can be overwritten if |
| extra_parameters = {'oauth_version': '2.0'} |
| request_url: Request token URL. The default is |
| 'https://www.google.com/accounts/OAuthGetRequestToken'. |
| oauth_callback: str (optional) If set, it is assume the client is using |
| the OAuth v1.0a protocol where the callback url is sent in the |
| request token step. If the oauth_callback is also set in |
| extra_params, this value will override that one. |
| |
| Returns: |
| The fetched request token as a gdata.auth.OAuthToken object. |
| |
| Raises: |
| FetchingOAuthRequestTokenFailed if the server responded to the request |
| with an error. |
| """ |
| if scopes is None: |
| scopes = lookup_scopes(self.service) |
| if not isinstance(scopes, (list, tuple)): |
| scopes = [scopes,] |
| if oauth_callback: |
| if extra_parameters is not None: |
| extra_parameters['oauth_callback'] = oauth_callback |
| else: |
| extra_parameters = {'oauth_callback': oauth_callback} |
| request_token_url = gdata.auth.GenerateOAuthRequestTokenUrl( |
| self._oauth_input_params, scopes, |
| request_token_url=request_url, |
| extra_parameters=extra_parameters) |
| response = self.http_client.request('GET', str(request_token_url)) |
| if response.status == 200: |
| token = gdata.auth.OAuthToken() |
| token.set_token_string(response.read()) |
| token.scopes = scopes |
| token.oauth_input_params = self._oauth_input_params |
| self.SetOAuthToken(token) |
| return token |
| error = { |
| 'status': response.status, |
| 'reason': 'Non 200 response on fetch request token', |
| 'body': response.read() |
| } |
| raise FetchingOAuthRequestTokenFailed(error) |
| |
| def SetOAuthToken(self, oauth_token): |
| """Attempts to set the current token and add it to the token store. |
| |
| The oauth_token can be any OAuth token i.e. unauthorized request token, |
| authorized request token or access token. |
| This method also attempts to add the token to the token store. |
| Use this method any time you want the current token to point to the |
| oauth_token passed. For e.g. call this method with the request token |
| you receive from FetchOAuthRequestToken. |
| |
| Args: |
| request_token: gdata.auth.OAuthToken OAuth request token. |
| """ |
| if self.auto_set_current_token: |
| self.current_token = oauth_token |
| if self.auto_store_tokens: |
| self.token_store.add_token(oauth_token) |
| |
| def GenerateOAuthAuthorizationURL( |
| self, request_token=None, callback_url=None, extra_params=None, |
| include_scopes_in_callback=False, |
| scopes_param_prefix=OAUTH_SCOPE_URL_PARAM_NAME, |
| request_url='%s/accounts/OAuthAuthorizeToken' % AUTH_SERVER_HOST): |
| """Generates URL at which user will login to authorize the request token. |
| |
| Args: |
| request_token: gdata.auth.OAuthToken (optional) OAuth request token. |
| If not specified, then the current token will be used if it is of |
| type <gdata.auth.OAuthToken>, else it is found by looking in the |
| token_store by looking for a token for the current scope. |
| callback_url: string (optional) The URL user will be sent to after |
| logging in and granting access. |
| extra_params: dict (optional) Additional parameters to be sent. |
| include_scopes_in_callback: Boolean (default=False) if set to True, and |
| if 'callback_url' is present, the 'callback_url' will be modified to |
| include the scope(s) from the request token as a URL parameter. The |
| key for the 'callback' URL's scope parameter will be |
| OAUTH_SCOPE_URL_PARAM_NAME. The benefit of including the scope URL as |
| a parameter to the 'callback' URL, is that the page which receives |
| the OAuth token will be able to tell which URLs the token grants |
| access to. |
| scopes_param_prefix: string (default='oauth_token_scope') The URL |
| parameter key which maps to the list of valid scopes for the token. |
| This URL parameter will be included in the callback URL along with |
| the scopes of the token as value if include_scopes_in_callback=True. |
| request_url: Authorization URL. The default is |
| 'https://www.google.com/accounts/OAuthAuthorizeToken'. |
| Returns: |
| A string URL at which the user is required to login. |
| |
| Raises: |
| NonOAuthToken if the user's request token is not an OAuth token or if a |
| request token was not available. |
| """ |
| if request_token and not isinstance(request_token, gdata.auth.OAuthToken): |
| raise NonOAuthToken |
| if not request_token: |
| if isinstance(self.current_token, gdata.auth.OAuthToken): |
| request_token = self.current_token |
| else: |
| current_scopes = lookup_scopes(self.service) |
| if current_scopes: |
| token = self.token_store.find_token(current_scopes[0]) |
| if isinstance(token, gdata.auth.OAuthToken): |
| request_token = token |
| if not request_token: |
| raise NonOAuthToken |
| return str(gdata.auth.GenerateOAuthAuthorizationUrl( |
| request_token, |
| authorization_url=request_url, |
| callback_url=callback_url, extra_params=extra_params, |
| include_scopes_in_callback=include_scopes_in_callback, |
| scopes_param_prefix=scopes_param_prefix)) |
| |
| def UpgradeToOAuthAccessToken(self, authorized_request_token=None, |
| request_url='%s/accounts/OAuthGetAccessToken' \ |
| % AUTH_SERVER_HOST, oauth_version='1.0', |
| oauth_verifier=None): |
| """Upgrades the authorized request token to an access token and returns it |
| |
| Args: |
| authorized_request_token: gdata.auth.OAuthToken (optional) OAuth request |
| token. If not specified, then the current token will be used if it is |
| of type <gdata.auth.OAuthToken>, else it is found by looking in the |
| token_store by looking for a token for the current scope. |
| request_url: Access token URL. The default is |
| 'https://www.google.com/accounts/OAuthGetAccessToken'. |
| oauth_version: str (default='1.0') oauth_version parameter. All other |
| 'oauth_' parameters are added by default. This parameter too, is |
| added by default but here you can override it's value. |
| oauth_verifier: str (optional) If present, it is assumed that the client |
| will use the OAuth v1.0a protocol which includes passing the |
| oauth_verifier (as returned by the SP) in the access token step. |
| |
| Returns: |
| Access token |
| |
| Raises: |
| NonOAuthToken if the user's authorized request token is not an OAuth |
| token or if an authorized request token was not available. |
| TokenUpgradeFailed if the server responded to the request with an |
| error. |
| """ |
| if (authorized_request_token and |
| not isinstance(authorized_request_token, gdata.auth.OAuthToken)): |
| raise NonOAuthToken |
| if not authorized_request_token: |
| if isinstance(self.current_token, gdata.auth.OAuthToken): |
| authorized_request_token = self.current_token |
| else: |
| current_scopes = lookup_scopes(self.service) |
| if current_scopes: |
| token = self.token_store.find_token(current_scopes[0]) |
| if isinstance(token, gdata.auth.OAuthToken): |
| authorized_request_token = token |
| if not authorized_request_token: |
| raise NonOAuthToken |
| access_token_url = gdata.auth.GenerateOAuthAccessTokenUrl( |
| authorized_request_token, |
| self._oauth_input_params, |
| access_token_url=request_url, |
| oauth_version=oauth_version, |
| oauth_verifier=oauth_verifier) |
| response = self.http_client.request('GET', str(access_token_url)) |
| if response.status == 200: |
| token = gdata.auth.OAuthTokenFromHttpBody(response.read()) |
| token.scopes = authorized_request_token.scopes |
| token.oauth_input_params = authorized_request_token.oauth_input_params |
| self.SetOAuthToken(token) |
| return token |
| else: |
| raise TokenUpgradeFailed({'status': response.status, |
| 'reason': 'Non 200 response on upgrade', |
| 'body': response.read()}) |
| |
| def RevokeOAuthToken(self, request_url='%s/accounts/AuthSubRevokeToken' % \ |
| AUTH_SERVER_HOST): |
| """Revokes an existing OAuth token. |
| |
| request_url: Token revoke URL. The default is |
| 'https://www.google.com/accounts/AuthSubRevokeToken'. |
| Raises: |
| NonOAuthToken if the user's auth token is not an OAuth token. |
| RevokingOAuthTokenFailed if request for revoking an OAuth token failed. |
| """ |
| scopes = lookup_scopes(self.service) |
| token = self.token_store.find_token(scopes[0]) |
| if not isinstance(token, gdata.auth.OAuthToken): |
| raise NonOAuthToken |
| |
| response = token.perform_request(self.http_client, 'GET', request_url, |
| headers={'Content-Type':'application/x-www-form-urlencoded'}) |
| if response.status == 200: |
| self.token_store.remove_token(token) |
| else: |
| raise RevokingOAuthTokenFailed |
| |
| def GetAuthSubToken(self): |
| """Returns the AuthSub token as a string. |
| |
| If the token is an gdta.auth.AuthSubToken, the Authorization Label |
| ("AuthSub token") is removed. |
| |
| This method examines the current_token to see if it is an AuthSubToken |
| or SecureAuthSubToken. If not, it searches the token_store for a token |
| which matches the current scope. |
| |
| The current scope is determined by the service name string member. |
| |
| Returns: |
| If the current_token is set to an AuthSubToken/SecureAuthSubToken, |
| return the token string. If there is no current_token, a token string |
| for a token which matches the service object's default scope is returned. |
| If there are no tokens valid for the scope, returns None. |
| """ |
| if isinstance(self.current_token, gdata.auth.AuthSubToken): |
| return self.current_token.get_token_string() |
| current_scopes = lookup_scopes(self.service) |
| if current_scopes: |
| token = self.token_store.find_token(current_scopes[0]) |
| if isinstance(token, gdata.auth.AuthSubToken): |
| return token.get_token_string() |
| else: |
| token = self.token_store.find_token(atom.token_store.SCOPE_ALL) |
| if isinstance(token, gdata.auth.ClientLoginToken): |
| return token.get_token_string() |
| return None |
| |
| def SetAuthSubToken(self, token, scopes=None, rsa_key=None): |
| """Sets the token sent in requests to an AuthSub token. |
| |
| Sets the current_token and attempts to add the token to the token_store. |
| |
| Only use this method if you have received a token from the AuthSub |
| service. The auth token is set automatically when UpgradeToSessionToken() |
| is used. See documentation for Google AuthSub here: |
| http://code.google.com/apis/accounts/AuthForWebApps.html |
| |
| Args: |
| token: gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken or string |
| The token returned by the AuthSub service. If the token is an |
| AuthSubToken or SecureAuthSubToken, the scope information stored in |
| the token is used. If the token is a string, the scopes parameter is |
| used to determine the valid scopes. |
| scopes: list of URLs for which the token is valid. This is only used |
| if the token parameter is a string. |
| rsa_key: string (optional) Private key required for RSA_SHA1 signature |
| method. This parameter is necessary if the token is a string |
| representing a secure token. |
| """ |
| if not isinstance(token, gdata.auth.AuthSubToken): |
| token_string = token |
| if rsa_key: |
| token = gdata.auth.SecureAuthSubToken(rsa_key) |
| else: |
| token = gdata.auth.AuthSubToken() |
| |
| token.set_token_string(token_string) |
| |
| # If no scopes were set for the token, use the scopes passed in, or |
| # try to determine the scopes based on the current service name. If |
| # all else fails, set the token to match all requests. |
| if not token.scopes: |
| if scopes is None: |
| scopes = lookup_scopes(self.service) |
| if scopes is None: |
| scopes = [atom.token_store.SCOPE_ALL] |
| token.scopes = scopes |
| if self.auto_set_current_token: |
| self.current_token = token |
| if self.auto_store_tokens: |
| self.token_store.add_token(token) |
| |
| def GetClientLoginToken(self): |
| """Returns the token string for the current token or a token matching the |
| service scope. |
| |
| If the current_token is a ClientLoginToken, the token string for |
| the current token is returned. If the current_token is not set, this method |
| searches for a token in the token_store which is valid for the service |
| object's current scope. |
| |
| The current scope is determined by the service name string member. |
| The token string is the end of the Authorization header, it doesn not |
| include the ClientLogin label. |
| """ |
| if isinstance(self.current_token, gdata.auth.ClientLoginToken): |
| return self.current_token.get_token_string() |
| current_scopes = lookup_scopes(self.service) |
| if current_scopes: |
| token = self.token_store.find_token(current_scopes[0]) |
| if isinstance(token, gdata.auth.ClientLoginToken): |
| return token.get_token_string() |
| else: |
| token = self.token_store.find_token(atom.token_store.SCOPE_ALL) |
| if isinstance(token, gdata.auth.ClientLoginToken): |
| return token.get_token_string() |
| return None |
| |
| def SetClientLoginToken(self, token, scopes=None): |
| """Sets the token sent in requests to a ClientLogin token. |
| |
| This method sets the current_token to a new ClientLoginToken and it |
| also attempts to add the ClientLoginToken to the token_store. |
| |
| Only use this method if you have received a token from the ClientLogin |
| service. The auth_token is set automatically when ProgrammaticLogin() |
| is used. See documentation for Google ClientLogin here: |
| http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html |
| |
| Args: |
| token: string or instance of a ClientLoginToken. |
| """ |
| if not isinstance(token, gdata.auth.ClientLoginToken): |
| token_string = token |
| token = gdata.auth.ClientLoginToken() |
| token.set_token_string(token_string) |
| |
| if not token.scopes: |
| if scopes is None: |
| scopes = lookup_scopes(self.service) |
| if scopes is None: |
| scopes = [atom.token_store.SCOPE_ALL] |
| token.scopes = scopes |
| if self.auto_set_current_token: |
| self.current_token = token |
| if self.auto_store_tokens: |
| self.token_store.add_token(token) |
| |
| # Private methods to create the source property. |
| def __GetSource(self): |
| return self.__source |
| |
| def __SetSource(self, new_source): |
| self.__source = new_source |
| # Update the UserAgent header to include the new application name. |
| self.additional_headers['User-Agent'] = atom.http_interface.USER_AGENT % ( |
| self.__source,) |
| |
| source = property(__GetSource, __SetSource, |
| doc="""The source is the name of the application making the request. |
| It should be in the form company_id-app_name-app_version""") |
| |
| # Authentication operations |
| |
| def ProgrammaticLogin(self, captcha_token=None, captcha_response=None): |
| """Authenticates the user and sets the GData Auth token. |
| |
| Login retreives a temporary auth token which must be used with all |
| requests to GData services. The auth token is stored in the GData client |
| object. |
| |
| Login is also used to respond to a captcha challenge. If the user's login |
| attempt failed with a CaptchaRequired error, the user can respond by |
| calling Login with the captcha token and the answer to the challenge. |
| |
| Args: |
| captcha_token: string (optional) The identifier for the captcha challenge |
| which was presented to the user. |
| captcha_response: string (optional) The user's answer to the captch |
| challenge. |
| |
| Raises: |
| CaptchaRequired if the login service will require a captcha response |
| BadAuthentication if the login service rejected the username or password |
| Error if the login service responded with a 403 different from the above |
| """ |
| request_body = gdata.auth.generate_client_login_request_body(self.email, |
| self.password, self.service, self.source, self.account_type, |
| captcha_token, captcha_response) |
| |
| # If the user has defined their own authentication service URL, |
| # send the ClientLogin requests to this URL: |
| if not self.auth_service_url: |
| auth_request_url = AUTH_SERVER_HOST + '/accounts/ClientLogin' |
| else: |
| auth_request_url = self.auth_service_url |
| |
| auth_response = self.http_client.request('POST', auth_request_url, |
| data=request_body, |
| headers={'Content-Type':'application/x-www-form-urlencoded'}) |
| response_body = auth_response.read() |
| |
| if auth_response.status == 200: |
| # TODO: insert the token into the token_store directly. |
| self.SetClientLoginToken( |
| gdata.auth.get_client_login_token(response_body)) |
| self.__captcha_token = None |
| self.__captcha_url = None |
| |
| elif auth_response.status == 403: |
| # Examine each line to find the error type and the captcha token and |
| # captch URL if they are present. |
| captcha_parameters = gdata.auth.get_captcha_challenge(response_body, |
| captcha_base_url='%s/accounts/' % AUTH_SERVER_HOST) |
| if captcha_parameters: |
| self.__captcha_token = captcha_parameters['token'] |
| self.__captcha_url = captcha_parameters['url'] |
| raise CaptchaRequired, 'Captcha Required' |
| elif response_body.splitlines()[0] == 'Error=BadAuthentication': |
| self.__captcha_token = None |
| self.__captcha_url = None |
| raise BadAuthentication, 'Incorrect username or password' |
| else: |
| self.__captcha_token = None |
| self.__captcha_url = None |
| raise Error, 'Server responded with a 403 code' |
| elif auth_response.status == 302: |
| self.__captcha_token = None |
| self.__captcha_url = None |
| # Google tries to redirect all bad URLs back to |
| # http://www.google.<locale>. If a redirect |
| # attempt is made, assume the user has supplied an incorrect authentication URL |
| raise BadAuthenticationServiceURL, 'Server responded with a 302 code.' |
| |
| def ClientLogin(self, username, password, account_type=None, service=None, |
| auth_service_url=None, source=None, captcha_token=None, |
| captcha_response=None): |
| """Convenience method for authenticating using ProgrammaticLogin. |
| |
| Sets values for email, password, and other optional members. |
| |
| Args: |
| username: |
| password: |
| account_type: string (optional) |
| service: string (optional) |
| auth_service_url: string (optional) |
| captcha_token: string (optional) |
| captcha_response: string (optional) |
| """ |
| self.email = username |
| self.password = password |
| |
| if account_type: |
| self.account_type = account_type |
| if service: |
| self.service = service |
| if source: |
| self.source = source |
| if auth_service_url: |
| self.auth_service_url = auth_service_url |
| |
| self.ProgrammaticLogin(captcha_token, captcha_response) |
| |
| def GenerateAuthSubURL(self, next, scope, secure=False, session=True, |
| domain='default'): |
| """Generate a URL at which the user will login and be redirected back. |
| |
| Users enter their credentials on a Google login page and a token is sent |
| to the URL specified in next. See documentation for AuthSub login at: |
| http://code.google.com/apis/accounts/docs/AuthSub.html |
| |
| Args: |
| next: string The URL user will be sent to after logging in. |
| scope: string or list of strings. The URLs of the services to be |
| accessed. |
| secure: boolean (optional) Determines whether or not the issued token |
| is a secure token. |
| session: boolean (optional) Determines whether or not the issued token |
| can be upgraded to a session token. |
| """ |
| if not isinstance(scope, (list, tuple)): |
| scope = (scope,) |
| return gdata.auth.generate_auth_sub_url(next, scope, secure=secure, |
| session=session, |
| request_url='%s/accounts/AuthSubRequest' % AUTH_SERVER_HOST, |
| domain=domain) |
| |
| def UpgradeToSessionToken(self, token=None): |
| """Upgrades a single use AuthSub token to a session token. |
| |
| Args: |
| token: A gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken |
| (optional) which is good for a single use but can be upgraded |
| to a session token. If no token is passed in, the token |
| is found by looking in the token_store by looking for a token |
| for the current scope. |
| |
| Raises: |
| NonAuthSubToken if the user's auth token is not an AuthSub token |
| TokenUpgradeFailed if the server responded to the request with an |
| error. |
| """ |
| if token is None: |
| scopes = lookup_scopes(self.service) |
| if scopes: |
| token = self.token_store.find_token(scopes[0]) |
| else: |
| token = self.token_store.find_token(atom.token_store.SCOPE_ALL) |
| if not isinstance(token, gdata.auth.AuthSubToken): |
| raise NonAuthSubToken |
| |
| self.SetAuthSubToken(self.upgrade_to_session_token(token)) |
| |
| def upgrade_to_session_token(self, token): |
| """Upgrades a single use AuthSub token to a session token. |
| |
| Args: |
| token: A gdata.auth.AuthSubToken or gdata.auth.SecureAuthSubToken |
| which is good for a single use but can be upgraded to a |
| session token. |
| |
| Returns: |
| The upgraded token as a gdata.auth.AuthSubToken object. |
| |
| Raises: |
| TokenUpgradeFailed if the server responded to the request with an |
| error. |
| """ |
| response = token.perform_request(self.http_client, 'GET', |
| AUTH_SERVER_HOST + '/accounts/AuthSubSessionToken', |
| headers={'Content-Type':'application/x-www-form-urlencoded'}) |
| response_body = response.read() |
| if response.status == 200: |
| token.set_token_string( |
| gdata.auth.token_from_http_body(response_body)) |
| return token |
| else: |
| raise TokenUpgradeFailed({'status': response.status, |
| 'reason': 'Non 200 response on upgrade', |
| 'body': response_body}) |
| |
| def RevokeAuthSubToken(self): |
| """Revokes an existing AuthSub token. |
| |
| Raises: |
| NonAuthSubToken if the user's auth token is not an AuthSub token |
| """ |
| scopes = lookup_scopes(self.service) |
| token = self.token_store.find_token(scopes[0]) |
| if not isinstance(token, gdata.auth.AuthSubToken): |
| raise NonAuthSubToken |
| |
| response = token.perform_request(self.http_client, 'GET', |
| AUTH_SERVER_HOST + '/accounts/AuthSubRevokeToken', |
| headers={'Content-Type':'application/x-www-form-urlencoded'}) |
| if response.status == 200: |
| self.token_store.remove_token(token) |
| |
| def AuthSubTokenInfo(self): |
| """Fetches the AuthSub token's metadata from the server. |
| |
| Raises: |
| NonAuthSubToken if the user's auth token is not an AuthSub token |
| """ |
| scopes = lookup_scopes(self.service) |
| token = self.token_store.find_token(scopes[0]) |
| if not isinstance(token, gdata.auth.AuthSubToken): |
| raise NonAuthSubToken |
| |
| response = token.perform_request(self.http_client, 'GET', |
| AUTH_SERVER_HOST + '/accounts/AuthSubTokenInfo', |
| headers={'Content-Type':'application/x-www-form-urlencoded'}) |
| result_body = response.read() |
| if response.status == 200: |
| return result_body |
| else: |
| raise RequestError, {'status': response.status, |
| 'body': result_body} |
| |
| def GetWithRetries(self, uri, extra_headers=None, redirects_remaining=4, |
| encoding='UTF-8', converter=None, num_retries=DEFAULT_NUM_RETRIES, |
| delay=DEFAULT_DELAY, backoff=DEFAULT_BACKOFF, logger=None): |
| """This is a wrapper method for Get with retrying capability. |
| |
| To avoid various errors while retrieving bulk entities by retrying |
| specified times. |
| |
| Note this method relies on the time module and so may not be usable |
| by default in Python2.2. |
| |
| Args: |
| num_retries: Integer; the retry count. |
| delay: Integer; the initial delay for retrying. |
| backoff: Integer; how much the delay should lengthen after each failure. |
| logger: An object which has a debug(str) method to receive logging |
| messages. Recommended that you pass in the logging module. |
| Raises: |
| ValueError if any of the parameters has an invalid value. |
| RanOutOfTries on failure after number of retries. |
| """ |
| # Moved import for time module inside this method since time is not a |
| # default module in Python2.2. This method will not be usable in |
| # Python2.2. |
| import time |
| if backoff <= 1: |
| raise ValueError("backoff must be greater than 1") |
| num_retries = int(num_retries) |
| |
| if num_retries < 0: |
| raise ValueError("num_retries must be 0 or greater") |
| |
| if delay <= 0: |
| raise ValueError("delay must be greater than 0") |
| |
| # Let's start |
| mtries, mdelay = num_retries, delay |
| while mtries > 0: |
| if mtries != num_retries: |
| if logger: |
| logger.debug("Retrying: %s" % uri) |
| try: |
| rv = self.Get(uri, extra_headers=extra_headers, |
| redirects_remaining=redirects_remaining, |
| encoding=encoding, converter=converter) |
| except SystemExit: |
| # Allow this error |
| raise |
| except RequestError, e: |
| # Error 500 is 'internal server error' and warrants a retry |
| # Error 503 is 'service unavailable' and warrants a retry |
| if e[0]['status'] not in [500, 503]: |
| raise e |
| # Else, fall through to the retry code... |
| except Exception, e: |
| if logger: |
| logger.debug(e) |
| # Fall through to the retry code... |
| else: |
| # This is the right path. |
| return rv |
| mtries -= 1 |
| time.sleep(mdelay) |
| mdelay *= backoff |
| raise RanOutOfTries('Ran out of tries.') |
| |
| # CRUD operations |
| def Get(self, uri, extra_headers=None, redirects_remaining=4, |
| encoding='UTF-8', converter=None): |
| """Query the GData API with the given URI |
| |
| The uri is the portion of the URI after the server value |
| (ex: www.google.com). |
| |
| To perform a query against Google Base, set the server to |
| 'base.google.com' and set the uri to '/base/feeds/...', where ... is |
| your query. For example, to find snippets for all digital cameras uri |
| should be set to: '/base/feeds/snippets?bq=digital+camera' |
| |
| Args: |
| uri: string The query in the form of a URI. Example: |
| '/base/feeds/snippets?bq=digital+camera'. |
| extra_headers: dictionary (optional) Extra HTTP headers to be included |
| in the GET request. These headers are in addition to |
| those stored in the client's additional_headers property. |
| The client automatically sets the Content-Type and |
| Authorization headers. |
| redirects_remaining: int (optional) Tracks the number of additional |
| redirects this method will allow. If the service object receives |
| a redirect and remaining is 0, it will not follow the redirect. |
| This was added to avoid infinite redirect loops. |
| encoding: string (optional) The character encoding for the server's |
| response. Default is UTF-8 |
| converter: func (optional) A function which will transform |
| the server's results before it is returned. Example: use |
| GDataFeedFromString to parse the server response as if it |
| were a GDataFeed. |
| |
| Returns: |
| If there is no ResultsTransformer specified in the call, a GDataFeed |
| or GDataEntry depending on which is sent from the server. If the |
| response is niether a feed or entry and there is no ResultsTransformer, |
| return a string. If there is a ResultsTransformer, the returned value |
| will be that of the ResultsTransformer function. |
| """ |
| |
| if extra_headers is None: |
| extra_headers = {} |
| |
| if self.__gsessionid is not None: |
| if uri.find('gsessionid=') < 0: |
| if uri.find('?') > -1: |
| uri += '&gsessionid=%s' % (self.__gsessionid,) |
| else: |
| uri += '?gsessionid=%s' % (self.__gsessionid,) |
| |
| server_response = self.request('GET', uri, |
| headers=extra_headers) |
| result_body = server_response.read() |
| |
| if server_response.status == 200: |
| if converter: |
| return converter(result_body) |
| # There was no ResultsTransformer specified, so try to convert the |
| # server's response into a GDataFeed. |
| feed = gdata.GDataFeedFromString(result_body) |
| if not feed: |
| # If conversion to a GDataFeed failed, try to convert the server's |
| # response to a GDataEntry. |
| entry = gdata.GDataEntryFromString(result_body) |
| if not entry: |
| # The server's response wasn't a feed, or an entry, so return the |
| # response body as a string. |
| return result_body |
| return entry |
| return feed |
| elif server_response.status == 302: |
| if redirects_remaining > 0: |
| location = (server_response.getheader('Location') |
| or server_response.getheader('location')) |
| if location is not None: |
| m = re.compile('[\?\&]gsessionid=(\w*\-)').search(location) |
| if m is not None: |
| self.__gsessionid = m.group(1) |
| return GDataService.Get(self, location, extra_headers, redirects_remaining - 1, |
| encoding=encoding, converter=converter) |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': '302 received without Location header', |
| 'body': result_body} |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': 'Redirect received, but redirects_remaining <= 0', |
| 'body': result_body} |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': server_response.reason, 'body': result_body} |
| |
| def GetMedia(self, uri, extra_headers=None): |
| """Returns a MediaSource containing media and its metadata from the given |
| URI string. |
| """ |
| response_handle = self.request('GET', uri, |
| headers=extra_headers) |
| return gdata.MediaSource(response_handle, response_handle.getheader( |
| 'Content-Type'), |
| response_handle.getheader('Content-Length')) |
| |
| def GetEntry(self, uri, extra_headers=None): |
| """Query the GData API with the given URI and receive an Entry. |
| |
| See also documentation for gdata.service.Get |
| |
| Args: |
| uri: string The query in the form of a URI. Example: |
| '/base/feeds/snippets?bq=digital+camera'. |
| extra_headers: dictionary (optional) Extra HTTP headers to be included |
| in the GET request. These headers are in addition to |
| those stored in the client's additional_headers property. |
| The client automatically sets the Content-Type and |
| Authorization headers. |
| |
| Returns: |
| A GDataEntry built from the XML in the server's response. |
| """ |
| |
| result = GDataService.Get(self, uri, extra_headers, |
| converter=atom.EntryFromString) |
| if isinstance(result, atom.Entry): |
| return result |
| else: |
| raise UnexpectedReturnType, 'Server did not send an entry' |
| |
| def GetFeed(self, uri, extra_headers=None, |
| converter=gdata.GDataFeedFromString): |
| """Query the GData API with the given URI and receive a Feed. |
| |
| See also documentation for gdata.service.Get |
| |
| Args: |
| uri: string The query in the form of a URI. Example: |
| '/base/feeds/snippets?bq=digital+camera'. |
| extra_headers: dictionary (optional) Extra HTTP headers to be included |
| in the GET request. These headers are in addition to |
| those stored in the client's additional_headers property. |
| The client automatically sets the Content-Type and |
| Authorization headers. |
| |
| Returns: |
| A GDataFeed built from the XML in the server's response. |
| """ |
| |
| result = GDataService.Get(self, uri, extra_headers, converter=converter) |
| if isinstance(result, atom.Feed): |
| return result |
| else: |
| raise UnexpectedReturnType, 'Server did not send a feed' |
| |
| def GetNext(self, feed): |
| """Requests the next 'page' of results in the feed. |
| |
| This method uses the feed's next link to request an additional feed |
| and uses the class of the feed to convert the results of the GET request. |
| |
| Args: |
| feed: atom.Feed or a subclass. The feed should contain a next link and |
| the type of the feed will be applied to the results from the |
| server. The new feed which is returned will be of the same class |
| as this feed which was passed in. |
| |
| Returns: |
| A new feed representing the next set of results in the server's feed. |
| The type of this feed will match that of the feed argument. |
| """ |
| next_link = feed.GetNextLink() |
| # Create a closure which will convert an XML string to the class of |
| # the feed object passed in. |
| def ConvertToFeedClass(xml_string): |
| return atom.CreateClassFromXMLString(feed.__class__, xml_string) |
| # Make a GET request on the next link and use the above closure for the |
| # converted which processes the XML string from the server. |
| if next_link and next_link.href: |
| return GDataService.Get(self, next_link.href, |
| converter=ConvertToFeedClass) |
| else: |
| return None |
| |
| def Post(self, data, uri, extra_headers=None, url_params=None, |
| escape_params=True, redirects_remaining=4, media_source=None, |
| converter=None): |
| """Insert or update data into a GData service at the given URI. |
| |
| Args: |
| data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry The |
| XML to be sent to the uri. |
| uri: string The location (feed) to which the data should be inserted. |
| Example: '/base/feeds/items'. |
| extra_headers: dict (optional) HTTP headers which are to be included. |
| The client automatically sets the Content-Type, |
| Authorization, and Content-Length headers. |
| url_params: dict (optional) Additional URL parameters to be included |
| in the URI. These are translated into query arguments |
| in the form '&dict_key=value&...'. |
| Example: {'max-results': '250'} becomes &max-results=250 |
| escape_params: boolean (optional) If false, the calling code has already |
| ensured that the query will form a valid URL (all |
| reserved characters have been escaped). If true, this |
| method will escape the query and any URL parameters |
| provided. |
| media_source: MediaSource (optional) Container for the media to be sent |
| along with the entry, if provided. |
| converter: func (optional) A function which will be executed on the |
| server's response. Often this is a function like |
| GDataEntryFromString which will parse the body of the server's |
| response and return a GDataEntry. |
| |
| Returns: |
| If the post succeeded, this method will return a GDataFeed, GDataEntry, |
| or the results of running converter on the server's result body (if |
| converter was specified). |
| """ |
| return GDataService.PostOrPut(self, 'POST', data, uri, |
| extra_headers=extra_headers, url_params=url_params, |
| escape_params=escape_params, redirects_remaining=redirects_remaining, |
| media_source=media_source, converter=converter) |
| |
| def PostOrPut(self, verb, data, uri, extra_headers=None, url_params=None, |
| escape_params=True, redirects_remaining=4, media_source=None, |
| converter=None): |
| """Insert data into a GData service at the given URI. |
| |
| Args: |
| verb: string, either 'POST' or 'PUT' |
| data: string, ElementTree._Element, atom.Entry, or gdata.GDataEntry The |
| XML to be sent to the uri. |
| uri: string The location (feed) to which the data should be inserted. |
| Example: '/base/feeds/items'. |
| extra_headers: dict (optional) HTTP headers which are to be included. |
| The client automatically sets the Content-Type, |
| Authorization, and Content-Length headers. |
| url_params: dict (optional) Additional URL parameters to be included |
| in the URI. These are translated into query arguments |
| in the form '&dict_key=value&...'. |
| Example: {'max-results': '250'} becomes &max-results=250 |
| escape_params: boolean (optional) If false, the calling code has already |
| ensured that the query will form a valid URL (all |
| reserved characters have been escaped). If true, this |
| method will escape the query and any URL parameters |
| provided. |
| media_source: MediaSource (optional) Container for the media to be sent |
| along with the entry, if provided. |
| converter: func (optional) A function which will be executed on the |
| server's response. Often this is a function like |
| GDataEntryFromString which will parse the body of the server's |
| response and return a GDataEntry. |
| |
| Returns: |
| If the post succeeded, this method will return a GDataFeed, GDataEntry, |
| or the results of running converter on the server's result body (if |
| converter was specified). |
| """ |
| if extra_headers is None: |
| extra_headers = {} |
| |
| if self.__gsessionid is not None: |
| if uri.find('gsessionid=') < 0: |
| if url_params is None: |
| url_params = {} |
| url_params['gsessionid'] = self.__gsessionid |
| |
| if data and media_source: |
| if ElementTree.iselement(data): |
| data_str = ElementTree.tostring(data) |
| else: |
| data_str = str(data) |
| |
| multipart = [] |
| multipart.append('Media multipart posting\r\n--END_OF_PART\r\n' + \ |
| 'Content-Type: application/atom+xml\r\n\r\n') |
| multipart.append('\r\n--END_OF_PART\r\nContent-Type: ' + \ |
| media_source.content_type+'\r\n\r\n') |
| multipart.append('\r\n--END_OF_PART--\r\n') |
| |
| extra_headers['MIME-version'] = '1.0' |
| extra_headers['Content-Length'] = str(len(multipart[0]) + |
| len(multipart[1]) + len(multipart[2]) + |
| len(data_str) + media_source.content_length) |
| |
| extra_headers['Content-Type'] = 'multipart/related; boundary=END_OF_PART' |
| server_response = self.request(verb, uri, |
| data=[multipart[0], data_str, multipart[1], media_source.file_handle, |
| multipart[2]], headers=extra_headers, url_params=url_params) |
| result_body = server_response.read() |
| |
| elif media_source or isinstance(data, gdata.MediaSource): |
| if isinstance(data, gdata.MediaSource): |
| media_source = data |
| extra_headers['Content-Length'] = str(media_source.content_length) |
| extra_headers['Content-Type'] = media_source.content_type |
| server_response = self.request(verb, uri, |
| data=media_source.file_handle, headers=extra_headers, |
| url_params=url_params) |
| result_body = server_response.read() |
| |
| else: |
| http_data = data |
| if 'Content-Type' not in extra_headers: |
| content_type = 'application/atom+xml' |
| extra_headers['Content-Type'] = content_type |
| server_response = self.request(verb, uri, data=http_data, |
| headers=extra_headers, url_params=url_params) |
| result_body = server_response.read() |
| |
| # Server returns 201 for most post requests, but when performing a batch |
| # request the server responds with a 200 on success. |
| if server_response.status == 201 or server_response.status == 200: |
| if converter: |
| return converter(result_body) |
| feed = gdata.GDataFeedFromString(result_body) |
| if not feed: |
| entry = gdata.GDataEntryFromString(result_body) |
| if not entry: |
| return result_body |
| return entry |
| return feed |
| elif server_response.status == 302: |
| if redirects_remaining > 0: |
| location = (server_response.getheader('Location') |
| or server_response.getheader('location')) |
| if location is not None: |
| m = re.compile('[\?\&]gsessionid=(\w*\-)').search(location) |
| if m is not None: |
| self.__gsessionid = m.group(1) |
| return GDataService.PostOrPut(self, verb, data, location, |
| extra_headers, url_params, escape_params, |
| redirects_remaining - 1, media_source, converter=converter) |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': '302 received without Location header', |
| 'body': result_body} |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': 'Redirect received, but redirects_remaining <= 0', |
| 'body': result_body} |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': server_response.reason, 'body': result_body} |
| |
| def Put(self, data, uri, extra_headers=None, url_params=None, |
| escape_params=True, redirects_remaining=3, media_source=None, |
| converter=None): |
| """Updates an entry at the given URI. |
| |
| Args: |
| data: string, ElementTree._Element, or xml_wrapper.ElementWrapper The |
| XML containing the updated data. |
| uri: string A URI indicating entry to which the update will be applied. |
| Example: '/base/feeds/items/ITEM-ID' |
| extra_headers: dict (optional) HTTP headers which are to be included. |
| The client automatically sets the Content-Type, |
| Authorization, and Content-Length headers. |
| url_params: dict (optional) Additional URL parameters to be included |
| in the URI. These are translated into query arguments |
| in the form '&dict_key=value&...'. |
| Example: {'max-results': '250'} becomes &max-results=250 |
| escape_params: boolean (optional) If false, the calling code has already |
| ensured that the query will form a valid URL (all |
| reserved characters have been escaped). If true, this |
| method will escape the query and any URL parameters |
| provided. |
| converter: func (optional) A function which will be executed on the |
| server's response. Often this is a function like |
| GDataEntryFromString which will parse the body of the server's |
| response and return a GDataEntry. |
| |
| Returns: |
| If the put succeeded, this method will return a GDataFeed, GDataEntry, |
| or the results of running converter on the server's result body (if |
| converter was specified). |
| """ |
| return GDataService.PostOrPut(self, 'PUT', data, uri, |
| extra_headers=extra_headers, url_params=url_params, |
| escape_params=escape_params, redirects_remaining=redirects_remaining, |
| media_source=media_source, converter=converter) |
| |
| def Delete(self, uri, extra_headers=None, url_params=None, |
| escape_params=True, redirects_remaining=4): |
| """Deletes the entry at the given URI. |
| |
| Args: |
| uri: string The URI of the entry to be deleted. Example: |
| '/base/feeds/items/ITEM-ID' |
| extra_headers: dict (optional) HTTP headers which are to be included. |
| The client automatically sets the Content-Type and |
| Authorization headers. |
| url_params: dict (optional) Additional URL parameters to be included |
| in the URI. These are translated into query arguments |
| in the form '&dict_key=value&...'. |
| Example: {'max-results': '250'} becomes &max-results=250 |
| escape_params: boolean (optional) If false, the calling code has already |
| ensured that the query will form a valid URL (all |
| reserved characters have been escaped). If true, this |
| method will escape the query and any URL parameters |
| provided. |
| |
| Returns: |
| True if the entry was deleted. |
| """ |
| if extra_headers is None: |
| extra_headers = {} |
| |
| if self.__gsessionid is not None: |
| if uri.find('gsessionid=') < 0: |
| if url_params is None: |
| url_params = {} |
| url_params['gsessionid'] = self.__gsessionid |
| |
| server_response = self.request('DELETE', uri, |
| headers=extra_headers, url_params=url_params) |
| result_body = server_response.read() |
| |
| if server_response.status == 200: |
| return True |
| elif server_response.status == 302: |
| if redirects_remaining > 0: |
| location = (server_response.getheader('Location') |
| or server_response.getheader('location')) |
| if location is not None: |
| m = re.compile('[\?\&]gsessionid=(\w*\-)').search(location) |
| if m is not None: |
| self.__gsessionid = m.group(1) |
| return GDataService.Delete(self, location, extra_headers, |
| url_params, escape_params, redirects_remaining - 1) |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': '302 received without Location header', |
| 'body': result_body} |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': 'Redirect received, but redirects_remaining <= 0', |
| 'body': result_body} |
| else: |
| raise RequestError, {'status': server_response.status, |
| 'reason': server_response.reason, 'body': result_body} |
| |
| |
| def ExtractToken(url, scopes_included_in_next=True): |
| """Gets the AuthSub token from the current page's URL. |
| |
| Designed to be used on the URL that the browser is sent to after the user |
| authorizes this application at the page given by GenerateAuthSubRequestUrl. |
| |
| Args: |
| url: The current page's URL. It should contain the token as a URL |
| parameter. Example: 'http://example.com/?...&token=abcd435' |
| scopes_included_in_next: If True, this function looks for a scope value |
| associated with the token. The scope is a URL parameter with the |
| key set to SCOPE_URL_PARAM_NAME. This parameter should be present |
| if the AuthSub request URL was generated using |
| GenerateAuthSubRequestUrl with include_scope_in_next set to True. |
| |
| Returns: |
| A tuple containing the token string and a list of scope strings for which |
| this token should be valid. If the scope was not included in the URL, the |
| tuple will contain (token, None). |
| """ |
| parsed = urlparse.urlparse(url) |
| token = gdata.auth.AuthSubTokenFromUrl(parsed[4]) |
| scopes = '' |
| if scopes_included_in_next: |
| for pair in parsed[4].split('&'): |
| if pair.startswith('%s=' % SCOPE_URL_PARAM_NAME): |
| scopes = urllib.unquote_plus(pair.split('=')[1]) |
| return (token, scopes.split(' ')) |
| |
| |
| def GenerateAuthSubRequestUrl(next, scopes, hd='default', secure=False, |
| session=True, request_url='https://www.google.com/accounts/AuthSubRequest', |
| include_scopes_in_next=True): |
| """Creates a URL to request an AuthSub token to access Google services. |
| |
| For more details on AuthSub, see the documentation here: |
| http://code.google.com/apis/accounts/docs/AuthSub.html |
| |
| Args: |
| next: The URL where the browser should be sent after the user authorizes |
| the application. This page is responsible for receiving the token |
| which is embeded in the URL as a parameter. |
| scopes: The base URL to which access will be granted. Example: |
| 'http://www.google.com/calendar/feeds' will grant access to all |
| URLs in the Google Calendar data API. If you would like a token for |
| multiple scopes, pass in a list of URL strings. |
| hd: The domain to which the user's account belongs. This is set to the |
| domain name if you are using Google Apps. Example: 'example.org' |
| Defaults to 'default' |
| secure: If set to True, all requests should be signed. The default is |
| False. |
| session: If set to True, the token received by the 'next' URL can be |
| upgraded to a multiuse session token. If session is set to False, the |
| token may only be used once and cannot be upgraded. Default is True. |
| request_url: The base of the URL to which the user will be sent to |
| authorize this application to access their data. The default is |
| 'https://www.google.com/accounts/AuthSubRequest'. |
| include_scopes_in_next: Boolean if set to true, the 'next' parameter will |
| be modified to include the requested scope as a URL parameter. The |
| key for the next's scope parameter will be SCOPE_URL_PARAM_NAME. The |
| benefit of including the scope URL as a parameter to the next URL, is |
| that the page which receives the AuthSub token will be able to tell |
| which URLs the token grants access to. |
| |
| Returns: |
| A URL string to which the browser should be sent. |
| """ |
| if isinstance(scopes, list): |
| scope = ' '.join(scopes) |
| else: |
| scope = scopes |
| if include_scopes_in_next: |
| if next.find('?') > -1: |
| next += '&%s' % urllib.urlencode({SCOPE_URL_PARAM_NAME:scope}) |
| else: |
| next += '?%s' % urllib.urlencode({SCOPE_URL_PARAM_NAME:scope}) |
| return gdata.auth.GenerateAuthSubUrl(next=next, scope=scope, secure=secure, |
| session=session, request_url=request_url, domain=hd) |
| |
| |
| class Query(dict): |
| """Constructs a query URL to be used in GET requests |
| |
| Url parameters are created by adding key-value pairs to this object as a |
| dict. For example, to add &max-results=25 to the URL do |
| my_query['max-results'] = 25 |
| |
| Category queries are created by adding category strings to the categories |
| member. All items in the categories list will be concatenated with the / |
| symbol (symbolizing a category x AND y restriction). If you would like to OR |
| 2 categories, append them as one string with a | between the categories. |
| For example, do query.categories.append('Fritz|Laurie') to create a query |
| like this feed/-/Fritz%7CLaurie . This query will look for results in both |
| categories. |
| """ |
| |
| def __init__(self, feed=None, text_query=None, params=None, |
| categories=None): |
| """Constructor for Query |
| |
| Args: |
| feed: str (optional) The path for the feed (Examples: |
| '/base/feeds/snippets' or 'calendar/feeds/jo@gmail.com/private/full' |
| text_query: str (optional) The contents of the q query parameter. The |
| contents of the text_query are URL escaped upon conversion to a URI. |
| params: dict (optional) Parameter value string pairs which become URL |
| params when translated to a URI. These parameters are added to the |
| query's items (key-value pairs). |
| categories: list (optional) List of category strings which should be |
| included as query categories. See |
| http://code.google.com/apis/gdata/reference.html#Queries for |
| details. If you want to get results from category A or B (both |
| categories), specify a single list item 'A|B'. |
| """ |
| |
| self.feed = feed |
| self.categories = [] |
| if text_query: |
| self.text_query = text_query |
| if isinstance(params, dict): |
| for param in params: |
| self[param] = params[param] |
| if isinstance(categories, list): |
| for category in categories: |
| self.categories.append(category) |
| |
| def _GetTextQuery(self): |
| if 'q' in self.keys(): |
| return self['q'] |
| else: |
| return None |
| |
| def _SetTextQuery(self, query): |
| self['q'] = query |
| |
| text_query = property(_GetTextQuery, _SetTextQuery, |
| doc="""The feed query's q parameter""") |
| |
| def _GetAuthor(self): |
| if 'author' in self.keys(): |
| return self['author'] |
| else: |
| return None |
| |
| def _SetAuthor(self, query): |
| self['author'] = query |
| |
| author = property(_GetAuthor, _SetAuthor, |
| doc="""The feed query's author parameter""") |
| |
| def _GetAlt(self): |
| if 'alt' in self.keys(): |
| return self['alt'] |
| else: |
| return None |
| |
| def _SetAlt(self, query): |
| self['alt'] = query |
| |
| alt = property(_GetAlt, _SetAlt, |
| doc="""The feed query's alt parameter""") |
| |
| def _GetUpdatedMin(self): |
| if 'updated-min' in self.keys(): |
| return self['updated-min'] |
| else: |
| return None |
| |
| def _SetUpdatedMin(self, query): |
| self['updated-min'] = query |
| |
| updated_min = property(_GetUpdatedMin, _SetUpdatedMin, |
| doc="""The feed query's updated-min parameter""") |
| |
| def _GetUpdatedMax(self): |
| if 'updated-max' in self.keys(): |
| return self['updated-max'] |
| else: |
| return None |
| |
| def _SetUpdatedMax(self, query): |
| self['updated-max'] = query |
| |
| updated_max = property(_GetUpdatedMax, _SetUpdatedMax, |
| doc="""The feed query's updated-max parameter""") |
| |
| def _GetPublishedMin(self): |
| if 'published-min' in self.keys(): |
| return self['published-min'] |
| else: |
| return None |
| |
| def _SetPublishedMin(self, query): |
| self['published-min'] = query |
| |
| published_min = property(_GetPublishedMin, _SetPublishedMin, |
| doc="""The feed query's published-min parameter""") |
| |
| def _GetPublishedMax(self): |
| if 'published-max' in self.keys(): |
| return self['published-max'] |
| else: |
| return None |
| |
| def _SetPublishedMax(self, query): |
| self['published-max'] = query |
| |
| published_max = property(_GetPublishedMax, _SetPublishedMax, |
| doc="""The feed query's published-max parameter""") |
| |
| def _GetStartIndex(self): |
| if 'start-index' in self.keys(): |
| return self['start-index'] |
| else: |
| return None |
| |
| def _SetStartIndex(self, query): |
| if not isinstance(query, str): |
| query = str(query) |
| self['start-index'] = query |
| |
| start_index = property(_GetStartIndex, _SetStartIndex, |
| doc="""The feed query's start-index parameter""") |
| |
| def _GetMaxResults(self): |
| if 'max-results' in self.keys(): |
| return self['max-results'] |
| else: |
| return None |
| |
| def _SetMaxResults(self, query): |
| if not isinstance(query, str): |
| query = str(query) |
| self['max-results'] = query |
| |
| max_results = property(_GetMaxResults, _SetMaxResults, |
| doc="""The feed query's max-results parameter""") |
| |
| def _GetOrderBy(self): |
| if 'orderby' in self.keys(): |
| return self['orderby'] |
| else: |
| return None |
| |
| def _SetOrderBy(self, query): |
| self['orderby'] = query |
| |
| orderby = property(_GetOrderBy, _SetOrderBy, |
| doc="""The feed query's orderby parameter""") |
| |
| def ToUri(self): |
| q_feed = self.feed or '' |
| category_string = '/'.join( |
| [urllib.quote_plus(c) for c in self.categories]) |
| # Add categories to the feed if there are any. |
| if len(self.categories) > 0: |
| q_feed = q_feed + '/-/' + category_string |
| return atom.service.BuildUri(q_feed, self) |
| |
| def __str__(self): |
| return self.ToUri() |