blob: fa796ed5b5a69b1e046ac2729259139165842085 [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
Joe Gregorioabda96f2011-02-11 20:19:33 -050023__all__ = [
24 'build', 'build_from_document'
25 ]
Joe Gregorio48d361f2010-08-18 13:19:21 -040026
27import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010028import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040029import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040030import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040031import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040032import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040033import urlparse
ade@google.comc5eb46f2010-09-27 23:35:39 +010034try:
35 from urlparse import parse_qsl
36except ImportError:
37 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050038
Joe Gregoriob843fa22010-12-13 16:26:07 -050039from http import HttpRequest
Joe Gregorio034e7002010-12-15 08:45:03 -050040from anyjson import simplejson
Joe Gregoriob843fa22010-12-13 16:26:07 -050041from model import JsonModel
Joe Gregoriob843fa22010-12-13 16:26:07 -050042from errors import UnknownLinkType
Joe Gregorio48d361f2010-08-18 13:19:21 -040043
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050044URITEMPLATE = re.compile('{[^}]*}')
45VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio2379ecc2010-10-26 10:51:28 -040046DISCOVERY_URI = ('https://www.googleapis.com/discovery/v0.2beta1/describe/'
47 '{api}/{apiVersion}')
Joe Gregorio48d361f2010-08-18 13:19:21 -040048
49
Joe Gregorio48d361f2010-08-18 13:19:21 -040050def key2param(key):
51 """
52 max-results -> max_results
53 """
54 result = []
55 key = list(key)
56 if not key[0].isalpha():
57 result.append('x')
58 for c in key:
59 if c.isalnum():
60 result.append(c)
61 else:
62 result.append('_')
63
64 return ''.join(result)
65
66
Joe Gregorioaf276d22010-12-09 14:26:58 -050067def build(serviceName, version,
Joe Gregorio3fada332011-01-07 17:07:45 -050068 http=None,
69 discoveryServiceUrl=DISCOVERY_URI,
70 developerKey=None,
71 model=JsonModel(),
72 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -050073 """Construct a Resource for interacting with an API.
74
75 Construct a Resource object for interacting with
76 an API. The serviceName and version are the
77 names from the Discovery service.
78
79 Args:
80 serviceName: string, name of the service
81 version: string, the version of the service
82 discoveryServiceUrl: string, a URI Template that points to
83 the location of the discovery service. It should have two
84 parameters {api} and {apiVersion} that when filled in
85 produce an absolute URI to the discovery document for
86 that service.
87 developerKey: string, key obtained from https://code.google.com/apis/console
88 model: apiclient.Model, converts to and from the wire format
89 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP request
90
91 Returns:
92 A Resource object with methods for interacting with
93 the service.
94 """
Joe Gregorio48d361f2010-08-18 13:19:21 -040095 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040096 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -040097 'apiVersion': version
98 }
ade@google.com850cf552010-08-20 23:24:56 +010099
Joe Gregorioc204b642010-09-21 12:01:23 -0400100 if http is None:
101 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100102 requested_url = uritemplate.expand(discoveryServiceUrl, params)
103 logging.info('URL being requested: %s' % requested_url)
104 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400105 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400106
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400107 fn = os.path.join(os.path.dirname(__file__), "contrib",
108 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400109 try:
110 f = file(fn, "r")
Joe Gregorio292b9b82011-01-12 11:36:11 -0500111 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400112 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400113 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500114 future = None
115
116 return build_from_document(content, discoveryServiceUrl, future,
117 http, developerKey, model, requestBuilder)
118
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500119
Joe Gregorio292b9b82011-01-12 11:36:11 -0500120def build_from_document(
121 service,
122 base,
123 future=None,
124 http=None,
125 developerKey=None,
126 model=JsonModel(),
127 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500128 """Create a Resource for interacting with an API.
129
130 Same as `build()`, but constructs the Resource object
131 from a discovery document that is it given, as opposed to
132 retrieving one over HTTP.
133
Joe Gregorio292b9b82011-01-12 11:36:11 -0500134 Args:
135 service: string, discovery document
136 base: string, base URI for all HTTP requests, usually the discovery URI
137 future: string, discovery document with future capabilities
138 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500139 http: httplib2.Http, An instance of httplib2.Http or something that acts
140 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500141 developerKey: string, Key for controlling API usage, generated
142 from the API Console.
143 model: Model class instance that serializes and
144 de-serializes requests and responses.
145 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500146
147 Returns:
148 A Resource object with methods for interacting with
149 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500150 """
151
152 service = simplejson.loads(service)
153 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500154 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500155 future = simplejson.loads(future)
156 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500157 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400158 future = {}
159 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400160
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500161 resource = createResource(http, base, model, requestBuilder, developerKey,
162 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400163
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500164 def auth_method():
165 """Discovery information about the authentication the API uses."""
166 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400167
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500168 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400169
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500170 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400171
172
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500173def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500174 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175
176 class Resource(object):
177 """A class for interacting with a resource."""
178
179 def __init__(self):
180 self._http = http
181 self._baseUrl = baseUrl
182 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400183 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500184 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400185
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400186 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400187 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400188 pathUrl = re.sub(r'\{', r'{+', pathUrl)
189 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500190 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400191
ade@google.com850cf552010-08-20 23:24:56 +0100192 argmap = {}
193 if httpMethod in ['PUT', 'POST']:
194 argmap['body'] = 'body'
195
196
197 required_params = [] # Required parameters
198 pattern_params = {} # Parameters that must match a regex
199 query_params = [] # Parameters that will be used in the query string
200 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400201 if 'parameters' in methodDesc:
202 for arg, desc in methodDesc['parameters'].iteritems():
203 param = key2param(arg)
204 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400205
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400206 if desc.get('pattern', ''):
207 pattern_params[param] = desc['pattern']
208 if desc.get('required', False):
209 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400210 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400211 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400212 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400213 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400214
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500215 for match in URITEMPLATE.finditer(pathUrl):
216 for namematch in VARNAME.finditer(match.group(0)):
217 name = key2param(namematch.group(0))
218 path_params[name] = name
219 if name in query_params:
220 query_params.remove(name)
221
Joe Gregorio48d361f2010-08-18 13:19:21 -0400222 def method(self, **kwargs):
223 for name in kwargs.iterkeys():
224 if name not in argmap:
225 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400226
ade@google.com850cf552010-08-20 23:24:56 +0100227 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400228 if name not in kwargs:
229 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400230
ade@google.com850cf552010-08-20 23:24:56 +0100231 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400232 if name in kwargs:
233 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400234 raise TypeError(
235 'Parameter "%s" value "%s" does not match the pattern "%s"' %
236 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400237
ade@google.com850cf552010-08-20 23:24:56 +0100238 actual_query_params = {}
239 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400240 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100241 if key in query_params:
242 actual_query_params[argmap[key]] = value
243 if key in path_params:
244 actual_path_params[argmap[key]] = value
245 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400246
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400247 if self._developerKey:
248 actual_query_params['key'] = self._developerKey
249
Joe Gregorio48d361f2010-08-18 13:19:21 -0400250 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400251 headers, params, query, body = self._model.request(headers,
252 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400253
Joe Gregorioaf276d22010-12-09 14:26:58 -0500254 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
255 # document. Base URLs should not contain any path elements. If they do
256 # then urlparse.urljoin will strip them out This results in an incorrect
257 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100258 url_result = urlparse.urlsplit(self._baseUrl)
259 new_base_url = url_result.scheme + '://' + url_result.netloc
260
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400261 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500262 url = urlparse.urljoin(new_base_url,
263 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400264
ade@google.com850cf552010-08-20 23:24:56 +0100265 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500266 return self._requestBuilder(self._http,
267 self._model.response,
268 url,
269 method=httpMethod,
270 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500271 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500272 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400273
274 docs = ['A description of how to use this function\n\n']
275 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400276 required = ""
277 if arg in required_params:
278 required = " (required)"
279 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400280
281 setattr(method, '__doc__', ''.join(docs))
282 setattr(theclass, methodName, method)
283
Joe Gregorioaf276d22010-12-09 14:26:58 -0500284 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
285 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400286
287 def method(self, previous):
288 """
289 Takes a single argument, 'body', which is the results
290 from the last call, and returns the next set of items
291 in the collection.
292
293 Returns None if there are no more items in
294 the collection.
295 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500296 if futureDesc['type'] != 'uri':
297 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400298
299 try:
300 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500301 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400302 p = p[key]
303 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400304 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400305 return None
306
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400307 if self._developerKey:
308 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100309 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400310 q.append(('key', self._developerKey))
311 parsed[4] = urllib.urlencode(q)
312 url = urlparse.urlunparse(parsed)
313
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400314 headers = {}
315 headers, params, query, body = self._model.request(headers, {}, {}, None)
316
317 logging.info('URL being requested: %s' % url)
318 resp, content = self._http.request(url, method='GET', headers=headers)
319
Joe Gregorioabda96f2011-02-11 20:19:33 -0500320 return self._requestBuilder(self._http,
321 self._model.response,
322 url,
323 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500324 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500325 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400326
327 setattr(theclass, methodName, method)
328
329 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400330 if 'methods' in resourceDesc:
331 for methodName, methodDesc in resourceDesc['methods'].iteritems():
332 if futureDesc:
333 future = futureDesc['methods'].get(methodName, {})
334 else:
335 future = None
336 createMethod(Resource, methodName, methodDesc, future)
337
338 # Add in nested resources
339 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500340
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400341 def createMethod(theclass, methodName, methodDesc, futureDesc):
342
343 def method(self):
344 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500345 self._requestBuilder, self._developerKey,
346 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400347
348 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400349 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400350 setattr(theclass, methodName, method)
351
352 for methodName, methodDesc in resourceDesc['resources'].iteritems():
353 if futureDesc and 'resources' in futureDesc:
354 future = futureDesc['resources'].get(methodName, {})
355 else:
356 future = {}
Joe Gregorioaf276d22010-12-09 14:26:58 -0500357 createMethod(Resource, methodName, methodDesc,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500358 future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400359
360 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500361 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400362 for methodName, methodDesc in futureDesc['methods'].iteritems():
363 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500364 createNextMethod(Resource, methodName + "_next",
365 resourceDesc['methods'][methodName],
366 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400367
368 return Resource()