1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 """Client for discovery based APIs.
16
17 A client library for Google's discovery based APIs.
18 """
19 from __future__ import absolute_import
20 import six
21 from six.moves import zip
22
23 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
24 __all__ = [
25 'build',
26 'build_from_document',
27 'fix_method_name',
28 'key2param',
29 ]
30
31 from six import BytesIO
32 from six.moves import http_client
33 from six.moves.urllib.parse import urlencode, urlparse, urljoin, \
34 urlunparse, parse_qsl
35
36
37 import copy
38 try:
39 from email.generator import BytesGenerator
40 except ImportError:
41 from email.generator import Generator as BytesGenerator
42 from email.mime.multipart import MIMEMultipart
43 from email.mime.nonmultipart import MIMENonMultipart
44 import json
45 import keyword
46 import logging
47 import mimetypes
48 import os
49 import re
50
51
52 import httplib2
53 import uritemplate
54
55
56 from googleapiclient import _auth
57 from googleapiclient import mimeparse
58 from googleapiclient.errors import HttpError
59 from googleapiclient.errors import InvalidJsonError
60 from googleapiclient.errors import MediaUploadSizeError
61 from googleapiclient.errors import UnacceptableMimeTypeError
62 from googleapiclient.errors import UnknownApiNameOrVersion
63 from googleapiclient.errors import UnknownFileType
64 from googleapiclient.http import build_http
65 from googleapiclient.http import BatchHttpRequest
66 from googleapiclient.http import HttpMock
67 from googleapiclient.http import HttpMockSequence
68 from googleapiclient.http import HttpRequest
69 from googleapiclient.http import MediaFileUpload
70 from googleapiclient.http import MediaUpload
71 from googleapiclient.model import JsonModel
72 from googleapiclient.model import MediaModel
73 from googleapiclient.model import RawModel
74 from googleapiclient.schema import Schemas
75 from oauth2client.client import GoogleCredentials
76
77
78
79 try:
80 from oauth2client.util import _add_query_parameter
81 from oauth2client.util import positional
82 except ImportError:
83 from oauth2client._helpers import _add_query_parameter
84 from oauth2client._helpers import positional
85
86
87
88 httplib2.RETRIES = 1
89
90 logger = logging.getLogger(__name__)
91
92 URITEMPLATE = re.compile('{[^}]*}')
93 VARNAME = re.compile('[a-zA-Z0-9_-]+')
94 DISCOVERY_URI = ('https://www.googleapis.com/discovery/v1/apis/'
95 '{api}/{apiVersion}/rest')
96 V1_DISCOVERY_URI = DISCOVERY_URI
97 V2_DISCOVERY_URI = ('https://{api}.googleapis.com/$discovery/rest?'
98 'version={apiVersion}')
99 DEFAULT_METHOD_DOC = 'A description of how to use this function'
100 HTTP_PAYLOAD_METHODS = frozenset(['PUT', 'POST', 'PATCH'])
101
102 _MEDIA_SIZE_BIT_SHIFTS = {'KB': 10, 'MB': 20, 'GB': 30, 'TB': 40}
103 BODY_PARAMETER_DEFAULT_VALUE = {
104 'description': 'The request body.',
105 'type': 'object',
106 'required': True,
107 }
108 MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
109 'description': ('The filename of the media request body, or an instance '
110 'of a MediaUpload object.'),
111 'type': 'string',
112 'required': False,
113 }
114 MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
115 'description': ('The MIME type of the media request body, or an instance '
116 'of a MediaUpload object.'),
117 'type': 'string',
118 'required': False,
119 }
120 _PAGE_TOKEN_NAMES = ('pageToken', 'nextPageToken')
121
122
123
124 STACK_QUERY_PARAMETERS = frozenset(['trace', 'pp', 'userip', 'strict'])
125 STACK_QUERY_PARAMETER_DEFAULT_VALUE = {'type': 'string', 'location': 'query'}
126
127
128 RESERVED_WORDS = frozenset(['body'])
134
136 """Fix method names to avoid reserved word conflicts.
137
138 Args:
139 name: string, method name.
140
141 Returns:
142 The name with a '_' prefixed if the name is a reserved word.
143 """
144 if keyword.iskeyword(name) or name in RESERVED_WORDS:
145 return name + '_'
146 else:
147 return name
148
151 """Converts key names into parameter names.
152
153 For example, converting "max-results" -> "max_results"
154
155 Args:
156 key: string, the method key name.
157
158 Returns:
159 A safe method name based on the key name.
160 """
161 result = []
162 key = list(key)
163 if not key[0].isalpha():
164 result.append('x')
165 for c in key:
166 if c.isalnum():
167 result.append(c)
168 else:
169 result.append('_')
170
171 return ''.join(result)
172
173
174 @positional(2)
175 -def build(serviceName,
176 version,
177 http=None,
178 discoveryServiceUrl=DISCOVERY_URI,
179 developerKey=None,
180 model=None,
181 requestBuilder=HttpRequest,
182 credentials=None,
183 cache_discovery=True,
184 cache=None):
185 """Construct a Resource for interacting with an API.
186
187 Construct a Resource object for interacting with an API. The serviceName and
188 version are the names from the Discovery service.
189
190 Args:
191 serviceName: string, name of the service.
192 version: string, the version of the service.
193 http: httplib2.Http, An instance of httplib2.Http or something that acts
194 like it that HTTP requests will be made through.
195 discoveryServiceUrl: string, a URI Template that points to the location of
196 the discovery service. It should have two parameters {api} and
197 {apiVersion} that when filled in produce an absolute URI to the discovery
198 document for that service.
199 developerKey: string, key obtained from
200 https://code.google.com/apis/console.
201 model: googleapiclient.Model, converts to and from the wire format.
202 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
203 request.
204 credentials: oauth2client.Credentials or
205 google.auth.credentials.Credentials, credentials to be used for
206 authentication.
207 cache_discovery: Boolean, whether or not to cache the discovery doc.
208 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
209 cache object for the discovery documents.
210
211 Returns:
212 A Resource object with methods for interacting with the service.
213 """
214 params = {
215 'api': serviceName,
216 'apiVersion': version
217 }
218
219 if http is None:
220 discovery_http = build_http()
221 else:
222 discovery_http = http
223
224 for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI,):
225 requested_url = uritemplate.expand(discovery_url, params)
226
227 try:
228 content = _retrieve_discovery_doc(
229 requested_url, discovery_http, cache_discovery, cache)
230 return build_from_document(content, base=discovery_url, http=http,
231 developerKey=developerKey, model=model, requestBuilder=requestBuilder,
232 credentials=credentials)
233 except HttpError as e:
234 if e.resp.status == http_client.NOT_FOUND:
235 continue
236 else:
237 raise e
238
239 raise UnknownApiNameOrVersion(
240 "name: %s version: %s" % (serviceName, version))
241
244 """Retrieves the discovery_doc from cache or the internet.
245
246 Args:
247 url: string, the URL of the discovery document.
248 http: httplib2.Http, An instance of httplib2.Http or something that acts
249 like it through which HTTP requests will be made.
250 cache_discovery: Boolean, whether or not to cache the discovery doc.
251 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
252 object for the discovery documents.
253
254 Returns:
255 A unicode string representation of the discovery document.
256 """
257 if cache_discovery:
258 from . import discovery_cache
259 from .discovery_cache import base
260 if cache is None:
261 cache = discovery_cache.autodetect()
262 if cache:
263 content = cache.get(url)
264 if content:
265 return content
266
267 actual_url = url
268
269
270
271
272 if 'REMOTE_ADDR' in os.environ:
273 actual_url = _add_query_parameter(url, 'userIp', os.environ['REMOTE_ADDR'])
274 logger.info('URL being requested: GET %s', actual_url)
275
276 resp, content = http.request(actual_url)
277
278 if resp.status >= 400:
279 raise HttpError(resp, content, uri=actual_url)
280
281 try:
282 content = content.decode('utf-8')
283 except AttributeError:
284 pass
285
286 try:
287 service = json.loads(content)
288 except ValueError as e:
289 logger.error('Failed to parse as JSON: ' + content)
290 raise InvalidJsonError()
291 if cache_discovery and cache:
292 cache.set(url, content)
293 return content
294
295
296 @positional(1)
297 -def build_from_document(
298 service,
299 base=None,
300 future=None,
301 http=None,
302 developerKey=None,
303 model=None,
304 requestBuilder=HttpRequest,
305 credentials=None):
306 """Create a Resource for interacting with an API.
307
308 Same as `build()`, but constructs the Resource object from a discovery
309 document that is it given, as opposed to retrieving one over HTTP.
310
311 Args:
312 service: string or object, the JSON discovery document describing the API.
313 The value passed in may either be the JSON string or the deserialized
314 JSON.
315 base: string, base URI for all HTTP requests, usually the discovery URI.
316 This parameter is no longer used as rootUrl and servicePath are included
317 within the discovery document. (deprecated)
318 future: string, discovery document with future capabilities (deprecated).
319 http: httplib2.Http, An instance of httplib2.Http or something that acts
320 like it that HTTP requests will be made through.
321 developerKey: string, Key for controlling API usage, generated
322 from the API Console.
323 model: Model class instance that serializes and de-serializes requests and
324 responses.
325 requestBuilder: Takes an http request and packages it up to be executed.
326 credentials: oauth2client.Credentials or
327 google.auth.credentials.Credentials, credentials to be used for
328 authentication.
329
330 Returns:
331 A Resource object with methods for interacting with the service.
332 """
333
334 if http is not None and credentials is not None:
335 raise ValueError('Arguments http and credentials are mutually exclusive.')
336
337 if developerKey is not None and credentials is not None:
338 raise ValueError(
339 'Arguments developerKey and credentials are mutually exclusive.')
340
341 if isinstance(service, six.string_types):
342 service = json.loads(service)
343
344 if 'rootUrl' not in service and (isinstance(http, (HttpMock,
345 HttpMockSequence))):
346 logger.error("You are using HttpMock or HttpMockSequence without" +
347 "having the service discovery doc in cache. Try calling " +
348 "build() without mocking once first to populate the " +
349 "cache.")
350 raise InvalidJsonError()
351
352 base = urljoin(service['rootUrl'], service['servicePath'])
353 schema = Schemas(service)
354
355
356
357
358 if http is None:
359
360 scopes = list(
361 service.get('auth', {}).get('oauth2', {}).get('scopes', {}).keys())
362
363
364
365 if scopes and not developerKey:
366
367
368 if credentials is None:
369 credentials = _auth.default_credentials()
370
371
372 credentials = _auth.with_scopes(credentials, scopes)
373
374
375 http = _auth.authorized_http(credentials)
376
377
378
379 else:
380 http = build_http()
381
382 if model is None:
383 features = service.get('features', [])
384 model = JsonModel('dataWrapper' in features)
385
386 return Resource(http=http, baseUrl=base, model=model,
387 developerKey=developerKey, requestBuilder=requestBuilder,
388 resourceDesc=service, rootDesc=service, schema=schema)
389
390
391 -def _cast(value, schema_type):
392 """Convert value to a string based on JSON Schema type.
393
394 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
395 JSON Schema.
396
397 Args:
398 value: any, the value to convert
399 schema_type: string, the type that value should be interpreted as
400
401 Returns:
402 A string representation of 'value' based on the schema_type.
403 """
404 if schema_type == 'string':
405 if type(value) == type('') or type(value) == type(u''):
406 return value
407 else:
408 return str(value)
409 elif schema_type == 'integer':
410 return str(int(value))
411 elif schema_type == 'number':
412 return str(float(value))
413 elif schema_type == 'boolean':
414 return str(bool(value)).lower()
415 else:
416 if type(value) == type('') or type(value) == type(u''):
417 return value
418 else:
419 return str(value)
420
439
460
463 """Updates parameters of an API method with values specific to this library.
464
465 Specifically, adds whatever global parameters are specified by the API to the
466 parameters for the individual method. Also adds parameters which don't
467 appear in the discovery document, but are available to all discovery based
468 APIs (these are listed in STACK_QUERY_PARAMETERS).
469
470 SIDE EFFECTS: This updates the parameters dictionary object in the method
471 description.
472
473 Args:
474 method_desc: Dictionary with metadata describing an API method. Value comes
475 from the dictionary of methods stored in the 'methods' key in the
476 deserialized discovery document.
477 root_desc: Dictionary; the entire original deserialized discovery document.
478 http_method: String; the HTTP method used to call the API method described
479 in method_desc.
480
481 Returns:
482 The updated Dictionary stored in the 'parameters' key of the method
483 description dictionary.
484 """
485 parameters = method_desc.setdefault('parameters', {})
486
487
488 for name, description in six.iteritems(root_desc.get('parameters', {})):
489 parameters[name] = description
490
491
492 for name in STACK_QUERY_PARAMETERS:
493 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
494
495
496
497 if http_method in HTTP_PAYLOAD_METHODS and 'request' in method_desc:
498 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
499 body.update(method_desc['request'])
500 parameters['body'] = body
501
502 return parameters
503
548
551 """Updates a method description in a discovery document.
552
553 SIDE EFFECTS: Changes the parameters dictionary in the method description with
554 extra parameters which are used locally.
555
556 Args:
557 method_desc: Dictionary with metadata describing an API method. Value comes
558 from the dictionary of methods stored in the 'methods' key in the
559 deserialized discovery document.
560 root_desc: Dictionary; the entire original deserialized discovery document.
561
562 Returns:
563 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
564 where:
565 - path_url is a String; the relative URL for the API method. Relative to
566 the API root, which is specified in the discovery document.
567 - http_method is a String; the HTTP method used to call the API method
568 described in the method description.
569 - method_id is a String; the name of the RPC method associated with the
570 API method, and is in the method description in the 'id' key.
571 - accept is a list of strings representing what content types are
572 accepted for media upload. Defaults to empty list if not in the
573 discovery document.
574 - max_size is a long representing the max size in bytes allowed for a
575 media upload. Defaults to 0L if not in the discovery document.
576 - media_path_url is a String; the absolute URI for media upload for the
577 API method. Constructed using the API root URI and service path from
578 the discovery document and the relative path for the API method. If
579 media upload is not supported, this is None.
580 """
581 path_url = method_desc['path']
582 http_method = method_desc['httpMethod']
583 method_id = method_desc['id']
584
585 parameters = _fix_up_parameters(method_desc, root_desc, http_method)
586
587
588
589 accept, max_size, media_path_url = _fix_up_media_upload(
590 method_desc, root_desc, path_url, parameters)
591
592 return path_url, http_method, method_id, accept, max_size, media_path_url
593
596 """Custom urljoin replacement supporting : before / in url."""
597
598
599
600
601
602
603
604
605 if url.startswith('http://') or url.startswith('https://'):
606 return urljoin(base, url)
607 new_base = base if base.endswith('/') else base + '/'
608 new_url = url[1:] if url.startswith('/') else url
609 return new_base + new_url
610
614 """Represents the parameters associated with a method.
615
616 Attributes:
617 argmap: Map from method parameter name (string) to query parameter name
618 (string).
619 required_params: List of required parameters (represented by parameter
620 name as string).
621 repeated_params: List of repeated parameters (represented by parameter
622 name as string).
623 pattern_params: Map from method parameter name (string) to regular
624 expression (as a string). If the pattern is set for a parameter, the
625 value for that parameter must match the regular expression.
626 query_params: List of parameters (represented by parameter name as string)
627 that will be used in the query string.
628 path_params: Set of parameters (represented by parameter name as string)
629 that will be used in the base URL path.
630 param_types: Map from method parameter name (string) to parameter type. Type
631 can be any valid JSON schema type; valid values are 'any', 'array',
632 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
633 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
634 enum_params: Map from method parameter name (string) to list of strings,
635 where each list of strings is the list of acceptable enum values.
636 """
637
639 """Constructor for ResourceMethodParameters.
640
641 Sets default values and defers to set_parameters to populate.
642
643 Args:
644 method_desc: Dictionary with metadata describing an API method. Value
645 comes from the dictionary of methods stored in the 'methods' key in
646 the deserialized discovery document.
647 """
648 self.argmap = {}
649 self.required_params = []
650 self.repeated_params = []
651 self.pattern_params = {}
652 self.query_params = []
653
654
655 self.path_params = set()
656 self.param_types = {}
657 self.enum_params = {}
658
659 self.set_parameters(method_desc)
660
662 """Populates maps and lists based on method description.
663
664 Iterates through each parameter for the method and parses the values from
665 the parameter dictionary.
666
667 Args:
668 method_desc: Dictionary with metadata describing an API method. Value
669 comes from the dictionary of methods stored in the 'methods' key in
670 the deserialized discovery document.
671 """
672 for arg, desc in six.iteritems(method_desc.get('parameters', {})):
673 param = key2param(arg)
674 self.argmap[param] = arg
675
676 if desc.get('pattern'):
677 self.pattern_params[param] = desc['pattern']
678 if desc.get('enum'):
679 self.enum_params[param] = desc['enum']
680 if desc.get('required'):
681 self.required_params.append(param)
682 if desc.get('repeated'):
683 self.repeated_params.append(param)
684 if desc.get('location') == 'query':
685 self.query_params.append(param)
686 if desc.get('location') == 'path':
687 self.path_params.add(param)
688 self.param_types[param] = desc.get('type', 'string')
689
690
691
692
693 for match in URITEMPLATE.finditer(method_desc['path']):
694 for namematch in VARNAME.finditer(match.group(0)):
695 name = key2param(namematch.group(0))
696 self.path_params.add(name)
697 if name in self.query_params:
698 self.query_params.remove(name)
699
700
701 -def createMethod(methodName, methodDesc, rootDesc, schema):
702 """Creates a method for attaching to a Resource.
703
704 Args:
705 methodName: string, name of the method to use.
706 methodDesc: object, fragment of deserialized discovery document that
707 describes the method.
708 rootDesc: object, the entire deserialized discovery document.
709 schema: object, mapping of schema names to schema descriptions.
710 """
711 methodName = fix_method_name(methodName)
712 (pathUrl, httpMethod, methodId, accept,
713 maxSize, mediaPathUrl) = _fix_up_method_description(methodDesc, rootDesc)
714
715 parameters = ResourceMethodParameters(methodDesc)
716
717 def method(self, **kwargs):
718
719
720 for name in six.iterkeys(kwargs):
721 if name not in parameters.argmap:
722 raise TypeError('Got an unexpected keyword argument "%s"' % name)
723
724
725 keys = list(kwargs.keys())
726 for name in keys:
727 if kwargs[name] is None:
728 del kwargs[name]
729
730 for name in parameters.required_params:
731 if name not in kwargs:
732
733
734 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
735 _methodProperties(methodDesc, schema, 'response')):
736 raise TypeError('Missing required parameter "%s"' % name)
737
738 for name, regex in six.iteritems(parameters.pattern_params):
739 if name in kwargs:
740 if isinstance(kwargs[name], six.string_types):
741 pvalues = [kwargs[name]]
742 else:
743 pvalues = kwargs[name]
744 for pvalue in pvalues:
745 if re.match(regex, pvalue) is None:
746 raise TypeError(
747 'Parameter "%s" value "%s" does not match the pattern "%s"' %
748 (name, pvalue, regex))
749
750 for name, enums in six.iteritems(parameters.enum_params):
751 if name in kwargs:
752
753
754
755 if (name in parameters.repeated_params and
756 not isinstance(kwargs[name], six.string_types)):
757 values = kwargs[name]
758 else:
759 values = [kwargs[name]]
760 for value in values:
761 if value not in enums:
762 raise TypeError(
763 'Parameter "%s" value "%s" is not an allowed value in "%s"' %
764 (name, value, str(enums)))
765
766 actual_query_params = {}
767 actual_path_params = {}
768 for key, value in six.iteritems(kwargs):
769 to_type = parameters.param_types.get(key, 'string')
770
771 if key in parameters.repeated_params and type(value) == type([]):
772 cast_value = [_cast(x, to_type) for x in value]
773 else:
774 cast_value = _cast(value, to_type)
775 if key in parameters.query_params:
776 actual_query_params[parameters.argmap[key]] = cast_value
777 if key in parameters.path_params:
778 actual_path_params[parameters.argmap[key]] = cast_value
779 body_value = kwargs.get('body', None)
780 media_filename = kwargs.get('media_body', None)
781 media_mime_type = kwargs.get('media_mime_type', None)
782
783 if self._developerKey:
784 actual_query_params['key'] = self._developerKey
785
786 model = self._model
787 if methodName.endswith('_media'):
788 model = MediaModel()
789 elif 'response' not in methodDesc:
790 model = RawModel()
791
792 headers = {}
793 headers, params, query, body = model.request(headers,
794 actual_path_params, actual_query_params, body_value)
795
796 expanded_url = uritemplate.expand(pathUrl, params)
797 url = _urljoin(self._baseUrl, expanded_url + query)
798
799 resumable = None
800 multipart_boundary = ''
801
802 if media_filename:
803
804 if isinstance(media_filename, six.string_types):
805 if media_mime_type is None:
806 logger.warning(
807 'media_mime_type argument not specified: trying to auto-detect for %s',
808 media_filename)
809 media_mime_type, _ = mimetypes.guess_type(media_filename)
810 if media_mime_type is None:
811 raise UnknownFileType(media_filename)
812 if not mimeparse.best_match([media_mime_type], ','.join(accept)):
813 raise UnacceptableMimeTypeError(media_mime_type)
814 media_upload = MediaFileUpload(media_filename,
815 mimetype=media_mime_type)
816 elif isinstance(media_filename, MediaUpload):
817 media_upload = media_filename
818 else:
819 raise TypeError('media_filename must be str or MediaUpload.')
820
821
822 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
823 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
824
825
826 expanded_url = uritemplate.expand(mediaPathUrl, params)
827 url = _urljoin(self._baseUrl, expanded_url + query)
828 if media_upload.resumable():
829 url = _add_query_parameter(url, 'uploadType', 'resumable')
830
831 if media_upload.resumable():
832
833
834 resumable = media_upload
835 else:
836
837 if body is None:
838
839 headers['content-type'] = media_upload.mimetype()
840 body = media_upload.getbytes(0, media_upload.size())
841 url = _add_query_parameter(url, 'uploadType', 'media')
842 else:
843
844 msgRoot = MIMEMultipart('related')
845
846 setattr(msgRoot, '_write_headers', lambda self: None)
847
848
849 msg = MIMENonMultipart(*headers['content-type'].split('/'))
850 msg.set_payload(body)
851 msgRoot.attach(msg)
852
853
854 msg = MIMENonMultipart(*media_upload.mimetype().split('/'))
855 msg['Content-Transfer-Encoding'] = 'binary'
856
857 payload = media_upload.getbytes(0, media_upload.size())
858 msg.set_payload(payload)
859 msgRoot.attach(msg)
860
861
862 fp = BytesIO()
863 g = _BytesGenerator(fp, mangle_from_=False)
864 g.flatten(msgRoot, unixfrom=False)
865 body = fp.getvalue()
866
867 multipart_boundary = msgRoot.get_boundary()
868 headers['content-type'] = ('multipart/related; '
869 'boundary="%s"') % multipart_boundary
870 url = _add_query_parameter(url, 'uploadType', 'multipart')
871
872 logger.info('URL being requested: %s %s' % (httpMethod,url))
873 return self._requestBuilder(self._http,
874 model.response,
875 url,
876 method=httpMethod,
877 body=body,
878 headers=headers,
879 methodId=methodId,
880 resumable=resumable)
881
882 docs = [methodDesc.get('description', DEFAULT_METHOD_DOC), '\n\n']
883 if len(parameters.argmap) > 0:
884 docs.append('Args:\n')
885
886
887 skip_parameters = list(rootDesc.get('parameters', {}).keys())
888 skip_parameters.extend(STACK_QUERY_PARAMETERS)
889
890 all_args = list(parameters.argmap.keys())
891 args_ordered = [key2param(s) for s in methodDesc.get('parameterOrder', [])]
892
893
894 if 'body' in all_args:
895 args_ordered.append('body')
896
897 for name in all_args:
898 if name not in args_ordered:
899 args_ordered.append(name)
900
901 for arg in args_ordered:
902 if arg in skip_parameters:
903 continue
904
905 repeated = ''
906 if arg in parameters.repeated_params:
907 repeated = ' (repeated)'
908 required = ''
909 if arg in parameters.required_params:
910 required = ' (required)'
911 paramdesc = methodDesc['parameters'][parameters.argmap[arg]]
912 paramdoc = paramdesc.get('description', 'A parameter')
913 if '$ref' in paramdesc:
914 docs.append(
915 (' %s: object, %s%s%s\n The object takes the'
916 ' form of:\n\n%s\n\n') % (arg, paramdoc, required, repeated,
917 schema.prettyPrintByName(paramdesc['$ref'])))
918 else:
919 paramtype = paramdesc.get('type', 'string')
920 docs.append(' %s: %s, %s%s%s\n' % (arg, paramtype, paramdoc, required,
921 repeated))
922 enum = paramdesc.get('enum', [])
923 enumDesc = paramdesc.get('enumDescriptions', [])
924 if enum and enumDesc:
925 docs.append(' Allowed values\n')
926 for (name, desc) in zip(enum, enumDesc):
927 docs.append(' %s - %s\n' % (name, desc))
928 if 'response' in methodDesc:
929 if methodName.endswith('_media'):
930 docs.append('\nReturns:\n The media object as a string.\n\n ')
931 else:
932 docs.append('\nReturns:\n An object of the form:\n\n ')
933 docs.append(schema.prettyPrintSchema(methodDesc['response']))
934
935 setattr(method, '__doc__', ''.join(docs))
936 return (methodName, method)
937
938
939 -def createNextMethod(methodName,
940 pageTokenName='pageToken',
941 nextPageTokenName='nextPageToken',
942 isPageTokenParameter=True):
943 """Creates any _next methods for attaching to a Resource.
944
945 The _next methods allow for easy iteration through list() responses.
946
947 Args:
948 methodName: string, name of the method to use.
949 pageTokenName: string, name of request page token field.
950 nextPageTokenName: string, name of response page token field.
951 isPageTokenParameter: Boolean, True if request page token is a query
952 parameter, False if request page token is a field of the request body.
953 """
954 methodName = fix_method_name(methodName)
955
956 def methodNext(self, previous_request, previous_response):
957 """Retrieves the next page of results.
958
959 Args:
960 previous_request: The request for the previous page. (required)
961 previous_response: The response from the request for the previous page. (required)
962
963 Returns:
964 A request object that you can call 'execute()' on to request the next
965 page. Returns None if there are no more items in the collection.
966 """
967
968
969
970 nextPageToken = previous_response.get(nextPageTokenName, None)
971 if not nextPageToken:
972 return None
973
974 request = copy.copy(previous_request)
975
976 if isPageTokenParameter:
977
978 request.uri = _add_query_parameter(
979 request.uri, pageTokenName, nextPageToken)
980 logger.info('Next page request URL: %s %s' % (methodName, request.uri))
981 else:
982
983 model = self._model
984 body = model.deserialize(request.body)
985 body[pageTokenName] = nextPageToken
986 request.body = model.serialize(body)
987 logger.info('Next page request body: %s %s' % (methodName, body))
988
989 return request
990
991 return (methodName, methodNext)
992
995 """A class for interacting with a resource."""
996
997 - def __init__(self, http, baseUrl, model, requestBuilder, developerKey,
998 resourceDesc, rootDesc, schema):
999 """Build a Resource from the API description.
1000
1001 Args:
1002 http: httplib2.Http, Object to make http requests with.
1003 baseUrl: string, base URL for the API. All requests are relative to this
1004 URI.
1005 model: googleapiclient.Model, converts to and from the wire format.
1006 requestBuilder: class or callable that instantiates an
1007 googleapiclient.HttpRequest object.
1008 developerKey: string, key obtained from
1009 https://code.google.com/apis/console
1010 resourceDesc: object, section of deserialized discovery document that
1011 describes a resource. Note that the top level discovery document
1012 is considered a resource.
1013 rootDesc: object, the entire deserialized discovery document.
1014 schema: object, mapping of schema names to schema descriptions.
1015 """
1016 self._dynamic_attrs = []
1017
1018 self._http = http
1019 self._baseUrl = baseUrl
1020 self._model = model
1021 self._developerKey = developerKey
1022 self._requestBuilder = requestBuilder
1023 self._resourceDesc = resourceDesc
1024 self._rootDesc = rootDesc
1025 self._schema = schema
1026
1027 self._set_service_methods()
1028
1030 """Sets an instance attribute and tracks it in a list of dynamic attributes.
1031
1032 Args:
1033 attr_name: string; The name of the attribute to be set
1034 value: The value being set on the object and tracked in the dynamic cache.
1035 """
1036 self._dynamic_attrs.append(attr_name)
1037 self.__dict__[attr_name] = value
1038
1040 """Trim the state down to something that can be pickled.
1041
1042 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1043 will be wiped and restored on pickle serialization.
1044 """
1045 state_dict = copy.copy(self.__dict__)
1046 for dynamic_attr in self._dynamic_attrs:
1047 del state_dict[dynamic_attr]
1048 del state_dict['_dynamic_attrs']
1049 return state_dict
1050
1052 """Reconstitute the state of the object from being pickled.
1053
1054 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1055 will be wiped and restored on pickle serialization.
1056 """
1057 self.__dict__.update(state)
1058 self._dynamic_attrs = []
1059 self._set_service_methods()
1060
1065
1067
1068 if resourceDesc == rootDesc:
1069 batch_uri = '%s%s' % (
1070 rootDesc['rootUrl'], rootDesc.get('batchPath', 'batch'))
1071 def new_batch_http_request(callback=None):
1072 """Create a BatchHttpRequest object based on the discovery document.
1073
1074 Args:
1075 callback: callable, A callback to be called for each response, of the
1076 form callback(id, response, exception). The first parameter is the
1077 request id, and the second is the deserialized response object. The
1078 third is an apiclient.errors.HttpError exception object if an HTTP
1079 error occurred while processing the request, or None if no error
1080 occurred.
1081
1082 Returns:
1083 A BatchHttpRequest object based on the discovery document.
1084 """
1085 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
1086 self._set_dynamic_attr('new_batch_http_request', new_batch_http_request)
1087
1088
1089 if 'methods' in resourceDesc:
1090 for methodName, methodDesc in six.iteritems(resourceDesc['methods']):
1091 fixedMethodName, method = createMethod(
1092 methodName, methodDesc, rootDesc, schema)
1093 self._set_dynamic_attr(fixedMethodName,
1094 method.__get__(self, self.__class__))
1095
1096
1097 if methodDesc.get('supportsMediaDownload', False):
1098 fixedMethodName, method = createMethod(
1099 methodName + '_media', methodDesc, rootDesc, schema)
1100 self._set_dynamic_attr(fixedMethodName,
1101 method.__get__(self, self.__class__))
1102
1104
1105 if 'resources' in resourceDesc:
1106
1107 def createResourceMethod(methodName, methodDesc):
1108 """Create a method on the Resource to access a nested Resource.
1109
1110 Args:
1111 methodName: string, name of the method to use.
1112 methodDesc: object, fragment of deserialized discovery document that
1113 describes the method.
1114 """
1115 methodName = fix_method_name(methodName)
1116
1117 def methodResource(self):
1118 return Resource(http=self._http, baseUrl=self._baseUrl,
1119 model=self._model, developerKey=self._developerKey,
1120 requestBuilder=self._requestBuilder,
1121 resourceDesc=methodDesc, rootDesc=rootDesc,
1122 schema=schema)
1123
1124 setattr(methodResource, '__doc__', 'A collection resource.')
1125 setattr(methodResource, '__is_resource__', True)
1126
1127 return (methodName, methodResource)
1128
1129 for methodName, methodDesc in six.iteritems(resourceDesc['resources']):
1130 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1131 self._set_dynamic_attr(fixedMethodName,
1132 method.__get__(self, self.__class__))
1133
1135
1136
1137
1138 if 'methods' not in resourceDesc:
1139 return
1140 for methodName, methodDesc in six.iteritems(resourceDesc['methods']):
1141 nextPageTokenName = _findPageTokenName(
1142 _methodProperties(methodDesc, schema, 'response'))
1143 if not nextPageTokenName:
1144 continue
1145 isPageTokenParameter = True
1146 pageTokenName = _findPageTokenName(methodDesc.get('parameters', {}))
1147 if not pageTokenName:
1148 isPageTokenParameter = False
1149 pageTokenName = _findPageTokenName(
1150 _methodProperties(methodDesc, schema, 'request'))
1151 if not pageTokenName:
1152 continue
1153 fixedMethodName, method = createNextMethod(
1154 methodName + '_next', pageTokenName, nextPageTokenName,
1155 isPageTokenParameter)
1156 self._set_dynamic_attr(fixedMethodName,
1157 method.__get__(self, self.__class__))
1158
1159
1160 -def _findPageTokenName(fields):
1161 """Search field names for one like a page token.
1162
1163 Args:
1164 fields: container of string, names of fields.
1165
1166 Returns:
1167 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1168 otherwise None.
1169 """
1170 return next((tokenName for tokenName in _PAGE_TOKEN_NAMES
1171 if tokenName in fields), None)
1172
1174 """Get properties of a field in a method description.
1175
1176 Args:
1177 methodDesc: object, fragment of deserialized discovery document that
1178 describes the method.
1179 schema: object, mapping of schema names to schema descriptions.
1180 name: string, name of top-level field in method description.
1181
1182 Returns:
1183 Object representing fragment of deserialized discovery document
1184 corresponding to 'properties' field of object corresponding to named field
1185 in method description, if it exists, otherwise empty dict.
1186 """
1187 desc = methodDesc.get(name, {})
1188 if '$ref' in desc:
1189 desc = schema.get(desc['$ref'], {})
1190 return desc.get('properties', {})
1191