/gdata/alt/appengine.py
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