# Copyright 2010 Google Inc. All Rights Reserved. """Classes to encapsulate a single HTTP request. The classes implement a command pattern, with every object supporting an execute() method that does the actuall HTTP request. """ __author__ = 'jcgregorio@google.com (Joe Gregorio)' __all__ = [ 'HttpRequest', 'RequestMockBuilder', 'HttpMock' 'set_user_agent', 'tunnel_patch' ] import httplib2 import os from model import JsonModel from errors import HttpError from anyjson import simplejson class HttpRequest(object): """Encapsulates a single HTTP request. """ def __init__(self, http, postproc, uri, method='GET', body=None, headers=None, methodId=None): """Constructor for an HttpRequest. Args: http: httplib2.Http, the transport object to use to make a request postproc: callable, called on the HTTP response and content to transform it into a data object before returning, or raising an exception on an error. uri: string, the absolute URI to send the request to method: string, the HTTP method to use body: string, the request body of the HTTP request headers: dict, the HTTP request headers methodId: string, a unique identifier for the API method being called. """ self.uri = uri self.method = method self.body = body self.headers = headers or {} self.http = http self.postproc = postproc def execute(self, http=None): """Execute the request. Args: http: httplib2.Http, an http object to be used in place of the one the HttpRequest request object was constructed with. Returns: A deserialized object model of the response body as determined by the postproc. Raises: apiclient.errors.HttpError if the response was not a 2xx. httplib2.Error if a transport error has occured. """ if http is None: http = self.http resp, content = http.request(self.uri, self.method, body=self.body, headers=self.headers) if resp.status >= 300: raise HttpError(resp, content, self.uri) return self.postproc(resp, content) class HttpRequestMock(object): """Mock of HttpRequest. Do not construct directly, instead use RequestMockBuilder. """ def __init__(self, resp, content, postproc): """Constructor for HttpRequestMock Args: resp: httplib2.Response, the response to emulate coming from the request content: string, the response body postproc: callable, the post processing function usually supplied by the model class. See model.JsonModel.response() as an example. """ self.resp = resp self.content = content self.postproc = postproc if resp is None: self.resp = httplib2.Response({'status': 200, 'reason': 'OK'}) if 'reason' in self.resp: self.resp.reason = self.resp['reason'] def execute(self, http=None): """Execute the request. Same behavior as HttpRequest.execute(), but the response is mocked and not really from an HTTP request/response. """ return self.postproc(self.resp, self.content) class RequestMockBuilder(object): """A simple mock of HttpRequest Pass in a dictionary to the constructor that maps request methodIds to tuples of (httplib2.Response, content) that should be returned when that method is called. None may also be passed in for the httplib2.Response, in which case a 200 OK response will be generated. Example: response = '{"data": {"id": "tag:google.c...' requestBuilder = RequestMockBuilder( { 'chili.activities.get': (None, response), } ) apiclient.discovery.build("buzz", "v1", requestBuilder=requestBuilder) Methods that you do not supply a response for will return a 200 OK with an empty string as the response content. The methodId is taken from the rpcName in the discovery document. For more details see the project wiki. """ def __init__(self, responses): """Constructor for RequestMockBuilder The constructed object should be a callable object that can replace the class HttpResponse. responses - A dictionary that maps methodIds into tuples of (httplib2.Response, content). The methodId comes from the 'rpcName' field in the discovery document. """ self.responses = responses def __call__(self, http, postproc, uri, method='GET', body=None, headers=None, methodId=None): """Implements the callable interface that discovery.build() expects of requestBuilder, which is to build an object compatible with HttpRequest.execute(). See that method for the description of the parameters and the expected response. """ if methodId in self.responses: resp, content = self.responses[methodId] return HttpRequestMock(resp, content, postproc) else: model = JsonModel(False) return HttpRequestMock(None, '{}', model.response) class HttpMock(object): """Mock of httplib2.Http""" def __init__(self, filename, headers=None): """ Args: filename: string, absolute filename to read response from headers: dict, header to return with response """ if headers is None: headers = {'status': '200 OK'} f = file(filename, 'r') self.data = f.read() f.close() self.headers = headers def request(self, uri, method='GET', body=None, headers=None, redirections=1, connection_type=None): return httplib2.Response(self.headers), self.data class HttpMockSequence(object): """Mock of httplib2.Http Mocks a sequence of calls to request returning different responses for each call. Create an instance initialized with the desired response headers and content and then use as if an httplib2.Http instance. http = HttpMockSequence([ ({'status': '401'}, ''), ({'status': '200'}, '{"access_token":"1/3w","expires_in":3600}'), ({'status': '200'}, 'echo_request_headers'), ]) resp, content = http.request("http://examples.com") There are special values you can pass in for content to trigger behavours that are helpful in testing. 'echo_request_headers' means return the request headers in the response body 'echo_request_headers_as_json' means return the request headers in the response body 'echo_request_body' means return the request body in the response body """ def __init__(self, iterable): """ Args: iterable: iterable, a sequence of pairs of (headers, body) """ self._iterable = iterable def request(self, uri, method='GET', body=None, headers=None, redirections=1, connection_type=None): resp, content = self._iterable.pop(0) if content == 'echo_request_headers': content = headers elif content == 'echo_request_headers_as_json': content = simplejson.dumps(headers) elif content == 'echo_request_body': content = body return httplib2.Response(resp), content def set_user_agent(http, user_agent): """Set the user-agent on every request. Args: http - An instance of httplib2.Http or something that acts like it. user_agent: string, the value for the user-agent header. Returns: A modified instance of http that was passed in. Example: h = httplib2.Http() h = set_user_agent(h, "my-app-name/6.0") Most of the time the user-agent will be set doing auth, this is for the rare cases where you are accessing an unauthenticated endpoint. """ request_orig = http.request # The closure that will replace 'httplib2.Http.request'. def new_request(uri, method='GET', body=None, headers=None, redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): """Modify the request headers to add the user-agent.""" if headers is None: headers = {} if 'user-agent' in headers: headers['user-agent'] = user_agent + ' ' + headers['user-agent'] else: headers['user-agent'] = user_agent resp, content = request_orig(uri, method, body, headers, redirections, connection_type) return resp, content http.request = new_request return http def tunnel_patch(http): """Tunnel PATCH requests over POST. Args: http - An instance of httplib2.Http or something that acts like it. Returns: A modified instance of http that was passed in. Example: h = httplib2.Http() h = tunnel_patch(h, "my-app-name/6.0") Useful if you are running on a platform that doesn't support PATCH. Apply this last if you are using OAuth 1.0, as changing the method will result in a different signature. """ request_orig = http.request # The closure that will replace 'httplib2.Http.request'. def new_request(uri, method='GET', body=None, headers=None, redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): """Modify the request headers to add the user-agent.""" if headers is None: headers = {} if method == 'PATCH': if 'oauth_token' in headers.get('authorization', ''): logging.warning( 'OAuth 1.0 request made with Credentials after tunnel_patch.') headers['x-http-method-override'] = "PATCH" method = 'POST' resp, content = request_orig(uri, method, body, headers, redirections, connection_type) return resp, content http.request = new_request return http