blob: 22cb4e06a05929a72bca7f1e0f16cd15a2c3489f [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.
Joe Gregoriodeeb0202011-02-15 14:49:57 -050087 developerKey: string, key obtained
88 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -050089 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -050090 requestBuilder: apiclient.http.HttpRequest, encapsulator for
91 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -050092
93 Returns:
94 A Resource object with methods for interacting with
95 the service.
96 """
Joe Gregorio48d361f2010-08-18 13:19:21 -040097 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040098 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -040099 'apiVersion': version
100 }
ade@google.com850cf552010-08-20 23:24:56 +0100101
Joe Gregorioc204b642010-09-21 12:01:23 -0400102 if http is None:
103 http = httplib2.Http()
ade@google.com850cf552010-08-20 23:24:56 +0100104 requested_url = uritemplate.expand(discoveryServiceUrl, params)
105 logging.info('URL being requested: %s' % requested_url)
106 resp, content = http.request(requested_url)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400107 service = simplejson.loads(content)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400108
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400109 fn = os.path.join(os.path.dirname(__file__), "contrib",
110 serviceName, "future.json")
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400111 try:
112 f = file(fn, "r")
Joe Gregorio292b9b82011-01-12 11:36:11 -0500113 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400114 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400115 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500116 future = None
117
118 return build_from_document(content, discoveryServiceUrl, future,
119 http, developerKey, model, requestBuilder)
120
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500121
Joe Gregorio292b9b82011-01-12 11:36:11 -0500122def build_from_document(
123 service,
124 base,
125 future=None,
126 http=None,
127 developerKey=None,
128 model=JsonModel(),
129 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500130 """Create a Resource for interacting with an API.
131
132 Same as `build()`, but constructs the Resource object
133 from a discovery document that is it given, as opposed to
134 retrieving one over HTTP.
135
Joe Gregorio292b9b82011-01-12 11:36:11 -0500136 Args:
137 service: string, discovery document
138 base: string, base URI for all HTTP requests, usually the discovery URI
139 future: string, discovery document with future capabilities
140 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500141 http: httplib2.Http, An instance of httplib2.Http or something that acts
142 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500143 developerKey: string, Key for controlling API usage, generated
144 from the API Console.
145 model: Model class instance that serializes and
146 de-serializes requests and responses.
147 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500148
149 Returns:
150 A Resource object with methods for interacting with
151 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500152 """
153
154 service = simplejson.loads(service)
155 base = urlparse.urljoin(base, service['restBasePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500156 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500157 future = simplejson.loads(future)
158 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500159 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400160 future = {}
161 auth_discovery = {}
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400162
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500163 resource = createResource(http, base, model, requestBuilder, developerKey,
164 service, future)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400165
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500166 def auth_method():
167 """Discovery information about the authentication the API uses."""
168 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400169
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500170 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400171
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500172 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400173
174
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500175def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500176 developerKey, resourceDesc, futureDesc):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400177
178 class Resource(object):
179 """A class for interacting with a resource."""
180
181 def __init__(self):
182 self._http = http
183 self._baseUrl = baseUrl
184 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400185 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500186 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400187
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400188 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400189 pathUrl = methodDesc['restPath']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400190 pathUrl = re.sub(r'\{', r'{+', pathUrl)
191 httpMethod = methodDesc['httpMethod']
Joe Gregorioaf276d22010-12-09 14:26:58 -0500192 methodId = methodDesc['rpcMethod']
Joe Gregorio21f11672010-08-18 17:23:17 -0400193
ade@google.com850cf552010-08-20 23:24:56 +0100194 argmap = {}
195 if httpMethod in ['PUT', 'POST']:
196 argmap['body'] = 'body'
197
198
199 required_params = [] # Required parameters
200 pattern_params = {} # Parameters that must match a regex
201 query_params = [] # Parameters that will be used in the query string
202 path_params = {} # Parameters that will be used in the base URL
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400203 if 'parameters' in methodDesc:
204 for arg, desc in methodDesc['parameters'].iteritems():
205 param = key2param(arg)
206 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400207
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400208 if desc.get('pattern', ''):
209 pattern_params[param] = desc['pattern']
210 if desc.get('required', False):
211 required_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400212 if desc.get('restParameterType') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400213 query_params.append(param)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400214 if desc.get('restParameterType') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400215 path_params[param] = param
Joe Gregorio48d361f2010-08-18 13:19:21 -0400216
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500217 for match in URITEMPLATE.finditer(pathUrl):
218 for namematch in VARNAME.finditer(match.group(0)):
219 name = key2param(namematch.group(0))
220 path_params[name] = name
221 if name in query_params:
222 query_params.remove(name)
223
Joe Gregorio48d361f2010-08-18 13:19:21 -0400224 def method(self, **kwargs):
225 for name in kwargs.iterkeys():
226 if name not in argmap:
227 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400228
ade@google.com850cf552010-08-20 23:24:56 +0100229 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400230 if name not in kwargs:
231 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400232
ade@google.com850cf552010-08-20 23:24:56 +0100233 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400234 if name in kwargs:
235 if re.match(regex, kwargs[name]) is None:
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400236 raise TypeError(
237 'Parameter "%s" value "%s" does not match the pattern "%s"' %
238 (name, kwargs[name], regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400239
ade@google.com850cf552010-08-20 23:24:56 +0100240 actual_query_params = {}
241 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400242 for key, value in kwargs.iteritems():
ade@google.com850cf552010-08-20 23:24:56 +0100243 if key in query_params:
244 actual_query_params[argmap[key]] = value
245 if key in path_params:
246 actual_path_params[argmap[key]] = value
247 body_value = kwargs.get('body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400248
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400249 if self._developerKey:
250 actual_query_params['key'] = self._developerKey
251
Joe Gregorio48d361f2010-08-18 13:19:21 -0400252 headers = {}
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400253 headers, params, query, body = self._model.request(headers,
254 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400255
Joe Gregorioaf276d22010-12-09 14:26:58 -0500256 # TODO(ade) This exists to fix a bug in V1 of the Buzz discovery
257 # document. Base URLs should not contain any path elements. If they do
258 # then urlparse.urljoin will strip them out This results in an incorrect
259 # URL which returns a 404
ade@google.com7ebb2ca2010-09-29 16:42:15 +0100260 url_result = urlparse.urlsplit(self._baseUrl)
261 new_base_url = url_result.scheme + '://' + url_result.netloc
262
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400263 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorioaf276d22010-12-09 14:26:58 -0500264 url = urlparse.urljoin(new_base_url,
265 url_result.path + expanded_url + query)
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400266
ade@google.com850cf552010-08-20 23:24:56 +0100267 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500268 return self._requestBuilder(self._http,
269 self._model.response,
270 url,
271 method=httpMethod,
272 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500273 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500274 methodId=methodId)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400275
276 docs = ['A description of how to use this function\n\n']
277 for arg in argmap.iterkeys():
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400278 required = ""
279 if arg in required_params:
280 required = " (required)"
281 docs.append('%s - A parameter%s\n' % (arg, required))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400282
283 setattr(method, '__doc__', ''.join(docs))
284 setattr(theclass, methodName, method)
285
Joe Gregorioaf276d22010-12-09 14:26:58 -0500286 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
287 methodId = methodDesc['rpcMethod'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400288
289 def method(self, previous):
290 """
291 Takes a single argument, 'body', which is the results
292 from the last call, and returns the next set of items
293 in the collection.
294
295 Returns None if there are no more items in
296 the collection.
297 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500298 if futureDesc['type'] != 'uri':
299 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400300
301 try:
302 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500303 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400304 p = p[key]
305 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400306 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400307 return None
308
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400309 if self._developerKey:
310 parsed = list(urlparse.urlparse(url))
ade@google.comc5eb46f2010-09-27 23:35:39 +0100311 q = parse_qsl(parsed[4])
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400312 q.append(('key', self._developerKey))
313 parsed[4] = urllib.urlencode(q)
314 url = urlparse.urlunparse(parsed)
315
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400316 headers = {}
317 headers, params, query, body = self._model.request(headers, {}, {}, None)
318
319 logging.info('URL being requested: %s' % url)
320 resp, content = self._http.request(url, method='GET', headers=headers)
321
Joe Gregorioabda96f2011-02-11 20:19:33 -0500322 return self._requestBuilder(self._http,
323 self._model.response,
324 url,
325 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500326 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500327 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400328
329 setattr(theclass, methodName, method)
330
331 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400332 if 'methods' in resourceDesc:
333 for methodName, methodDesc in resourceDesc['methods'].iteritems():
334 if futureDesc:
335 future = futureDesc['methods'].get(methodName, {})
336 else:
337 future = None
338 createMethod(Resource, methodName, methodDesc, future)
339
340 # Add in nested resources
341 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500342
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400343 def createMethod(theclass, methodName, methodDesc, futureDesc):
344
345 def method(self):
346 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500347 self._requestBuilder, self._developerKey,
348 methodDesc, futureDesc)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400349
350 setattr(method, '__doc__', 'A description of how to use this function')
Joe Gregorio20cfcda2010-10-26 11:58:08 -0400351 setattr(method, '__is_resource__', True)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400352 setattr(theclass, methodName, method)
353
354 for methodName, methodDesc in resourceDesc['resources'].iteritems():
355 if futureDesc and 'resources' in futureDesc:
356 future = futureDesc['resources'].get(methodName, {})
357 else:
358 future = {}
Joe Gregorioaf276d22010-12-09 14:26:58 -0500359 createMethod(Resource, methodName, methodDesc,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500360 future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400361
362 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500363 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400364 for methodName, methodDesc in futureDesc['methods'].iteritems():
365 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500366 createNextMethod(Resource, methodName + "_next",
367 resourceDesc['methods'][methodName],
368 methodDesc['next'])
Joe Gregorio48d361f2010-08-18 13:19:21 -0400369
370 return Resource()