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