blob: b557b8b35fc5eb6b95c77c94f0fb1360c95978da [file] [log] [blame]
Joe Gregorio48d361f2010-08-18 13:19:21 -04001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Client for discovery based APIs
16
17A client library for Google's discovery
18based APIs.
19"""
20
21__author__ = 'jcgregorio@google.com (Joe Gregorio)'
22
23
24import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010025import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040026import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040027import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040028import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040029import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040030import urlparse
ade@google.comc5eb46f2010-09-27 23:35:39 +010031try:
32 from urlparse import parse_qsl
33except ImportError:
34 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050035
Joe Gregoriob843fa22010-12-13 16:26:07 -050036from http import HttpRequest
Joe Gregorio034e7002010-12-15 08:45:03 -050037from anyjson import simplejson
Joe Gregoriob843fa22010-12-13 16:26:07 -050038from model import JsonModel
Joe Gregoriob843fa22010-12-13 16:26:07 -050039from errors import UnknownLinkType
Joe Gregorio48d361f2010-08-18 13:19:21 -040040
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050041URITEMPLATE = re.compile('{[^}]*}')
42VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio2379ecc2010-10-26 10:51:28 -040043DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
44 '{api}/{apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040045
46
Joe Gregorio48d361f2010-08-18 13:19:21 -040047def key2param(key):
48 """
49 max-results -> max_results
50 """
51 result = []
52 key = list(key)
53 if not key[0].isalpha():
54 result.append('x')
55 for c in key:
56 if c.isalnum():
57 result.append(c)
58 else:
59 result.append('_')
60
61 return ''.join(result)
62
63
Joe Gregorioaf276d22010-12-09 14:26:58 -050064def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050065 http=None,
66 discoveryServiceUrl=DISCOVERY_URI,
67 developerKey=None,
68 model=JsonModel(),
69 requestBuilder=HttpRequest):
Joe Gregorio48d361f2010-08-18 13:19:21 -040070 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040071 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -040072 'apiVersion': version
73 }
ade@google.com850cf552010-08-20 23:24:56 +010074
Joe Gregorioc204b642010-09-21 12:01:23 -040075 if http is None:
76 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +010077 requested_url = uritemplate.expand(discoveryServiceUrl, params)
78 logging.info('URL being requested: %s' % requested_url)
79 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -040080 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040081
Joe Gregorio3bbbf662010-08-30 16:41:53 -040082 fn = os.path.join(os.path.dirname(__file__), "contrib",
83 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -040084 try:
85 f = file(fn, "r")
Joe Gregorio292b9b82011-01-12 11:36:11 -050086 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -040087 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -040088 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -050089 future = None
90
91 return build_from_document(content, discoveryServiceUrl, future,
92 http, developerKey, model, requestBuilder)
93
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050094
Joe Gregorio292b9b82011-01-12 11:36:11 -050095def build_from_document(
96 service,
97 base,
98 future=None,
99 http=None,
100 developerKey=None,
101 model=JsonModel(),
102 requestBuilder=HttpRequest):
103 """
104 Args:
105 service: string, discovery document
106 base: string, base URI for all HTTP requests, usually the discovery URI
107 future: string, discovery document with future capabilities
108 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500109 http: httplib2.Http, An instance of httplib2.Http or something that acts
110 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500111 developerKey: string, Key for controlling API usage, generated
112 from the API Console.
113 model: Model class instance that serializes and
114 de-serializes requests and responses.
115 requestBuilder: Takes an http request and packages it up to be executed.
116 """
117
118 service = simplejson.loads(service)
119 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500120 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500121 future = simplejson.loads(future)
122 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500123 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400124 future = {}
125 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400126
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500127 resource = createResource(http, base, model, requestBuilder, developerKey,
128 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400129
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500130 def auth_method():
131 """Discovery information about the authentication the API uses."""
132 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400133
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500134 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400135
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500136 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400137
138
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500139def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500140 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400141
142 class Resource(object):
143 """A class for interacting with a resource."""
144
145 def __init__(self):
146 self._http = http
147 self._baseUrl = baseUrl
148 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400149 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500150 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400151
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400152 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400153 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400154 pathUrl = re.sub(r'\{', r'{+', pathUrl)
155 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500156 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400157
ade@google.com850cf552010-08-20 23:24:56 +0100158 argmap = {}
159 if httpMethod in ['PUT', 'POST']:
160 argmap['body'] = 'body'
161
162
163 required_params = [] # Required parameters
164 pattern_params = {} # Parameters that must match a regex
165 query_params = [] # Parameters that will be used in the query string
166 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400167 if 'parameters' in methodDesc:
168 for arg, desc in methodDesc['parameters'].iteritems():
169 param = key2param(arg)
170 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400171
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400172 if desc.get('pattern', ''):
173 pattern_params[param] = desc['pattern']
174 if desc.get('required', False):
175 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400176 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400177 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400178 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400179 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400180
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500181 for match in URITEMPLATE.finditer(pathUrl):
182 for namematch in VARNAME.finditer(match.group(0)):
183 name = key2param(namematch.group(0))
184 path_params[name] = name
185 if name in query_params:
186 query_params.remove(name)
187
Joe Gregorio48d361f2010-08-18 13:19:21 -0400188 def method(self, **kwargs):
189 for name in kwargs.iterkeys():
190 if name not in argmap:
191 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400192
ade@google.com850cf552010-08-20 23:24:56 +0100193 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400194 if name not in kwargs:
195 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400196
ade@google.com850cf552010-08-20 23:24:56 +0100197 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400198 if name in kwargs:
199 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400200 raise TypeError(
201 'Parameter "%s" value "%s" does not match the pattern "%s"' %
202 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400203
ade@google.com850cf552010-08-20 23:24:56 +0100204 actual_query_params = {}
205 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400206 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100207 if key in query_params:
208 actual_query_params[argmap[key]] = value
209 if key in path_params:
210 actual_path_params[argmap[key]] = value
211 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400212
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400213 if self._developerKey:
214 actual_query_params['key'] = self._developerKey
215
Joe Gregorio48d361f2010-08-18 13:19:21 -0400216 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400217 headers, params, query, body = self._model.request(headers,
218 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400219
Joe Gregorioaf276d22010-12-09 14:26:58 -0500220 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
221 # document. Base URLs should not contain any path elements. If they do
222 # then urlparse.urljoin will strip them out This results in an incorrect
223 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100224 url_result = urlparse.urlsplit(self._baseUrl)
225 new_base_url = url_result.scheme + '://' + url_result.netloc
226
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400227 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500228 url = urlparse.urljoin(new_base_url,
229 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400230
ade@google.com850cf552010-08-20 23:24:56 +0100231 logging.info('URL being requested: %s' % url)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500232 return self._requestBuilder(self._http, url,
233 method=httpMethod, body=body,
234 headers=headers,
235 postproc=self._model.response,
236 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400237
238 docs = ['A description of how to use this function\n\n']
239 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400240 required = ""
241 if arg in required_params:
242 required = " (required)"
243 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400244
245 setattr(method, '__doc__', ''.join(docs))
246 setattr(theclass, methodName, method)
247
Joe Gregorioaf276d22010-12-09 14:26:58 -0500248 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
249 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400250
251 def method(self, previous):
252 """
253 Takes a single argument, 'body', which is the results
254 from the last call, and returns the next set of items
255 in the collection.
256
257 Returns None if there are no more items in
258 the collection.
259 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500260 if futureDesc['type'] != 'uri':
261 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400262
263 try:
264 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500265 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400266 p = p[key]
267 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400268 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400269 return None
270
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400271 if self._developerKey:
272 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100273 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400274 q.append(('key', self._developerKey))
275 parsed[4] = urllib.urlencode(q)
276 url = urlparse.urlunparse(parsed)
277
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400278 headers = {}
279 headers, params, query, body = self._model.request(headers, {}, {}, None)
280
281 logging.info('URL being requested: %s' % url)
282 resp, content = self._http.request(url, method='GET', headers=headers)
283
Joe Gregorioaf276d22010-12-09 14:26:58 -0500284 return self._requestBuilder(self._http, url, method='GET',
285 headers=headers,
286 postproc=self._model.response,
287 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400288
289 setattr(theclass, methodName, method)
290
291 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400292 if 'methods' in resourceDesc:
293 for methodName, methodDesc in resourceDesc['methods'].iteritems():
294 if futureDesc:
295 future = futureDesc['methods'].get(methodName, {})
296 else:
297 future = None
298 createMethod(Resource, methodName, methodDesc, future)
299
300 # Add in nested resources
301 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500302
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400303 def createMethod(theclass, methodName, methodDesc, futureDesc):
304
305 def method(self):
306 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500307 self._requestBuilder, self._developerKey,
308 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400309
310 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400311 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400312 setattr(theclass, methodName, method)
313
314 for methodName, methodDesc in resourceDesc['resources'].iteritems():
315 if futureDesc and 'resources' in futureDesc:
316 future = futureDesc['resources'].get(methodName, {})
317 else:
318 future = {}
Joe Gregorioaf276d22010-12-09 14:26:58 -0500319 createMethod(Resource, methodName, methodDesc,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500320 future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400321
322 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500323 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400324 for methodName, methodDesc in futureDesc['methods'].iteritems():
325 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500326 createNextMethod(Resource, methodName + "_next",
327 resourceDesc['methods'][methodName],
328 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400329
330 return Resource()