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