blob: 0c374aef2edbd94a6d7cecb1fc5d8c6050c1f63f [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__ = [
22 'build', 'build_from_document'
23 ]
Joe Gregorio48d361f2010-08-18 13:19:21 -040024
Joe Gregorio3c676f92011-07-25 10:38:14 -040025import copy
Joe Gregorio48d361f2010-08-18 13:19:21 -040026import httplib2
ade@google.com850cf552010-08-20 23:24:56 +010027import logging
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040028import os
Joe Gregoriod0bd3882011-11-22 09:49:47 -050029import random
Joe Gregorio48d361f2010-08-18 13:19:21 -040030import re
Joe Gregorio48d361f2010-08-18 13:19:21 -040031import uritemplate
Joe Gregoriofe695fb2010-08-30 12:04:04 -040032import urllib
Joe Gregorio6d5e94f2010-08-25 23:49:30 -040033import urlparse
Joe Gregoriofdf7c802011-06-30 12:33:38 -040034import mimeparse
Joe Gregorio922b78c2011-05-26 21:36:34 -040035import mimetypes
36
ade@google.comc5eb46f2010-09-27 23:35:39 +010037try:
38 from urlparse import parse_qsl
39except ImportError:
40 from cgi import parse_qsl
Joe Gregorioaf276d22010-12-09 14:26:58 -050041
Joe Gregorio2b781282011-12-08 12:00:25 -050042from apiclient.errors import HttpError
43from apiclient.errors import InvalidJsonError
44from apiclient.errors import MediaUploadSizeError
45from apiclient.errors import UnacceptableMimeTypeError
46from apiclient.errors import UnknownApiNameOrVersion
47from apiclient.errors import UnknownLinkType
48from apiclient.http import HttpRequest
49from apiclient.http import MediaFileUpload
50from apiclient.http import MediaUpload
51from apiclient.model import JsonModel
52from apiclient.model import RawModel
53from apiclient.schema import Schemas
Joe Gregorio922b78c2011-05-26 21:36:34 -040054from email.mime.multipart import MIMEMultipart
55from email.mime.nonmultipart import MIMENonMultipart
Joe Gregorio549230c2012-01-11 10:38:05 -050056from oauth2client.anyjson import simplejson
Joe Gregorio2b781282011-12-08 12:00:25 -050057
Joe Gregorio48d361f2010-08-18 13:19:21 -040058
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -050059URITEMPLATE = re.compile('{[^}]*}')
60VARNAME = re.compile('[a-zA-Z0-9_-]+')
Joe Gregorio6a63a762011-05-02 22:36:05 -040061DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
62 '{api}/{apiVersion}/rest')
Joe Gregorioc3fae8a2011-02-18 14:19:50 -050063DEFAULT_METHOD_DOC = 'A description of how to use this function'
Joe Gregorioca876e42011-02-22 19:39:42 -050064
65# Query parameters that work, but don't appear in discovery
Joe Gregorio06d852b2011-03-25 15:03:10 -040066STACK_QUERY_PARAMETERS = ['trace', 'fields', 'pp', 'prettyPrint', 'userIp',
Joe Gregorio3eecaa92011-05-17 13:40:12 -040067 'userip', 'strict']
Joe Gregorio48d361f2010-08-18 13:19:21 -040068
Joe Gregorio562b7312011-09-15 09:06:38 -040069RESERVED_WORDS = ['and', 'assert', 'break', 'class', 'continue', 'def', 'del',
Joe Gregoriod92897c2011-07-07 11:44:56 -040070 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from',
71 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or',
72 'pass', 'print', 'raise', 'return', 'try', 'while' ]
73
Joe Gregorio562b7312011-09-15 09:06:38 -040074
Joe Gregoriod92897c2011-07-07 11:44:56 -040075def _fix_method_name(name):
76 if name in RESERVED_WORDS:
77 return name + '_'
78 else:
79 return name
Joe Gregorio48d361f2010-08-18 13:19:21 -040080
Joe Gregorioa98733f2011-09-16 10:12:28 -040081
Joe Gregorio922b78c2011-05-26 21:36:34 -040082def _write_headers(self):
83 # Utility no-op method for multipart media handling
84 pass
85
86
Joe Gregorioa98733f2011-09-16 10:12:28 -040087def _add_query_parameter(url, name, value):
Joe Gregoriode860442012-03-02 15:55:52 -050088 """Adds a query parameter to a url.
89
90 Replaces the current value if it already exists in the URL.
Joe Gregorioa98733f2011-09-16 10:12:28 -040091
92 Args:
93 url: string, url to add the query parameter to.
94 name: string, query parameter name.
95 value: string, query parameter value.
96
97 Returns:
98 Updated query parameter. Does not update the url if value is None.
99 """
100 if value is None:
101 return url
102 else:
103 parsed = list(urlparse.urlparse(url))
Joe Gregoriode860442012-03-02 15:55:52 -0500104 q = dict(parse_qsl(parsed[4]))
105 q[name] = value
Joe Gregorioa98733f2011-09-16 10:12:28 -0400106 parsed[4] = urllib.urlencode(q)
107 return urlparse.urlunparse(parsed)
108
109
Joe Gregorio48d361f2010-08-18 13:19:21 -0400110def key2param(key):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500111 """Converts key names into parameter names.
112
113 For example, converting "max-results" -> "max_results"
Joe Gregorio48d361f2010-08-18 13:19:21 -0400114 """
115 result = []
116 key = list(key)
117 if not key[0].isalpha():
118 result.append('x')
119 for c in key:
120 if c.isalnum():
121 result.append(c)
122 else:
123 result.append('_')
124
125 return ''.join(result)
126
127
Joe Gregorio01770a52012-02-24 11:11:10 -0500128def build(serviceName,
129 version,
Joe Gregorio3fada332011-01-07 17:07:45 -0500130 http=None,
131 discoveryServiceUrl=DISCOVERY_URI,
132 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500133 model=None,
Joe Gregorio3fada332011-01-07 17:07:45 -0500134 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500135 """Construct a Resource for interacting with an API.
136
137 Construct a Resource object for interacting with
138 an API. The serviceName and version are the
139 names from the Discovery service.
140
141 Args:
142 serviceName: string, name of the service
143 version: string, the version of the service
Joe Gregorio01770a52012-02-24 11:11:10 -0500144 http: httplib2.Http, An instance of httplib2.Http or something that acts
145 like it that HTTP requests will be made through.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500146 discoveryServiceUrl: string, a URI Template that points to
147 the location of the discovery service. It should have two
148 parameters {api} and {apiVersion} that when filled in
149 produce an absolute URI to the discovery document for
150 that service.
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500151 developerKey: string, key obtained
152 from https://code.google.com/apis/console
Joe Gregorioabda96f2011-02-11 20:19:33 -0500153 model: apiclient.Model, converts to and from the wire format
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500154 requestBuilder: apiclient.http.HttpRequest, encapsulator for
155 an HTTP request
Joe Gregorioabda96f2011-02-11 20:19:33 -0500156
157 Returns:
158 A Resource object with methods for interacting with
159 the service.
160 """
Joe Gregorio48d361f2010-08-18 13:19:21 -0400161 params = {
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400162 'api': serviceName,
Joe Gregorio48d361f2010-08-18 13:19:21 -0400163 'apiVersion': version
164 }
ade@google.com850cf552010-08-20 23:24:56 +0100165
Joe Gregorioc204b642010-09-21 12:01:23 -0400166 if http is None:
167 http = httplib2.Http()
Joe Gregorioa98733f2011-09-16 10:12:28 -0400168
ade@google.com850cf552010-08-20 23:24:56 +0100169 requested_url = uritemplate.expand(discoveryServiceUrl, params)
Joe Gregorio583d9e42011-09-16 15:54:15 -0400170
Joe Gregorio66f57522011-11-30 11:00:00 -0500171 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
172 # variable that contains the network address of the client sending the
173 # request. If it exists then add that to the request for the discovery
174 # document to avoid exceeding the quota on discovery requests.
Joe Gregorio583d9e42011-09-16 15:54:15 -0400175 if 'REMOTE_ADDR' in os.environ:
176 requested_url = _add_query_parameter(requested_url, 'userIp',
177 os.environ['REMOTE_ADDR'])
ade@google.com850cf552010-08-20 23:24:56 +0100178 logging.info('URL being requested: %s' % requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400179
ade@google.com850cf552010-08-20 23:24:56 +0100180 resp, content = http.request(requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400181
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500182 if resp.status == 404:
Joe Gregoriodae2f552011-11-21 08:16:56 -0500183 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
Joe Gregorio8b4df3f2011-11-18 15:44:48 -0500184 version))
Joe Gregorioa98733f2011-09-16 10:12:28 -0400185 if resp.status >= 400:
Joe Gregorio49396552011-03-08 10:39:00 -0500186 raise HttpError(resp, content, requested_url)
Joe Gregorioa98733f2011-09-16 10:12:28 -0400187
Joe Gregorioc0e0fe92011-03-04 16:16:55 -0500188 try:
189 service = simplejson.loads(content)
190 except ValueError, e:
Joe Gregorio205e73a2011-03-12 09:55:31 -0500191 logging.error('Failed to parse as JSON: ' + content)
Joe Gregorio49396552011-03-08 10:39:00 -0500192 raise InvalidJsonError()
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400193
Joe Gregorioa98733f2011-09-16 10:12:28 -0400194 filename = os.path.join(os.path.dirname(__file__), 'contrib',
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500195 serviceName, 'future.json')
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400196 try:
Joe Gregorioa98733f2011-09-16 10:12:28 -0400197 f = file(filename, 'r')
Joe Gregorio292b9b82011-01-12 11:36:11 -0500198 future = f.read()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400199 f.close()
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400200 except IOError:
Joe Gregorio292b9b82011-01-12 11:36:11 -0500201 future = None
202
203 return build_from_document(content, discoveryServiceUrl, future,
204 http, developerKey, model, requestBuilder)
205
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500206
Joe Gregorio292b9b82011-01-12 11:36:11 -0500207def build_from_document(
208 service,
209 base,
210 future=None,
211 http=None,
212 developerKey=None,
Joe Gregoriod433b2a2011-02-22 10:51:51 -0500213 model=None,
Joe Gregorio292b9b82011-01-12 11:36:11 -0500214 requestBuilder=HttpRequest):
Joe Gregorioabda96f2011-02-11 20:19:33 -0500215 """Create a Resource for interacting with an API.
216
217 Same as `build()`, but constructs the Resource object
218 from a discovery document that is it given, as opposed to
219 retrieving one over HTTP.
220
Joe Gregorio292b9b82011-01-12 11:36:11 -0500221 Args:
222 service: string, discovery document
223 base: string, base URI for all HTTP requests, usually the discovery URI
224 future: string, discovery document with future capabilities
225 auth_discovery: dict, information about the authentication the API supports
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500226 http: httplib2.Http, An instance of httplib2.Http or something that acts
227 like it that HTTP requests will be made through.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500228 developerKey: string, Key for controlling API usage, generated
229 from the API Console.
230 model: Model class instance that serializes and
231 de-serializes requests and responses.
232 requestBuilder: Takes an http request and packages it up to be executed.
Joe Gregorioabda96f2011-02-11 20:19:33 -0500233
234 Returns:
235 A Resource object with methods for interacting with
236 the service.
Joe Gregorio292b9b82011-01-12 11:36:11 -0500237 """
238
239 service = simplejson.loads(service)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400240 base = urlparse.urljoin(base, service['basePath'])
Joe Gregorio292b9b82011-01-12 11:36:11 -0500241 if future:
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500242 future = simplejson.loads(future)
243 auth_discovery = future.get('auth', {})
Joe Gregorio292b9b82011-01-12 11:36:11 -0500244 else:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400245 future = {}
246 auth_discovery = {}
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 Gregorio7a6df3a2011-01-31 21:55:21 -0500252 resource = createResource(http, base, model, requestBuilder, developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400253 service, future, schema)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400254
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500255 def auth_method():
256 """Discovery information about the authentication the API uses."""
257 return auth_discovery
Joe Gregorio48d361f2010-08-18 13:19:21 -0400258
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500259 setattr(resource, 'auth_discovery', auth_method)
Joe Gregorioa2f56e72010-09-09 15:15:56 -0400260
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500261 return resource
Joe Gregorio48d361f2010-08-18 13:19:21 -0400262
263
Joe Gregorio61d7e962011-02-22 22:52:07 -0500264def _cast(value, schema_type):
Joe Gregoriobee86832011-02-22 10:00:19 -0500265 """Convert value to a string based on JSON Schema type.
266
267 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
268 JSON Schema.
269
270 Args:
271 value: any, the value to convert
272 schema_type: string, the type that value should be interpreted as
273
274 Returns:
275 A string representation of 'value' based on the schema_type.
276 """
277 if schema_type == 'string':
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500278 if type(value) == type('') or type(value) == type(u''):
279 return value
280 else:
281 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500282 elif schema_type == 'integer':
283 return str(int(value))
284 elif schema_type == 'number':
285 return str(float(value))
286 elif schema_type == 'boolean':
287 return str(bool(value)).lower()
288 else:
Joe Gregoriof863f7a2011-02-24 03:24:44 -0500289 if type(value) == type('') or type(value) == type(u''):
290 return value
291 else:
292 return str(value)
Joe Gregoriobee86832011-02-22 10:00:19 -0500293
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):
303 """Convert a string media size, such as 10GB or 3TB into an integer."""
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400304 if len(maxSize) < 2:
305 return 0
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400306 units = maxSize[-2:].upper()
307 multiplier = MULTIPLIERS.get(units, 0)
308 if multiplier:
Joe Gregorio562b7312011-09-15 09:06:38 -0400309 return int(maxSize[:-2]) * multiplier
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400310 else:
311 return int(maxSize)
312
Joe Gregoriobee86832011-02-22 10:00:19 -0500313
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500314def createResource(http, baseUrl, model, requestBuilder,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400315 developerKey, resourceDesc, futureDesc, schema):
Joe Gregorio48d361f2010-08-18 13:19:21 -0400316
317 class Resource(object):
318 """A class for interacting with a resource."""
319
320 def __init__(self):
321 self._http = http
322 self._baseUrl = baseUrl
323 self._model = model
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400324 self._developerKey = developerKey
Joe Gregorioaf276d22010-12-09 14:26:58 -0500325 self._requestBuilder = requestBuilder
Joe Gregorio48d361f2010-08-18 13:19:21 -0400326
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400327 def createMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400328 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400329 pathUrl = methodDesc['path']
Joe Gregorio48d361f2010-08-18 13:19:21 -0400330 httpMethod = methodDesc['httpMethod']
Joe Gregorio6a63a762011-05-02 22:36:05 -0400331 methodId = methodDesc['id']
Joe Gregorio21f11672010-08-18 17:23:17 -0400332
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400333 mediaPathUrl = None
334 accept = []
335 maxSize = 0
336 if 'mediaUpload' in methodDesc:
337 mediaUpload = methodDesc['mediaUpload']
Joe Gregoriode860442012-03-02 15:55:52 -0500338 # TODO(jcgregorio) Use URLs from discovery once it is updated.
339 parsed = list(urlparse.urlparse(baseUrl))
340 basePath = parsed[2]
341 mediaPathUrl = '/upload' + basePath + pathUrl
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400342 accept = mediaUpload['accept']
Joe Gregorio84d3c1f2011-07-25 10:39:45 -0400343 maxSize = _media_size_to_long(mediaUpload.get('maxSize', ''))
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400344
Joe Gregorioca876e42011-02-22 19:39:42 -0500345 if 'parameters' not in methodDesc:
346 methodDesc['parameters'] = {}
347 for name in STACK_QUERY_PARAMETERS:
348 methodDesc['parameters'][name] = {
349 'type': 'string',
Joe Gregorio6a63a762011-05-02 22:36:05 -0400350 'location': 'query'
Joe Gregorioca876e42011-02-22 19:39:42 -0500351 }
352
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500353 if httpMethod in ['PUT', 'POST', 'PATCH'] and 'request' in methodDesc:
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500354 methodDesc['parameters']['body'] = {
355 'description': 'The request body.',
Joe Gregorioc2a73932011-02-22 10:17:06 -0500356 'type': 'object',
Joe Gregorio1ae3e742011-02-25 15:17:14 -0500357 'required': True,
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500358 }
Joe Gregorio2b781282011-12-08 12:00:25 -0500359 if 'request' in methodDesc:
360 methodDesc['parameters']['body'].update(methodDesc['request'])
361 else:
362 methodDesc['parameters']['body']['type'] = 'object'
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500363 if 'mediaUpload' in methodDesc:
364 methodDesc['parameters']['media_body'] = {
365 'description': 'The filename of the media request body.',
366 'type': 'string',
367 'required': False,
368 }
369 if 'body' in methodDesc['parameters']:
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400370 methodDesc['parameters']['body']['required'] = False
ade@google.com850cf552010-08-20 23:24:56 +0100371
Joe Gregorioca876e42011-02-22 19:39:42 -0500372 argmap = {} # Map from method parameter name to query parameter name
ade@google.com850cf552010-08-20 23:24:56 +0100373 required_params = [] # Required parameters
Joe Gregorio61d7e962011-02-22 22:52:07 -0500374 repeated_params = [] # Repeated parameters
ade@google.com850cf552010-08-20 23:24:56 +0100375 pattern_params = {} # Parameters that must match a regex
376 query_params = [] # Parameters that will be used in the query string
377 path_params = {} # Parameters that will be used in the base URL
Joe Gregoriobee86832011-02-22 10:00:19 -0500378 param_type = {} # The type of the parameter
Joe Gregorioca876e42011-02-22 19:39:42 -0500379 enum_params = {} # Allowable enumeration values for each parameter
380
381
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400382 if 'parameters' in methodDesc:
383 for arg, desc in methodDesc['parameters'].iteritems():
384 param = key2param(arg)
385 argmap[param] = arg
Joe Gregorio21f11672010-08-18 17:23:17 -0400386
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400387 if desc.get('pattern', ''):
388 pattern_params[param] = desc['pattern']
Joe Gregoriobee86832011-02-22 10:00:19 -0500389 if desc.get('enum', ''):
390 enum_params[param] = desc['enum']
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400391 if desc.get('required', False):
392 required_params.append(param)
Joe Gregorio61d7e962011-02-22 22:52:07 -0500393 if desc.get('repeated', False):
394 repeated_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400395 if desc.get('location') == 'query':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400396 query_params.append(param)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400397 if desc.get('location') == 'path':
Joe Gregorio4292c6e2010-09-09 14:32:43 -0400398 path_params[param] = param
Joe Gregoriobee86832011-02-22 10:00:19 -0500399 param_type[param] = desc.get('type', 'string')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400400
Joe Gregoriobc2ff9b2010-11-08 09:20:48 -0500401 for match in URITEMPLATE.finditer(pathUrl):
402 for namematch in VARNAME.finditer(match.group(0)):
403 name = key2param(namematch.group(0))
404 path_params[name] = name
405 if name in query_params:
406 query_params.remove(name)
407
Joe Gregorio48d361f2010-08-18 13:19:21 -0400408 def method(self, **kwargs):
409 for name in kwargs.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500410 if name not in argmap:
Joe Gregorio48d361f2010-08-18 13:19:21 -0400411 raise TypeError('Got an unexpected keyword argument "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400412
ade@google.com850cf552010-08-20 23:24:56 +0100413 for name in required_params:
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400414 if name not in kwargs:
415 raise TypeError('Missing required parameter "%s"' % name)
Joe Gregorio21f11672010-08-18 17:23:17 -0400416
ade@google.com850cf552010-08-20 23:24:56 +0100417 for name, regex in pattern_params.iteritems():
Joe Gregorio21f11672010-08-18 17:23:17 -0400418 if name in kwargs:
Joe Gregorio6804c7a2011-11-18 14:30:32 -0500419 if isinstance(kwargs[name], basestring):
420 pvalues = [kwargs[name]]
421 else:
422 pvalues = kwargs[name]
423 for pvalue in pvalues:
424 if re.match(regex, pvalue) is None:
425 raise TypeError(
426 'Parameter "%s" value "%s" does not match the pattern "%s"' %
427 (name, pvalue, regex))
Joe Gregorio21f11672010-08-18 17:23:17 -0400428
Joe Gregoriobee86832011-02-22 10:00:19 -0500429 for name, enums in enum_params.iteritems():
430 if name in kwargs:
Craig Citro1e742822012-03-01 12:59:22 -0800431 # We need to handle the case of a repeated enum
432 # name differently, since we want to handle both
433 # arg='value' and arg=['value1', 'value2']
434 if (name in repeated_params and
435 not isinstance(kwargs[name], basestring)):
436 values = kwargs[name]
437 else:
438 values = [kwargs[name]]
439 for value in values:
440 if value not in enums:
441 raise TypeError(
442 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
443 (name, value, str(enums)))
Joe Gregoriobee86832011-02-22 10:00:19 -0500444
ade@google.com850cf552010-08-20 23:24:56 +0100445 actual_query_params = {}
446 actual_path_params = {}
Joe Gregorio21f11672010-08-18 17:23:17 -0400447 for key, value in kwargs.iteritems():
Joe Gregorio61d7e962011-02-22 22:52:07 -0500448 to_type = param_type.get(key, 'string')
449 # For repeated parameters we cast each member of the list.
450 if key in repeated_params and type(value) == type([]):
451 cast_value = [_cast(x, to_type) for x in value]
452 else:
453 cast_value = _cast(value, to_type)
ade@google.com850cf552010-08-20 23:24:56 +0100454 if key in query_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500455 actual_query_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100456 if key in path_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500457 actual_path_params[argmap[key]] = cast_value
ade@google.com850cf552010-08-20 23:24:56 +0100458 body_value = kwargs.get('body', None)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400459 media_filename = kwargs.get('media_body', None)
Joe Gregorio21f11672010-08-18 17:23:17 -0400460
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400461 if self._developerKey:
462 actual_query_params['key'] = self._developerKey
463
Joe Gregorioe08a1662011-12-07 09:48:22 -0500464 model = self._model
465 # If there is no schema for the response then presume a binary blob.
466 if 'response' not in methodDesc:
467 model = RawModel()
468
Joe Gregorio48d361f2010-08-18 13:19:21 -0400469 headers = {}
Joe Gregorioe08a1662011-12-07 09:48:22 -0500470 headers, params, query, body = model.request(headers,
Joe Gregorio3bbbf662010-08-30 16:41:53 -0400471 actual_path_params, actual_query_params, body_value)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400472
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400473 expanded_url = uritemplate.expand(pathUrl, params)
Joe Gregorio922b78c2011-05-26 21:36:34 -0400474 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
475
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500476 resumable = None
477 multipart_boundary = ''
478
Joe Gregorio922b78c2011-05-26 21:36:34 -0400479 if media_filename:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500480 # Ensure we end up with a valid MediaUpload object.
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500481 if isinstance(media_filename, basestring):
482 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
483 if media_mime_type is None:
484 raise UnknownFileType(media_filename)
485 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
486 raise UnacceptableMimeTypeError(media_mime_type)
487 media_upload = MediaFileUpload(media_filename, media_mime_type)
488 elif isinstance(media_filename, MediaUpload):
489 media_upload = media_filename
490 else:
Joe Gregorio66f57522011-11-30 11:00:00 -0500491 raise TypeError('media_filename must be str or MediaUpload.')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500492
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400493 # Check the maxSize
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500494 if maxSize > 0 and media_upload.size() > maxSize:
495 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400496
497 # Use the media path uri for media uploads
Joe Gregoriode860442012-03-02 15:55:52 -0500498 expanded_url = uritemplate.expand(mediaPathUrl, params)
Joe Gregoriofdf7c802011-06-30 12:33:38 -0400499 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
Joe Gregoriode860442012-03-02 15:55:52 -0500500 if media_upload.resumable():
501 url = _add_query_parameter(url, 'uploadType', 'resumable')
Joe Gregorio922b78c2011-05-26 21:36:34 -0400502
Joe Gregorio945be3e2012-01-27 17:01:06 -0500503 if media_upload.resumable():
504 # This is all we need to do for resumable, if the body exists it gets
505 # sent in the first request, otherwise an empty body is sent.
506 resumable = media_upload
Joe Gregorio922b78c2011-05-26 21:36:34 -0400507 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500508 # A non-resumable upload
509 if body is None:
510 # This is a simple media upload
511 headers['content-type'] = media_upload.mimetype()
512 body = media_upload.getbytes(0, media_upload.size())
Joe Gregoriode860442012-03-02 15:55:52 -0500513 url = _add_query_parameter(url, 'uploadType', 'media')
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500514 else:
Joe Gregorio945be3e2012-01-27 17:01:06 -0500515 # This is a multipart/related upload.
516 msgRoot = MIMEMultipart('related')
517 # msgRoot should not write out it's own headers
518 setattr(msgRoot, '_write_headers', lambda self: None)
519
520 # attach the body as one part
521 msg = MIMENonMultipart(*headers['content-type'].split('/'))
522 msg.set_payload(body)
523 msgRoot.attach(msg)
524
525 # attach the media as the second part
526 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
527 msg['Content-Transfer-Encoding'] = 'binary'
528
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500529 payload = media_upload.getbytes(0, media_upload.size())
530 msg.set_payload(payload)
531 msgRoot.attach(msg)
532 body = msgRoot.as_string()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400533
Joe Gregorio945be3e2012-01-27 17:01:06 -0500534 multipart_boundary = msgRoot.get_boundary()
535 headers['content-type'] = ('multipart/related; '
536 'boundary="%s"') % multipart_boundary
Joe Gregoriode860442012-03-02 15:55:52 -0500537 url = _add_query_parameter(url, 'uploadType', 'multipart')
Joe Gregoriofbf9d0d2010-08-18 16:50:47 -0400538
ade@google.com850cf552010-08-20 23:24:56 +0100539 logging.info('URL being requested: %s' % url)
Joe Gregorioabda96f2011-02-11 20:19:33 -0500540 return self._requestBuilder(self._http,
Joe Gregorioe08a1662011-12-07 09:48:22 -0500541 model.response,
Joe Gregorioabda96f2011-02-11 20:19:33 -0500542 url,
543 method=httpMethod,
544 body=body,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500545 headers=headers,
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500546 methodId=methodId,
547 resumable=resumable)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400548
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500549 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
550 if len(argmap) > 0:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500551 docs.append('Args:\n')
Joe Gregorio48d361f2010-08-18 13:19:21 -0400552 for arg in argmap.iterkeys():
Joe Gregorioca876e42011-02-22 19:39:42 -0500553 if arg in STACK_QUERY_PARAMETERS:
554 continue
Joe Gregorio61d7e962011-02-22 22:52:07 -0500555 repeated = ''
556 if arg in repeated_params:
557 repeated = ' (repeated)'
558 required = ''
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400559 if arg in required_params:
Joe Gregorio61d7e962011-02-22 22:52:07 -0500560 required = ' (required)'
Joe Gregorioc2a73932011-02-22 10:17:06 -0500561 paramdesc = methodDesc['parameters'][argmap[arg]]
562 paramdoc = paramdesc.get('description', 'A parameter')
Joe Gregorio2b781282011-12-08 12:00:25 -0500563 if '$ref' in paramdesc:
564 docs.append(
565 (' %s: object, %s%s%s\n The object takes the'
566 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
567 schema.prettyPrintByName(paramdesc['$ref'])))
568 else:
569 paramtype = paramdesc.get('type', 'string')
570 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
571 repeated))
Joe Gregorioc2a73932011-02-22 10:17:06 -0500572 enum = paramdesc.get('enum', [])
573 enumDesc = paramdesc.get('enumDescriptions', [])
574 if enum and enumDesc:
575 docs.append(' Allowed values\n')
576 for (name, desc) in zip(enum, enumDesc):
577 docs.append(' %s - %s\n' % (name, desc))
Joe Gregorio2b781282011-12-08 12:00:25 -0500578 if 'response' in methodDesc:
579 docs.append('\nReturns:\n An object of the form\n\n ')
580 docs.append(schema.prettyPrintSchema(methodDesc['response']))
Joe Gregorio48d361f2010-08-18 13:19:21 -0400581
582 setattr(method, '__doc__', ''.join(docs))
583 setattr(theclass, methodName, method)
584
Joe Gregorio3c676f92011-07-25 10:38:14 -0400585 def createNextMethodFromFuture(theclass, methodName, methodDesc, futureDesc):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400586 """ This is a legacy method, as only Buzz and Moderator use the future.json
587 functionality for generating _next methods. It will be kept around as long
588 as those API versions are around, but no new APIs should depend upon it.
589 """
Joe Gregoriod92897c2011-07-07 11:44:56 -0400590 methodName = _fix_method_name(methodName)
Joe Gregorio6a63a762011-05-02 22:36:05 -0400591 methodId = methodDesc['id'] + '.next'
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400592
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500593 def methodNext(self, previous):
Joe Gregorioa98733f2011-09-16 10:12:28 -0400594 """Retrieve the next page of results.
595
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400596 Takes a single argument, 'body', which is the results
597 from the last call, and returns the next set of items
598 in the collection.
599
Joe Gregorioa98733f2011-09-16 10:12:28 -0400600 Returns:
601 None if there are no more items in the collection.
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400602 """
Joe Gregorioaf276d22010-12-09 14:26:58 -0500603 if futureDesc['type'] != 'uri':
604 raise UnknownLinkType(futureDesc['type'])
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400605
606 try:
607 p = previous
Joe Gregorioaf276d22010-12-09 14:26:58 -0500608 for key in futureDesc['location']:
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400609 p = p[key]
610 url = p
Joe Gregorioc5c5a372010-09-22 11:42:32 -0400611 except (KeyError, TypeError):
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400612 return None
613
Joe Gregorioa98733f2011-09-16 10:12:28 -0400614 url = _add_query_parameter(url, 'key', self._developerKey)
Joe Gregorio00cf1d92010-09-27 09:22:03 -0400615
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400616 headers = {}
617 headers, params, query, body = self._model.request(headers, {}, {}, None)
618
619 logging.info('URL being requested: %s' % url)
620 resp, content = self._http.request(url, method='GET', headers=headers)
621
Joe Gregorioabda96f2011-02-11 20:19:33 -0500622 return self._requestBuilder(self._http,
623 self._model.response,
624 url,
625 method='GET',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500626 headers=headers,
Joe Gregorioaf276d22010-12-09 14:26:58 -0500627 methodId=methodId)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400628
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500629 setattr(theclass, methodName, methodNext)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400630
Joe Gregorio3c676f92011-07-25 10:38:14 -0400631 def createNextMethod(theclass, methodName, methodDesc, futureDesc):
632 methodName = _fix_method_name(methodName)
633 methodId = methodDesc['id'] + '.next'
634
635 def methodNext(self, previous_request, previous_response):
636 """Retrieves the next page of results.
637
638 Args:
639 previous_request: The request for the previous page.
640 previous_response: The response from the request for the previous page.
641
642 Returns:
643 A request object that you can call 'execute()' on to request the next
644 page. Returns None if there are no more items in the collection.
645 """
646 # Retrieve nextPageToken from previous_response
647 # Use as pageToken in previous_request to create new request.
648
649 if 'nextPageToken' not in previous_response:
650 return None
651
652 request = copy.copy(previous_request)
653
654 pageToken = previous_response['nextPageToken']
655 parsed = list(urlparse.urlparse(request.uri))
656 q = parse_qsl(parsed[4])
657
658 # Find and remove old 'pageToken' value from URI
659 newq = [(key, value) for (key, value) in q if key != 'pageToken']
660 newq.append(('pageToken', pageToken))
661 parsed[4] = urllib.urlencode(newq)
662 uri = urlparse.urlunparse(parsed)
663
664 request.uri = uri
665
666 logging.info('URL being requested: %s' % uri)
667
668 return request
669
670 setattr(theclass, methodName, methodNext)
671
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400672 # Add basic methods to Resource
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400673 if 'methods' in resourceDesc:
674 for methodName, methodDesc in resourceDesc['methods'].iteritems():
675 if futureDesc:
676 future = futureDesc['methods'].get(methodName, {})
677 else:
678 future = None
679 createMethod(Resource, methodName, methodDesc, future)
680
681 # Add in nested resources
682 if 'resources' in resourceDesc:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500683
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500684 def createResourceMethod(theclass, methodName, methodDesc, futureDesc):
Joe Gregoriod92897c2011-07-07 11:44:56 -0400685 methodName = _fix_method_name(methodName)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400686
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500687 def methodResource(self):
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400688 return createResource(self._http, self._baseUrl, self._model,
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500689 self._requestBuilder, self._developerKey,
Joe Gregorio3c676f92011-07-25 10:38:14 -0400690 methodDesc, futureDesc, schema)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400691
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500692 setattr(methodResource, '__doc__', 'A collection resource.')
693 setattr(methodResource, '__is_resource__', True)
694 setattr(theclass, methodName, methodResource)
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400695
696 for methodName, methodDesc in resourceDesc['resources'].iteritems():
697 if futureDesc and 'resources' in futureDesc:
698 future = futureDesc['resources'].get(methodName, {})
699 else:
700 future = {}
Joe Gregorioc3fae8a2011-02-18 14:19:50 -0500701 createResourceMethod(Resource, methodName, methodDesc, future)
Joe Gregorio6d5e94f2010-08-25 23:49:30 -0400702
703 # Add <m>_next() methods to Resource
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500704 if futureDesc and 'methods' in futureDesc:
Joe Gregorio2379ecc2010-10-26 10:51:28 -0400705 for methodName, methodDesc in futureDesc['methods'].iteritems():
706 if 'next' in methodDesc and methodName in resourceDesc['methods']:
Joe Gregorio3c676f92011-07-25 10:38:14 -0400707 createNextMethodFromFuture(Resource, methodName + '_next',
Joe Gregorioaf276d22010-12-09 14:26:58 -0500708 resourceDesc['methods'][methodName],
709 methodDesc['next'])
Joe Gregorio3c676f92011-07-25 10:38:14 -0400710 # Add _next() methods
711 # Look for response bodies in schema that contain nextPageToken, and methods
712 # that take a pageToken parameter.
713 if 'methods' in resourceDesc:
714 for methodName, methodDesc in resourceDesc['methods'].iteritems():
715 if 'response' in methodDesc:
716 responseSchema = methodDesc['response']
717 if '$ref' in responseSchema:
Joe Gregorio2b781282011-12-08 12:00:25 -0500718 responseSchema = schema.get(responseSchema['$ref'])
Joe Gregorio555f33c2011-08-19 14:56:07 -0400719 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
720 {})
Joe Gregorio3c676f92011-07-25 10:38:14 -0400721 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
722 if hasNextPageToken and hasPageToken:
723 createNextMethod(Resource, methodName + '_next',
724 resourceDesc['methods'][methodName],
725 methodName)
Joe Gregorio48d361f2010-08-18 13:19:21 -0400726
727 return Resource()