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

Side by Side Diff: third_party/google_api_python_client/googleapiclient/discovery.py

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

Powered by Google App Engine
This is Rietveld 408576698