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