| # Copyright (C) 2010 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. |
| |
| """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__ = '[email protected] (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 |