imported patch partial-and-patch

This commit is contained in:
Joe Gregorio
2011-03-18 22:45:18 -04:00
parent 6abf870742
commit f415342d46
5 changed files with 362 additions and 61 deletions

View File

@@ -48,7 +48,7 @@ DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.3/describe/'
DEFAULT_METHOD_DOC = 'A description of how to use this function'
# Query parameters that work, but don't appear in discovery
STACK_QUERY_PARAMETERS = ['trace']
STACK_QUERY_PARAMETERS = ['trace', 'fields']
def key2param(key):
@@ -243,7 +243,7 @@ def createResource(http, baseUrl, model, requestBuilder,
'restParameterType': 'query'
}
if httpMethod in ['PUT', 'POST']:
if httpMethod in ['PUT', 'POST', 'PATCH']:
methodDesc['parameters']['body'] = {
'description': 'The request body.',
'type': 'object',

View File

@@ -10,6 +10,7 @@ actuall HTTP request.
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
__all__ = [
'HttpRequest', 'RequestMockBuilder', 'HttpMock'
'set_user_agent', 'tunnel_patch'
]
import httplib2
@@ -17,6 +18,7 @@ import os
from model import JsonModel
from errors import HttpError
from anyjson import simplejson
class HttpRequest(object):
@@ -201,6 +203,7 @@ class HttpMockSequence(object):
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
"""
@@ -220,13 +223,16 @@ class HttpMockSequence(object):
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.
@@ -262,3 +268,43 @@ def set_user_agent(http, user_agent):
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

View File

@@ -18,39 +18,53 @@ from apiclient.ext.authtools import run
from apiclient.ext.file import Storage
from apiclient.oauth import CredentialsInvalidError
import gflags
import sys
import httplib2
import logging
import os
import pprint
import sys
# Uncomment the next line to get very detailed logging
#httplib2.debuglevel = 4
FLAGS = gflags.FLAGS
gflags.DEFINE_enum('logging_level', 'ERROR',
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
'Set the level of logging detail.')
def main():
def main(argv):
try:
argv = FLAGS(argv)
except gflags.FlagsError, e:
print '%s\\nUsage: %s ARGS\\n%s' % (e, argv[0], FLAGS)
sys.exit(1)
logging.getLogger().setLevel(getattr(logging, FLAGS.logging_level))
storage = Storage('buzz.dat')
credentials = storage.get()
if credentials is None or credentials.invalid == True:
buzz_discovery = build("buzz", "v1").auth_discovery()
flow = FlowThreeLegged(buzz_discovery,
consumer_key='anonymous',
consumer_secret='anonymous',
user_agent='python-buzz-sample/1.0',
domain='anonymous',
scope='https://www.googleapis.com/auth/buzz',
xoauth_displayname='Google API Client Example App')
consumer_key='anonymous',
consumer_secret='anonymous',
user_agent='python-buzz-sample/1.0',
domain='anonymous',
scope='https://www.googleapis.com/auth/buzz',
xoauth_displayname='Google API Client Example App')
credentials = run(flow, storage)
http = httplib2.Http()
http = credentials.authorize(http)
# Load the local copy of the discovery document
f = file("buzz.json", "r")
f = file(os.path.join(os.path.dirname(__file__), "buzz.json"), "r")
discovery = f.read()
f.close()
# Optionally load a futures discovery document
f = file("../../apiclient/contrib/buzz/future.json", "r")
f = file(os.path.join(os.path.dirname(__file__), "../../apiclient/contrib/buzz/future.json"), "r")
future = f.read()
f.close()
@@ -73,4 +87,4 @@ def main():
if __name__ == '__main__':
main()
main(sys.argv)

View File

@@ -1,9 +1,117 @@
{
"kind": "discovery#describeItem",
"name": "zoo",
"version": "v1",
"description": "Zoo API used for testing",
"restBasePath": "/zoo",
"description": "Zoo API used for Apiary testing",
"restBasePath": "/zoo/",
"rpcPath": "/rpc",
"features": [
"dataWrapper"
],
"schemas": {
"Animal": {
"id": "Animal",
"type": "object",
"properties": {
"etag": {
"type": "string"
},
"kind": {
"type": "string",
"default": "zoo#animal"
},
"name": {
"type": "string"
},
"photo": {
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"hash": {
"type": "string"
},
"hashAlgorithm": {
"type": "string"
},
"size": {
"type": "integer"
},
"type": {
"type": "string"
}
}
}
}
},
"Animal2": {
"id": "Animal2",
"type": "object",
"properties": {
"kind": {
"type": "string",
"default": "zoo#animal"
},
"name": {
"type": "string"
}
}
},
"AnimalFeed": {
"id": "AnimalFeed",
"type": "object",
"properties": {
"etag": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "Animal"
}
},
"kind": {
"type": "string",
"default": "zoo#animalFeed"
}
}
},
"LoadFeed": {
"id": "LoadFeed",
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"doubleVal": {
"type": "number"
},
"enumVal": {
"type": "string"
},
"kind": {
"type": "string",
"default": "zoo#loadValue"
},
"longVal": {
"type": "integer"
},
"stringVal": {
"type": "string"
}
}
}
},
"kind": {
"type": "string",
"default": "zoo#loadFeed"
}
}
}
},
"methods": {
"query": {
"restPath": "query",
@@ -85,106 +193,218 @@
"animals": {
"methods": {
"crossbreed": {
"restPath": "/animals/crossbreed",
"restPath": "animals/crossbreed",
"rpcMethod": "zoo.animals.crossbreed",
"httpMethod": "GET",
"parameters": {
"father": {
"restParameterType": "query",
"required": false
},
"mother": {
"restParameterType": "query",
"required": false
}
"httpMethod": "POST",
"description": "Cross-breed animals",
"response": {
"$ref": "Animal2"
}
},
"delete": {
"restPath": "/animals/{name}",
"restPath": "animals/{name}",
"rpcMethod": "zoo.animals.delete",
"httpMethod": "DELETE",
"description": "Delete animals",
"parameters": {
"name": {
"restParameterType": "path",
"pattern": "[^/]+",
"required": true
"required": true,
"description": "Name of the animal to delete",
"type": "string"
}
}
},
"parameterOrder": [
"name"
]
},
"get": {
"restPath": "/animals/{name}",
"restPath": "animals/{name}",
"rpcMethod": "zoo.animals.get",
"httpMethod": "GET",
"description": "Get animals",
"parameters": {
"name": {
"restParameterType": "path",
"pattern": "[^/]+",
"required": true
"required": true,
"description": "Name of the animal to load",
"type": "string"
},
"projection": {
"restParameterType": "query",
"required": false
"type": "string",
"enum": [
"full"
],
"enumDescriptions": [
"Include everything"
]
}
},
"parameterOrder": [
"name"
],
"response": {
"$ref": "Animal"
}
},
"insert": {
"restPath": "/animals",
"restPath": "animals",
"rpcMethod": "zoo.animals.insert",
"httpMethod": "POST",
"parameters": {
"photo": {
"restParameterType": "query",
"required": false
}
"description": "Insert animals",
"request": {
"$ref": "Animal"
},
"response": {
"$ref": "Animal"
}
},
"list": {
"restPath": "/animals",
"restPath": "animals",
"rpcMethod": "zoo.animals.list",
"httpMethod": "GET",
"description": "List animals",
"parameters": {
"max-results": {
"restParameterType": "query",
"required": false
"description": "Maximum number of results to return",
"type": "integer",
"minimum": "0"
},
"name": {
"restParameterType": "query",
"required": false
"description": "Restrict result to animals with this name",
"type": "string"
},
"projection": {
"restParameterType": "query",
"required": false
"type": "string",
"enum": [
"full"
],
"enumDescriptions": [
"Include absolutely everything"
]
},
"start-token": {
"restParameterType": "query",
"required": false
"description": "Pagination token",
"type": "string"
}
},
"response": {
"$ref": "AnimalFeed"
}
},
"patch": {
"restPath": "animals/{name}",
"rpcMethod": "zoo.animals.patch",
"httpMethod": "PATCH",
"description": "Update animals",
"parameters": {
"name": {
"restParameterType": "path",
"required": true,
"description": "Name of the animal to update",
"type": "string"
}
},
"parameterOrder": [
"name"
],
"request": {
"$ref": "Animal"
},
"response": {
"$ref": "Animal"
}
},
"update": {
"restPath": "/animals/{animal.name}",
"restPath": "animals/{name}",
"rpcMethod": "zoo.animals.update",
"httpMethod": "PUT"
"httpMethod": "PUT",
"description": "Update animals",
"parameters": {
"name": {
"restParameterType": "path",
"description": "Name of the animal to update",
"type": "string"
}
},
"parameterOrder": [
"name"
],
"request": {
"$ref": "Animal"
},
"response": {
"$ref": "Animal"
}
}
}
},
"load": {
"methods": {
"list": {
"restPath": "/load",
"restPath": "load",
"rpcMethod": "zoo.load.list",
"httpMethod": "GET"
"httpMethod": "GET",
"response": {
"$ref": "LoadFeed"
}
}
}
},
"loadNoTemplate": {
"methods": {
"list": {
"restPath": "/loadNoTemplate",
"restPath": "loadNoTemplate",
"rpcMethod": "zoo.loadNoTemplate.list",
"httpMethod": "GET"
}
}
},
"scopedAnimals": {
"methods": {
"list": {
"restPath": "scopedanimals",
"rpcMethod": "zoo.scopedAnimals.list",
"httpMethod": "GET",
"description": "List animals (scoped)",
"parameters": {
"max-results": {
"restParameterType": "query",
"description": "Maximum number of results to return",
"type": "integer",
"minimum": "0"
},
"name": {
"restParameterType": "query",
"description": "Restrict result to animals with this name",
"type": "string"
},
"projection": {
"restParameterType": "query",
"type": "string",
"enum": [
"full"
],
"enumDescriptions": [
"Include absolutely everything"
]
},
"start-token": {
"restParameterType": "query",
"description": "Pagination token",
"type": "string"
}
},
"response": {
"$ref": "AnimalFeed"
}
}
}
}
}
}

View File

@@ -34,6 +34,8 @@ except ImportError:
from apiclient.discovery import build, key2param
from apiclient.http import HttpMock
from apiclient.http import tunnel_patch
from apiclient.http import HttpMockSequence
from apiclient.errors import HttpError
from apiclient.errors import InvalidJsonError
@@ -107,8 +109,8 @@ class Discovery(unittest.TestCase):
self.assertEqual(q['e'], ['bar'])
def test_type_coercion(self):
self.http = HttpMock(datafile('zoo.json'), {'status': '200'})
zoo = build('zoo', 'v1', self.http)
http = HttpMock(datafile('zoo.json'), {'status': '200'})
zoo = build('zoo', 'v1', http)
request = zoo.query(q="foo", i=1.0, n=1.0, b=0, a=[1,2,3], o={'a':1}, e='bar')
self._check_query_types(request)
@@ -119,13 +121,32 @@ class Discovery(unittest.TestCase):
self._check_query_types(request)
def test_optional_stack_query_parameters(self):
self.http = HttpMock(datafile('zoo.json'), {'status': '200'})
zoo = build('zoo', 'v1', self.http)
request = zoo.query(trace='html')
http = HttpMock(datafile('zoo.json'), {'status': '200'})
zoo = build('zoo', 'v1', http)
request = zoo.query(trace='html', fields='description')
parsed = urlparse.urlparse(request.uri)
q = parse_qs(parsed[4])
self.assertEqual(q['trace'], ['html'])
self.assertEqual(q['fields'], ['description'])
def test_patch(self):
http = HttpMock(datafile('zoo.json'), {'status': '200'})
zoo = build('zoo', 'v1', http)
request = zoo.animals().patch(name='lion', body='{"description": "foo"}')
self.assertEqual(request.method, 'PATCH')
def test_tunnel_patch(self):
http = HttpMockSequence([
({'status': '200'}, file(datafile('zoo.json'), 'r').read()),
({'status': '200'}, 'echo_request_headers_as_json'),
])
http = tunnel_patch(http)
zoo = build('zoo', 'v1', http)
resp = zoo.animals().patch(name='lion', body='{"description": "foo"}').execute()
self.assertTrue('x-http-method-override' in resp)
def test_buzz_resources(self):
self.http = HttpMock(datafile('buzz.json'), {'status': '200'})
@@ -150,11 +171,11 @@ class Discovery(unittest.TestCase):
zoo = build('zoo', 'v1', self.http)
self.assertTrue(getattr(zoo, 'animals'))
request = zoo.animals().list(name='bat', projection="size")
request = zoo.animals().list(name='bat', projection="full")
parsed = urlparse.urlparse(request.uri)
q = parse_qs(parsed[4])
self.assertEqual(q['name'], ['bat'])
self.assertEqual(q['projection'], ['size'])
self.assertEqual(q['projection'], ['full'])
def test_nested_resources(self):
self.http = HttpMock(datafile('zoo.json'), {'status': '200'})