Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(31)

Side by Side Diff: reviewbot/third_party/google-api-python-client/apiclient/discovery.py

Issue 20515002: Add google-api-python-client in third_party/ (Closed) Base URL: https://src.chromium.org/chrome/trunk/tools/
Patch Set: Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 # 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
17 A client library for Google's discovery based APIs.
18 """
19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 __all__ = [
22 'build',
23 'build_from_document',
24 'fix_method_name',
25 'key2param',
26 ]
27
28
29 # Standard library imports
30 import copy
31 from email.mime.multipart import MIMEMultipart
32 from email.mime.nonmultipart import MIMENonMultipart
33 import keyword
34 import logging
35 import mimetypes
36 import os
37 import re
38 import urllib
39 import urlparse
40
41 try:
42 from urlparse import parse_qsl
43 except ImportError:
44 from cgi import parse_qsl
45
46 # Third-party imports
47 import httplib2
48 import mimeparse
49 import uritemplate
50
51 # Local imports
52 from apiclient.errors import HttpError
53 from apiclient.errors import InvalidJsonError
54 from apiclient.errors import MediaUploadSizeError
55 from apiclient.errors import UnacceptableMimeTypeError
56 from apiclient.errors import UnknownApiNameOrVersion
57 from apiclient.errors import UnknownFileType
58 from apiclient.http import HttpRequest
59 from apiclient.http import MediaFileUpload
60 from apiclient.http import MediaUpload
61 from apiclient.model import JsonModel
62 from apiclient.model import MediaModel
63 from apiclient.model import RawModel
64 from apiclient.schema import Schemas
65 from oauth2client.anyjson import simplejson
66 from oauth2client.util import _add_query_parameter
67 from oauth2client.util import positional
68
69
70 # The client library requires a version of httplib2 that supports RETRIES.
71 httplib2.RETRIES = 1
72
73 logger = logging.getLogger(__name__)
74
75 URITEMPLATE = re.compile('{[^}]*}')
76 VARNAME = re.compile('[a-zA-Z0-9_-]+')
77 DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
78 '{api}/{apiVersion}/rest')
79 DEFAULT_METHOD_DOC = 'A description of how to use this function'
80 HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH'])
81 _MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40}
82 BODY_PARAMETER_DEFAULT_VALUE = {
83 'description': 'The request body.',
84 'type': 'object',
85 'required': True,
86 }
87 MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
88 'description': ('The filename of the media request body, or an instance '
89 'of a MediaUpload object.'),
90 'type': 'string',
91 'required': False,
92 }
93
94 # Parameters accepted by the stack, but not visible via discovery.
95 # TODO(dhermes): Remove 'userip' in 'v2'.
96 STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict'])
97 STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'}
98
99 # Library-specific reserved words beyond Python keywords.
100 RESERVED_WORDS = frozenset(['body'])
101
102
103 def fix_method_name(name):
104 """Fix method names to avoid reserved word conflicts.
105
106 Args:
107 name: string, method name.
108
109 Returns:
110 The name with a '_' prefixed if the name is a reserved word.
111 """
112 if keyword.iskeyword(name) or name in RESERVED_WORDS:
113 return name + '_'
114 else:
115 return name
116
117
118 def key2param(key):
119 """Converts key names into parameter names.
120
121 For example, converting "max-results" -> "max_results"
122
123 Args:
124 key: string, the method key name.
125
126 Returns:
127 A safe method name based on the key name.
128 """
129 result = []
130 key = list(key)
131 if not key[0].isalpha():
132 result.append('x')
133 for c in key:
134 if c.isalnum():
135 result.append(c)
136 else:
137 result.append('_')
138
139 return ''.join(result)
140
141
142 @positional(2)
143 def build(serviceName,
144 version,
145 http=None,
146 discoveryServiceUrl=DISCOVERY_URI,
147 developerKey=None,
148 model=None,
149 requestBuilder=HttpRequest):
150 """Construct a Resource for interacting with an API.
151
152 Construct a Resource object for interacting with an API. The serviceName and
153 version are the names from the Discovery service.
154
155 Args:
156 serviceName: string, name of the service.
157 version: string, the version of the service.
158 http: httplib2.Http, An instance of httplib2.Http or something that acts
159 like it that HTTP requests will be made through.
160 discoveryServiceUrl: string, a URI Template that points to the location of
161 the discovery service. It should have two parameters {api} and
162 {apiVersion} that when filled in produce an absolute URI to the discovery
163 document for that service.
164 developerKey: string, key obtained from
165 https://code.google.com/apis/console.
166 model: apiclient.Model, converts to and from the wire format.
167 requestBuilder: apiclient.http.HttpRequest, encapsulator for an HTTP
168 request.
169
170 Returns:
171 A Resource object with methods for interacting with the service.
172 """
173 params = {
174 'api': serviceName,
175 'apiVersion': version
176 }
177
178 if http is None:
179 http = httplib2.Http()
180
181 requested_url = uritemplate.expand(discoveryServiceUrl, params)
182
183 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
184 # variable that contains the network address of the client sending the
185 # request. If it exists then add that to the request for the discovery
186 # document to avoid exceeding the quota on discovery requests.
187 if 'REMOTE_ADDR' in os.environ:
188 requested_url = _add_query_parameter(requested_url, 'userIp',
189 os.environ['REMOTE_ADDR'])
190 logger.info('URL being requested: %s' % requested_url)
191
192 resp, content = http.request(requested_url)
193
194 if resp.status == 404:
195 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName,
196 version))
197 if resp.status >= 400:
198 raise HttpError(resp, content, uri=requested_url)
199
200 try:
201 service = simplejson.loads(content)
202 except ValueError, e:
203 logger.error('Failed to parse as JSON: ' + content)
204 raise InvalidJsonError()
205
206 return build_from_document(content, base=discoveryServiceUrl, http=http,
207 developerKey=developerKey, model=model, requestBuilder=requestBuilder)
208
209
210 @positional(1)
211 def build_from_document(
212 service,
213 base=None,
214 future=None,
215 http=None,
216 developerKey=None,
217 model=None,
218 requestBuilder=HttpRequest):
219 """Create a Resource for interacting with an API.
220
221 Same as `build()`, but constructs the Resource object from a discovery
222 document that is it given, as opposed to retrieving one over HTTP.
223
224 Args:
225 service: string or object, the JSON discovery document describing the API.
226 The value passed in may either be the JSON string or the deserialized
227 JSON.
228 base: string, base URI for all HTTP requests, usually the discovery URI.
229 This parameter is no longer used as rootUrl and servicePath are included
230 within the discovery document. (deprecated)
231 future: string, discovery document with future capabilities (deprecated).
232 http: httplib2.Http, An instance of httplib2.Http or something that acts
233 like it that HTTP requests will be made through.
234 developerKey: string, Key for controlling API usage, generated
235 from the API Console.
236 model: Model class instance that serializes and de-serializes requests and
237 responses.
238 requestBuilder: Takes an http request and packages it up to be executed.
239
240 Returns:
241 A Resource object with methods for interacting with the service.
242 """
243
244 # future is no longer used.
245 future = {}
246
247 if isinstance(service, basestring):
248 service = simplejson.loads(service)
249 base = urlparse.urljoin(service['rootUrl'], service['servicePath'])
250 schema = Schemas(service)
251
252 if model is None:
253 features = service.get('features', [])
254 model = JsonModel('dataWrapper' in features)
255 return Resource(http=http, baseUrl=base, model=model,
256 developerKey=developerKey, requestBuilder=requestBuilder,
257 resourceDesc=service, rootDesc=service, schema=schema)
258
259
260 def _cast(value, schema_type):
261 """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':
274 if type(value) == type('') or type(value) == type(u''):
275 return value
276 else:
277 return str(value)
278 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:
285 if type(value) == type('') or type(value) == type(u''):
286 return value
287 else:
288 return str(value)
289
290
291 def _media_size_to_long(maxSize):
292 """Convert a string media size, such as 10GB or 3TB into an integer.
293
294 Args:
295 maxSize: string, size as a string, such as 2MB or 7GB.
296
297 Returns:
298 The size as an integer value.
299 """
300 if len(maxSize) < 2:
301 return 0L
302 units = maxSize[-2:].upper()
303 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
304 if bit_shift is not None:
305 return long(maxSize[:-2]) << bit_shift
306 else:
307 return long(maxSize)
308
309
310 def _media_path_url_from_info(root_desc, path_url):
311 """Creates an absolute media path URL.
312
313 Constructed using the API root URI and service path from the discovery
314 document and the relative path for the API method.
315
316 Args:
317 root_desc: Dictionary; the entire original deserialized discovery document.
318 path_url: String; the relative URL for the API method. Relative to the API
319 root, which is specified in the discovery document.
320
321 Returns:
322 String; the absolute URI for media upload for the API method.
323 """
324 return '%(root)supload/%(service_path)s%(path)s' % {
325 'root': root_desc['rootUrl'],
326 'service_path': root_desc['servicePath'],
327 'path': path_url,
328 }
329
330
331 def _fix_up_parameters(method_desc, root_desc, http_method):
332 """Updates parameters of an API method with values specific to this library.
333
334 Specifically, adds whatever global parameters are specified by the API to the
335 parameters for the individual method. Also adds parameters which don't
336 appear in the discovery document, but are available to all discovery based
337 APIs (these are listed in STACK_QUERY_PARAMETERS).
338
339 SIDE EFFECTS: This updates the parameters dictionary object in the method
340 description.
341
342 Args:
343 method_desc: Dictionary with metadata describing an API method. Value comes
344 from the dictionary of methods stored in the 'methods' key in the
345 deserialized discovery document.
346 root_desc: Dictionary; the entire original deserialized discovery document.
347 http_method: String; the HTTP method used to call the API method described
348 in method_desc.
349
350 Returns:
351 The updated Dictionary stored in the 'parameters' key of the method
352 description dictionary.
353 """
354 parameters = method_desc.setdefault('parameters', {})
355
356 # Add in the parameters common to all methods.
357 for name, description in root_desc.get('parameters', {}).iteritems():
358 parameters[name] = description
359
360 # Add in undocumented query parameters.
361 for name in STACK_QUERY_PARAMETERS:
362 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
363
364 # Add 'body' (our own reserved word) to parameters if the method supports
365 # a request payload.
366 if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc:
367 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
368 body.update(method_desc['request'])
369 parameters['body'] = body
370
371 return parameters
372
373
374 def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
375 """Updates parameters of API by adding 'media_body' if supported by method.
376
377 SIDE EFFECTS: If the method supports media upload and has a required body,
378 sets body to be optional (required=False) instead. Also, if there is a
379 'mediaUpload' in the method description, adds 'media_upload' key to
380 parameters.
381
382 Args:
383 method_desc: Dictionary with metadata describing an API method. Value comes
384 from the dictionary of methods stored in the 'methods' key in the
385 deserialized discovery document.
386 root_desc: Dictionary; the entire original deserialized discovery document.
387 path_url: String; the relative URL for the API method. Relative to the API
388 root, which is specified in the discovery document.
389 parameters: A dictionary describing method parameters for method described
390 in method_desc.
391
392 Returns:
393 Triple (accept, max_size, media_path_url) where:
394 - accept is a list of strings representing what content types are
395 accepted for media upload. Defaults to empty list if not in the
396 discovery document.
397 - max_size is a long representing the max size in bytes allowed for a
398 media upload. Defaults to 0L if not in the discovery document.
399 - media_path_url is a String; the absolute URI for media upload for the
400 API method. Constructed using the API root URI and service path from
401 the discovery document and the relative path for the API method. If
402 media upload is not supported, this is None.
403 """
404 media_upload = method_desc.get('mediaUpload', {})
405 accept = media_upload.get('accept', [])
406 max_size = _media_size_to_long(media_upload.get('maxSize', ''))
407 media_path_url = None
408
409 if media_upload:
410 media_path_url = _media_path_url_from_info(root_desc, path_url)
411 parameters['media_body'] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
412 if 'body' in parameters:
413 parameters['body']['required'] = False
414
415 return accept, max_size, media_path_url
416
417
418 def _fix_up_method_description(method_desc, root_desc):
419 """Updates a method description in a discovery document.
420
421 SIDE EFFECTS: Changes the parameters dictionary in the method description with
422 extra parameters which are used locally.
423
424 Args:
425 method_desc: Dictionary with metadata describing an API method. Value comes
426 from the dictionary of methods stored in the 'methods' key in the
427 deserialized discovery document.
428 root_desc: Dictionary; the entire original deserialized discovery document.
429
430 Returns:
431 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
432 where:
433 - path_url is a String; the relative URL for the API method. Relative to
434 the API root, which is specified in the discovery document.
435 - http_method is a String; the HTTP method used to call the API method
436 described in the method description.
437 - method_id is a String; the name of the RPC method associated with the
438 API method, and is in the method description in the 'id' key.
439 - accept is a list of strings representing what content types are
440 accepted for media upload. Defaults to empty list if not in the
441 discovery document.
442 - max_size is a long representing the max size in bytes allowed for a
443 media upload. Defaults to 0L if not in the discovery document.
444 - media_path_url is a String; the absolute URI for media upload for the
445 API method. Constructed using the API root URI and service path from
446 the discovery document and the relative path for the API method. If
447 media upload is not supported, this is None.
448 """
449 path_url = method_desc['path']
450 http_method = method_desc['httpMethod']
451 method_id = method_desc['id']
452
453 parameters = _fix_up_parameters(method_desc, root_desc, http_method)
454 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
455 # 'parameters' key and needs to know if there is a 'body' parameter because it
456 # also sets a 'media_body' parameter.
457 accept, max_size, media_path_url = _fix_up_media_upload(
458 method_desc, root_desc, path_url, parameters)
459
460 return path_url, http_method, method_id, accept, max_size, media_path_url
461
462
463 # TODO(dhermes): Convert this class to ResourceMethod and make it callable
464 class ResourceMethodParameters(object):
465 """Represents the parameters associated with a method.
466
467 Attributes:
468 argmap: Map from method parameter name (string) to query parameter name
469 (string).
470 required_params: List of required parameters (represented by parameter
471 name as string).
472 repeated_params: List of repeated parameters (represented by parameter
473 name as string).
474 pattern_params: Map from method parameter name (string) to regular
475 expression (as a string). If the pattern is set for a parameter, the
476 value for that parameter must match the regular expression.
477 query_params: List of parameters (represented by parameter name as string)
478 that will be used in the query string.
479 path_params: Set of parameters (represented by parameter name as string)
480 that will be used in the base URL path.
481 param_types: Map from method parameter name (string) to parameter type. Type
482 can be any valid JSON schema type; valid values are 'any', 'array',
483 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
484 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
485 enum_params: Map from method parameter name (string) to list of strings,
486 where each list of strings is the list of acceptable enum values.
487 """
488
489 def __init__(self, method_desc):
490 """Constructor for ResourceMethodParameters.
491
492 Sets default values and defers to set_parameters to populate.
493
494 Args:
495 method_desc: Dictionary with metadata describing an API method. Value
496 comes from the dictionary of methods stored in the 'methods' key in
497 the deserialized discovery document.
498 """
499 self.argmap = {}
500 self.required_params = []
501 self.repeated_params = []
502 self.pattern_params = {}
503 self.query_params = []
504 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
505 # parsing is gotten rid of.
506 self.path_params = set()
507 self.param_types = {}
508 self.enum_params = {}
509
510 self.set_parameters(method_desc)
511
512 def set_parameters(self, method_desc):
513 """Populates maps and lists based on method description.
514
515 Iterates through each parameter for the method and parses the values from
516 the parameter dictionary.
517
518 Args:
519 method_desc: Dictionary with metadata describing an API method. Value
520 comes from the dictionary of methods stored in the 'methods' key in
521 the deserialized discovery document.
522 """
523 for arg, desc in method_desc.get('parameters', {}).iteritems():
524 param = key2param(arg)
525 self.argmap[param] = arg
526
527 if desc.get('pattern'):
528 self.pattern_params[param] = desc['pattern']
529 if desc.get('enum'):
530 self.enum_params[param] = desc['enum']
531 if desc.get('required'):
532 self.required_params.append(param)
533 if desc.get('repeated'):
534 self.repeated_params.append(param)
535 if desc.get('location') == 'query':
536 self.query_params.append(param)
537 if desc.get('location') == 'path':
538 self.path_params.add(param)
539 self.param_types[param] = desc.get('type', 'string')
540
541 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
542 # should have all path parameters already marked with
543 # 'location: path'.
544 for match in URITEMPLATE.finditer(method_desc['path']):
545 for namematch in VARNAME.finditer(match.group(0)):
546 name = key2param(namematch.group(0))
547 self.path_params.add(name)
548 if name in self.query_params:
549 self.query_params.remove(name)
550
551
552 def createMethod(methodName, methodDesc, rootDesc, schema):
553 """Creates a method for attaching to a Resource.
554
555 Args:
556 methodName: string, name of the method to use.
557 methodDesc: object, fragment of deserialized discovery document that
558 describes the method.
559 rootDesc: object, the entire deserialized discovery document.
560 schema: object, mapping of schema names to schema descriptions.
561 """
562 methodName = fix_method_name(methodName)
563 (pathUrl, httpMethod, methodId, accept,
564 maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc)
565
566 parameters = ResourceMethodParameters(methodDesc)
567
568 def method(self, **kwargs):
569 # Don't bother with doc string, it will be over-written by createMethod.
570
571 for name in kwargs.iterkeys():
572 if name not in parameters.argmap:
573 raise TypeError('Got an unexpected keyword argument "%s"' % name)
574
575 # Remove args that have a value of None.
576 keys = kwargs.keys()
577 for name in keys:
578 if kwargs[name] is None:
579 del kwargs[name]
580
581 for name in parameters.required_params:
582 if name not in kwargs:
583 raise TypeError('Missing required parameter "%s"' % name)
584
585 for name, regex in parameters.pattern_params.iteritems():
586 if name in kwargs:
587 if isinstance(kwargs[name], basestring):
588 pvalues = [kwargs[name]]
589 else:
590 pvalues = kwargs[name]
591 for pvalue in pvalues:
592 if re.match(regex, pvalue) is None:
593 raise TypeError(
594 'Parameter "%s" value "%s" does not match the pattern "%s"' %
595 (name, pvalue, regex))
596
597 for name, enums in parameters.enum_params.iteritems():
598 if name in kwargs:
599 # We need to handle the case of a repeated enum
600 # name differently, since we want to handle both
601 # arg='value' and arg=['value1', 'value2']
602 if (name in parameters.repeated_params and
603 not isinstance(kwargs[name], basestring)):
604 values = kwargs[name]
605 else:
606 values = [kwargs[name]]
607 for value in values:
608 if value not in enums:
609 raise TypeError(
610 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
611 (name, value, str(enums)))
612
613 actual_query_params = {}
614 actual_path_params = {}
615 for key, value in kwargs.iteritems():
616 to_type = parameters.param_types.get(key, 'string')
617 # For repeated parameters we cast each member of the list.
618 if key in parameters.repeated_params and type(value) == type([]):
619 cast_value = [_cast(x, to_type) for x in value]
620 else:
621 cast_value = _cast(value, to_type)
622 if key in parameters.query_params:
623 actual_query_params[parameters.argmap[key]] = cast_value
624 if key in parameters.path_params:
625 actual_path_params[parameters.argmap[key]] = cast_value
626 body_value = kwargs.get('body', None)
627 media_filename = kwargs.get('media_body', None)
628
629 if self._developerKey:
630 actual_query_params['key'] = self._developerKey
631
632 model = self._model
633 if methodName.endswith('_media'):
634 model = MediaModel()
635 elif 'response' not in methodDesc:
636 model = RawModel()
637
638 headers = {}
639 headers, params, query, body = model.request(headers,
640 actual_path_params, actual_query_params, body_value)
641
642 expanded_url = uritemplate.expand(pathUrl, params)
643 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
644
645 resumable = None
646 multipart_boundary = ''
647
648 if media_filename:
649 # Ensure we end up with a valid MediaUpload object.
650 if isinstance(media_filename, basestring):
651 (media_mime_type, encoding) = mimetypes.guess_type(media_filename)
652 if media_mime_type is None:
653 raise UnknownFileType(media_filename)
654 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
655 raise UnacceptableMimeTypeError(media_mime_type)
656 media_upload = MediaFileUpload(media_filename,
657 mimetype=media_mime_type)
658 elif isinstance(media_filename, MediaUpload):
659 media_upload = media_filename
660 else:
661 raise TypeError('media_filename must be str or MediaUpload.')
662
663 # Check the maxSize
664 if maxSize > 0 and media_upload.size() > maxSize:
665 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
666
667 # Use the media path uri for media uploads
668 expanded_url = uritemplate.expand(mediaPathUrl, params)
669 url = urlparse.urljoin(self._baseUrl, expanded_url + query)
670 if media_upload.resumable():
671 url = _add_query_parameter(url, 'uploadType', 'resumable')
672
673 if media_upload.resumable():
674 # This is all we need to do for resumable, if the body exists it gets
675 # sent in the first request, otherwise an empty body is sent.
676 resumable = media_upload
677 else:
678 # A non-resumable upload
679 if body is None:
680 # This is a simple media upload
681 headers['content-type'] = media_upload.mimetype()
682 body = media_upload.getbytes(0, media_upload.size())
683 url = _add_query_parameter(url, 'uploadType', 'media')
684 else:
685 # This is a multipart/related upload.
686 msgRoot = MIMEMultipart('related')
687 # msgRoot should not write out it's own headers
688 setattr(msgRoot, '_write_headers', lambda self: None)
689
690 # attach the body as one part
691 msg = MIMENonMultipart(*headers['content-type'].split('/'))
692 msg.set_payload(body)
693 msgRoot.attach(msg)
694
695 # attach the media as the second part
696 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
697 msg['Content-Transfer-Encoding'] = 'binary'
698
699 payload = media_upload.getbytes(0, media_upload.size())
700 msg.set_payload(payload)
701 msgRoot.attach(msg)
702 body = msgRoot.as_string()
703
704 multipart_boundary = msgRoot.get_boundary()
705 headers['content-type'] = ('multipart/related; '
706 'boundary="%s"') % multipart_boundary
707 url = _add_query_parameter(url, 'uploadType', 'multipart')
708
709 logger.info('URL being requested: %s' % url)
710 return self._requestBuilder(self._http,
711 model.response,
712 url,
713 method=httpMethod,
714 body=body,
715 headers=headers,
716 methodId=methodId,
717 resumable=resumable)
718
719 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
720 if len(parameters.argmap) > 0:
721 docs.append('Args:\n')
722
723 # Skip undocumented params and params common to all methods.
724 skip_parameters = rootDesc.get('parameters', {}).keys()
725 skip_parameters.extend(STACK_QUERY_PARAMETERS)
726
727 all_args = parameters.argmap.keys()
728 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
729
730 # Move body to the front of the line.
731 if 'body' in all_args:
732 args_ordered.append('body')
733
734 for name in all_args:
735 if name not in args_ordered:
736 args_ordered.append(name)
737
738 for arg in args_ordered:
739 if arg in skip_parameters:
740 continue
741
742 repeated = ''
743 if arg in parameters.repeated_params:
744 repeated = ' (repeated)'
745 required = ''
746 if arg in parameters.required_params:
747 required = ' (required)'
748 paramdesc = methodDesc['parameters'][parameters.argmap[arg]]
749 paramdoc = paramdesc.get('description', 'A parameter')
750 if '$ref' in paramdesc:
751 docs.append(
752 (' %s: object, %s%s%s\n The object takes the'
753 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
754 schema.prettyPrintByName(paramdesc['$ref'])))
755 else:
756 paramtype = paramdesc.get('type', 'string')
757 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
758 repeated))
759 enum = paramdesc.get('enum', [])
760 enumDesc = paramdesc.get('enumDescriptions', [])
761 if enum and enumDesc:
762 docs.append(' Allowed values\n')
763 for (name, desc) in zip(enum, enumDesc):
764 docs.append(' %s - %s\n' % (name, desc))
765 if 'response' in methodDesc:
766 if methodName.endswith('_media'):
767 docs.append('\nReturns:\n The media object as a string.\n\n ')
768 else:
769 docs.append('\nReturns:\n An object of the form:\n\n ')
770 docs.append(schema.prettyPrintSchema(methodDesc['response']))
771
772 setattr(method, '__doc__', ''.join(docs))
773 return (methodName, method)
774
775
776 def createNextMethod(methodName):
777 """Creates any _next methods for attaching to a Resource.
778
779 The _next methods allow for easy iteration through list() responses.
780
781 Args:
782 methodName: string, name of the method to use.
783 """
784 methodName = fix_method_name(methodName)
785
786 def methodNext(self, previous_request, previous_response):
787 """Retrieves the next page of results.
788
789 Args:
790 previous_request: The request for the previous page. (required)
791 previous_response: The response from the request for the previous page. (requi red)
792
793 Returns:
794 A request object that you can call 'execute()' on to request the next
795 page. Returns None if there are no more items in the collection.
796 """
797 # Retrieve nextPageToken from previous_response
798 # Use as pageToken in previous_request to create new request.
799
800 if 'nextPageToken' not in previous_response:
801 return None
802
803 request = copy.copy(previous_request)
804
805 pageToken = previous_response['nextPageToken']
806 parsed = list(urlparse.urlparse(request.uri))
807 q = parse_qsl(parsed[4])
808
809 # Find and remove old 'pageToken' value from URI
810 newq = [(key, value) for (key, value) in q if key != 'pageToken']
811 newq.append(('pageToken', pageToken))
812 parsed[4] = urllib.urlencode(newq)
813 uri = urlparse.urlunparse(parsed)
814
815 request.uri = uri
816
817 logger.info('URL being requested: %s' % uri)
818
819 return request
820
821 return (methodName, methodNext)
822
823
824 class Resource(object):
825 """A class for interacting with a resource."""
826
827 def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
828 resourceDesc, rootDesc, schema):
829 """Build a Resource from the API description.
830
831 Args:
832 http: httplib2.Http, Object to make http requests with.
833 baseUrl: string, base URL for the API. All requests are relative to this
834 URI.
835 model: apiclient.Model, converts to and from the wire format.
836 requestBuilder: class or callable that instantiates an
837 apiclient.HttpRequest object.
838 developerKey: string, key obtained from
839 https://code.google.com/apis/console
840 resourceDesc: object, section of deserialized discovery document that
841 describes a resource. Note that the top level discovery document
842 is considered a resource.
843 rootDesc: object, the entire deserialized discovery document.
844 schema: object, mapping of schema names to schema descriptions.
845 """
846 self._dynamic_attrs = []
847
848 self._http = http
849 self._baseUrl = baseUrl
850 self._model = model
851 self._developerKey = developerKey
852 self._requestBuilder = requestBuilder
853 self._resourceDesc = resourceDesc
854 self._rootDesc = rootDesc
855 self._schema = schema
856
857 self._set_service_methods()
858
859 def _set_dynamic_attr(self, attr_name, value):
860 """Sets an instance attribute and tracks it in a list of dynamic attributes.
861
862 Args:
863 attr_name: string; The name of the attribute to be set
864 value: The value being set on the object and tracked in the dynamic cache.
865 """
866 self._dynamic_attrs.append(attr_name)
867 self.__dict__[attr_name] = value
868
869 def __getstate__(self):
870 """Trim the state down to something that can be pickled.
871
872 Uses the fact that the instance variable _dynamic_attrs holds attrs that
873 will be wiped and restored on pickle serialization.
874 """
875 state_dict = copy.copy(self.__dict__)
876 for dynamic_attr in self._dynamic_attrs:
877 del state_dict[dynamic_attr]
878 del state_dict['_dynamic_attrs']
879 return state_dict
880
881 def __setstate__(self, state):
882 """Reconstitute the state of the object from being pickled.
883
884 Uses the fact that the instance variable _dynamic_attrs holds attrs that
885 will be wiped and restored on pickle serialization.
886 """
887 self.__dict__.update(state)
888 self._dynamic_attrs = []
889 self._set_service_methods()
890
891 def _set_service_methods(self):
892 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
893 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
894 self._add_next_methods(self._resourceDesc, self._schema)
895
896 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
897 # Add basic methods to Resource
898 if 'methods' in resourceDesc:
899 for methodName, methodDesc in resourceDesc['methods'].iteritems():
900 fixedMethodName, method = createMethod(
901 methodName, methodDesc, rootDesc, schema)
902 self._set_dynamic_attr(fixedMethodName,
903 method.__get__(self, self.__class__))
904 # Add in _media methods. The functionality of the attached method will
905 # change when it sees that the method name ends in _media.
906 if methodDesc.get('supportsMediaDownload', False):
907 fixedMethodName, method = createMethod(
908 methodName + '_media', methodDesc, rootDesc, schema)
909 self._set_dynamic_attr(fixedMethodName,
910 method.__get__(self, self.__class__))
911
912 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
913 # Add in nested resources
914 if 'resources' in resourceDesc:
915
916 def createResourceMethod(methodName, methodDesc):
917 """Create a method on the Resource to access a nested Resource.
918
919 Args:
920 methodName: string, name of the method to use.
921 methodDesc: object, fragment of deserialized discovery document that
922 describes the method.
923 """
924 methodName = fix_method_name(methodName)
925
926 def methodResource(self):
927 return Resource(http=self._http, baseUrl=self._baseUrl,
928 model=self._model, developerKey=self._developerKey,
929 requestBuilder=self._requestBuilder,
930 resourceDesc=methodDesc, rootDesc=rootDesc,
931 schema=schema)
932
933 setattr(methodResource, '__doc__', 'A collection resource.')
934 setattr(methodResource, '__is_resource__', True)
935
936 return (methodName, methodResource)
937
938 for methodName, methodDesc in resourceDesc['resources'].iteritems():
939 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
940 self._set_dynamic_attr(fixedMethodName,
941 method.__get__(self, self.__class__))
942
943 def _add_next_methods(self, resourceDesc, schema):
944 # Add _next() methods
945 # Look for response bodies in schema that contain nextPageToken, and methods
946 # that take a pageToken parameter.
947 if 'methods' in resourceDesc:
948 for methodName, methodDesc in resourceDesc['methods'].iteritems():
949 if 'response' in methodDesc:
950 responseSchema = methodDesc['response']
951 if '$ref' in responseSchema:
952 responseSchema = schema.get(responseSchema['$ref'])
953 hasNextPageToken = 'nextPageToken' in responseSchema.get('properties',
954 {})
955 hasPageToken = 'pageToken' in methodDesc.get('parameters', {})
956 if hasNextPageToken and hasPageToken:
957 fixedMethodName, method = createNextMethod(methodName + '_next')
958 self._set_dynamic_attr(fixedMethodName,
959 method.__get__(self, self.__class__))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698