blob: b39e1b25cd421b47f4179ebe5afe29a5b66e7f8f [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
94def build_from_document(
95 service,
96 base,
97 future=None,
98 http=None,
99 developerKey=None,
100 model=JsonModel(),
101 requestBuilder=HttpRequest):
102 """
103 Args:
104 service: string, discovery document
105 base: string, base URI for all HTTP requests, usually the discovery URI
106 future: string, discovery document with future capabilities
107 auth_discovery: dict, information about the authentication the API supports
108 http: httplib2.Http, An instance of httplib2.Http or something that acts like
109 it that HTTP requests will be made through.
110 developerKey: string, Key for controlling API usage, generated
111 from the API Console.
112 model: Model class instance that serializes and
113 de-serializes requests and responses.
114 requestBuilder: Takes an http request and packages it up to be executed.
115 """
116
117 service = simplejson.loads(service)
118 base = urlparse.urljoin(base, service['restBasePath'])
119 resources = service['resources']
120 if future:
121 doc = simplejson.loads(future)
122 future = doc['resources']
123 auth_discovery = doc.get('auth', {})
124 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400125 future = {}
126 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400127
Joe Gregorio48d361f2010-08-18 13:19:21 -0400128 class Service(object):
129 """Top level interface for a service"""
130
131 def __init__(self, http=http):
132 self._http = http
133 self._baseUrl = base
134 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400135 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500136 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400137
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400138 def auth_discovery(self):
139 return auth_discovery
140
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400141 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio41cf7972010-08-18 15:21:06 -0400142
Joe Gregorio59cd9512010-10-04 12:46:46 -0400143 def method(self):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400144 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500145 self._requestBuilder, methodName,
146 self._developerKey, methodDesc, futureDesc)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400147
148 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400149 setattr(method, '__is_resource__', True)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400150 setattr(theclass, methodName, method)
151
152 for methodName, methodDesc in resources.iteritems():
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400153 createMethod(Service, methodName, methodDesc, future.get(methodName, {}))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400154 return Service()
155
156
Joe Gregorioaf276d22010-12-09 14:26:58 -0500157def createResource(http, baseUrl, model, requestBuilder, resourceName,
158 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400159
160 class Resource(object):
161 """A class for interacting with a resource."""
162
163 def __init__(self):
164 self._http = http
165 self._baseUrl = baseUrl
166 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400167 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500168 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400169
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400170 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400171 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400172 pathUrl = re.sub(r'\{', r'{+', pathUrl)
173 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500174 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400175
ade@google.com850cf552010-08-20 23:24:56 +0100176 argmap = {}
177 if httpMethod in ['PUT', 'POST']:
178 argmap['body'] = 'body'
179
180
181 required_params = [] # Required parameters
182 pattern_params = {} # Parameters that must match a regex
183 query_params = [] # Parameters that will be used in the query string
184 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400185 if 'parameters' in methodDesc:
186 for arg, desc in methodDesc['parameters'].iteritems():
187 param = key2param(arg)
188 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400189
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400190 if desc.get('pattern', ''):
191 pattern_params[param] = desc['pattern']
192 if desc.get('required', False):
193 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400194 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400195 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400196 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400197 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400198
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500199 for match in URITEMPLATE.finditer(pathUrl):
200 for namematch in VARNAME.finditer(match.group(0)):
201 name = key2param(namematch.group(0))
202 path_params[name] = name
203 if name in query_params:
204 query_params.remove(name)
205
Joe Gregorio48d361f2010-08-18 13:19:21 -0400206 def method(self, **kwargs):
207 for name in kwargs.iterkeys():
208 if name not in argmap:
209 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400210
ade@google.com850cf552010-08-20 23:24:56 +0100211 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400212 if name not in kwargs:
213 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400214
ade@google.com850cf552010-08-20 23:24:56 +0100215 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400216 if name in kwargs:
217 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400218 raise TypeError(
219 'Parameter "%s" value "%s" does not match the pattern "%s"' %
220 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400221
ade@google.com850cf552010-08-20 23:24:56 +0100222 actual_query_params = {}
223 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400224 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100225 if key in query_params:
226 actual_query_params[argmap[key]] = value
227 if key in path_params:
228 actual_path_params[argmap[key]] = value
229 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400230
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400231 if self._developerKey:
232 actual_query_params['key'] = self._developerKey
233
Joe Gregorio48d361f2010-08-18 13:19:21 -0400234 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400235 headers, params, query, body = self._model.request(headers,
236 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400237
Joe Gregorioaf276d22010-12-09 14:26:58 -0500238 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
239 # document. Base URLs should not contain any path elements. If they do
240 # then urlparse.urljoin will strip them out This results in an incorrect
241 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100242 url_result = urlparse.urlsplit(self._baseUrl)
243 new_base_url = url_result.scheme + '://' + url_result.netloc
244
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400245 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500246 url = urlparse.urljoin(new_base_url,
247 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400248
ade@google.com850cf552010-08-20 23:24:56 +0100249 logging.info('URL being requested: %s' % url)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500250 return self._requestBuilder(self._http, url,
251 method=httpMethod, body=body,
252 headers=headers,
253 postproc=self._model.response,
254 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400255
256 docs = ['A description of how to use this function\n\n']
257 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400258 required = ""
259 if arg in required_params:
260 required = " (required)"
261 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400262
263 setattr(method, '__doc__', ''.join(docs))
264 setattr(theclass, methodName, method)
265
Joe Gregorioaf276d22010-12-09 14:26:58 -0500266 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
267 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400268
269 def method(self, previous):
270 """
271 Takes a single argument, 'body', which is the results
272 from the last call, and returns the next set of items
273 in the collection.
274
275 Returns None if there are no more items in
276 the collection.
277 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500278 if futureDesc['type'] != 'uri':
279 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400280
281 try:
282 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500283 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400284 p = p[key]
285 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400286 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400287 return None
288
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400289 if self._developerKey:
290 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100291 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400292 q.append(('key', self._developerKey))
293 parsed[4] = urllib.urlencode(q)
294 url = urlparse.urlunparse(parsed)
295
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400296 headers = {}
297 headers, params, query, body = self._model.request(headers, {}, {}, None)
298
299 logging.info('URL being requested: %s' % url)
300 resp, content = self._http.request(url, method='GET', headers=headers)
301
Joe Gregorioaf276d22010-12-09 14:26:58 -0500302 return self._requestBuilder(self._http, url, method='GET',
303 headers=headers,
304 postproc=self._model.response,
305 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400306
307 setattr(theclass, methodName, method)
308
309 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400310 if 'methods' in resourceDesc:
311 for methodName, methodDesc in resourceDesc['methods'].iteritems():
312 if futureDesc:
313 future = futureDesc['methods'].get(methodName, {})
314 else:
315 future = None
316 createMethod(Resource, methodName, methodDesc, future)
317
318 # Add in nested resources
319 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500320
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400321 def createMethod(theclass, methodName, methodDesc, futureDesc):
322
323 def method(self):
324 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio3fada332011-01-07 17:07:45 -0500325 self._requestBuilder, methodName,
326 self._developerKey, methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400327
328 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400329 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400330 setattr(theclass, methodName, method)
331
332 for methodName, methodDesc in resourceDesc['resources'].iteritems():
333 if futureDesc and 'resources' in futureDesc:
334 future = futureDesc['resources'].get(methodName, {})
335 else:
336 future = {}
Joe Gregorioaf276d22010-12-09 14:26:58 -0500337 createMethod(Resource, methodName, methodDesc,
338 future.get(methodName, {}))
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400339
340 # Add <m>_next() methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400341 if futureDesc:
342 for methodName, methodDesc in futureDesc['methods'].iteritems():
343 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500344 createNextMethod(Resource, methodName + "_next",
345 resourceDesc['methods'][methodName],
346 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400347
348 return Resource()