Added mocks for the generated service objects. Also fixed a bunch of formatting.

This commit is contained in:
Joe Gregorio
2010-12-09 14:26:58 -05:00
parent a12c7d68d7
commit af276d2904
20 changed files with 235 additions and 142 deletions

View File

@@ -32,31 +32,15 @@ try:
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 @@ def key2param(key):
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 @@ def build(serviceName, version, http=None,
self._baseUrl = base
self._model = model
self._developerKey = developerKey
self._requestBuilder = requestBuilder
def auth_discovery(self):
return auth_discovery
@@ -167,7 +112,8 @@ def build(serviceName, version, http=None,
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 @@ def build(serviceName, version, http=None,
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 @@ def createResource(http, baseUrl, model, resourceName, developerKey,
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 @@ def createResource(http, baseUrl, model, resourceName, developerKey,
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 @@ def createResource(http, baseUrl, model, resourceName, developerKey,
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 @@ def createResource(http, baseUrl, model, resourceName, developerKey,
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 @@ def createResource(http, baseUrl, model, resourceName, developerKey,
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 @@ def createResource(http, baseUrl, model, resourceName, developerKey,
# Add in nested resources
if 'resources' in resourceDesc:
def createMethod(theclass, methodName, methodDesc, futureDesc):
def method(self):
@@ -346,12 +303,15 @@ def createResource(http, baseUrl, model, resourceName, developerKey,
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()

View File

@@ -1,5 +1,6 @@
from django.db import models
class OAuthCredentialsField(models.Field):
__metaclass__ = models.SubfieldBase
@@ -17,6 +18,7 @@ class OAuthCredentialsField(models.Field):
def get_db_prep_value(self, value):
return base64.b64encode(pickle.dumps(value))
class FlowThreeLeggedField(models.Field):
__metaclass__ = models.SubfieldBase

View File

@@ -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 @@ class HttpRequest(object):
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 @@ class HttpRequest(object):
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)

View File

@@ -131,9 +131,9 @@ class OAuthCredentials(Credentials):
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)

View File

@@ -28,7 +28,11 @@ from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
# Replicate render_doc here from pydoc.py as it isn't available in Python 2.5
class _OldStyleClass: pass
class _OldStyleClass:
pass
def render_doc(thing, title='Python Library Documentation: %s', forceload=0):
"""Render text documentation, given an object or a path to an object."""
@@ -77,7 +81,8 @@ class ServiceHandler(webapp.RequestHandler):
def get(self, service_name, version):
service = build(service_name, version)
page = "<p><a href='/'>Home</a></p><pre>%s</pre>" % pydoc.plain(render_doc(service))
page = "<p><a href='/'>Home</a></p><pre>%s</pre>" % (
pydoc.plain(render_doc(service)),)
collections = []
for name in dir(service):
@@ -85,7 +90,8 @@ class ServiceHandler(webapp.RequestHandler):
collections.append(name)
for name in collections:
page = re.sub('(%s) =' % name, r'<a href="/%s/%s/%s">\1</a> =' % (service_name, version, name), page)
page = re.sub('(%s) =' % name, r'<a href="/%s/%s/%s">\1</a> =' % (
service_name, version, name), page)
self.response.out.write(page)
@@ -101,16 +107,19 @@ class CollectionHandler(webapp.RequestHandler):
service = getattr(service, method)()
method = getattr(service, path[-1])
obj = method()
page = "<p><a href='/'>Home</a></p><pre>%s</pre>" % pydoc.plain(render_doc(obj))
page = "<p><a href='/'>Home</a></p><pre>%s</pre>" % (
pydoc.plain(render_doc(obj)),)
if hasattr(method, '__is_resource__'):
collections = []
for name in dir(obj):
if not "_" in name and callable(getattr(obj, name)) and hasattr(getattr(obj, name), '__is_resource__'):
if not "_" in name and callable(getattr(obj, name)) and hasattr(
getattr(obj, name), '__is_resource__'):
collections.append(name)
for name in collections:
page = re.sub('(%s) =' % name, r'<a href="/%s/%s/%s">\1</a> =' % (service_name, version, collection + "/" + name), page)
page = re.sub('(%s) =' % name, r'<a href="/%s/%s/%s">\1</a> =' % (
service_name, version, collection + "/" + name), page)
self.response.out.write(page)

View File

@@ -20,6 +20,7 @@ import pprint
# Uncomment the next line to get very detailed logging
#httplib2.debuglevel = 4
def main():
f = open("buzz.dat", "r")
credentials = pickle.loads(f.read())

View File

@@ -17,9 +17,10 @@ import pprint
# Uncomment the next line to get very detailed logging
# httplib2.debuglevel = 4
def main():
p = build("customsearch", "v1", developerKey="AIzaSyDRRpR3GS1F1_jKNNM9HCNd2wJQyPG3oN0")
def main():
p = build("customsearch", "v1",
developerKey="AIzaSyDRRpR3GS1F1_jKNNM9HCNd2wJQyPG3oN0")
res = p.cse().list(
q='lectures',
cx='017576662512468239146:omuauf_lfve',

View File

@@ -20,9 +20,10 @@ import pprint
# Uncomment the next line to get very detailed logging
# httplib2.debuglevel = 4
def main():
p = build("diacritize", "v1", developerKey="AIzaSyDRRpR3GS1F1_jKNNM9HCNd2wJQyPG3oN0")
def main():
p = build("diacritize", "v1",
developerKey="AIzaSyDRRpR3GS1F1_jKNNM9HCNd2wJQyPG3oN0")
print p.diacritize().corpus().get(
lang='ar',
last_letter='false',

View File

@@ -8,22 +8,26 @@ from django.db import models
from apiclient.ext.django_orm import FlowThreeLeggedField
from apiclient.ext.django_orm import OAuthCredentialsField
# Create your models here.
# The Flow could also be stored in memcache since it is short lived.
class Flow(models.Model):
id = models.ForeignKey(User, primary_key=True)
flow = FlowThreeLeggedField()
class Credential(models.Model):
id = models.ForeignKey(User, primary_key=True)
credential = OAuthCredentialsField()
class CredentialAdmin(admin.ModelAdmin):
pass
class FlowAdmin(admin.ModelAdmin):
pass
admin.site.register(Credential, CredentialAdmin)
admin.site.register(Flow, FlowAdmin)

View File

@@ -7,7 +7,9 @@ Replace these with more appropriate tests for your application.
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
@@ -20,4 +22,3 @@ Another way to test that 1 + 1 is equal to 2.
>>> 1 + 1 == 2
True
"""}

View File

@@ -11,9 +11,9 @@ from apiclient.oauth import FlowThreeLegged
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
print os.environ
STEP2_URI = 'http://localhost:8000/auth_return'
@login_required
def index(request):
try:
@@ -45,6 +45,7 @@ def index(request):
f.save()
return HttpResponseRedirect(authorize_url)
@login_required
def auth_return(request):
try:

View File

@@ -10,12 +10,12 @@ ADMINS = (
MANAGERS = ADMINS
DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = 'database.sqlite3' # Or path to database file if using sqlite3.
DATABASE_USER = '' # Not used with sqlite3.
DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
DATABASE_ENGINE = 'sqlite3'
DATABASE_NAME = 'database.sqlite3'
DATABASE_USER = ''
DATABASE_PASSWORD = ''
DATABASE_HOST = ''
DATABASE_PORT = ''
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
@@ -67,7 +67,7 @@ MIDDLEWARE_CLASSES = (
ROOT_URLCONF = 'django_sample.urls'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Put strings here, like "/home/html/django_templates"
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
os.path.join(os.path.dirname(__file__), 'templates')

View File

@@ -20,6 +20,7 @@ import pickle
# Uncomment to get detailed logging
# httplib2.debuglevel = 4
def main():
f = open("latitude.dat", "r")
credentials = pickle.loads(f.read())
@@ -32,11 +33,11 @@ def main():
body = {
"data": {
"kind":"latitude#location",
"latitude":37.420352,
"longitude":-122.083389,
"accuracy":130,
"altitude":35
"kind": "latitude#location",
"latitude": 37.420352,
"longitude": -122.083389,
"accuracy": 130,
"altitude": 35
}
}
print p.currentLocation().insert(body=body).execute()

View File

@@ -36,10 +36,10 @@ flow = FlowThreeLegged(moderator_discovery,
# https://www.google.com/accounts/ManageDomains
consumer_key='REGISTERED DOMAIN NAME',
consumer_secret='KEY GIVEN DURING REGISTRATION',
user_agent='google-api-client-python-latitude-cmdline/1.0',
user_agent='google-api-client-python-latitude/1.0',
domain='REGISTERED DOMAIN NAME',
scope='https://www.googleapis.com/auth/latitude',
xoauth_displayname='Google API Latitude Client Example App',
xoauth_displayname='Google API Latitude Example',
location='current',
granularity='city'
)

View File

@@ -22,9 +22,10 @@ import httplib2
import pickle
import pprint
DISCOVERY_URI = ('http://gregorio-ub.i:3990/discovery/v0.2beta1/describe/'
DISCOVERY_URI = ('http://localhost:3990/discovery/v0.2beta1/describe/'
'{api}/{apiVersion}')
def main():
http = httplib2.Http()

View File

@@ -20,6 +20,7 @@ import pickle
# Uncomment to get detailed logging
# httplib2.debuglevel = 4
def main():
f = open("moderator.dat", "r")
credentials = pickle.loads(f.read())
@@ -32,7 +33,7 @@ def main():
series_body = {
"data": {
"description": "Share and rank tips for eating healthily on the cheaps!",
"description": "Share and rank tips for eating healthy and cheap!",
"name": "Eating Healthy & Cheap",
"videoSubmissionAllowed": False
}
@@ -47,7 +48,8 @@ def main():
"presenter": "liz"
}
}
topic = p.topics().insert(seriesId=series['id']['seriesId'], body=topic_body).execute()
topic = p.topics().insert(seriesId=series['id']['seriesId'],
body=topic_body).execute()
print "Created a new topic"
submission_body = {
@@ -69,7 +71,9 @@ def main():
"vote": "PLUS"
}
}
p.votes().insert(seriesId=topic['id']['seriesId'], submissionId=submission['id']['submissionId'], body=vote_body)
p.votes().insert(seriesId=topic['id']['seriesId'],
submissionId=submission['id']['submissionId'],
body=vote_body)
print "Voted on the submission"

View File

@@ -22,6 +22,7 @@ class Backoff:
Implements an exponential backoff algorithm.
"""
def __init__(self, maxretries=8):
self.retry = 0
self.maxretries = maxretries
@@ -36,7 +37,7 @@ class Backoff:
def fail(self):
self.retry += 1
delay = 2**self.retry
delay = 2 ** self.retry
time.sleep(delay)
@@ -67,8 +68,8 @@ def start_threads(credentials):
t.daemon = True
t.start()
def main():
def main():
f = open("moderator.dat", "r")
credentials = pickle.loads(f.read())
f.close()
@@ -98,7 +99,8 @@ def main():
"presenter": "me"
}
}
topic_request = p.topics().insert(seriesId=series['id']['seriesId'], body=topic_body)
topic_request = p.topics().insert(seriesId=series['id']['seriesId'],
body=topic_body)
print "Adding request to queue"
queue.put(topic_request)

View File

@@ -32,7 +32,7 @@ moderator_discovery = build("moderator", "v1").auth_discovery()
flow = FlowThreeLegged(moderator_discovery,
consumer_key='anonymous',
consumer_secret='anonymous',
user_agent='google-api-client-python-threadqueue-sample/1.0',
user_agent='google-api-client-python-thread-sample/1.0',
domain='anonymous',
scope='https://www.googleapis.com/auth/moderator',
#scope='tag:google.com,2010:auth/moderator',

View File

@@ -18,9 +18,11 @@ import pprint
# Uncomment the next line to get very detailed logging
# httplib2.debuglevel = 4
def main():
p = build("translate", "v2", developerKey="AIzaSyDRRpR3GS1F1_jKNNM9HCNd2wJQyPG3oN0")
p = build("translate", "v2",
developerKey="AIzaSyDRRpR3GS1F1_jKNNM9HCNd2wJQyPG3oN0")
print p.translations().list(
source="en",
target="fr",

View File

@@ -32,20 +32,7 @@ except ImportError:
from cgi import parse_qs
from apiclient.discovery import build, key2param
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
class HttpMock(object):
def __init__(self, filename, headers):
f = file(os.path.join(DATA_DIR, 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
from tests.util import HttpMock
class Utilities(unittest.TestCase):