#!/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. HttpRequest: Function that wraps google.appengine.api.urlfetch.Fetch in a common interface which is used by gdata.service.GDataService. In other words, this module can be used as the gdata service request handler so that all HTTP requests will be performed by the hosting Google App Engine server. """ __author__ = 'api.jscudder (Jeff Scudder)' import StringIO import atom.service import atom.http_interface from google.appengine.api import urlfetch def run_on_appengine(gdata_service): """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. """ gdata_service.http_client = AppEngineHttpClient() class AppEngineHttpClient(atom.http_interface.GenericHttpClient): def __init__(self, headers=None): self.debug = False self.headers = headers or {} 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 = [__ConvertDataPart(x) for x in data] data_str = ''.join(converted_parts) else: data_str = __ConvertDataPart(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'] = 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 return HttpResponse(urlfetch.Fetch(url=str(url), payload=data_str, method=method, headers=all_headers)) def HttpRequest(service, operation, data, uri, extra_headers=None, url_params=None, escape_params=True, content_type='application/atom+xml'): """Performs an HTTP call to the server, supports GET, POST, PUT, and DELETE. This function is deprecated, use AppEngineHttpClient.request instead. To use this module with gdata.service, you can set this module to be the http_request_handler so that HTTP requests use Google App Engine's urlfetch. import gdata.service import gdata.urlfetch gdata.service.http_request_handler = gdata.urlfetch Args: service: atom.AtomService object which contains some of the parameters needed to make the request. The following members are used to construct the HTTP call: server (str), additional_headers (dict), port (int), and ssl (bool). 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 PUT. 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. uri: The beginning of the URL to which the request should be sent. Examples: '/', '/base/feeds/snippets', '/m8/feeds/contacts/default/base' extra_headers: dict of strings. HTTP headers which should be sent in the request. These headers are in addition to those stored in service.additional_headers. url_params: dict of strings. Key value pairs to be added to the URL as URL parameters. For example {'foo':'bar', 'test':'param'} will become ?foo=bar&test=param. escape_params: bool default True. If true, the keys and values in url_params will be URL escaped when the form is constructed (Special characters converted to %XX form.) content_type: str The MIME type for the data being sent. Defaults to 'application/atom+xml', this is only used if data is set. """ full_uri = atom.service.BuildUri(uri, url_params, escape_params) (server, port, ssl, partial_uri) = atom.service.ProcessUrl(service, full_uri) # Construct the full URL for the request. if ssl: full_url = 'https://%s%s' % (server, partial_uri) else: full_url = 'http://%s%s' % (server, partial_uri) # 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 = [__ConvertDataPart(x) for x in data] data_str = ''.join(converted_parts) else: data_str = __ConvertDataPart(data) # Construct the dictionary of HTTP headers. headers = {} if isinstance(service.additional_headers, dict): headers = service.additional_headers.copy() if isinstance(extra_headers, dict): for header, value in extra_headers.iteritems(): headers[header] = value # Add the content type header (we don't need to calculate content length, # since urlfetch.Fetch will calculate for us). if content_type: headers['Content-Type'] = content_type # 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 return HttpResponse(urlfetch.Fetch(url=full_url, payload=data_str, method=method, headers=headers)) def __ConvertDataPart(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]