blob: eb179ffe9d82010d1f36814df1f3fa4bab2eb413 [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 Gregorioc8e421c2012-06-06 14:03:13 -0400228 service: string, discovery document.
229 base: string, base URI for all HTTP requests, usually the discovery URI.
Joe Gregorioa2838152012-07-16 11:52:17 -0400230 This parameter is no longer used as rootUrl and servicePath are included
231 within the discovery document. (deprecated)
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400232 future: string, discovery document with future capabilities (deprecated).
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500233 http: httplib2.Http, An instance of httplib2.Http or something that acts
234 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500235 developerKey: string, Key for controlling API usage, generated
236 from the API Console.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400237 model: Model class instance that serializes and de-serializes requests and
238 responses.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500239 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500240
241 Returns:
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400242 A Resource object with methods for interacting with the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500243 """
244
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400245 # future is no longer used.
246 future = {}
247
Joe Gregorio292b9b82011-01-12 11:36:11 -0500248 service = simplejson.loads(service)
Joe Gregorioa2838152012-07-16 11:52:17 -0400249 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
Joe Gregorio2b781282011-12-08 12:00:25 -0500250 schema = Schemas(service)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400251
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500252 if model is None:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500253 features = service.get('features', [])
Joe Gregorio266c6442011-02-23 16:08:54 -0500254 model = JsonModel('dataWrapper' in features)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500255 return Resource(http=http, baseUrl=base, model=model,
256 developerKey=developerKey, requestBuilder=requestBuilder,
257 resourceDesc=service, rootDesc=service, schema=schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400258
259
Joe Gregorio61d7e962011-02-22 22:52:07 -0500260def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500261 """Convert value to a string based on JSON Schema type.
262
263 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
264 JSON Schema.
265
266 Args:
267 value: any, the value to convert
268 schema_type: string, the type that value should be interpreted as
269
270 Returns:
271 A string representation of 'value' based on the schema_type.
272 """
273 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500274 if type(value) == type('') or type(value) == type(u''):
275 return value
276 else:
277 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500278 elif schema_type == 'integer':
279 return str(int(value))
280 elif schema_type == 'number':
281 return str(float(value))
282 elif schema_type == 'boolean':
283 return str(bool(value)).lower()
284 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500285 if type(value) == type('') or type(value) == type(u''):
286 return value
287 else:
288 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500289
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400290
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400291MULTIPLIERS = {
Joe Gregorio562b7312011-09-15 09:06:38 -0400292 "KB": 2 ** 10,
293 "MB": 2 ** 20,
294 "GB": 2 ** 30,
295 "TB": 2 ** 40,
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400296 }
297
Joe Gregorioa98733f2011-09-16 10:12:28 -0400298
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400299def _media_size_to_long(maxSize):
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400300 """Convert a string media size, such as 10GB or 3TB into an integer.
301
302 Args:
303 maxSize: string, size as a string, such as 2MB or 7GB.
304
305 Returns:
306 The size as an integer value.
307 """
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400308 if len(maxSize) < 2:
309 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400310 units = maxSize[-2:].upper()
311 multiplier = MULTIPLIERS.get(units, 0)
312 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400313 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400314 else:
315 return int(maxSize)
316
Joe Gregoriobee86832011-02-22 10:00:19 -0500317
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500318def createMethod(methodName, methodDesc, rootDesc, schema):
319 """Creates a method for attaching to a Resource.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400320
321 Args:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500322 methodName: string, name of the method to use.
323 methodDesc: object, fragment of deserialized discovery document that
324 describes the method.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400325 rootDesc: object, the entire deserialized discovery document.
326 schema: object, mapping of schema names to schema descriptions.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400327 """
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500328 methodName = fix_method_name(methodName)
329 pathUrl = methodDesc['path']
330 httpMethod = methodDesc['httpMethod']
331 methodId = methodDesc['id']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400332
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500333 mediaPathUrl = None
334 accept = []
335 maxSize = 0
336 if 'mediaUpload' in methodDesc:
337 mediaUpload = methodDesc['mediaUpload']
338 mediaPathUrl = (rootDesc['rootUrl'] + 'upload/' + rootDesc['servicePath']
339 + pathUrl)
340 accept = mediaUpload['accept']
341 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400342
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500343 if 'parameters' not in methodDesc:
344 methodDesc['parameters'] = {}
Joe Gregorio48d361f2010-08-18 13:19:21 -0400345
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500346 # Add in the parameters common to all methods.
347 for name, desc in rootDesc.get('parameters', {}).iteritems():
348 methodDesc['parameters'][name] = desc
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400349
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500350 # Add in undocumented query parameters.
351 for name in STACK_QUERY_PARAMETERS:
352 methodDesc['parameters'][name] = {
353 'type': 'string',
354 'location': 'query'
355 }
Joe Gregorio21f11672010-08-18 17:23:17 -0400356
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500357 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
358 methodDesc['parameters']['body'] = {
359 'description': 'The request body.',
360 'type': 'object',
361 'required': True,
362 }
363 if 'request' in methodDesc:
364 methodDesc['parameters']['body'].update(methodDesc['request'])
365 else:
366 methodDesc['parameters']['body']['type'] = 'object'
367 if 'mediaUpload' in methodDesc:
368 methodDesc['parameters']['media_body'] = {
369 'description':
370 'The filename of the media request body, or an instance of a '
371 'MediaUpload object.',
372 'type': 'string',
373 'required': False,
374 }
375 if 'body' in methodDesc['parameters']:
376 methodDesc['parameters']['body']['required'] = False
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400377
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500378 argmap = {} # Map from method parameter name to query parameter name
379 required_params = [] # Required parameters
380 repeated_params = [] # Repeated parameters
381 pattern_params = {} # Parameters that must match a regex
382 query_params = [] # Parameters that will be used in the query string
383 path_params = {} # Parameters that will be used in the base URL
384 param_type = {} # The type of the parameter
385 enum_params = {} # Allowable enumeration values for each parameter
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400386
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500387 if 'parameters' in methodDesc:
388 for arg, desc in methodDesc['parameters'].iteritems():
389 param = key2param(arg)
390 argmap[param] = arg
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400391
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500392 if desc.get('pattern', ''):
393 pattern_params[param] = desc['pattern']
394 if desc.get('enum', ''):
395 enum_params[param] = desc['enum']
396 if desc.get('required', False):
397 required_params.append(param)
398 if desc.get('repeated', False):
399 repeated_params.append(param)
400 if desc.get('location') == 'query':
401 query_params.append(param)
402 if desc.get('location') == 'path':
403 path_params[param] = param
404 param_type[param] = desc.get('type', 'string')
Joe Gregorioca876e42011-02-22 19:39:42 -0500405
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500406 for match in URITEMPLATE.finditer(pathUrl):
407 for namematch in VARNAME.finditer(match.group(0)):
408 name = key2param(namematch.group(0))
409 path_params[name] = name
410 if name in query_params:
411 query_params.remove(name)
ade@google.com850cf552010-08-20 23:24:56 +0100412
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500413 def method(self, **kwargs):
414 # Don't bother with doc string, it will be over-written by createMethod.
Joe Gregorioca876e42011-02-22 19:39:42 -0500415
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500416 for name in kwargs.iterkeys():
417 if name not in argmap:
418 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorioca876e42011-02-22 19:39:42 -0500419
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500420 # Remove args that have a value of None.
421 keys = kwargs.keys()
422 for name in keys:
423 if kwargs[name] is None:
424 del kwargs[name]
Joe Gregorio21f11672010-08-18 17:23:17 -0400425
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500426 for name in required_params:
427 if name not in kwargs:
428 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400429
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500430 for name, regex in pattern_params.iteritems():
431 if name in kwargs:
432 if isinstance(kwargs[name], basestring):
433 pvalues = [kwargs[name]]
Joe Gregorio61d7e962011-02-22 22:52:07 -0500434 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500435 pvalues = kwargs[name]
436 for pvalue in pvalues:
437 if re.match(regex, pvalue) is None:
438 raise TypeError(
439 'Parameter "%s" value "%s" does not match the pattern "%s"' %
440 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400441
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500442 for name, enums in enum_params.iteritems():
443 if name in kwargs:
444 # We need to handle the case of a repeated enum
445 # name differently, since we want to handle both
446 # arg='value' and arg=['value1', 'value2']
447 if (name in repeated_params and
448 not isinstance(kwargs[name], basestring)):
449 values = kwargs[name]
450 else:
451 values = [kwargs[name]]
452 for value in values:
453 if value not in enums:
454 raise TypeError(
455 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
456 (name, value, str(enums)))
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400457
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500458 actual_query_params = {}
459 actual_path_params = {}
460 for key, value in kwargs.iteritems():
461 to_type = param_type.get(key, 'string')
462 # For repeated parameters we cast each member of the list.
463 if key in repeated_params and type(value) == type([]):
464 cast_value = [_cast(x, to_type) for x in value]
465 else:
466 cast_value = _cast(value, to_type)
467 if key in query_params:
468 actual_query_params[argmap[key]] = cast_value
469 if key in path_params:
470 actual_path_params[argmap[key]] = cast_value
471 body_value = kwargs.get('body', None)
472 media_filename = kwargs.get('media_body', None)
Joe Gregorioe08a1662011-12-07 09:48:22 -0500473
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500474 if self._developerKey:
475 actual_query_params['key'] = self._developerKey
Joe Gregorio48d361f2010-08-18 13:19:21 -0400476
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500477 model = self._model
478 if methodName.endswith('_media'):
479 model = MediaModel()
480 elif 'response' not in methodDesc:
481 model = RawModel()
482
483 headers = {}
484 headers, params, query, body = model.request(headers,
485 actual_path_params, actual_query_params, body_value)
486
487 expanded_url = uritemplate.expand(pathUrl, params)
488 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
489
490 resumable = None
491 multipart_boundary = ''
492
493 if media_filename:
494 # Ensure we end up with a valid MediaUpload object.
495 if isinstance(media_filename, basestring):
496 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
497 if media_mime_type is None:
498 raise UnknownFileType(media_filename)
499 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
500 raise UnacceptableMimeTypeError(media_mime_type)
501 media_upload = MediaFileUpload(media_filename,
502 mimetype=media_mime_type)
503 elif isinstance(media_filename, MediaUpload):
504 media_upload = media_filename
505 else:
506 raise TypeError('media_filename must be str or MediaUpload.')
507
508 # Check the maxSize
509 if maxSize > 0 and media_upload.size() > maxSize:
510 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
511
512 # Use the media path uri for media uploads
513 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400514 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500515 if media_upload.resumable():
516 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400517
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500518 if media_upload.resumable():
519 # This is all we need to do for resumable, if the body exists it gets
520 # sent in the first request, otherwise an empty body is sent.
521 resumable = media_upload
Joe Gregorio2b781282011-12-08 12:00:25 -0500522 else:
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500523 # A non-resumable upload
524 if body is None:
525 # This is a simple media upload
526 headers['content-type'] = media_upload.mimetype()
527 body = media_upload.getbytes(0, media_upload.size())
528 url = _add_query_parameter(url, 'uploadType', 'media')
529 else:
530 # This is a multipart/related upload.
531 msgRoot = MIMEMultipart('related')
532 # msgRoot should not write out it's own headers
533 setattr(msgRoot, '_write_headers', lambda self: None)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400534
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500535 # attach the body as one part
536 msg = MIMENonMultipart(*headers['content-type'].split('/'))
537 msg.set_payload(body)
538 msgRoot.attach(msg)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400539
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500540 # attach the media as the second part
541 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
542 msg['Content-Transfer-Encoding'] = 'binary'
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400543
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500544 payload = media_upload.getbytes(0, media_upload.size())
545 msg.set_payload(payload)
546 msgRoot.attach(msg)
547 body = msgRoot.as_string()
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400548
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500549 multipart_boundary = msgRoot.get_boundary()
550 headers['content-type'] = ('multipart/related; '
551 'boundary="%s"') % multipart_boundary
552 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400553
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500554 logger.info('URL being requested: %s' % url)
555 return self._requestBuilder(self._http,
556 model.response,
557 url,
558 method=httpMethod,
559 body=body,
560 headers=headers,
561 methodId=methodId,
562 resumable=resumable)
563
564 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
565 if len(argmap) > 0:
566 docs.append('Args:\n')
567
568 # Skip undocumented params and params common to all methods.
569 skip_parameters = rootDesc.get('parameters', {}).keys()
570 skip_parameters.extend(STACK_QUERY_PARAMETERS)
571
572 all_args = argmap.keys()
573 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
574
575 # Move body to the front of the line.
576 if 'body' in all_args:
577 args_ordered.append('body')
578
579 for name in all_args:
580 if name not in args_ordered:
581 args_ordered.append(name)
582
583 for arg in args_ordered:
584 if arg in skip_parameters:
585 continue
586
587 repeated = ''
588 if arg in repeated_params:
589 repeated = ' (repeated)'
590 required = ''
591 if arg in required_params:
592 required = ' (required)'
593 paramdesc = methodDesc['parameters'][argmap[arg]]
594 paramdoc = paramdesc.get('description', 'A parameter')
595 if '$ref' in paramdesc:
596 docs.append(
597 (' %s: object, %s%s%s\n The object takes the'
598 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
599 schema.prettyPrintByName(paramdesc['$ref'])))
600 else:
601 paramtype = paramdesc.get('type', 'string')
602 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
603 repeated))
604 enum = paramdesc.get('enum', [])
605 enumDesc = paramdesc.get('enumDescriptions', [])
606 if enum and enumDesc:
607 docs.append(' Allowed values\n')
608 for (name, desc) in zip(enum, enumDesc):
609 docs.append(' %s - %s\n' % (name, desc))
610 if 'response' in methodDesc:
611 if methodName.endswith('_media'):
612 docs.append('\nReturns:\n The media object as a string.\n\n ')
613 else:
614 docs.append('\nReturns:\n An object of the form:\n\n ')
615 docs.append(schema.prettyPrintSchema(methodDesc['response']))
616
617 setattr(method, '__doc__', ''.join(docs))
618 return (methodName, method)
619
620
621def createNextMethod(methodName):
622 """Creates any _next methods for attaching to a Resource.
623
624 The _next methods allow for easy iteration through list() responses.
625
626 Args:
627 methodName: string, name of the method to use.
628 """
629 methodName = fix_method_name(methodName)
630
631 def methodNext(self, previous_request, previous_response):
632 """Retrieves the next page of results.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400633
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400634Args:
635 previous_request: The request for the previous page. (required)
636 previous_response: The response from the request for the previous page. (required)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400637
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400638Returns:
639 A request object that you can call 'execute()' on to request the next
640 page. Returns None if there are no more items in the collection.
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500641 """
642 # Retrieve nextPageToken from previous_response
643 # Use as pageToken in previous_request to create new request.
Joe Gregorio3c676f92011-07-25 10:38:14 -0400644
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500645 if 'nextPageToken' not in previous_response:
646 return None
Joe Gregorio3c676f92011-07-25 10:38:14 -0400647
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500648 request = copy.copy(previous_request)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400649
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500650 pageToken = previous_response['nextPageToken']
651 parsed = list(urlparse.urlparse(request.uri))
652 q = parse_qsl(parsed[4])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400653
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500654 # Find and remove old 'pageToken' value from URI
655 newq = [(key, value) for (key, value) in q if key != 'pageToken']
656 newq.append(('pageToken', pageToken))
657 parsed[4] = urllib.urlencode(newq)
658 uri = urlparse.urlunparse(parsed)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400659
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500660 request.uri = uri
Joe Gregorio3c676f92011-07-25 10:38:14 -0400661
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500662 logger.info('URL being requested: %s' % uri)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400663
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500664 return request
Joe Gregorio3c676f92011-07-25 10:38:14 -0400665
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500666 return (methodName, methodNext)
Joe Gregorio3c676f92011-07-25 10:38:14 -0400667
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400668
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500669class Resource(object):
670 """A class for interacting with a resource."""
Joe Gregorioaf276d22010-12-09 14:26:58 -0500671
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500672 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
673 resourceDesc, rootDesc, schema):
674 """Build a Resource from the API description.
Joe Gregorioc8e421c2012-06-06 14:03:13 -0400675
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500676 Args:
677 http: httplib2.Http, Object to make http requests with.
678 baseUrl: string, base URL for the API. All requests are relative to this
679 URI.
680 model: apiclient.Model, converts to and from the wire format.
681 requestBuilder: class or callable that instantiates an
682 apiclient.HttpRequest object.
683 developerKey: string, key obtained from
684 https://code.google.com/apis/console
685 resourceDesc: object, section of deserialized discovery document that
686 describes a resource. Note that the top level discovery document
687 is considered a resource.
688 rootDesc: object, the entire deserialized discovery document.
689 schema: object, mapping of schema names to schema descriptions.
690 """
691 self._dynamic_attrs = []
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400692
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500693 self._http = http
694 self._baseUrl = baseUrl
695 self._model = model
696 self._developerKey = developerKey
697 self._requestBuilder = requestBuilder
698 self._resourceDesc = resourceDesc
699 self._rootDesc = rootDesc
700 self._schema = schema
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400701
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500702 self._set_service_methods()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400703
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500704 def _set_dynamic_attr(self, attr_name, value):
705 """Sets an instance attribute and tracks it in a list of dynamic attributes.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400706
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500707 Args:
708 attr_name: string; The name of the attribute to be set
709 value: The value being set on the object and tracked in the dynamic cache.
710 """
711 self._dynamic_attrs.append(attr_name)
712 self.__dict__[attr_name] = value
Joe Gregorio48d361f2010-08-18 13:19:21 -0400713
Joe Gregoriodc106fc2012-11-20 14:30:14 -0500714 def __getstate__(self):
715 """Trim the state down to something that can be pickled.
716
717 Uses the fact that the instance variable _dynamic_attrs holds attrs that
718 will be wiped and restored on pickle serialization.
719 """
720 state_dict = copy.copy(self.__dict__)
721 for dynamic_attr in self._dynamic_attrs:
722 del state_dict[dynamic_attr]
723 del state_dict['_dynamic_attrs']
724 return state_dict
725
726 def __setstate__(self, state):
727 """Reconstitute the state of the object from being pickled.
728
729 Uses the fact that the instance variable _dynamic_attrs holds attrs that
730 will be wiped and restored on pickle serialization.
731 """
732 self.__dict__.update(state)
733 self._dynamic_attrs = []
734 self._set_service_methods()
735
736 def _set_service_methods(self):
737 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
738 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
739 self._add_next_methods(self._resourceDesc, self._schema)
740
741 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
742 # Add basic methods to Resource
743 if 'methods' in resourceDesc:
744 for methodName, methodDesc in resourceDesc['methods'].iteritems():
745 fixedMethodName, method = createMethod(
746 methodName, methodDesc, rootDesc, schema)
747 self._set_dynamic_attr(fixedMethodName,
748 method.__get__(self, self.__class__))
749 # Add in _media methods. The functionality of the attached method will
750 # change when it sees that the method name ends in _media.
751 if methodDesc.get('supportsMediaDownload', False):
752 fixedMethodName, method = createMethod(
753 methodName + '_media', methodDesc, rootDesc, schema)
754 self._set_dynamic_attr(fixedMethodName,
755 method.__get__(self, self.__class__))
756
757 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
758 # Add in nested resources
759 if 'resources' in resourceDesc:
760
761 def createResourceMethod(methodName, methodDesc):
762 """Create a method on the Resource to access a nested Resource.
763
764 Args:
765 methodName: string, name of the method to use.
766 methodDesc: object, fragment of deserialized discovery document that
767 describes the method.
768 """
769 methodName = fix_method_name(methodName)
770
771 def methodResource(self):
772 return Resource(http=self._http, baseUrl=self._baseUrl,
773 model=self._model, developerKey=self._developerKey,
774 requestBuilder=self._requestBuilder,
775 resourceDesc=methodDesc, rootDesc=rootDesc,
776 schema=schema)
777
778 setattr(methodResource, '__doc__', 'A collection resource.')
779 setattr(methodResource, '__is_resource__', True)
780
781 return (methodName, methodResource)
782
783 for methodName, methodDesc in resourceDesc['resources'].iteritems():
784 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
785 self._set_dynamic_attr(fixedMethodName,
786 method.__get__(self, self.__class__))
787
788 def _add_next_methods(self, resourceDesc, schema):
789 # Add _next() methods
790 # Look for response bodies in schema that contain nextPageToken, and methods
791 # that take a pageToken parameter.
792 if 'methods' in resourceDesc:
793 for methodName, methodDesc in resourceDesc['methods'].iteritems():
794 if 'response' in methodDesc:
795 responseSchema = methodDesc['response']
796 if '$ref' in responseSchema:
797 responseSchema = schema.get(responseSchema['$ref'])
798 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
799 {})
800 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
801 if hasNextPageToken and hasPageToken:
802 fixedMethodName, method = createNextMethod(methodName + '_next')
803 self._set_dynamic_attr(fixedMethodName,
804 method.__get__(self, self.__class__))