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