313 lines
9.6 KiB
Python
313 lines
9.6 KiB
Python
# 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
|