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 from collections import OrderedDict
33
34 try:
35 from email.generator import BytesGenerator
36 except ImportError:
37 from email.generator import Generator as BytesGenerator
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 import google.api_core.client_options
51 from google.auth.transport import mtls
52 from google.auth.exceptions import MutualTLSChannelError
53
54 try:
55 import google_auth_httplib2
56 except ImportError:
57 google_auth_httplib2 = None
58
59
60 from googleapiclient import _auth
61 from googleapiclient import mimeparse
62 from googleapiclient.errors import HttpError
63 from googleapiclient.errors import InvalidJsonError
64 from googleapiclient.errors import MediaUploadSizeError
65 from googleapiclient.errors import UnacceptableMimeTypeError
66 from googleapiclient.errors import UnknownApiNameOrVersion
67 from googleapiclient.errors import UnknownFileType
68 from googleapiclient.http import build_http
69 from googleapiclient.http import BatchHttpRequest
70 from googleapiclient.http import HttpMock
71 from googleapiclient.http import HttpMockSequence
72 from googleapiclient.http import HttpRequest
73 from googleapiclient.http import MediaFileUpload
74 from googleapiclient.http import MediaUpload
75 from googleapiclient.model import JsonModel
76 from googleapiclient.model import MediaModel
77 from googleapiclient.model import RawModel
78 from googleapiclient.schema import Schemas
79
80 from googleapiclient._helpers import _add_query_parameter
81 from googleapiclient._helpers import positional
82
83
84
85 httplib2.RETRIES = 1
86
87 logger = logging.getLogger(__name__)
88
89 URITEMPLATE = re.compile("{[^}]*}")
90 VARNAME = re.compile("[a-zA-Z0-9_-]+")
91 DISCOVERY_URI = (
92 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
93 )
94 V1_DISCOVERY_URI = DISCOVERY_URI
95 V2_DISCOVERY_URI = (
96 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
97 )
98 DEFAULT_METHOD_DOC = "A description of how to use this function"
99 HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
100
101 _MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
102 BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
103 MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
104 "description": (
105 "The filename of the media request body, or an instance "
106 "of a MediaUpload object."
107 ),
108 "type": "string",
109 "required": False,
110 }
111 MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
112 "description": (
113 "The MIME type of the media request body, or an instance "
114 "of a MediaUpload object."
115 ),
116 "type": "string",
117 "required": False,
118 }
119 _PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
120
121
122 GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
123 GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT"
124
125
126
127 STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
128 STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
129
130
131 RESERVED_WORDS = frozenset(["body"])
137
140 """Fix method names to avoid '$' characters and reserved word conflicts.
141
142 Args:
143 name: string, method name.
144
145 Returns:
146 The name with '_' appended if the name is a reserved word and '$' and '-'
147 replaced with '_'.
148 """
149 name = name.replace("$", "_").replace("-", "_")
150 if keyword.iskeyword(name) or name in RESERVED_WORDS:
151 return name + "_"
152 else:
153 return name
154
157 """Converts key names into parameter names.
158
159 For example, converting "max-results" -> "max_results"
160
161 Args:
162 key: string, the method key name.
163
164 Returns:
165 A safe method name based on the key name.
166 """
167 result = []
168 key = list(key)
169 if not key[0].isalpha():
170 result.append("x")
171 for c in key:
172 if c.isalnum():
173 result.append(c)
174 else:
175 result.append("_")
176
177 return "".join(result)
178
179
180 @positional(2)
181 -def build(
182 serviceName,
183 version,
184 http=None,
185 discoveryServiceUrl=DISCOVERY_URI,
186 developerKey=None,
187 model=None,
188 requestBuilder=HttpRequest,
189 credentials=None,
190 cache_discovery=True,
191 cache=None,
192 client_options=None,
193 adc_cert_path=None,
194 adc_key_path=None,
195 num_retries=1,
196 ):
197 """Construct a Resource for interacting with an API.
198
199 Construct a Resource object for interacting with an API. The serviceName and
200 version are the names from the Discovery service.
201
202 Args:
203 serviceName: string, name of the service.
204 version: string, the version of the service.
205 http: httplib2.Http, An instance of httplib2.Http or something that acts
206 like it that HTTP requests will be made through.
207 discoveryServiceUrl: string, a URI Template that points to the location of
208 the discovery service. It should have two parameters {api} and
209 {apiVersion} that when filled in produce an absolute URI to the discovery
210 document for that service.
211 developerKey: string, key obtained from
212 https://code.google.com/apis/console.
213 model: googleapiclient.Model, converts to and from the wire format.
214 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
215 request.
216 credentials: oauth2client.Credentials or
217 google.auth.credentials.Credentials, credentials to be used for
218 authentication.
219 cache_discovery: Boolean, whether or not to cache the discovery doc.
220 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
221 cache object for the discovery documents.
222 client_options: Mapping object or google.api_core.client_options, client
223 options to set user options on the client.
224 (1) The API endpoint should be set through client_options. If API endpoint
225 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
226 to control which endpoint to use.
227 (2) client_cert_source is not supported, client cert should be provided using
228 client_encrypted_cert_source instead. In order to use the provided client
229 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
230 set to `true`.
231 More details on the environment variables are here:
232 https://google.aip.dev/auth/4114
233 adc_cert_path: str, client certificate file path to save the application
234 default client certificate for mTLS. This field is required if you want to
235 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
236 environment variable must be set to `true` in order to use this field,
237 otherwise this field doesn't nothing.
238 More details on the environment variables are here:
239 https://google.aip.dev/auth/4114
240 adc_key_path: str, client encrypted private key file path to save the
241 application default client encrypted private key for mTLS. This field is
242 required if you want to use the default client certificate.
243 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
244 `true` in order to use this field, otherwise this field doesn't nothing.
245 More details on the environment variables are here:
246 https://google.aip.dev/auth/4114
247 num_retries: Integer, number of times to retry discovery with
248 randomized exponential backoff in case of intermittent/connection issues.
249
250 Returns:
251 A Resource object with methods for interacting with the service.
252
253 Raises:
254 google.auth.exceptions.MutualTLSChannelError: if there are any problems
255 setting up mutual TLS channel.
256 """
257 params = {"api": serviceName, "apiVersion": version}
258
259 if http is None:
260 discovery_http = build_http()
261 else:
262 discovery_http = http
263
264 service = None
265
266 for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
267 requested_url = uritemplate.expand(discovery_url, params)
268
269 try:
270 content = _retrieve_discovery_doc(
271 requested_url,
272 discovery_http,
273 cache_discovery,
274 cache,
275 developerKey,
276 num_retries=num_retries,
277 )
278 service = build_from_document(
279 content,
280 base=discovery_url,
281 http=http,
282 developerKey=developerKey,
283 model=model,
284 requestBuilder=requestBuilder,
285 credentials=credentials,
286 client_options=client_options,
287 adc_cert_path=adc_cert_path,
288 adc_key_path=adc_key_path,
289 )
290 break
291 except HttpError as e:
292 if e.resp.status == http_client.NOT_FOUND:
293 continue
294 else:
295 raise e
296
297
298
299 if http is None:
300 discovery_http.close()
301
302 if service is None:
303 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
304 else:
305 return service
306
309 """
310 Returns Discovery URIs to be used for attemnting to build the API Resource.
311
312 Args:
313 discoveryServiceUrl:
314 string, the Original Discovery Service URL preferred by the customer.
315 version:
316 string, API Version requested
317
318 Returns:
319 A list of URIs to be tried for the Service Discovery, in order.
320 """
321
322 urls = [discoveryServiceUrl, V2_DISCOVERY_URI]
323
324 if discoveryServiceUrl == V1_DISCOVERY_URI and version is None:
325 logger.warning(
326 "Discovery V1 does not support empty versions. Defaulting to V2..."
327 )
328 urls.pop(0)
329 return list(OrderedDict.fromkeys(urls))
330
331
332 -def _retrieve_discovery_doc(
333 url, http, cache_discovery, cache=None, developerKey=None, num_retries=1
334 ):
335 """Retrieves the discovery_doc from cache or the internet.
336
337 Args:
338 url: string, the URL of the discovery document.
339 http: httplib2.Http, An instance of httplib2.Http or something that acts
340 like it through which HTTP requests will be made.
341 cache_discovery: Boolean, whether or not to cache the discovery doc.
342 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
343 object for the discovery documents.
344 developerKey: string, Key for controlling API usage, generated
345 from the API Console.
346 num_retries: Integer, number of times to retry discovery with
347 randomized exponential backoff in case of intermittent/connection issues.
348
349 Returns:
350 A unicode string representation of the discovery document.
351 """
352 if cache_discovery:
353 from . import discovery_cache
354
355 if cache is None:
356 cache = discovery_cache.autodetect()
357 if cache:
358 content = cache.get(url)
359 if content:
360 return content
361
362 actual_url = url
363
364
365
366
367 if "REMOTE_ADDR" in os.environ:
368 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
369 if developerKey:
370 actual_url = _add_query_parameter(url, "key", developerKey)
371 logger.debug("URL being requested: GET %s", actual_url)
372
373
374
375 req = HttpRequest(http, HttpRequest.null_postproc, actual_url)
376 resp, content = req.execute(num_retries=num_retries)
377
378 try:
379 content = content.decode("utf-8")
380 except AttributeError:
381 pass
382
383 try:
384 service = json.loads(content)
385 except ValueError as e:
386 logger.error("Failed to parse as JSON: " + content)
387 raise InvalidJsonError()
388 if cache_discovery and cache:
389 cache.set(url, content)
390 return content
391
392
393 @positional(1)
394 -def build_from_document(
395 service,
396 base=None,
397 future=None,
398 http=None,
399 developerKey=None,
400 model=None,
401 requestBuilder=HttpRequest,
402 credentials=None,
403 client_options=None,
404 adc_cert_path=None,
405 adc_key_path=None,
406 ):
407 """Create a Resource for interacting with an API.
408
409 Same as `build()`, but constructs the Resource object from a discovery
410 document that is it given, as opposed to retrieving one over HTTP.
411
412 Args:
413 service: string or object, the JSON discovery document describing the API.
414 The value passed in may either be the JSON string or the deserialized
415 JSON.
416 base: string, base URI for all HTTP requests, usually the discovery URI.
417 This parameter is no longer used as rootUrl and servicePath are included
418 within the discovery document. (deprecated)
419 future: string, discovery document with future capabilities (deprecated).
420 http: httplib2.Http, An instance of httplib2.Http or something that acts
421 like it that HTTP requests will be made through.
422 developerKey: string, Key for controlling API usage, generated
423 from the API Console.
424 model: Model class instance that serializes and de-serializes requests and
425 responses.
426 requestBuilder: Takes an http request and packages it up to be executed.
427 credentials: oauth2client.Credentials or
428 google.auth.credentials.Credentials, credentials to be used for
429 authentication.
430 client_options: Mapping object or google.api_core.client_options, client
431 options to set user options on the client.
432 (1) The API endpoint should be set through client_options. If API endpoint
433 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used
434 to control which endpoint to use.
435 (2) client_cert_source is not supported, client cert should be provided using
436 client_encrypted_cert_source instead. In order to use the provided client
437 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be
438 set to `true`.
439 More details on the environment variables are here:
440 https://google.aip.dev/auth/4114
441 adc_cert_path: str, client certificate file path to save the application
442 default client certificate for mTLS. This field is required if you want to
443 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE`
444 environment variable must be set to `true` in order to use this field,
445 otherwise this field doesn't nothing.
446 More details on the environment variables are here:
447 https://google.aip.dev/auth/4114
448 adc_key_path: str, client encrypted private key file path to save the
449 application default client encrypted private key for mTLS. This field is
450 required if you want to use the default client certificate.
451 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to
452 `true` in order to use this field, otherwise this field doesn't nothing.
453 More details on the environment variables are here:
454 https://google.aip.dev/auth/4114
455
456 Returns:
457 A Resource object with methods for interacting with the service.
458
459 Raises:
460 google.auth.exceptions.MutualTLSChannelError: if there are any problems
461 setting up mutual TLS channel.
462 """
463
464 if client_options is None:
465 client_options = google.api_core.client_options.ClientOptions()
466 if isinstance(client_options, six.moves.collections_abc.Mapping):
467 client_options = google.api_core.client_options.from_dict(client_options)
468
469 if http is not None:
470
471 banned_options = [
472 (credentials, "credentials"),
473 (client_options.credentials_file, "client_options.credentials_file"),
474 ]
475 for option, name in banned_options:
476 if option is not None:
477 raise ValueError("Arguments http and {} are mutually exclusive".format(name))
478
479 if isinstance(service, six.string_types):
480 service = json.loads(service)
481 elif isinstance(service, six.binary_type):
482 service = json.loads(service.decode("utf-8"))
483
484 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)):
485 logger.error(
486 "You are using HttpMock or HttpMockSequence without"
487 + "having the service discovery doc in cache. Try calling "
488 + "build() without mocking once first to populate the "
489 + "cache."
490 )
491 raise InvalidJsonError()
492
493
494 base = urljoin(service["rootUrl"], service["servicePath"])
495 if client_options.api_endpoint:
496 base = client_options.api_endpoint
497
498 schema = Schemas(service)
499
500
501
502
503 if http is None:
504
505 scopes = list(
506 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
507 )
508
509
510
511 if scopes and not developerKey:
512
513 if client_options.credentials_file and credentials:
514 raise google.api_core.exceptions.DuplicateCredentialArgs(
515 "client_options.credentials_file and credentials are mutually exclusive."
516 )
517
518 if client_options.credentials_file:
519 credentials = _auth.credentials_from_file(
520 client_options.credentials_file,
521 scopes=client_options.scopes,
522 quota_project_id=client_options.quota_project_id,
523 )
524
525
526 if credentials is None:
527 credentials = _auth.default_credentials(
528 scopes=client_options.scopes,
529 quota_project_id=client_options.quota_project_id,
530 )
531
532
533
534 if not client_options.scopes:
535 credentials = _auth.with_scopes(credentials, scopes)
536
537
538
539 if credentials:
540 http = _auth.authorized_http(credentials)
541
542
543
544 else:
545 http = build_http()
546
547
548 client_cert_to_use = None
549 use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false")
550 if not use_client_cert in ("true", "false"):
551 raise MutualTLSChannelError(
552 "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false"
553 )
554 if client_options and client_options.client_cert_source:
555 raise MutualTLSChannelError(
556 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source."
557 )
558 if use_client_cert == "true":
559 if (
560 client_options
561 and hasattr(client_options, "client_encrypted_cert_source")
562 and client_options.client_encrypted_cert_source
563 ):
564 client_cert_to_use = client_options.client_encrypted_cert_source
565 elif (
566 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source()
567 ):
568 client_cert_to_use = mtls.default_client_encrypted_cert_source(
569 adc_cert_path, adc_key_path
570 )
571 if client_cert_to_use:
572 cert_path, key_path, passphrase = client_cert_to_use()
573
574
575
576
577 http_channel = (
578 http.http
579 if google_auth_httplib2
580 and isinstance(http, google_auth_httplib2.AuthorizedHttp)
581 else http
582 )
583 http_channel.add_certificate(key_path, cert_path, "", passphrase)
584
585
586
587 if "mtlsRootUrl" in service and (
588 not client_options or not client_options.api_endpoint
589 ):
590 mtls_endpoint = urljoin(service["mtlsRootUrl"], service["servicePath"])
591 use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto")
592
593 if not use_mtls_endpoint in ("never", "auto", "always"):
594 raise MutualTLSChannelError(
595 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always"
596 )
597
598
599
600 if use_mtls_endpoint == "always" or (
601 use_mtls_endpoint == "auto" and client_cert_to_use
602 ):
603 base = mtls_endpoint
604
605 if model is None:
606 features = service.get("features", [])
607 model = JsonModel("dataWrapper" in features)
608
609 return Resource(
610 http=http,
611 baseUrl=base,
612 model=model,
613 developerKey=developerKey,
614 requestBuilder=requestBuilder,
615 resourceDesc=service,
616 rootDesc=service,
617 schema=schema,
618 )
619
620
621 -def _cast(value, schema_type):
622 """Convert value to a string based on JSON Schema type.
623
624 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
625 JSON Schema.
626
627 Args:
628 value: any, the value to convert
629 schema_type: string, the type that value should be interpreted as
630
631 Returns:
632 A string representation of 'value' based on the schema_type.
633 """
634 if schema_type == "string":
635 if type(value) == type("") or type(value) == type(u""):
636 return value
637 else:
638 return str(value)
639 elif schema_type == "integer":
640 return str(int(value))
641 elif schema_type == "number":
642 return str(float(value))
643 elif schema_type == "boolean":
644 return str(bool(value)).lower()
645 else:
646 if type(value) == type("") or type(value) == type(u""):
647 return value
648 else:
649 return str(value)
650
669
690
693 """Updates parameters of an API method with values specific to this library.
694
695 Specifically, adds whatever global parameters are specified by the API to the
696 parameters for the individual method. Also adds parameters which don't
697 appear in the discovery document, but are available to all discovery based
698 APIs (these are listed in STACK_QUERY_PARAMETERS).
699
700 SIDE EFFECTS: This updates the parameters dictionary object in the method
701 description.
702
703 Args:
704 method_desc: Dictionary with metadata describing an API method. Value comes
705 from the dictionary of methods stored in the 'methods' key in the
706 deserialized discovery document.
707 root_desc: Dictionary; the entire original deserialized discovery document.
708 http_method: String; the HTTP method used to call the API method described
709 in method_desc.
710 schema: Object, mapping of schema names to schema descriptions.
711
712 Returns:
713 The updated Dictionary stored in the 'parameters' key of the method
714 description dictionary.
715 """
716 parameters = method_desc.setdefault("parameters", {})
717
718
719 for name, description in six.iteritems(root_desc.get("parameters", {})):
720 parameters[name] = description
721
722
723 for name in STACK_QUERY_PARAMETERS:
724 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
725
726
727
728 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
729 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
730 body.update(method_desc["request"])
731 parameters["body"] = body
732
733 return parameters
734
775
778 """Updates a method description in a discovery document.
779
780 SIDE EFFECTS: Changes the parameters dictionary in the method description with
781 extra parameters which are used locally.
782
783 Args:
784 method_desc: Dictionary with metadata describing an API method. Value comes
785 from the dictionary of methods stored in the 'methods' key in the
786 deserialized discovery document.
787 root_desc: Dictionary; the entire original deserialized discovery document.
788 schema: Object, mapping of schema names to schema descriptions.
789
790 Returns:
791 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
792 where:
793 - path_url is a String; the relative URL for the API method. Relative to
794 the API root, which is specified in the discovery document.
795 - http_method is a String; the HTTP method used to call the API method
796 described in the method description.
797 - method_id is a String; the name of the RPC method associated with the
798 API method, and is in the method description in the 'id' key.
799 - accept is a list of strings representing what content types are
800 accepted for media upload. Defaults to empty list if not in the
801 discovery document.
802 - max_size is a long representing the max size in bytes allowed for a
803 media upload. Defaults to 0L if not in the discovery document.
804 - media_path_url is a String; the absolute URI for media upload for the
805 API method. Constructed using the API root URI and service path from
806 the discovery document and the relative path for the API method. If
807 media upload is not supported, this is None.
808 """
809 path_url = method_desc["path"]
810 http_method = method_desc["httpMethod"]
811 method_id = method_desc["id"]
812
813 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
814
815
816
817 accept, max_size, media_path_url = _fix_up_media_upload(
818 method_desc, root_desc, path_url, parameters
819 )
820
821 return path_url, http_method, method_id, accept, max_size, media_path_url
822
825 """Custom urljoin replacement supporting : before / in url."""
826
827
828
829
830
831
832
833
834 if url.startswith("http://") or url.startswith("https://"):
835 return urljoin(base, url)
836 new_base = base if base.endswith("/") else base + "/"
837 new_url = url[1:] if url.startswith("/") else url
838 return new_base + new_url
839
843 """Represents the parameters associated with a method.
844
845 Attributes:
846 argmap: Map from method parameter name (string) to query parameter name
847 (string).
848 required_params: List of required parameters (represented by parameter
849 name as string).
850 repeated_params: List of repeated parameters (represented by parameter
851 name as string).
852 pattern_params: Map from method parameter name (string) to regular
853 expression (as a string). If the pattern is set for a parameter, the
854 value for that parameter must match the regular expression.
855 query_params: List of parameters (represented by parameter name as string)
856 that will be used in the query string.
857 path_params: Set of parameters (represented by parameter name as string)
858 that will be used in the base URL path.
859 param_types: Map from method parameter name (string) to parameter type. Type
860 can be any valid JSON schema type; valid values are 'any', 'array',
861 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
862 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
863 enum_params: Map from method parameter name (string) to list of strings,
864 where each list of strings is the list of acceptable enum values.
865 """
866
868 """Constructor for ResourceMethodParameters.
869
870 Sets default values and defers to set_parameters to populate.
871
872 Args:
873 method_desc: Dictionary with metadata describing an API method. Value
874 comes from the dictionary of methods stored in the 'methods' key in
875 the deserialized discovery document.
876 """
877 self.argmap = {}
878 self.required_params = []
879 self.repeated_params = []
880 self.pattern_params = {}
881 self.query_params = []
882
883
884 self.path_params = set()
885 self.param_types = {}
886 self.enum_params = {}
887
888 self.set_parameters(method_desc)
889
891 """Populates maps and lists based on method description.
892
893 Iterates through each parameter for the method and parses the values from
894 the parameter dictionary.
895
896 Args:
897 method_desc: Dictionary with metadata describing an API method. Value
898 comes from the dictionary of methods stored in the 'methods' key in
899 the deserialized discovery document.
900 """
901 for arg, desc in six.iteritems(method_desc.get("parameters", {})):
902 param = key2param(arg)
903 self.argmap[param] = arg
904
905 if desc.get("pattern"):
906 self.pattern_params[param] = desc["pattern"]
907 if desc.get("enum"):
908 self.enum_params[param] = desc["enum"]
909 if desc.get("required"):
910 self.required_params.append(param)
911 if desc.get("repeated"):
912 self.repeated_params.append(param)
913 if desc.get("location") == "query":
914 self.query_params.append(param)
915 if desc.get("location") == "path":
916 self.path_params.add(param)
917 self.param_types[param] = desc.get("type", "string")
918
919
920
921
922 for match in URITEMPLATE.finditer(method_desc["path"]):
923 for namematch in VARNAME.finditer(match.group(0)):
924 name = key2param(namematch.group(0))
925 self.path_params.add(name)
926 if name in self.query_params:
927 self.query_params.remove(name)
928
929
930 -def createMethod(methodName, methodDesc, rootDesc, schema):
931 """Creates a method for attaching to a Resource.
932
933 Args:
934 methodName: string, name of the method to use.
935 methodDesc: object, fragment of deserialized discovery document that
936 describes the method.
937 rootDesc: object, the entire deserialized discovery document.
938 schema: object, mapping of schema names to schema descriptions.
939 """
940 methodName = fix_method_name(methodName)
941 (
942 pathUrl,
943 httpMethod,
944 methodId,
945 accept,
946 maxSize,
947 mediaPathUrl,
948 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
949
950 parameters = ResourceMethodParameters(methodDesc)
951
952 def method(self, **kwargs):
953
954
955 for name in six.iterkeys(kwargs):
956 if name not in parameters.argmap:
957 raise TypeError('Got an unexpected keyword argument "%s"' % name)
958
959
960 keys = list(kwargs.keys())
961 for name in keys:
962 if kwargs[name] is None:
963 del kwargs[name]
964
965 for name in parameters.required_params:
966 if name not in kwargs:
967
968
969 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
970 _methodProperties(methodDesc, schema, "response")
971 ):
972 raise TypeError('Missing required parameter "%s"' % name)
973
974 for name, regex in six.iteritems(parameters.pattern_params):
975 if name in kwargs:
976 if isinstance(kwargs[name], six.string_types):
977 pvalues = [kwargs[name]]
978 else:
979 pvalues = kwargs[name]
980 for pvalue in pvalues:
981 if re.match(regex, pvalue) is None:
982 raise TypeError(
983 'Parameter "%s" value "%s" does not match the pattern "%s"'
984 % (name, pvalue, regex)
985 )
986
987 for name, enums in six.iteritems(parameters.enum_params):
988 if name in kwargs:
989
990
991
992 if name in parameters.repeated_params and not isinstance(
993 kwargs[name], six.string_types
994 ):
995 values = kwargs[name]
996 else:
997 values = [kwargs[name]]
998 for value in values:
999 if value not in enums:
1000 raise TypeError(
1001 'Parameter "%s" value "%s" is not an allowed value in "%s"'
1002 % (name, value, str(enums))
1003 )
1004
1005 actual_query_params = {}
1006 actual_path_params = {}
1007 for key, value in six.iteritems(kwargs):
1008 to_type = parameters.param_types.get(key, "string")
1009
1010 if key in parameters.repeated_params and type(value) == type([]):
1011 cast_value = [_cast(x, to_type) for x in value]
1012 else:
1013 cast_value = _cast(value, to_type)
1014 if key in parameters.query_params:
1015 actual_query_params[parameters.argmap[key]] = cast_value
1016 if key in parameters.path_params:
1017 actual_path_params[parameters.argmap[key]] = cast_value
1018 body_value = kwargs.get("body", None)
1019 media_filename = kwargs.get("media_body", None)
1020 media_mime_type = kwargs.get("media_mime_type", None)
1021
1022 if self._developerKey:
1023 actual_query_params["key"] = self._developerKey
1024
1025 model = self._model
1026 if methodName.endswith("_media"):
1027 model = MediaModel()
1028 elif "response" not in methodDesc:
1029 model = RawModel()
1030
1031 headers = {}
1032 headers, params, query, body = model.request(
1033 headers, actual_path_params, actual_query_params, body_value
1034 )
1035
1036 expanded_url = uritemplate.expand(pathUrl, params)
1037 url = _urljoin(self._baseUrl, expanded_url + query)
1038
1039 resumable = None
1040 multipart_boundary = ""
1041
1042 if media_filename:
1043
1044 if isinstance(media_filename, six.string_types):
1045 if media_mime_type is None:
1046 logger.warning(
1047 "media_mime_type argument not specified: trying to auto-detect for %s",
1048 media_filename,
1049 )
1050 media_mime_type, _ = mimetypes.guess_type(media_filename)
1051 if media_mime_type is None:
1052 raise UnknownFileType(media_filename)
1053 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
1054 raise UnacceptableMimeTypeError(media_mime_type)
1055 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
1056 elif isinstance(media_filename, MediaUpload):
1057 media_upload = media_filename
1058 else:
1059 raise TypeError("media_filename must be str or MediaUpload.")
1060
1061
1062 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
1063 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
1064
1065
1066 expanded_url = uritemplate.expand(mediaPathUrl, params)
1067 url = _urljoin(self._baseUrl, expanded_url + query)
1068 if media_upload.resumable():
1069 url = _add_query_parameter(url, "uploadType", "resumable")
1070
1071 if media_upload.resumable():
1072
1073
1074 resumable = media_upload
1075 else:
1076
1077 if body is None:
1078
1079 headers["content-type"] = media_upload.mimetype()
1080 body = media_upload.getbytes(0, media_upload.size())
1081 url = _add_query_parameter(url, "uploadType", "media")
1082 else:
1083
1084 msgRoot = MIMEMultipart("related")
1085
1086 setattr(msgRoot, "_write_headers", lambda self: None)
1087
1088
1089 msg = MIMENonMultipart(*headers["content-type"].split("/"))
1090 msg.set_payload(body)
1091 msgRoot.attach(msg)
1092
1093
1094 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
1095 msg["Content-Transfer-Encoding"] = "binary"
1096
1097 payload = media_upload.getbytes(0, media_upload.size())
1098 msg.set_payload(payload)
1099 msgRoot.attach(msg)
1100
1101
1102 fp = BytesIO()
1103 g = _BytesGenerator(fp, mangle_from_=False)
1104 g.flatten(msgRoot, unixfrom=False)
1105 body = fp.getvalue()
1106
1107 multipart_boundary = msgRoot.get_boundary()
1108 headers["content-type"] = (
1109 "multipart/related; " 'boundary="%s"'
1110 ) % multipart_boundary
1111 url = _add_query_parameter(url, "uploadType", "multipart")
1112
1113 logger.debug("URL being requested: %s %s" % (httpMethod, url))
1114 return self._requestBuilder(
1115 self._http,
1116 model.response,
1117 url,
1118 method=httpMethod,
1119 body=body,
1120 headers=headers,
1121 methodId=methodId,
1122 resumable=resumable,
1123 )
1124
1125 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
1126 if len(parameters.argmap) > 0:
1127 docs.append("Args:\n")
1128
1129
1130 skip_parameters = list(rootDesc.get("parameters", {}).keys())
1131 skip_parameters.extend(STACK_QUERY_PARAMETERS)
1132
1133 all_args = list(parameters.argmap.keys())
1134 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
1135
1136
1137 if "body" in all_args:
1138 args_ordered.append("body")
1139
1140 for name in all_args:
1141 if name not in args_ordered:
1142 args_ordered.append(name)
1143
1144 for arg in args_ordered:
1145 if arg in skip_parameters:
1146 continue
1147
1148 repeated = ""
1149 if arg in parameters.repeated_params:
1150 repeated = " (repeated)"
1151 required = ""
1152 if arg in parameters.required_params:
1153 required = " (required)"
1154 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
1155 paramdoc = paramdesc.get("description", "A parameter")
1156 if "$ref" in paramdesc:
1157 docs.append(
1158 (" %s: object, %s%s%s\n The object takes the" " form of:\n\n%s\n\n")
1159 % (
1160 arg,
1161 paramdoc,
1162 required,
1163 repeated,
1164 schema.prettyPrintByName(paramdesc["$ref"]),
1165 )
1166 )
1167 else:
1168 paramtype = paramdesc.get("type", "string")
1169 docs.append(
1170 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
1171 )
1172 enum = paramdesc.get("enum", [])
1173 enumDesc = paramdesc.get("enumDescriptions", [])
1174 if enum and enumDesc:
1175 docs.append(" Allowed values\n")
1176 for (name, desc) in zip(enum, enumDesc):
1177 docs.append(" %s - %s\n" % (name, desc))
1178 if "response" in methodDesc:
1179 if methodName.endswith("_media"):
1180 docs.append("\nReturns:\n The media object as a string.\n\n ")
1181 else:
1182 docs.append("\nReturns:\n An object of the form:\n\n ")
1183 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
1184
1185 setattr(method, "__doc__", "".join(docs))
1186 return (methodName, method)
1187
1188
1189 -def createNextMethod(
1190 methodName,
1191 pageTokenName="pageToken",
1192 nextPageTokenName="nextPageToken",
1193 isPageTokenParameter=True,
1194 ):
1195 """Creates any _next methods for attaching to a Resource.
1196
1197 The _next methods allow for easy iteration through list() responses.
1198
1199 Args:
1200 methodName: string, name of the method to use.
1201 pageTokenName: string, name of request page token field.
1202 nextPageTokenName: string, name of response page token field.
1203 isPageTokenParameter: Boolean, True if request page token is a query
1204 parameter, False if request page token is a field of the request body.
1205 """
1206 methodName = fix_method_name(methodName)
1207
1208 def methodNext(self, previous_request, previous_response):
1209 """Retrieves the next page of results.
1210
1211 Args:
1212 previous_request: The request for the previous page. (required)
1213 previous_response: The response from the request for the previous page. (required)
1214
1215 Returns:
1216 A request object that you can call 'execute()' on to request the next
1217 page. Returns None if there are no more items in the collection.
1218 """
1219
1220
1221
1222 nextPageToken = previous_response.get(nextPageTokenName, None)
1223 if not nextPageToken:
1224 return None
1225
1226 request = copy.copy(previous_request)
1227
1228 if isPageTokenParameter:
1229
1230 request.uri = _add_query_parameter(
1231 request.uri, pageTokenName, nextPageToken
1232 )
1233 logger.debug("Next page request URL: %s %s" % (methodName, request.uri))
1234 else:
1235
1236 model = self._model
1237 body = model.deserialize(request.body)
1238 body[pageTokenName] = nextPageToken
1239 request.body = model.serialize(body)
1240 logger.debug("Next page request body: %s %s" % (methodName, body))
1241
1242 return request
1243
1244 return (methodName, methodNext)
1245
1248 """A class for interacting with a resource."""
1249
1250 - def __init__(
1251 self,
1252 http,
1253 baseUrl,
1254 model,
1255 requestBuilder,
1256 developerKey,
1257 resourceDesc,
1258 rootDesc,
1259 schema,
1260 ):
1261 """Build a Resource from the API description.
1262
1263 Args:
1264 http: httplib2.Http, Object to make http requests with.
1265 baseUrl: string, base URL for the API. All requests are relative to this
1266 URI.
1267 model: googleapiclient.Model, converts to and from the wire format.
1268 requestBuilder: class or callable that instantiates an
1269 googleapiclient.HttpRequest object.
1270 developerKey: string, key obtained from
1271 https://code.google.com/apis/console
1272 resourceDesc: object, section of deserialized discovery document that
1273 describes a resource. Note that the top level discovery document
1274 is considered a resource.
1275 rootDesc: object, the entire deserialized discovery document.
1276 schema: object, mapping of schema names to schema descriptions.
1277 """
1278 self._dynamic_attrs = []
1279
1280 self._http = http
1281 self._baseUrl = baseUrl
1282 self._model = model
1283 self._developerKey = developerKey
1284 self._requestBuilder = requestBuilder
1285 self._resourceDesc = resourceDesc
1286 self._rootDesc = rootDesc
1287 self._schema = schema
1288
1289 self._set_service_methods()
1290
1292 """Sets an instance attribute and tracks it in a list of dynamic attributes.
1293
1294 Args:
1295 attr_name: string; The name of the attribute to be set
1296 value: The value being set on the object and tracked in the dynamic cache.
1297 """
1298 self._dynamic_attrs.append(attr_name)
1299 self.__dict__[attr_name] = value
1300
1302 """Trim the state down to something that can be pickled.
1303
1304 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1305 will be wiped and restored on pickle serialization.
1306 """
1307 state_dict = copy.copy(self.__dict__)
1308 for dynamic_attr in self._dynamic_attrs:
1309 del state_dict[dynamic_attr]
1310 del state_dict["_dynamic_attrs"]
1311 return state_dict
1312
1314 """Reconstitute the state of the object from being pickled.
1315
1316 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1317 will be wiped and restored on pickle serialization.
1318 """
1319 self.__dict__.update(state)
1320 self._dynamic_attrs = []
1321 self._set_service_methods()
1322
1323
1326
1327 - def __exit__(self, exc_type, exc, exc_tb):
1329
1331 """Close httplib2 connections."""
1332
1333
1334
1335 self._http.http.close()
1336
1341
1343
1344 if resourceDesc == rootDesc:
1345 batch_uri = "%s%s" % (
1346 rootDesc["rootUrl"],
1347 rootDesc.get("batchPath", "batch"),
1348 )
1349
1350 def new_batch_http_request(callback=None):
1351 """Create a BatchHttpRequest object based on the discovery document.
1352
1353 Args:
1354 callback: callable, A callback to be called for each response, of the
1355 form callback(id, response, exception). The first parameter is the
1356 request id, and the second is the deserialized response object. The
1357 third is an apiclient.errors.HttpError exception object if an HTTP
1358 error occurred while processing the request, or None if no error
1359 occurred.
1360
1361 Returns:
1362 A BatchHttpRequest object based on the discovery document.
1363 """
1364 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
1365
1366 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
1367
1368
1369 if "methods" in resourceDesc:
1370 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1371 fixedMethodName, method = createMethod(
1372 methodName, methodDesc, rootDesc, schema
1373 )
1374 self._set_dynamic_attr(
1375 fixedMethodName, method.__get__(self, self.__class__)
1376 )
1377
1378
1379 if methodDesc.get("supportsMediaDownload", False):
1380 fixedMethodName, method = createMethod(
1381 methodName + "_media", methodDesc, rootDesc, schema
1382 )
1383 self._set_dynamic_attr(
1384 fixedMethodName, method.__get__(self, self.__class__)
1385 )
1386
1388
1389 if "resources" in resourceDesc:
1390
1391 def createResourceMethod(methodName, methodDesc):
1392 """Create a method on the Resource to access a nested Resource.
1393
1394 Args:
1395 methodName: string, name of the method to use.
1396 methodDesc: object, fragment of deserialized discovery document that
1397 describes the method.
1398 """
1399 methodName = fix_method_name(methodName)
1400
1401 def methodResource(self):
1402 return Resource(
1403 http=self._http,
1404 baseUrl=self._baseUrl,
1405 model=self._model,
1406 developerKey=self._developerKey,
1407 requestBuilder=self._requestBuilder,
1408 resourceDesc=methodDesc,
1409 rootDesc=rootDesc,
1410 schema=schema,
1411 )
1412
1413 setattr(methodResource, "__doc__", "A collection resource.")
1414 setattr(methodResource, "__is_resource__", True)
1415
1416 return (methodName, methodResource)
1417
1418 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1419 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1420 self._set_dynamic_attr(
1421 fixedMethodName, method.__get__(self, self.__class__)
1422 )
1423
1425
1426
1427
1428 if "methods" not in resourceDesc:
1429 return
1430 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1431 nextPageTokenName = _findPageTokenName(
1432 _methodProperties(methodDesc, schema, "response")
1433 )
1434 if not nextPageTokenName:
1435 continue
1436 isPageTokenParameter = True
1437 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1438 if not pageTokenName:
1439 isPageTokenParameter = False
1440 pageTokenName = _findPageTokenName(
1441 _methodProperties(methodDesc, schema, "request")
1442 )
1443 if not pageTokenName:
1444 continue
1445 fixedMethodName, method = createNextMethod(
1446 methodName + "_next",
1447 pageTokenName,
1448 nextPageTokenName,
1449 isPageTokenParameter,
1450 )
1451 self._set_dynamic_attr(
1452 fixedMethodName, method.__get__(self, self.__class__)
1453 )
1454
1455
1456 -def _findPageTokenName(fields):
1457 """Search field names for one like a page token.
1458
1459 Args:
1460 fields: container of string, names of fields.
1461
1462 Returns:
1463 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1464 otherwise None.
1465 """
1466 return next(
1467 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1468 )
1469
1472 """Get properties of a field in a method description.
1473
1474 Args:
1475 methodDesc: object, fragment of deserialized discovery document that
1476 describes the method.
1477 schema: object, mapping of schema names to schema descriptions.
1478 name: string, name of top-level field in method description.
1479
1480 Returns:
1481 Object representing fragment of deserialized discovery document
1482 corresponding to 'properties' field of object corresponding to named field
1483 in method description, if it exists, otherwise empty dict.
1484 """
1485 desc = methodDesc.get(name, {})
1486 if "$ref" in desc:
1487 desc = schema.get(desc["$ref"], {})
1488 return desc.get("properties", {})
1489