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