Added mocks for the generated service objects. Also fixed a bunch of formatting.
diff --git a/apiclient/discovery.py b/apiclient/discovery.py
index 1660288..36d38e7 100644
--- a/apiclient/discovery.py
+++ b/apiclient/discovery.py
@@ -32,31 +32,15 @@
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
+
from apiclient.http import HttpRequest
from apiclient.json import simplejson
+from apiclient.model import JsonModel
+from apiclient.errors import HttpError
+from apiclient.errors import UnknownLinkType
URITEMPLATE = re.compile('{[^}]*}')
VARNAME = re.compile('[a-zA-Z0-9_-]+')
-
-class Error(Exception):
- """Base error for this module."""
- pass
-
-
-class HttpError(Error):
- """HTTP data was invalid or unexpected."""
- def __init__(self, resp, detail):
- self.resp = resp
- self.detail = detail
- def __str__(self):
- return self.detail
-
-
-class UnknownLinkType(Error):
- """Link type unknown or unexpected."""
- pass
-
-
DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
'{api}/{apiVersion}')
@@ -78,52 +62,12 @@
return ''.join(result)
-class JsonModel(object):
-
- def request(self, headers, path_params, query_params, body_value):
- query = self.build_query(query_params)
- headers['accept'] = 'application/json'
- if 'user-agent' in headers:
- headers['user-agent'] += ' '
- else:
- headers['user-agent'] = ''
- headers['user-agent'] += 'google-api-python-client/1.0'
- if body_value is None:
- return (headers, path_params, query, None)
- else:
- headers['content-type'] = 'application/json'
- return (headers, path_params, query, simplejson.dumps(body_value))
-
- def build_query(self, params):
- params.update({'alt': 'json'})
- astuples = []
- for key, value in params.iteritems():
- if getattr(value, 'encode', False) and callable(value.encode):
- value = value.encode('utf-8')
- astuples.append((key, value))
- return '?' + urllib.urlencode(astuples)
-
- def response(self, resp, content):
- # Error handling is TBD, for example, do we retry
- # for some operation/error combinations?
- if resp.status < 300:
- if resp.status == 204:
- # A 204: No Content response should be treated differently to all the other success states
- return simplejson.loads('{}')
- body = simplejson.loads(content)
- if isinstance(body, dict) and 'data' in body:
- body = body['data']
- return body
- else:
- logging.debug('Content from bad request was: %s' % content)
- if resp.get('content-type', '').startswith('application/json'):
- raise HttpError(resp, simplejson.loads(content)['error'])
- else:
- raise HttpError(resp, '%d %s' % (resp.status, resp.reason))
-
-
-def build(serviceName, version, http=None,
- discoveryServiceUrl=DISCOVERY_URI, developerKey=None, model=JsonModel()):
+def build(serviceName, version,
+ http=None,
+ discoveryServiceUrl=DISCOVERY_URI,
+ developerKey=None,
+ model=JsonModel(),
+ requestBuilder=HttpRequest):
params = {
'api': serviceName,
'apiVersion': version
@@ -159,6 +103,7 @@
self._baseUrl = base
self._model = model
self._developerKey = developerKey
+ self._requestBuilder = requestBuilder
def auth_discovery(self):
return auth_discovery
@@ -167,7 +112,8 @@
def method(self):
return createResource(self._http, self._baseUrl, self._model,
- methodName, self._developerKey, methodDesc, futureDesc)
+ self._requestBuilder, methodName,
+ self._developerKey, methodDesc, futureDesc)
setattr(method, '__doc__', 'A description of how to use this function')
setattr(method, '__is_resource__', True)
@@ -178,8 +124,8 @@
return Service()
-def createResource(http, baseUrl, model, resourceName, developerKey,
- resourceDesc, futureDesc):
+def createResource(http, baseUrl, model, requestBuilder, resourceName,
+ developerKey, resourceDesc, futureDesc):
class Resource(object):
"""A class for interacting with a resource."""
@@ -189,11 +135,13 @@
self._baseUrl = baseUrl
self._model = model
self._developerKey = developerKey
+ self._requestBuilder = requestBuilder
def createMethod(theclass, methodName, methodDesc, futureDesc):
pathUrl = methodDesc['restPath']
pathUrl = re.sub(r'\{', r'{+', pathUrl)
httpMethod = methodDesc['httpMethod']
+ methodId = methodDesc['rpcMethod']
argmap = {}
if httpMethod in ['PUT', 'POST']:
@@ -257,18 +205,23 @@
headers, params, query, body = self._model.request(headers,
actual_path_params, actual_query_params, body_value)
- # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery document.
- # Base URLs should not contain any path elements. If they do then urlparse.urljoin will strip them out
- # This results in an incorrect URL which returns a 404
+ # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
+ # document. Base URLs should not contain any path elements. If they do
+ # then urlparse.urljoin will strip them out This results in an incorrect
+ # URL which returns a 404
url_result = urlparse.urlsplit(self._baseUrl)
new_base_url = url_result.scheme + '://' + url_result.netloc
expanded_url = uritemplate.expand(pathUrl, params)
- url = urlparse.urljoin(new_base_url, url_result.path + expanded_url + query)
+ url = urlparse.urljoin(new_base_url,
+ url_result.path + expanded_url + query)
logging.info('URL being requested: %s' % url)
- return HttpRequest(self._http, url, method=httpMethod, body=body,
- headers=headers, postproc=self._model.response)
+ return self._requestBuilder(self._http, url,
+ method=httpMethod, body=body,
+ headers=headers,
+ postproc=self._model.response,
+ methodId=methodId)
docs = ['A description of how to use this function\n\n']
for arg in argmap.iterkeys():
@@ -280,7 +233,8 @@
setattr(method, '__doc__', ''.join(docs))
setattr(theclass, methodName, method)
- def createNextMethod(theclass, methodName, methodDesc):
+ def createNextMethod(theclass, methodName, methodDesc, futureDesc):
+ methodId = methodDesc['rpcMethod'] + '.next'
def method(self, previous):
"""
@@ -291,12 +245,12 @@
Returns None if there are no more items in
the collection.
"""
- if methodDesc['type'] != 'uri':
- raise UnknownLinkType(methodDesc['type'])
+ if futureDesc['type'] != 'uri':
+ raise UnknownLinkType(futureDesc['type'])
try:
p = previous
- for key in methodDesc['location']:
+ for key in futureDesc['location']:
p = p[key]
url = p
except (KeyError, TypeError):
@@ -315,8 +269,10 @@
logging.info('URL being requested: %s' % url)
resp, content = self._http.request(url, method='GET', headers=headers)
- return HttpRequest(self._http, url, method='GET',
- headers=headers, postproc=self._model.response)
+ return self._requestBuilder(self._http, url, method='GET',
+ headers=headers,
+ postproc=self._model.response,
+ methodId=methodId)
setattr(theclass, methodName, method)
@@ -331,6 +287,7 @@
# Add in nested resources
if 'resources' in resourceDesc:
+
def createMethod(theclass, methodName, methodDesc, futureDesc):
def method(self):
@@ -346,12 +303,15 @@
future = futureDesc['resources'].get(methodName, {})
else:
future = {}
- createMethod(Resource, methodName, methodDesc, future.get(methodName, {}))
+ createMethod(Resource, methodName, methodDesc,
+ future.get(methodName, {}))
# Add <m>_next() methods to Resource
if futureDesc:
for methodName, methodDesc in futureDesc['methods'].iteritems():
if 'next' in methodDesc and methodName in resourceDesc['methods']:
- createNextMethod(Resource, methodName + "_next", methodDesc['next'])
+ createNextMethod(Resource, methodName + "_next",
+ resourceDesc['methods'][methodName],
+ methodDesc['next'])
return Resource()
diff --git a/apiclient/ext/django_orm.py b/apiclient/ext/django_orm.py
index 4217eaa..d8cb92d 100644
--- a/apiclient/ext/django_orm.py
+++ b/apiclient/ext/django_orm.py
@@ -1,5 +1,6 @@
from django.db import models
+
class OAuthCredentialsField(models.Field):
__metaclass__ = models.SubfieldBase
@@ -17,6 +18,7 @@
def get_db_prep_value(self, value):
return base64.b64encode(pickle.dumps(value))
+
class FlowThreeLeggedField(models.Field):
__metaclass__ = models.SubfieldBase
diff --git a/apiclient/http.py b/apiclient/http.py
index 25d646e..d616c07 100644
--- a/apiclient/http.py
+++ b/apiclient/http.py
@@ -1,19 +1,42 @@
# Copyright 2010 Google Inc. All Rights Reserved.
-"""One-line documentation for http module.
+"""Classes to encapsulate a single HTTP request.
-A detailed description of http.
+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'
+ ]
+
+from httplib2 import Response
+from apiclient.model import JsonModel
class HttpRequest(object):
- """Encapsulate an HTTP request.
+ """Encapsulates a single HTTP request.
"""
def __init__(self, http, uri, method="GET", body=None, headers=None,
- postproc=None):
+ postproc=None, methodId=None):
+ """Constructor for an HttpRequest.
+
+ Only http and uri are required.
+
+ Args:
+ http: httplib2.Http, the transport object to use to make a request
+ 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
+ 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.
+ methodId: string, a unique identifier for the API method being called.
+ """
self.uri = uri
self.method = method
self.body = body
@@ -24,8 +47,17 @@
def execute(self, http=None):
"""Execute the request.
- If an http object is passed in it is used instead of the
- httplib2.Http object that the request was constructed with.
+ 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
@@ -33,3 +65,87 @@
body=self.body,
headers=self.headers)
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 = 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, uri, method="GET", body=None, headers=None,
+ postproc=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()
+ return HttpRequestMock(None, '{}', model.response)
diff --git a/apiclient/oauth.py b/apiclient/oauth.py
index 9907c46..9cc6e66 100644
--- a/apiclient/oauth.py
+++ b/apiclient/oauth.py
@@ -131,9 +131,9 @@
headers = {}
headers.update(req.to_header())
if 'user-agent' in headers:
- headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
+ headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
else:
- headers['user-agent'] = self.user_agent
+ headers['user-agent'] = self.user_agent
return request_orig(uri, method, body, headers,
redirections, connection_type)