|  | # 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 'authorization' in headers and 'oauth_token' in headers['authorization']: | 
|  | logging.warning('OAuth 1.0 request made with Credentials applied 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 |