PageRenderTime 35ms CodeModel.GetById 16ms app.highlight 14ms RepoModel.GetById 1ms app.codeStats 0ms

/gdata/alt/appengine.py

http://radioappz.googlecode.com/
Python | 321 lines | 285 code | 4 blank | 32 comment | 14 complexity | 9b1d6fbc0e0187b36a21185669ca445d MD5 | raw file
  1#!/usr/bin/python
  2#
  3# Copyright (C) 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"""Provides HTTP functions for gdata.service to use on Google App Engine
 19
 20AppEngineHttpClient: Provides an HTTP request method which uses App Engine's
 21   urlfetch API. Set the http_client member of a GDataService object to an
 22   instance of an AppEngineHttpClient to allow the gdata library to run on
 23   Google App Engine.
 24
 25run_on_appengine: Function which will modify an existing GDataService object
 26   to allow it to run on App Engine. It works by creating a new instance of
 27   the AppEngineHttpClient and replacing the GDataService object's
 28   http_client.
 29"""
 30
 31
 32__author__ = 'api.jscudder (Jeff Scudder)'
 33
 34
 35import StringIO
 36import pickle
 37import atom.http_interface
 38import atom.token_store
 39from google.appengine.api import urlfetch
 40from google.appengine.ext import db
 41from google.appengine.api import users
 42from google.appengine.api import memcache
 43
 44
 45def run_on_appengine(gdata_service, store_tokens=True, 
 46    single_user_mode=False, deadline=None):
 47  """Modifies a GDataService object to allow it to run on App Engine.
 48
 49  Args:
 50    gdata_service: An instance of AtomService, GDataService, or any
 51        of their subclasses which has an http_client member and a 
 52        token_store member.
 53    store_tokens: Boolean, defaults to True. If True, the gdata_service
 54                  will attempt to add each token to it's token_store when
 55                  SetClientLoginToken or SetAuthSubToken is called. If False
 56                  the tokens will not automatically be added to the 
 57                  token_store.
 58    single_user_mode: Boolean, defaults to False. If True, the current_token
 59                      member of gdata_service will be set when 
 60                      SetClientLoginToken or SetAuthTubToken is called. If set
 61                      to True, the current_token is set in the gdata_service
 62                      and anyone who accesses the object will use the same 
 63                      token. 
 64                      
 65                      Note: If store_tokens is set to False and 
 66                      single_user_mode is set to False, all tokens will be 
 67                      ignored, since the library assumes: the tokens should not
 68                      be stored in the datastore and they should not be stored
 69                      in the gdata_service object. This will make it 
 70                      impossible to make requests which require authorization.
 71    deadline: int (optional) The number of seconds to wait for a response
 72              before timing out on the HTTP request. If no deadline is
 73              specified, the deafault deadline for HTTP requests from App
 74              Engine is used. The maximum is currently 10 (for 10 seconds).
 75              The default deadline for App Engine is 5 seconds.
 76  """
 77  gdata_service.http_client = AppEngineHttpClient(deadline=deadline)
 78  gdata_service.token_store = AppEngineTokenStore()
 79  gdata_service.auto_store_tokens = store_tokens
 80  gdata_service.auto_set_current_token = single_user_mode
 81  return gdata_service
 82
 83
 84class AppEngineHttpClient(atom.http_interface.GenericHttpClient):
 85  def __init__(self, headers=None, deadline=None):
 86    self.debug = False
 87    self.headers = headers or {}
 88    self.deadline = deadline
 89
 90  def request(self, operation, url, data=None, headers=None):
 91    """Performs an HTTP call to the server, supports GET, POST, PUT, and
 92    DELETE.
 93
 94    Usage example, perform and HTTP GET on http://www.google.com/:
 95      import atom.http
 96      client = atom.http.HttpClient()
 97      http_response = client.request('GET', 'http://www.google.com/')
 98
 99    Args:
100      operation: str The HTTP operation to be performed. This is usually one
101          of 'GET', 'POST', 'PUT', or 'DELETE'
102      data: filestream, list of parts, or other object which can be converted
103          to a string. Should be set to None when performing a GET or DELETE.
104          If data is a file-like object which can be read, this method will
105          read a chunk of 100K bytes at a time and send them.
106          If the data is a list of parts to be sent, each part will be
107          evaluated and sent.
108      url: The full URL to which the request should be sent. Can be a string
109          or atom.url.Url.
110      headers: dict of strings. HTTP headers which should be sent
111          in the request.
112    """
113    all_headers = self.headers.copy()
114    if headers:
115      all_headers.update(headers)
116
117    # Construct the full payload.
118    # Assume that data is None or a string.
119    data_str = data
120    if data:
121      if isinstance(data, list):
122        # If data is a list of different objects, convert them all to strings
123        # and join them together.
124        converted_parts = [_convert_data_part(x) for x in data]
125        data_str = ''.join(converted_parts)
126      else:
127        data_str = _convert_data_part(data)
128
129    # If the list of headers does not include a Content-Length, attempt to
130    # calculate it based on the data object.
131    if data and 'Content-Length' not in all_headers:
132      all_headers['Content-Length'] = str(len(data_str))
133
134    # Set the content type to the default value if none was set.
135    if 'Content-Type' not in all_headers:
136      all_headers['Content-Type'] = 'application/atom+xml'
137
138    # Lookup the urlfetch operation which corresponds to the desired HTTP verb.
139    if operation == 'GET':
140      method = urlfetch.GET
141    elif operation == 'POST':
142      method = urlfetch.POST
143    elif operation == 'PUT':
144      method = urlfetch.PUT
145    elif operation == 'DELETE':
146      method = urlfetch.DELETE
147    else:
148      method = None
149    if self.deadline is None:
150      return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
151          method=method, headers=all_headers, follow_redirects=False))
152    return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str,
153        method=method, headers=all_headers, follow_redirects=False,
154        deadline=self.deadline))
155
156
157def _convert_data_part(data):
158  if not data or isinstance(data, str):
159    return data
160  elif hasattr(data, 'read'):
161    # data is a file like object, so read it completely.
162    return data.read()
163  # The data object was not a file.
164  # Try to convert to a string and send the data.
165  return str(data)
166
167
168class HttpResponse(object):
169  """Translates a urlfetch resoinse to look like an hhtplib resoinse.
170
171  Used to allow the resoinse from HttpRequest to be usable by gdata.service
172  methods.
173  """
174
175  def __init__(self, urlfetch_response):
176    self.body = StringIO.StringIO(urlfetch_response.content)
177    self.headers = urlfetch_response.headers
178    self.status = urlfetch_response.status_code
179    self.reason = ''
180
181  def read(self, length=None):
182    if not length:
183      return self.body.read()
184    else:
185      return self.body.read(length)
186
187  def getheader(self, name):
188    if not self.headers.has_key(name):
189      return self.headers[name.lower()]
190    return self.headers[name]
191
192
193class TokenCollection(db.Model):
194  """Datastore Model which associates auth tokens with the current user."""
195  user = db.UserProperty()
196  pickled_tokens = db.BlobProperty()
197
198
199class AppEngineTokenStore(atom.token_store.TokenStore):
200  """Stores the user's auth tokens in the App Engine datastore.
201
202  Tokens are only written to the datastore if a user is signed in (if 
203  users.get_current_user() returns a user object).
204  """
205  def __init__(self):
206    self.user = None
207
208  def add_token(self, token):
209    """Associates the token with the current user and stores it.
210    
211    If there is no current user, the token will not be stored.
212
213    Returns:
214      False if the token was not stored. 
215    """
216    tokens = load_auth_tokens(self.user)
217    if not hasattr(token, 'scopes') or not token.scopes:
218      return False
219    for scope in token.scopes:
220      tokens[str(scope)] = token
221    key = save_auth_tokens(tokens, self.user)
222    if key:
223      return True
224    return False
225
226  def find_token(self, url):
227    """Searches the current user's collection of token for a token which can
228    be used for a request to the url.
229
230    Returns:
231      The stored token which belongs to the current user and is valid for the
232      desired URL. If there is no current user, or there is no valid user 
233      token in the datastore, a atom.http_interface.GenericToken is returned.
234    """
235    if url is None:
236      return None
237    if isinstance(url, (str, unicode)):
238      url = atom.url.parse_url(url)
239    tokens = load_auth_tokens(self.user)
240    if url in tokens:
241      token = tokens[url]
242      if token.valid_for_scope(url):
243        return token
244      else:
245        del tokens[url]
246        save_auth_tokens(tokens, self.user)
247    for scope, token in tokens.iteritems():
248      if token.valid_for_scope(url):
249        return token
250    return atom.http_interface.GenericToken()
251
252  def remove_token(self, token):
253    """Removes the token from the current user's collection in the datastore.
254    
255    Returns:
256      False if the token was not removed, this could be because the token was
257      not in the datastore, or because there is no current user.
258    """
259    token_found = False
260    scopes_to_delete = []
261    tokens = load_auth_tokens(self.user)
262    for scope, stored_token in tokens.iteritems():
263      if stored_token == token:
264        scopes_to_delete.append(scope)
265        token_found = True
266    for scope in scopes_to_delete:
267      del tokens[scope]
268    if token_found:
269      save_auth_tokens(tokens, self.user)
270    return token_found
271
272  def remove_all_tokens(self):
273    """Removes all of the current user's tokens from the datastore."""
274    save_auth_tokens({}, self.user)
275
276
277def save_auth_tokens(token_dict, user=None):
278  """Associates the tokens with the current user and writes to the datastore.
279  
280  If there us no current user, the tokens are not written and this function
281  returns None.
282
283  Returns:
284    The key of the datastore entity containing the user's tokens, or None if
285    there was no current user.
286  """
287  if user is None:
288    user = users.get_current_user()
289  if user is None:
290    return None
291  memcache.set('gdata_pickled_tokens:%s' % user, pickle.dumps(token_dict))
292  user_tokens = TokenCollection.all().filter('user =', user).get()
293  if user_tokens:
294    user_tokens.pickled_tokens = pickle.dumps(token_dict)
295    return user_tokens.put()
296  else:
297    user_tokens = TokenCollection(
298        user=user, 
299        pickled_tokens=pickle.dumps(token_dict))
300    return user_tokens.put()
301     
302
303def load_auth_tokens(user=None):
304  """Reads a dictionary of the current user's tokens from the datastore.
305  
306  If there is no current user (a user is not signed in to the app) or the user
307  does not have any tokens, an empty dictionary is returned.
308  """
309  if user is None:
310    user = users.get_current_user()
311  if user is None:
312    return {}
313  pickled_tokens = memcache.get('gdata_pickled_tokens:%s' % user)
314  if pickled_tokens:
315    return pickle.loads(pickled_tokens)
316  user_tokens = TokenCollection.all().filter('user =', user).get()
317  if user_tokens:
318    memcache.set('gdata_pickled_tokens:%s' % user, user_tokens.pickled_tokens)
319    return pickle.loads(user_tokens.pickled_tokens)
320  return {}
321