imported patch partial-and-patch
diff --git a/apiclient/discovery.py b/apiclient/discovery.py
index b90486a..2c89528 100644
--- a/apiclient/discovery.py
+++ b/apiclient/discovery.py
@@ -48,7 +48,7 @@
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 @@
'restParameterType': 'query'
}
- if httpMethod in ['PUT', 'POST']:
+ if httpMethod in ['PUT', 'POST', 'PATCH']:
methodDesc['parameters']['body'] = {
'description': 'The request body.',
'type': 'object',
diff --git a/apiclient/http.py b/apiclient/http.py
index f4627bd..206f3e6 100644
--- a/apiclient/http.py
+++ b/apiclient/http.py
@@ -10,6 +10,7 @@
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
__all__ = [
'HttpRequest', 'RequestMockBuilder', 'HttpMock'
+ 'set_user_agent', 'tunnel_patch'
]
import httplib2
@@ -17,6 +18,7 @@
from model import JsonModel
from errors import HttpError
+from anyjson import simplejson
class HttpRequest(object):
@@ -201,6 +203,7 @@
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 @@
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 @@
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
diff --git a/samples/localdiscovery/buzz.py b/samples/localdiscovery/buzz.py
index 3fb0c6f..c769576 100644
--- a/samples/localdiscovery/buzz.py
+++ b/samples/localdiscovery/buzz.py
@@ -18,39 +18,53 @@
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 @@
if __name__ == '__main__':
- main()
+ main(sys.argv)
diff --git a/tests/data/zoo.json b/tests/data/zoo.json
index 43af0e8..284619e 100644
--- a/tests/data/zoo.json
+++ b/tests/data/zoo.json
@@ -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"
+ }
+ }
+ }
}
}
}
diff --git a/tests/test_discovery.py b/tests/test_discovery.py
index ecb59f9..4bdff20 100644
--- a/tests/test_discovery.py
+++ b/tests/test_discovery.py
@@ -34,6 +34,8 @@
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 @@
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 @@
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 @@
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'})