blob: 1fc33ca5a49dddedaa629ec7fc7680bae1b51500 [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 Gregorio504a17f2012-12-07 14:14:26 -050062# The client library requires a version of httplib2 that supports RETRIES.
63httplib2.RETRIES = 1
64
Joe Gregorioe84c9442012-03-12 08:45:57 -040065logger = logging.getLogger(__name__)
Joe Gregorio48d361f2010-08-18 13:19:21 -040066
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050067URITEMPLATE = re.compile('{[^}]*}')
68VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040069DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
70 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050071DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050072
Joe Gregorioc8e421c2012-06-06 14:03:13 -040073# Parameters accepted by the stack, but not visible via discovery.
74STACK_QUERY_PARAMETERS = ['trace', 'pp', 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040075
Joe Gregorioc8e421c2012-06-06 14:03:13 -040076# Python reserved words.
Joe Gregorio562b7312011-09-15 09:06:38 -040077RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040078 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
79 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
Joe Gregorio81d92cc2012-07-09 16:46:02 -040080 'pass', 'print', 'raise', 'return', 'try', 'while', 'body']
Joe Gregoriod92897c2011-07-07 11:44:56 -040081
Joe Gregorio562b7312011-09-15 09:06:38 -040082
Joe Gregorioce31a972012-06-06 15:48:17 -040083def fix_method_name(name):
Joe Gregorioc8e421c2012-06-06 14:03:13 -040084 """Fix method names to avoid reserved word conflicts.
85
86 Args:
87 name: string, method name.
88
89 Returns:
90 The name with a '_' prefixed if the name is a reserved word.
91 """
Joe Gregoriod92897c2011-07-07 11:44:56 -040092 if name in RESERVED_WORDS:
93 return name + '_'
94 else:
95 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040096
Joe Gregorioa98733f2011-09-16 10:12:28 -040097
Joe Gregorioa98733f2011-09-16 10:12:28 -040098def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050099 """Adds a query parameter to a url.
100
101 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -0400102
103 Args:
104 url: string, url to add the query parameter to.
105 name: string, query parameter name.
106 value: string, query parameter value.
107
108 Returns:
109 Updated query parameter. Does not update the url if value is None.
110 """
111 if value is None:
112 return url
113 else:
114 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500115 q = dict(parse_qsl(parsed[4]))
116 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400117 parsed[4] = urllib.urlencode(q)
118 return urlparse.urlunparse(parsed)
119
120
Joe Gregorio48d361f2010-08-18 13:19:21 -0400121def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500122 """Converts key names into parameter names.
123
124 For example, converting "max-results" -> "max_results"
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400125
126 Args:
127 key: string, the method key name.
128
129 Returns:
130 A safe method name based on the key name.
Joe Gregorio48d361f2010-08-18 13:19:21 -0400131 """
132 result = []
133 key = list(key)
134 if not key[0].isalpha():
135 result.append('x')
136 for c in key:
137 if c.isalnum():
138 result.append(c)
139 else:
140 result.append('_')
141
142 return ''.join(result)
143
144
Joe Gregoriof4839b02012-09-06 13:47:24 -0400145@positional(2)
Joe Gregorio01770a52012-02-24 11:11:10 -0500146def build(serviceName,
147 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500148 http=None,
149 discoveryServiceUrl=DISCOVERY_URI,
150 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500151 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500152 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500153 """Construct a Resource for interacting with an API.
154
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400155 Construct a Resource object for interacting with an API. The serviceName and
156 version are the names from the Discovery service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500157
158 Args:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400159 serviceName: string, name of the service.
160 version: string, the version of the service.
Joe Gregorio01770a52012-02-24 11:11:10 -0500161 http: httplib2.Http, An instance of httplib2.Http or something that acts
162 like it that HTTP requests will be made through.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400163 discoveryServiceUrl: string, a URI Template that points to the location of
164 the discovery service. It should have two parameters {api} and
165 {apiVersion} that when filled in produce an absolute URI to the discovery
166 document for that service.
167 developerKey: string, key obtained from
168 https://code.google.com/apis/console.
169 model: apiclient.Model, converts to and from the wire format.
170 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
171 request.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500172
173 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400174 A Resource object with methods for interacting with the service.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500175 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400176 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400177 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400178 'apiVersion': version
179 }
ade@google.com850cf552010-08-20 23:24:56 +0100180
Joe Gregorioc204b642010-09-21 12:01:23 -0400181 if http is None:
182 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400183
ade@google.com850cf552010-08-20 23:24:56 +0100184 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400185
Joe Gregorio66f57522011-11-30 11:00:00 -0500186 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
187 # variable that contains the network address of the client sending the
188 # request. If it exists then add that to the request for the discovery
189 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400190 if 'REMOTE_ADDR' in os.environ:
191 requested_url = _add_query_parameter(requested_url, 'userIp',
192 os.environ['REMOTE_ADDR'])
Joe Gregorioe84c9442012-03-12 08:45:57 -0400193 logger.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400194
ade@google.com850cf552010-08-20 23:24:56 +0100195 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400196
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500197 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500198 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500199 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400200 if resp.status >= 400:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400201 raise HttpError(resp, content, uri=requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400202
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500203 try:
204 service = simplejson.loads(content)
205 except ValueError, e:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400206 logger.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500207 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400208
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400209 return build_from_document(content, base=discoveryServiceUrl, http=http,
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400210 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500211
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500212
Joe Gregoriof4839b02012-09-06 13:47:24 -0400213@positional(1)
Joe Gregorio292b9b82011-01-12 11:36:11 -0500214def build_from_document(
215 service,
Joe Gregorioa2838152012-07-16 11:52:17 -0400216 base=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500217 future=None,
218 http=None,
219 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500220 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500221 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500222 """Create a Resource for interacting with an API.
223
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400224 Same as `build()`, but constructs the Resource object from a discovery
225 document that is it given, as opposed to retrieving one over HTTP.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500226
Joe Gregorio292b9b82011-01-12 11:36:11 -0500227 Args:
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500228 service: string or object, the JSON discovery document describing the API.
229 The value passed in may either be the JSON string or the deserialized
230 JSON.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400231 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400232 This parameter is no longer used as rootUrl and servicePath are included
233 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400234 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500235 http: httplib2.Http, An instance of httplib2.Http or something that acts
236 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500237 developerKey: string, Key for controlling API usage, generated
238 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400239 model: Model class instance that serializes and de-serializes requests and
240 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500241 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500242
243 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400244 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500245 """
246
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400247 # future is no longer used.
248 future = {}
249
Joe Gregorio4772f3d2012-12-10 10:22:37 -0500250 if isinstance(service, basestring):
251 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400252 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500253 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400254
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500255 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500256 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500257 model = JsonModel('dataWrapper' in features)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500258 return Resource(http=http, baseUrl=base, model=model,
259 developerKey=developerKey, requestBuilder=requestBuilder,
260 resourceDesc=service, rootDesc=service, schema=schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400261
262
Joe Gregorio61d7e962011-02-22 22:52:07 -0500263def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500264 """Convert value to a string based on JSON Schema type.
265
266 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
267 JSON Schema.
268
269 Args:
270 value: any, the value to convert
271 schema_type: string, the type that value should be interpreted as
272
273 Returns:
274 A string representation of 'value' based on the schema_type.
275 """
276 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500277 if type(value) == type('') or type(value) == type(u''):
278 return value
279 else:
280 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500281 elif schema_type == 'integer':
282 return str(int(value))
283 elif schema_type == 'number':
284 return str(float(value))
285 elif schema_type == 'boolean':
286 return str(bool(value)).lower()
287 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500288 if type(value) == type('') or type(value) == type(u''):
289 return value
290 else:
291 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500292
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400293
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400294MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400295 "KB": 2 ** 10,
296 "MB": 2 ** 20,
297 "GB": 2 ** 30,
298 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400299 }
300
Joe Gregorioa98733f2011-09-16 10:12:28 -0400301
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400302def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400303 """Convert a string media size, such as 10GB or 3TB into an integer.
304
305 Args:
306 maxSize: string, size as a string, such as 2MB or 7GB.
307
308 Returns:
309 The size as an integer value.
310 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400311 if len(maxSize) < 2:
312 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400313 units = maxSize[-2:].upper()
314 multiplier = MULTIPLIERS.get(units, 0)
315 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400316 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400317 else:
318 return int(maxSize)
319
Joe Gregoriobee86832011-02-22 10:00:19 -0500320
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500321def createMethod(methodName, methodDesc, rootDesc, schema):
322 """Creates a method for attaching to a Resource.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400323
324 Args:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500325 methodName: string, name of the method to use.
326 methodDesc: object, fragment of deserialized discovery document that
327 describes the method.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400328 rootDesc: object, the entire deserialized discovery document.
329 schema: object, mapping of schema names to schema descriptions.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400330 """
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500331 methodName = fix_method_name(methodName)
332 pathUrl = methodDesc['path']
333 httpMethod = methodDesc['httpMethod']
334 methodId = methodDesc['id']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400335
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500336 mediaPathUrl = None
337 accept = []
338 maxSize = 0
339 if 'mediaUpload' in methodDesc:
340 mediaUpload = methodDesc['mediaUpload']
341 mediaPathUrl = (rootDesc['rootUrl'] + 'upload/' + rootDesc['servicePath']
342 + pathUrl)
343 accept = mediaUpload['accept']
344 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400345
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500346 if 'parameters' not in methodDesc:
347 methodDesc['parameters'] = {}
Joe Gregorio48d361f2010-08-18 13:19:21 -0400348
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500349 # Add in the parameters common to all methods.
350 for name, desc in rootDesc.get('parameters', {}).iteritems():
351 methodDesc['parameters'][name] = desc
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400352
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500353 # Add in undocumented query parameters.
354 for name in STACK_QUERY_PARAMETERS:
355 methodDesc['parameters'][name] = {
356 'type': 'string',
357 'location': 'query'
358 }
Joe Gregorio21f11672010-08-18 17:23:17 -0400359
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500360 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
361 methodDesc['parameters']['body'] = {
362 'description': 'The request body.',
363 'type': 'object',
364 'required': True,
365 }
366 if 'request' in methodDesc:
367 methodDesc['parameters']['body'].update(methodDesc['request'])
368 else:
369 methodDesc['parameters']['body']['type'] = 'object'
370 if 'mediaUpload' in methodDesc:
371 methodDesc['parameters']['media_body'] = {
372 'description':
373 'The filename of the media request body, or an instance of a '
374 'MediaUpload object.',
375 'type': 'string',
376 'required': False,
377 }
378 if 'body' in methodDesc['parameters']:
379 methodDesc['parameters']['body']['required'] = False
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400380
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500381 argmap = {} # Map from method parameter name to query parameter name
382 required_params = [] # Required parameters
383 repeated_params = [] # Repeated parameters
384 pattern_params = {} # Parameters that must match a regex
385 query_params = [] # Parameters that will be used in the query string
386 path_params = {} # Parameters that will be used in the base URL
387 param_type = {} # The type of the parameter
388 enum_params = {} # Allowable enumeration values for each parameter
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400389
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500390 if 'parameters' in methodDesc:
391 for arg, desc in methodDesc['parameters'].iteritems():
392 param = key2param(arg)
393 argmap[param] = arg
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400394
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500395 if desc.get('pattern', ''):
396 pattern_params[param] = desc['pattern']
397 if desc.get('enum', ''):
398 enum_params[param] = desc['enum']
399 if desc.get('required', False):
400 required_params.append(param)
401 if desc.get('repeated', False):
402 repeated_params.append(param)
403 if desc.get('location') == 'query':
404 query_params.append(param)
405 if desc.get('location') == 'path':
406 path_params[param] = param
407 param_type[param] = desc.get('type', 'string')
Joe Gregorioca876e42011-02-22 19:39:42 -0500408
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500409 for match in URITEMPLATE.finditer(pathUrl):
410 for namematch in VARNAME.finditer(match.group(0)):
411 name = key2param(namematch.group(0))
412 path_params[name] = name
413 if name in query_params:
414 query_params.remove(name)
ade@google.com850cf552010-08-20 23:24:56 +0100415
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500416 def method(self, **kwargs):
417 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorioca876e42011-02-22 19:39:42 -0500418
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500419 for name in kwargs.iterkeys():
420 if name not in argmap:
421 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorioca876e42011-02-22 19:39:42 -0500422
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500423 # Remove args that have a value of None.
424 keys = kwargs.keys()
425 for name in keys:
426 if kwargs[name] is None:
427 del kwargs[name]
Joe Gregorio21f11672010-08-18 17:23:17 -0400428
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500429 for name in required_params:
430 if name not in kwargs:
431 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400432
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500433 for name, regex in pattern_params.iteritems():
434 if name in kwargs:
435 if isinstance(kwargs[name], basestring):
436 pvalues = [kwargs[name]]
Joe Gregorio61d7e962011-02-22 22:52:07 -0500437 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500438 pvalues = kwargs[name]
439 for pvalue in pvalues:
440 if re.match(regex, pvalue) is None:
441 raise TypeError(
442 'Parameter "%s" value "%s" does not match the pattern "%s"' %
443 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400444
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500445 for name, enums in enum_params.iteritems():
446 if name in kwargs:
447 # We need to handle the case of a repeated enum
448 # name differently, since we want to handle both
449 # arg='value' and arg=['value1', 'value2']
450 if (name in repeated_params and
451 not isinstance(kwargs[name], basestring)):
452 values = kwargs[name]
453 else:
454 values = [kwargs[name]]
455 for value in values:
456 if value not in enums:
457 raise TypeError(
458 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
459 (name, value, str(enums)))
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400460
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500461 actual_query_params = {}
462 actual_path_params = {}
463 for key, value in kwargs.iteritems():
464 to_type = param_type.get(key, 'string')
465 # For repeated parameters we cast each member of the list.
466 if key in repeated_params and type(value) == type([]):
467 cast_value = [_cast(x, to_type) for x in value]
468 else:
469 cast_value = _cast(value, to_type)
470 if key in query_params:
471 actual_query_params[argmap[key]] = cast_value
472 if key in path_params:
473 actual_path_params[argmap[key]] = cast_value
474 body_value = kwargs.get('body', None)
475 media_filename = kwargs.get('media_body', None)
Joe Gregorioe08a1662011-12-07 09:48:22 -0500476
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500477 if self._developerKey:
478 actual_query_params['key'] = self._developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400479
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500480 model = self._model
481 if methodName.endswith('_media'):
482 model = MediaModel()
483 elif 'response' not in methodDesc:
484 model = RawModel()
485
486 headers = {}
487 headers, params, query, body = model.request(headers,
488 actual_path_params, actual_query_params, body_value)
489
490 expanded_url = uritemplate.expand(pathUrl, params)
491 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
492
493 resumable = None
494 multipart_boundary = ''
495
496 if media_filename:
497 # Ensure we end up with a valid MediaUpload object.
498 if isinstance(media_filename, basestring):
499 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
500 if media_mime_type is None:
501 raise UnknownFileType(media_filename)
502 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
503 raise UnacceptableMimeTypeError(media_mime_type)
504 media_upload = MediaFileUpload(media_filename,
505 mimetype=media_mime_type)
506 elif isinstance(media_filename, MediaUpload):
507 media_upload = media_filename
508 else:
509 raise TypeError('media_filename must be str or MediaUpload.')
510
511 # Check the maxSize
512 if maxSize > 0 and media_upload.size() > maxSize:
513 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
514
515 # Use the media path uri for media uploads
516 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400517 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500518 if media_upload.resumable():
519 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400520
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500521 if media_upload.resumable():
522 # This is all we need to do for resumable, if the body exists it gets
523 # sent in the first request, otherwise an empty body is sent.
524 resumable = media_upload
Joe Gregorio2b781282011-12-08 12:00:25 -0500525 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500526 # A non-resumable upload
527 if body is None:
528 # This is a simple media upload
529 headers['content-type'] = media_upload.mimetype()
530 body = media_upload.getbytes(0, media_upload.size())
531 url = _add_query_parameter(url, 'uploadType', 'media')
532 else:
533 # This is a multipart/related upload.
534 msgRoot = MIMEMultipart('related')
535 # msgRoot should not write out it's own headers
536 setattr(msgRoot, '_write_headers', lambda self: None)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400537
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500538 # attach the body as one part
539 msg = MIMENonMultipart(*headers['content-type'].split('/'))
540 msg.set_payload(body)
541 msgRoot.attach(msg)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400542
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500543 # attach the media as the second part
544 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
545 msg['Content-Transfer-Encoding'] = 'binary'
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400546
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500547 payload = media_upload.getbytes(0, media_upload.size())
548 msg.set_payload(payload)
549 msgRoot.attach(msg)
550 body = msgRoot.as_string()
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400551
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500552 multipart_boundary = msgRoot.get_boundary()
553 headers['content-type'] = ('multipart/related; '
554 'boundary="%s"') % multipart_boundary
555 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400556
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500557 logger.info('URL being requested: %s' % url)
558 return self._requestBuilder(self._http,
559 model.response,
560 url,
561 method=httpMethod,
562 body=body,
563 headers=headers,
564 methodId=methodId,
565 resumable=resumable)
566
567 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
568 if len(argmap) > 0:
569 docs.append('Args:\n')
570
571 # Skip undocumented params and params common to all methods.
572 skip_parameters = rootDesc.get('parameters', {}).keys()
573 skip_parameters.extend(STACK_QUERY_PARAMETERS)
574
575 all_args = argmap.keys()
576 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
577
578 # Move body to the front of the line.
579 if 'body' in all_args:
580 args_ordered.append('body')
581
582 for name in all_args:
583 if name not in args_ordered:
584 args_ordered.append(name)
585
586 for arg in args_ordered:
587 if arg in skip_parameters:
588 continue
589
590 repeated = ''
591 if arg in repeated_params:
592 repeated = ' (repeated)'
593 required = ''
594 if arg in required_params:
595 required = ' (required)'
596 paramdesc = methodDesc['parameters'][argmap[arg]]
597 paramdoc = paramdesc.get('description', 'A parameter')
598 if '$ref' in paramdesc:
599 docs.append(
600 (' %s: object, %s%s%s\n The object takes the'
601 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
602 schema.prettyPrintByName(paramdesc['$ref'])))
603 else:
604 paramtype = paramdesc.get('type', 'string')
605 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
606 repeated))
607 enum = paramdesc.get('enum', [])
608 enumDesc = paramdesc.get('enumDescriptions', [])
609 if enum and enumDesc:
610 docs.append(' Allowed values\n')
611 for (name, desc) in zip(enum, enumDesc):
612 docs.append(' %s - %s\n' % (name, desc))
613 if 'response' in methodDesc:
614 if methodName.endswith('_media'):
615 docs.append('\nReturns:\n The media object as a string.\n\n ')
616 else:
617 docs.append('\nReturns:\n An object of the form:\n\n ')
618 docs.append(schema.prettyPrintSchema(methodDesc['response']))
619
620 setattr(method, '__doc__', ''.join(docs))
621 return (methodName, method)
622
623
624def createNextMethod(methodName):
625 """Creates any _next methods for attaching to a Resource.
626
627 The _next methods allow for easy iteration through list() responses.
628
629 Args:
630 methodName: string, name of the method to use.
631 """
632 methodName = fix_method_name(methodName)
633
634 def methodNext(self, previous_request, previous_response):
635 """Retrieves the next page of results.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400636
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400637Args:
638 previous_request: The request for the previous page. (required)
639 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400640
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400641Returns:
642 A request object that you can call 'execute()' on to request the next
643 page. Returns None if there are no more items in the collection.
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500644 """
645 # Retrieve nextPageToken from previous_response
646 # Use as pageToken in previous_request to create new request.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400647
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500648 if 'nextPageToken' not in previous_response:
649 return None
Joe Gregorio3c676f92011-07-25 10:38:14 -0400650
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500651 request = copy.copy(previous_request)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400652
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500653 pageToken = previous_response['nextPageToken']
654 parsed = list(urlparse.urlparse(request.uri))
655 q = parse_qsl(parsed[4])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400656
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500657 # Find and remove old 'pageToken' value from URI
658 newq = [(key, value) for (key, value) in q if key != 'pageToken']
659 newq.append(('pageToken', pageToken))
660 parsed[4] = urllib.urlencode(newq)
661 uri = urlparse.urlunparse(parsed)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400662
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500663 request.uri = uri
Joe Gregorio3c676f92011-07-25 10:38:14 -0400664
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500665 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400666
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500667 return request
Joe Gregorio3c676f92011-07-25 10:38:14 -0400668
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500669 return (methodName, methodNext)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400670
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400671
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500672class Resource(object):
673 """A class for interacting with a resource."""
Joe Gregorioaf276d22010-12-09 14:26:58 -0500674
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500675 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
676 resourceDesc, rootDesc, schema):
677 """Build a Resource from the API description.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400678
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500679 Args:
680 http: httplib2.Http, Object to make http requests with.
681 baseUrl: string, base URL for the API. All requests are relative to this
682 URI.
683 model: apiclient.Model, converts to and from the wire format.
684 requestBuilder: class or callable that instantiates an
685 apiclient.HttpRequest object.
686 developerKey: string, key obtained from
687 https://code.google.com/apis/console
688 resourceDesc: object, section of deserialized discovery document that
689 describes a resource. Note that the top level discovery document
690 is considered a resource.
691 rootDesc: object, the entire deserialized discovery document.
692 schema: object, mapping of schema names to schema descriptions.
693 """
694 self._dynamic_attrs = []
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400695
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500696 self._http = http
697 self._baseUrl = baseUrl
698 self._model = model
699 self._developerKey = developerKey
700 self._requestBuilder = requestBuilder
701 self._resourceDesc = resourceDesc
702 self._rootDesc = rootDesc
703 self._schema = schema
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400704
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500705 self._set_service_methods()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400706
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500707 def _set_dynamic_attr(self, attr_name, value):
708 """Sets an instance attribute and tracks it in a list of dynamic attributes.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400709
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500710 Args:
711 attr_name: string; The name of the attribute to be set
712 value: The value being set on the object and tracked in the dynamic cache.
713 """
714 self._dynamic_attrs.append(attr_name)
715 self.__dict__[attr_name] = value
Joe Gregorio48d361f2010-08-18 13:19:21 -0400716
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500717 def __getstate__(self):
718 """Trim the state down to something that can be pickled.
719
720 Uses the fact that the instance variable _dynamic_attrs holds attrs that
721 will be wiped and restored on pickle serialization.
722 """
723 state_dict = copy.copy(self.__dict__)
724 for dynamic_attr in self._dynamic_attrs:
725 del state_dict[dynamic_attr]
726 del state_dict['_dynamic_attrs']
727 return state_dict
728
729 def __setstate__(self, state):
730 """Reconstitute the state of the object from being pickled.
731
732 Uses the fact that the instance variable _dynamic_attrs holds attrs that
733 will be wiped and restored on pickle serialization.
734 """
735 self.__dict__.update(state)
736 self._dynamic_attrs = []
737 self._set_service_methods()
738
739 def _set_service_methods(self):
740 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
741 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
742 self._add_next_methods(self._resourceDesc, self._schema)
743
744 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
745 # Add basic methods to Resource
746 if 'methods' in resourceDesc:
747 for methodName, methodDesc in resourceDesc['methods'].iteritems():
748 fixedMethodName, method = createMethod(
749 methodName, methodDesc, rootDesc, schema)
750 self._set_dynamic_attr(fixedMethodName,
751 method.__get__(self, self.__class__))
752 # Add in _media methods. The functionality of the attached method will
753 # change when it sees that the method name ends in _media.
754 if methodDesc.get('supportsMediaDownload', False):
755 fixedMethodName, method = createMethod(
756 methodName + '_media', methodDesc, rootDesc, schema)
757 self._set_dynamic_attr(fixedMethodName,
758 method.__get__(self, self.__class__))
759
760 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
761 # Add in nested resources
762 if 'resources' in resourceDesc:
763
764 def createResourceMethod(methodName, methodDesc):
765 """Create a method on the Resource to access a nested Resource.
766
767 Args:
768 methodName: string, name of the method to use.
769 methodDesc: object, fragment of deserialized discovery document that
770 describes the method.
771 """
772 methodName = fix_method_name(methodName)
773
774 def methodResource(self):
775 return Resource(http=self._http, baseUrl=self._baseUrl,
776 model=self._model, developerKey=self._developerKey,
777 requestBuilder=self._requestBuilder,
778 resourceDesc=methodDesc, rootDesc=rootDesc,
779 schema=schema)
780
781 setattr(methodResource, '__doc__', 'A collection resource.')
782 setattr(methodResource, '__is_resource__', True)
783
784 return (methodName, methodResource)
785
786 for methodName, methodDesc in resourceDesc['resources'].iteritems():
787 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
788 self._set_dynamic_attr(fixedMethodName,
789 method.__get__(self, self.__class__))
790
791 def _add_next_methods(self, resourceDesc, schema):
792 # Add _next() methods
793 # Look for response bodies in schema that contain nextPageToken, and methods
794 # that take a pageToken parameter.
795 if 'methods' in resourceDesc:
796 for methodName, methodDesc in resourceDesc['methods'].iteritems():
797 if 'response' in methodDesc:
798 responseSchema = methodDesc['response']
799 if '$ref' in responseSchema:
800 responseSchema = schema.get(responseSchema['$ref'])
801 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
802 {})
803 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
804 if hasNextPageToken and hasPageToken:
805 fixedMethodName, method = createNextMethod(methodName + '_next')
806 self._set_dynamic_attr(fixedMethodName,
807 method.__get__(self, self.__class__))