blob: ea61919ede23286936d62237eed682a9a7bf6a70 [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
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017A client library for Google's discovery based APIs.
Joe Gregorio48d361f2010-08-18 13:19:21 -040018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
Joe Gregorioabda96f2011-02-11 20:19:33 -050021__all__ = [
Joe Gregorioce31a972012-06-06 15:48:17 -040022 'build',
23 'build_from_document'
24 'fix_method_name',
25 'key2param'
Joe Gregorioabda96f2011-02-11 20:19:33 -050026 ]
Joe Gregorio48d361f2010-08-18 13:19:21 -040027
Joe Gregorio3c676f92011-07-25 10:38:14 -040028import copy
Joe Gregorio48d361f2010-08-18 13:19:21 -040029import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010030import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040031import os
Joe Gregorio48d361f2010-08-18 13:19:21 -040032import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040033import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040034import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040035import urlparse
Joe Gregoriofdf7c802011-06-30 12:33:38 -040036import mimeparse
Joe Gregorio922b78c2011-05-26 21:36:34 -040037import mimetypes
38
ade@google.comc5eb46f2010-09-27 23:35:39 +010039try:
Joe Gregoriodc106fc2012-11-20 14:30:14 -050040 from urlparse import parse_qsl
ade@google.comc5eb46f2010-09-27 23:35:39 +010041except ImportError:
Joe Gregoriodc106fc2012-11-20 14:30:14 -050042 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050043
Joe Gregorio2b781282011-12-08 12:00:25 -050044from apiclient.errors import HttpError
45from apiclient.errors import InvalidJsonError
46from apiclient.errors import MediaUploadSizeError
47from apiclient.errors import UnacceptableMimeTypeError
48from apiclient.errors import UnknownApiNameOrVersion
Joe Gregoriodc106fc2012-11-20 14:30:14 -050049from apiclient.errors import UnknownFileType
Joe Gregorio2b781282011-12-08 12:00:25 -050050from apiclient.http import HttpRequest
51from apiclient.http import MediaFileUpload
52from apiclient.http import MediaUpload
53from apiclient.model import JsonModel
Joe Gregorio708388c2012-06-15 13:43:04 -040054from apiclient.model import MediaModel
Joe Gregorio2b781282011-12-08 12:00:25 -050055from apiclient.model import RawModel
56from apiclient.schema import Schemas
Joe Gregorio922b78c2011-05-26 21:36:34 -040057from email.mime.multipart import MIMEMultipart
58from email.mime.nonmultipart import MIMENonMultipart
Joe Gregoriof4839b02012-09-06 13:47:24 -040059from oauth2client.util import positional
Joe Gregorio549230c2012-01-11 10:38:05 -050060from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050061
Joe Gregorioe84c9442012-03-12 08:45:57 -040062logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040063
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050064URITEMPLATE = re.compile('{[^}]*}')
65VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040066DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
67 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050068DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050069
Joe Gregorioc8e421c2012-06-06 14:03:13 -040070# Parameters accepted by the stack, but not visible via discovery.
71STACK_QUERY_PARAMETERS = ['trace', 'pp', 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040072
Joe Gregorioc8e421c2012-06-06 14:03:13 -040073# Python reserved words.
Joe Gregorio562b7312011-09-15 09:06:38 -040074RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040075 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
76 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
Joe Gregorio81d92cc2012-07-09 16:46:02 -040077 'pass', 'print', 'raise', 'return', 'try', 'while', 'body']
Joe Gregoriod92897c2011-07-07 11:44:56 -040078
Joe Gregorio562b7312011-09-15 09:06:38 -040079
Joe Gregorioce31a972012-06-06 15:48:17 -040080def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040081 """Fix method names to avoid reserved word conflicts.
82
83 Args:
84 name: string, method name.
85
86 Returns:
87 The name with a '_' prefixed if the name is a reserved word.
88 """
Joe Gregoriod92897c2011-07-07 11:44:56 -040089 if name in RESERVED_WORDS:
90 return name + '_'
91 else:
92 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040093
Joe Gregorioa98733f2011-09-16 10:12:28 -040094
Joe Gregorioa98733f2011-09-16 10:12:28 -040095def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050096 """Adds a query parameter to a url.
97
98 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -040099
100 Args:
101 url: string, url to add the query parameter to.
102 name: string, query parameter name.
103 value: string, query parameter value.
104
105 Returns:
106 Updated query parameter. Does not update the url if value is None.
107 """
108 if value is None:
109 return url
110 else:
111 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500112 q = dict(parse_qsl(parsed[4]))
113 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400114 parsed[4] = urllib.urlencode(q)
115 return urlparse.urlunparse(parsed)
116
117
Joe Gregorio48d361f2010-08-18 13:19:21 -0400118def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500119 """Converts key names into parameter names.
120
121 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400122
123 Args:
124 key: string, the method key name.
125
126 Returns:
127 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400128 """
129 result = []
130 key = list(key)
131 if not key[0].isalpha():
132 result.append('x')
133 for c in key:
134 if c.isalnum():
135 result.append(c)
136 else:
137 result.append('_')
138
139 return ''.join(result)
140
141
Joe Gregoriof4839b02012-09-06 13:47:24 -0400142@positional(2)
Joe Gregorio01770a52012-02-24 11:11:10 -0500143def build(serviceName,
144 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500145 http=None,
146 discoveryServiceUrl=DISCOVERY_URI,
147 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500148 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500149 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500150 """Construct a Resource for interacting with an API.
151
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400152 Construct a Resource object for interacting with an API. The serviceName and
153 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500154
155 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400156 serviceName: string, name of the service.
157 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500158 http: httplib2.Http, An instance of httplib2.Http or something that acts
159 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400160 discoveryServiceUrl: string, a URI Template that points to the location of
161 the discovery service. It should have two parameters {api} and
162 {apiVersion} that when filled in produce an absolute URI to the discovery
163 document for that service.
164 developerKey: string, key obtained from
165 https://code.google.com/apis/console.
166 model: apiclient.Model, converts to and from the wire format.
167 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
168 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500169
170 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400171 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500172 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400173 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400174 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400175 'apiVersion': version
176 }
ade@google.com850cf552010-08-20 23:24:56 +0100177
Joe Gregorioc204b642010-09-21 12:01:23 -0400178 if http is None:
179 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400180
ade@google.com850cf552010-08-20 23:24:56 +0100181 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400182
Joe Gregorio66f57522011-11-30 11:00:00 -0500183 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
184 # variable that contains the network address of the client sending the
185 # request. If it exists then add that to the request for the discovery
186 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400187 if 'REMOTE_ADDR' in os.environ:
188 requested_url = _add_query_parameter(requested_url, 'userIp',
189 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400190 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400191
ade@google.com850cf552010-08-20 23:24:56 +0100192 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400193
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500194 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500195 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500196 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400197 if resp.status >= 400:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400198 raise HttpError(resp, content, uri=requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400199
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500200 try:
201 service = simplejson.loads(content)
202 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400203 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500204 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400205
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400206 return build_from_document(content, base=discoveryServiceUrl, http=http,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400207 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500208
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500209
Joe Gregoriof4839b02012-09-06 13:47:24 -0400210@positional(1)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500211def build_from_document(
212 service,
Joe Gregorioa2838152012-07-16 11:52:17 -0400213 base=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500214 future=None,
215 http=None,
216 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500217 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500218 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500219 """Create a Resource for interacting with an API.
220
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400221 Same as `build()`, but constructs the Resource object from a discovery
222 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500223
Joe Gregorio292b9b82011-01-12 11:36:11 -0500224 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400225 service: string, discovery document.
226 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400227 This parameter is no longer used as rootUrl and servicePath are included
228 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400229 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500230 http: httplib2.Http, An instance of httplib2.Http or something that acts
231 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500232 developerKey: string, Key for controlling API usage, generated
233 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400234 model: Model class instance that serializes and de-serializes requests and
235 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500236 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500237
238 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400239 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500240 """
241
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400242 # future is no longer used.
243 future = {}
244
Joe Gregorio292b9b82011-01-12 11:36:11 -0500245 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400246 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500247 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400248
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500249 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500250 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500251 model = JsonModel('dataWrapper' in features)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500252 return Resource(http=http, baseUrl=base, model=model,
253 developerKey=developerKey, requestBuilder=requestBuilder,
254 resourceDesc=service, rootDesc=service, schema=schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400255
256
Joe Gregorio61d7e962011-02-22 22:52:07 -0500257def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500258 """Convert value to a string based on JSON Schema type.
259
260 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
261 JSON Schema.
262
263 Args:
264 value: any, the value to convert
265 schema_type: string, the type that value should be interpreted as
266
267 Returns:
268 A string representation of 'value' based on the schema_type.
269 """
270 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500271 if type(value) == type('') or type(value) == type(u''):
272 return value
273 else:
274 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500275 elif schema_type == 'integer':
276 return str(int(value))
277 elif schema_type == 'number':
278 return str(float(value))
279 elif schema_type == 'boolean':
280 return str(bool(value)).lower()
281 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500282 if type(value) == type('') or type(value) == type(u''):
283 return value
284 else:
285 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500286
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400287
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400288MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400289 "KB": 2 ** 10,
290 "MB": 2 ** 20,
291 "GB": 2 ** 30,
292 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400293 }
294
Joe Gregorioa98733f2011-09-16 10:12:28 -0400295
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400296def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400297 """Convert a string media size, such as 10GB or 3TB into an integer.
298
299 Args:
300 maxSize: string, size as a string, such as 2MB or 7GB.
301
302 Returns:
303 The size as an integer value.
304 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400305 if len(maxSize) < 2:
306 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400307 units = maxSize[-2:].upper()
308 multiplier = MULTIPLIERS.get(units, 0)
309 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400310 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400311 else:
312 return int(maxSize)
313
Joe Gregoriobee86832011-02-22 10:00:19 -0500314
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500315def createMethod(methodName, methodDesc, rootDesc, schema):
316 """Creates a method for attaching to a Resource.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400317
318 Args:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500319 methodName: string, name of the method to use.
320 methodDesc: object, fragment of deserialized discovery document that
321 describes the method.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400322 rootDesc: object, the entire deserialized discovery document.
323 schema: object, mapping of schema names to schema descriptions.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400324 """
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500325 methodName = fix_method_name(methodName)
326 pathUrl = methodDesc['path']
327 httpMethod = methodDesc['httpMethod']
328 methodId = methodDesc['id']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400329
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500330 mediaPathUrl = None
331 accept = []
332 maxSize = 0
333 if 'mediaUpload' in methodDesc:
334 mediaUpload = methodDesc['mediaUpload']
335 mediaPathUrl = (rootDesc['rootUrl'] + 'upload/' + rootDesc['servicePath']
336 + pathUrl)
337 accept = mediaUpload['accept']
338 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400339
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500340 if 'parameters' not in methodDesc:
341 methodDesc['parameters'] = {}
Joe Gregorio48d361f2010-08-18 13:19:21 -0400342
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500343 # Add in the parameters common to all methods.
344 for name, desc in rootDesc.get('parameters', {}).iteritems():
345 methodDesc['parameters'][name] = desc
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400346
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500347 # Add in undocumented query parameters.
348 for name in STACK_QUERY_PARAMETERS:
349 methodDesc['parameters'][name] = {
350 'type': 'string',
351 'location': 'query'
352 }
Joe Gregorio21f11672010-08-18 17:23:17 -0400353
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500354 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
355 methodDesc['parameters']['body'] = {
356 'description': 'The request body.',
357 'type': 'object',
358 'required': True,
359 }
360 if 'request' in methodDesc:
361 methodDesc['parameters']['body'].update(methodDesc['request'])
362 else:
363 methodDesc['parameters']['body']['type'] = 'object'
364 if 'mediaUpload' in methodDesc:
365 methodDesc['parameters']['media_body'] = {
366 'description':
367 'The filename of the media request body, or an instance of a '
368 'MediaUpload object.',
369 'type': 'string',
370 'required': False,
371 }
372 if 'body' in methodDesc['parameters']:
373 methodDesc['parameters']['body']['required'] = False
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400374
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500375 argmap = {} # Map from method parameter name to query parameter name
376 required_params = [] # Required parameters
377 repeated_params = [] # Repeated parameters
378 pattern_params = {} # Parameters that must match a regex
379 query_params = [] # Parameters that will be used in the query string
380 path_params = {} # Parameters that will be used in the base URL
381 param_type = {} # The type of the parameter
382 enum_params = {} # Allowable enumeration values for each parameter
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400383
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500384 if 'parameters' in methodDesc:
385 for arg, desc in methodDesc['parameters'].iteritems():
386 param = key2param(arg)
387 argmap[param] = arg
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400388
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500389 if desc.get('pattern', ''):
390 pattern_params[param] = desc['pattern']
391 if desc.get('enum', ''):
392 enum_params[param] = desc['enum']
393 if desc.get('required', False):
394 required_params.append(param)
395 if desc.get('repeated', False):
396 repeated_params.append(param)
397 if desc.get('location') == 'query':
398 query_params.append(param)
399 if desc.get('location') == 'path':
400 path_params[param] = param
401 param_type[param] = desc.get('type', 'string')
Joe Gregorioca876e42011-02-22 19:39:42 -0500402
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500403 for match in URITEMPLATE.finditer(pathUrl):
404 for namematch in VARNAME.finditer(match.group(0)):
405 name = key2param(namematch.group(0))
406 path_params[name] = name
407 if name in query_params:
408 query_params.remove(name)
ade@google.com850cf552010-08-20 23:24:56 +0100409
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500410 def method(self, **kwargs):
411 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorioca876e42011-02-22 19:39:42 -0500412
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500413 for name in kwargs.iterkeys():
414 if name not in argmap:
415 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorioca876e42011-02-22 19:39:42 -0500416
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500417 # Remove args that have a value of None.
418 keys = kwargs.keys()
419 for name in keys:
420 if kwargs[name] is None:
421 del kwargs[name]
Joe Gregorio21f11672010-08-18 17:23:17 -0400422
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500423 for name in required_params:
424 if name not in kwargs:
425 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400426
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500427 for name, regex in pattern_params.iteritems():
428 if name in kwargs:
429 if isinstance(kwargs[name], basestring):
430 pvalues = [kwargs[name]]
Joe Gregorio61d7e962011-02-22 22:52:07 -0500431 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500432 pvalues = kwargs[name]
433 for pvalue in pvalues:
434 if re.match(regex, pvalue) is None:
435 raise TypeError(
436 'Parameter "%s" value "%s" does not match the pattern "%s"' %
437 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400438
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500439 for name, enums in enum_params.iteritems():
440 if name in kwargs:
441 # We need to handle the case of a repeated enum
442 # name differently, since we want to handle both
443 # arg='value' and arg=['value1', 'value2']
444 if (name in repeated_params and
445 not isinstance(kwargs[name], basestring)):
446 values = kwargs[name]
447 else:
448 values = [kwargs[name]]
449 for value in values:
450 if value not in enums:
451 raise TypeError(
452 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
453 (name, value, str(enums)))
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400454
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500455 actual_query_params = {}
456 actual_path_params = {}
457 for key, value in kwargs.iteritems():
458 to_type = param_type.get(key, 'string')
459 # For repeated parameters we cast each member of the list.
460 if key in repeated_params and type(value) == type([]):
461 cast_value = [_cast(x, to_type) for x in value]
462 else:
463 cast_value = _cast(value, to_type)
464 if key in query_params:
465 actual_query_params[argmap[key]] = cast_value
466 if key in path_params:
467 actual_path_params[argmap[key]] = cast_value
468 body_value = kwargs.get('body', None)
469 media_filename = kwargs.get('media_body', None)
Joe Gregorioe08a1662011-12-07 09:48:22 -0500470
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500471 if self._developerKey:
472 actual_query_params['key'] = self._developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400473
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500474 model = self._model
475 if methodName.endswith('_media'):
476 model = MediaModel()
477 elif 'response' not in methodDesc:
478 model = RawModel()
479
480 headers = {}
481 headers, params, query, body = model.request(headers,
482 actual_path_params, actual_query_params, body_value)
483
484 expanded_url = uritemplate.expand(pathUrl, params)
485 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
486
487 resumable = None
488 multipart_boundary = ''
489
490 if media_filename:
491 # Ensure we end up with a valid MediaUpload object.
492 if isinstance(media_filename, basestring):
493 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
494 if media_mime_type is None:
495 raise UnknownFileType(media_filename)
496 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
497 raise UnacceptableMimeTypeError(media_mime_type)
498 media_upload = MediaFileUpload(media_filename,
499 mimetype=media_mime_type)
500 elif isinstance(media_filename, MediaUpload):
501 media_upload = media_filename
502 else:
503 raise TypeError('media_filename must be str or MediaUpload.')
504
505 # Check the maxSize
506 if maxSize > 0 and media_upload.size() > maxSize:
507 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
508
509 # Use the media path uri for media uploads
510 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400511 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500512 if media_upload.resumable():
513 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400514
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500515 if media_upload.resumable():
516 # This is all we need to do for resumable, if the body exists it gets
517 # sent in the first request, otherwise an empty body is sent.
518 resumable = media_upload
Joe Gregorio2b781282011-12-08 12:00:25 -0500519 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500520 # A non-resumable upload
521 if body is None:
522 # This is a simple media upload
523 headers['content-type'] = media_upload.mimetype()
524 body = media_upload.getbytes(0, media_upload.size())
525 url = _add_query_parameter(url, 'uploadType', 'media')
526 else:
527 # This is a multipart/related upload.
528 msgRoot = MIMEMultipart('related')
529 # msgRoot should not write out it's own headers
530 setattr(msgRoot, '_write_headers', lambda self: None)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400531
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500532 # attach the body as one part
533 msg = MIMENonMultipart(*headers['content-type'].split('/'))
534 msg.set_payload(body)
535 msgRoot.attach(msg)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400536
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500537 # attach the media as the second part
538 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
539 msg['Content-Transfer-Encoding'] = 'binary'
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400540
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500541 payload = media_upload.getbytes(0, media_upload.size())
542 msg.set_payload(payload)
543 msgRoot.attach(msg)
544 body = msgRoot.as_string()
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400545
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500546 multipart_boundary = msgRoot.get_boundary()
547 headers['content-type'] = ('multipart/related; '
548 'boundary="%s"') % multipart_boundary
549 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400550
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500551 logger.info('URL being requested: %s' % url)
552 return self._requestBuilder(self._http,
553 model.response,
554 url,
555 method=httpMethod,
556 body=body,
557 headers=headers,
558 methodId=methodId,
559 resumable=resumable)
560
561 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
562 if len(argmap) > 0:
563 docs.append('Args:\n')
564
565 # Skip undocumented params and params common to all methods.
566 skip_parameters = rootDesc.get('parameters', {}).keys()
567 skip_parameters.extend(STACK_QUERY_PARAMETERS)
568
569 all_args = argmap.keys()
570 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
571
572 # Move body to the front of the line.
573 if 'body' in all_args:
574 args_ordered.append('body')
575
576 for name in all_args:
577 if name not in args_ordered:
578 args_ordered.append(name)
579
580 for arg in args_ordered:
581 if arg in skip_parameters:
582 continue
583
584 repeated = ''
585 if arg in repeated_params:
586 repeated = ' (repeated)'
587 required = ''
588 if arg in required_params:
589 required = ' (required)'
590 paramdesc = methodDesc['parameters'][argmap[arg]]
591 paramdoc = paramdesc.get('description', 'A parameter')
592 if '$ref' in paramdesc:
593 docs.append(
594 (' %s: object, %s%s%s\n The object takes the'
595 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
596 schema.prettyPrintByName(paramdesc['$ref'])))
597 else:
598 paramtype = paramdesc.get('type', 'string')
599 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
600 repeated))
601 enum = paramdesc.get('enum', [])
602 enumDesc = paramdesc.get('enumDescriptions', [])
603 if enum and enumDesc:
604 docs.append(' Allowed values\n')
605 for (name, desc) in zip(enum, enumDesc):
606 docs.append(' %s - %s\n' % (name, desc))
607 if 'response' in methodDesc:
608 if methodName.endswith('_media'):
609 docs.append('\nReturns:\n The media object as a string.\n\n ')
610 else:
611 docs.append('\nReturns:\n An object of the form:\n\n ')
612 docs.append(schema.prettyPrintSchema(methodDesc['response']))
613
614 setattr(method, '__doc__', ''.join(docs))
615 return (methodName, method)
616
617
618def createNextMethod(methodName):
619 """Creates any _next methods for attaching to a Resource.
620
621 The _next methods allow for easy iteration through list() responses.
622
623 Args:
624 methodName: string, name of the method to use.
625 """
626 methodName = fix_method_name(methodName)
627
628 def methodNext(self, previous_request, previous_response):
629 """Retrieves the next page of results.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400630
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400631Args:
632 previous_request: The request for the previous page. (required)
633 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400634
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400635Returns:
636 A request object that you can call 'execute()' on to request the next
637 page. Returns None if there are no more items in the collection.
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500638 """
639 # Retrieve nextPageToken from previous_response
640 # Use as pageToken in previous_request to create new request.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400641
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500642 if 'nextPageToken' not in previous_response:
643 return None
Joe Gregorio3c676f92011-07-25 10:38:14 -0400644
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500645 request = copy.copy(previous_request)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400646
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500647 pageToken = previous_response['nextPageToken']
648 parsed = list(urlparse.urlparse(request.uri))
649 q = parse_qsl(parsed[4])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400650
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500651 # Find and remove old 'pageToken' value from URI
652 newq = [(key, value) for (key, value) in q if key != 'pageToken']
653 newq.append(('pageToken', pageToken))
654 parsed[4] = urllib.urlencode(newq)
655 uri = urlparse.urlunparse(parsed)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400656
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500657 request.uri = uri
Joe Gregorio3c676f92011-07-25 10:38:14 -0400658
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500659 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400660
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500661 return request
Joe Gregorio3c676f92011-07-25 10:38:14 -0400662
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500663 return (methodName, methodNext)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400664
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400665
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500666class Resource(object):
667 """A class for interacting with a resource."""
Joe Gregorioaf276d22010-12-09 14:26:58 -0500668
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500669 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
670 resourceDesc, rootDesc, schema):
671 """Build a Resource from the API description.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400672
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500673 Args:
674 http: httplib2.Http, Object to make http requests with.
675 baseUrl: string, base URL for the API. All requests are relative to this
676 URI.
677 model: apiclient.Model, converts to and from the wire format.
678 requestBuilder: class or callable that instantiates an
679 apiclient.HttpRequest object.
680 developerKey: string, key obtained from
681 https://code.google.com/apis/console
682 resourceDesc: object, section of deserialized discovery document that
683 describes a resource. Note that the top level discovery document
684 is considered a resource.
685 rootDesc: object, the entire deserialized discovery document.
686 schema: object, mapping of schema names to schema descriptions.
687 """
688 self._dynamic_attrs = []
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400689
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500690 self._http = http
691 self._baseUrl = baseUrl
692 self._model = model
693 self._developerKey = developerKey
694 self._requestBuilder = requestBuilder
695 self._resourceDesc = resourceDesc
696 self._rootDesc = rootDesc
697 self._schema = schema
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400698
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500699 self._set_service_methods()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400700
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500701 def _set_dynamic_attr(self, attr_name, value):
702 """Sets an instance attribute and tracks it in a list of dynamic attributes.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400703
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500704 Args:
705 attr_name: string; The name of the attribute to be set
706 value: The value being set on the object and tracked in the dynamic cache.
707 """
708 self._dynamic_attrs.append(attr_name)
709 self.__dict__[attr_name] = value
Joe Gregorio48d361f2010-08-18 13:19:21 -0400710
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500711 def __getstate__(self):
712 """Trim the state down to something that can be pickled.
713
714 Uses the fact that the instance variable _dynamic_attrs holds attrs that
715 will be wiped and restored on pickle serialization.
716 """
717 state_dict = copy.copy(self.__dict__)
718 for dynamic_attr in self._dynamic_attrs:
719 del state_dict[dynamic_attr]
720 del state_dict['_dynamic_attrs']
721 return state_dict
722
723 def __setstate__(self, state):
724 """Reconstitute the state of the object from being pickled.
725
726 Uses the fact that the instance variable _dynamic_attrs holds attrs that
727 will be wiped and restored on pickle serialization.
728 """
729 self.__dict__.update(state)
730 self._dynamic_attrs = []
731 self._set_service_methods()
732
733 def _set_service_methods(self):
734 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
735 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
736 self._add_next_methods(self._resourceDesc, self._schema)
737
738 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
739 # Add basic methods to Resource
740 if 'methods' in resourceDesc:
741 for methodName, methodDesc in resourceDesc['methods'].iteritems():
742 fixedMethodName, method = createMethod(
743 methodName, methodDesc, rootDesc, schema)
744 self._set_dynamic_attr(fixedMethodName,
745 method.__get__(self, self.__class__))
746 # Add in _media methods. The functionality of the attached method will
747 # change when it sees that the method name ends in _media.
748 if methodDesc.get('supportsMediaDownload', False):
749 fixedMethodName, method = createMethod(
750 methodName + '_media', methodDesc, rootDesc, schema)
751 self._set_dynamic_attr(fixedMethodName,
752 method.__get__(self, self.__class__))
753
754 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
755 # Add in nested resources
756 if 'resources' in resourceDesc:
757
758 def createResourceMethod(methodName, methodDesc):
759 """Create a method on the Resource to access a nested Resource.
760
761 Args:
762 methodName: string, name of the method to use.
763 methodDesc: object, fragment of deserialized discovery document that
764 describes the method.
765 """
766 methodName = fix_method_name(methodName)
767
768 def methodResource(self):
769 return Resource(http=self._http, baseUrl=self._baseUrl,
770 model=self._model, developerKey=self._developerKey,
771 requestBuilder=self._requestBuilder,
772 resourceDesc=methodDesc, rootDesc=rootDesc,
773 schema=schema)
774
775 setattr(methodResource, '__doc__', 'A collection resource.')
776 setattr(methodResource, '__is_resource__', True)
777
778 return (methodName, methodResource)
779
780 for methodName, methodDesc in resourceDesc['resources'].iteritems():
781 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
782 self._set_dynamic_attr(fixedMethodName,
783 method.__get__(self, self.__class__))
784
785 def _add_next_methods(self, resourceDesc, schema):
786 # Add _next() methods
787 # Look for response bodies in schema that contain nextPageToken, and methods
788 # that take a pageToken parameter.
789 if 'methods' in resourceDesc:
790 for methodName, methodDesc in resourceDesc['methods'].iteritems():
791 if 'response' in methodDesc:
792 responseSchema = methodDesc['response']
793 if '$ref' in responseSchema:
794 responseSchema = schema.get(responseSchema['$ref'])
795 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
796 {})
797 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
798 if hasNextPageToken and hasPageToken:
799 fixedMethodName, method = createNextMethod(methodName + '_next')
800 self._set_dynamic_attr(fixedMethodName,
801 method.__get__(self, self.__class__))