| #!/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] |
| |