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