322 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			322 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/python
 | 
						|
#
 | 
						|
# Copyright (C) 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.
 | 
						|
 | 
						|
 | 
						|
"""Provides HTTP functions for gdata.service to use on Google App Engine
 | 
						|
 | 
						|
AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
 | 
						|
   urlfetch API. Set the http_client member of a GDataService object to an
 | 
						|
   instance of an AppEngineHttpClient to allow the gdata library to run on
 | 
						|
   Google App Engine.
 | 
						|
 | 
						|
run_on_appengine: Function which will modify an existing GDataService object
 | 
						|
   to allow it to run on App Engine. It works by creating a new instance of
 | 
						|
   the AppEngineHttpClient and replacing the GDataService object's
 | 
						|
   http_client.
 | 
						|
"""
 | 
						|
 | 
						|
 | 
						|
__author__ = 'api.jscudder (Jeff Scudder)'
 | 
						|
 | 
						|
 | 
						|
import StringIO
 | 
						|
import pickle
 | 
						|
import atom.http_interface
 | 
						|
import atom.token_store
 | 
						|
from google.appengine.api import urlfetch
 | 
						|
from google.appengine.ext import db
 | 
						|
from google.appengine.api import users
 | 
						|
from google.appengine.api import memcache
 | 
						|
 | 
						|
 | 
						|
def run_on_appengine(gdata_service, store_tokens=True, 
 | 
						|
    single_user_mode=False, deadline=None):
 | 
						|
  """Modifies a GDataService object to allow it to run on App Engine.
 | 
						|
 | 
						|
  Args:
 | 
						|
    gdata_service: An instance of AtomService, GDataService, or any
 | 
						|
        of their subclasses which has an http_client member and a 
 | 
						|
        token_store member.
 | 
						|
    store_tokens: Boolean, defaults to True. If True, the gdata_service
 | 
						|
                  will attempt to add each token to it's token_store when
 | 
						|
                  SetClientLoginToken or SetAuthSubToken is called. If False
 | 
						|
                  the tokens will not automatically be added to the 
 | 
						|
                  token_store.
 | 
						|
    single_user_mode: Boolean, defaults to False. If True, the current_token
 | 
						|
                      member of gdata_service will be set when 
 | 
						|
                      SetClientLoginToken or SetAuthTubToken is called. If set
 | 
						|
                      to True, the current_token is set in the gdata_service
 | 
						|
                      and anyone who accesses the object will use the same 
 | 
						|
                      token. 
 | 
						|
                      
 | 
						|
                      Note: If store_tokens is set to False and 
 | 
						|
                      single_user_mode is set to False, all tokens will be 
 | 
						|
                      ignored, since the library assumes: the tokens should not
 | 
						|
                      be stored in the datastore and they should not be stored
 | 
						|
                      in the gdata_service object. This will make it 
 | 
						|
                      impossible to make requests which require authorization.
 | 
						|
    deadline: int (optional) The number of seconds to wait for a response
 | 
						|
              before timing out on the HTTP request. If no deadline is
 | 
						|
              specified, the deafault deadline for HTTP requests from App
 | 
						|
              Engine is used. The maximum is currently 10 (for 10 seconds).
 | 
						|
              The default deadline for App Engine is 5 seconds.
 | 
						|
  """
 | 
						|
  gdata_service.http_client = AppEngineHttpClient(deadline=deadline)
 | 
						|
  gdata_service.token_store = AppEngineTokenStore()
 | 
						|
  gdata_service.auto_store_tokens = store_tokens
 | 
						|
  gdata_service.auto_set_current_token = single_user_mode
 | 
						|
  return gdata_service
 | 
						|
 | 
						|
 | 
						|
class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
 | 
						|
  def __init__(self, headers=None, deadline=None):
 | 
						|
    self.debug = False
 | 
						|
    self.headers = headers or {}
 | 
						|
    self.deadline = deadline
 | 
						|
 | 
						|
  def request(self, operation, url, data=None, headers=None):
 | 
						|
    """Performs an HTTP call to the server, supports GET, POST, PUT, and
 | 
						|
    DELETE.
 | 
						|
 | 
						|
    Usage example, perform and HTTP GET on http://www.google.com/:
 | 
						|
      import atom.http
 | 
						|
      client = atom.http.HttpClient()
 | 
						|
      http_response = client.request('GET', 'http://www.google.com/')
 | 
						|
 | 
						|
    Args:
 | 
						|
      operation: str The HTTP operation to be performed. This is usually one
 | 
						|
          of 'GET', 'POST', 'PUT', or 'DELETE'
 | 
						|
      data: filestream, list of parts, or other object which can be converted
 | 
						|
          to a string. Should be set to None when performing a GET or DELETE.
 | 
						|
          If data is a file-like object which can be read, this method will
 | 
						|
          read a chunk of 100K bytes at a time and send them.
 | 
						|
          If the data is a list of parts to be sent, each part will be
 | 
						|
          evaluated and sent.
 | 
						|
      url: The full URL to which the request should be sent. Can be a string
 | 
						|
          or atom.url.Url.
 | 
						|
      headers: dict of strings. HTTP headers which should be sent
 | 
						|
          in the request.
 | 
						|
    """
 | 
						|
    all_headers = self.headers.copy()
 | 
						|
    if headers:
 | 
						|
      all_headers.update(headers)
 | 
						|
 | 
						|
    # Construct the full payload.
 | 
						|
    # Assume that data is None or a string.
 | 
						|
    data_str = data
 | 
						|
    if data:
 | 
						|
      if isinstance(data, list):
 | 
						|
        # If data is a list of different objects, convert them all to strings
 | 
						|
        # and join them together.
 | 
						|
        converted_parts = [_convert_data_part(x) for x in data]
 | 
						|
        data_str = ''.join(converted_parts)
 | 
						|
      else:
 | 
						|
        data_str = _convert_data_part(data)
 | 
						|
 | 
						|
    # If the list of headers does not include a Content-Length, attempt to
 | 
						|
    # calculate it based on the data object.
 | 
						|
    if data and 'Content-Length' not in all_headers:
 | 
						|
      all_headers['Content-Length'] = str(len(data_str))
 | 
						|
 | 
						|
    # Set the content type to the default value if none was set.
 | 
						|
    if 'Content-Type' not in all_headers:
 | 
						|
      all_headers['Content-Type'] = 'application/atom+xml'
 | 
						|
 | 
						|
    # Lookup the urlfetch operation which corresponds to the desired HTTP verb.
 | 
						|
    if operation == 'GET':
 | 
						|
      method = urlfetch.GET
 | 
						|
    elif operation == 'POST':
 | 
						|
      method = urlfetch.POST
 | 
						|
    elif operation == 'PUT':
 | 
						|
      method = urlfetch.PUT
 | 
						|
    elif operation == 'DELETE':
 | 
						|
      method = urlfetch.DELETE
 | 
						|
    else:
 | 
						|
      method = None
 | 
						|
    if self.deadline is None:
 | 
						|
      return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
 | 
						|
          method=method, headers=all_headers, follow_redirects=False))
 | 
						|
    return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
 | 
						|
        method=method, headers=all_headers, follow_redirects=False,
 | 
						|
        deadline=self.deadline))
 | 
						|
 | 
						|
 | 
						|
def _convert_data_part(data):
 | 
						|
  if not data or isinstance(data, str):
 | 
						|
    return data
 | 
						|
  elif hasattr(data, 'read'):
 | 
						|
    # data is a file like object, so read it completely.
 | 
						|
    return data.read()
 | 
						|
  # The data object was not a file.
 | 
						|
  # Try to convert to a string and send the data.
 | 
						|
  return str(data)
 | 
						|
 | 
						|
 | 
						|
class HttpResponse(object):
 | 
						|
  """Translates a urlfetch resoinse to look like an hhtplib resoinse.
 | 
						|
 | 
						|
  Used to allow the resoinse from HttpRequest to be usable by gdata.service
 | 
						|
  methods.
 | 
						|
  """
 | 
						|
 | 
						|
  def __init__(self, urlfetch_response):
 | 
						|
    self.body = StringIO.StringIO(urlfetch_response.content)
 | 
						|
    self.headers = urlfetch_response.headers
 | 
						|
    self.status = urlfetch_response.status_code
 | 
						|
    self.reason = ''
 | 
						|
 | 
						|
  def read(self, length=None):
 | 
						|
    if not length:
 | 
						|
      return self.body.read()
 | 
						|
    else:
 | 
						|
      return self.body.read(length)
 | 
						|
 | 
						|
  def getheader(self, name):
 | 
						|
    if not self.headers.has_key(name):
 | 
						|
      return self.headers[name.lower()]
 | 
						|
    return self.headers[name]
 | 
						|
 | 
						|
 | 
						|
class TokenCollection(db.Model):
 | 
						|
  """Datastore Model which associates auth tokens with the current user."""
 | 
						|
  user = db.UserProperty()
 | 
						|
  pickled_tokens = db.BlobProperty()
 | 
						|
 | 
						|
 | 
						|
class AppEngineTokenStore(atom.token_store.TokenStore):
 | 
						|
  """Stores the user's auth tokens in the App Engine datastore.
 | 
						|
 | 
						|
  Tokens are only written to the datastore if a user is signed in (if 
 | 
						|
  users.get_current_user() returns a user object).
 | 
						|
  """
 | 
						|
  def __init__(self):
 | 
						|
    self.user = None
 | 
						|
 | 
						|
  def add_token(self, token):
 | 
						|
    """Associates the token with the current user and stores it.
 | 
						|
    
 | 
						|
    If there is no current user, the token will not be stored.
 | 
						|
 | 
						|
    Returns:
 | 
						|
      False if the token was not stored. 
 | 
						|
    """
 | 
						|
    tokens = load_auth_tokens(self.user)
 | 
						|
    if not hasattr(token, 'scopes') or not token.scopes:
 | 
						|
      return False
 | 
						|
    for scope in token.scopes:
 | 
						|
      tokens[str(scope)] = token
 | 
						|
    key = save_auth_tokens(tokens, self.user)
 | 
						|
    if key:
 | 
						|
      return True
 | 
						|
    return False
 | 
						|
 | 
						|
  def find_token(self, url):
 | 
						|
    """Searches the current user's collection of token for a token which can
 | 
						|
    be used for a request to the url.
 | 
						|
 | 
						|
    Returns:
 | 
						|
      The stored token which belongs to the current user and is valid for the
 | 
						|
      desired URL. If there is no current user, or there is no valid user 
 | 
						|
      token in the datastore, a atom.http_interface.GenericToken is returned.
 | 
						|
    """
 | 
						|
    if url is None:
 | 
						|
      return None
 | 
						|
    if isinstance(url, (str, unicode)):
 | 
						|
      url = atom.url.parse_url(url)
 | 
						|
    tokens = load_auth_tokens(self.user)
 | 
						|
    if url in tokens:
 | 
						|
      token = tokens[url]
 | 
						|
      if token.valid_for_scope(url):
 | 
						|
        return token
 | 
						|
      else:
 | 
						|
        del tokens[url]
 | 
						|
        save_auth_tokens(tokens, self.user)
 | 
						|
    for scope, token in tokens.iteritems():
 | 
						|
      if token.valid_for_scope(url):
 | 
						|
        return token
 | 
						|
    return atom.http_interface.GenericToken()
 | 
						|
 | 
						|
  def remove_token(self, token):
 | 
						|
    """Removes the token from the current user's collection in the datastore.
 | 
						|
    
 | 
						|
    Returns:
 | 
						|
      False if the token was not removed, this could be because the token was
 | 
						|
      not in the datastore, or because there is no current user.
 | 
						|
    """
 | 
						|
    token_found = False
 | 
						|
    scopes_to_delete = []
 | 
						|
    tokens = load_auth_tokens(self.user)
 | 
						|
    for scope, stored_token in tokens.iteritems():
 | 
						|
      if stored_token == token:
 | 
						|
        scopes_to_delete.append(scope)
 | 
						|
        token_found = True
 | 
						|
    for scope in scopes_to_delete:
 | 
						|
      del tokens[scope]
 | 
						|
    if token_found:
 | 
						|
      save_auth_tokens(tokens, self.user)
 | 
						|
    return token_found
 | 
						|
 | 
						|
  def remove_all_tokens(self):
 | 
						|
    """Removes all of the current user's tokens from the datastore."""
 | 
						|
    save_auth_tokens({}, self.user)
 | 
						|
 | 
						|
 | 
						|
def save_auth_tokens(token_dict, user=None):
 | 
						|
  """Associates the tokens with the current user and writes to the datastore.
 | 
						|
  
 | 
						|
  If there us no current user, the tokens are not written and this function
 | 
						|
  returns None.
 | 
						|
 | 
						|
  Returns:
 | 
						|
    The key of the datastore entity containing the user's tokens, or None if
 | 
						|
    there was no current user.
 | 
						|
  """
 | 
						|
  if user is None:
 | 
						|
    user = users.get_current_user()
 | 
						|
  if user is None:
 | 
						|
    return None
 | 
						|
  memcache.set('gdata_pickled_tokens:%s' % user, pickle.dumps(token_dict))
 | 
						|
  user_tokens = TokenCollection.all().filter('user =', user).get()
 | 
						|
  if user_tokens:
 | 
						|
    user_tokens.pickled_tokens = pickle.dumps(token_dict)
 | 
						|
    return user_tokens.put()
 | 
						|
  else:
 | 
						|
    user_tokens = TokenCollection(
 | 
						|
        user=user, 
 | 
						|
        pickled_tokens=pickle.dumps(token_dict))
 | 
						|
    return user_tokens.put()
 | 
						|
     
 | 
						|
 | 
						|
def load_auth_tokens(user=None):
 | 
						|
  """Reads a dictionary of the current user's tokens from the datastore.
 | 
						|
  
 | 
						|
  If there is no current user (a user is not signed in to the app) or the user
 | 
						|
  does not have any tokens, an empty dictionary is returned.
 | 
						|
  """
 | 
						|
  if user is None:
 | 
						|
    user = users.get_current_user()
 | 
						|
  if user is None:
 | 
						|
    return {}
 | 
						|
  pickled_tokens = memcache.get('gdata_pickled_tokens:%s' % user)
 | 
						|
  if pickled_tokens:
 | 
						|
    return pickle.loads(pickled_tokens)
 | 
						|
  user_tokens = TokenCollection.all().filter('user =', user).get()
 | 
						|
  if user_tokens:
 | 
						|
    memcache.set('gdata_pickled_tokens:%s' % user, user_tokens.pickled_tokens)
 | 
						|
    return pickle.loads(user_tokens.pickled_tokens)
 | 
						|
  return {}
 | 
						|
 |