blob: 87403b9e80d09f5c911c139d021633e0636f482f [file] [log] [blame]
Craig Citro751b7fb2014-09-23 11:20:38 -07001# Copyright 2014 Google Inc. All Rights Reserved.
John Asmuth864311d2014-04-24 15:46:08 -04002#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Client for discovery based APIs.
16
17A client library for Google's discovery based APIs.
18"""
INADA Naoki0bceb332014-08-20 15:27:52 +090019from __future__ import absolute_import
INADA Naokie4ea1a92015-03-04 03:45:42 +090020import six
21from six.moves import zip
John Asmuth864311d2014-04-24 15:46:08 -040022
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070023__author__ = "jcgregorio@google.com (Joe Gregorio)"
24__all__ = ["build", "build_from_document", "fix_method_name", "key2param"]
John Asmuth864311d2014-04-24 15:46:08 -040025
Phil Ruffwind26178fc2015-10-13 19:00:33 -040026from six import BytesIO
Takashi Matsuo3772f9d2015-09-04 12:25:55 -070027from six.moves import http_client
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070028from six.moves.urllib.parse import urlencode, urlparse, urljoin, urlunparse, parse_qsl
John Asmuth864311d2014-04-24 15:46:08 -040029
30# Standard library imports
31import copy
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070032
Phil Ruffwind26178fc2015-10-13 19:00:33 -040033try:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070034 from email.generator import BytesGenerator
Phil Ruffwind26178fc2015-10-13 19:00:33 -040035except ImportError:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070036 from email.generator import Generator as BytesGenerator
John Asmuth864311d2014-04-24 15:46:08 -040037from email.mime.multipart import MIMEMultipart
38from email.mime.nonmultipart import MIMENonMultipart
Craig Citro6ae34d72014-08-18 23:10:09 -070039import json
John Asmuth864311d2014-04-24 15:46:08 -040040import keyword
41import logging
42import mimetypes
43import os
44import re
John Asmuth864311d2014-04-24 15:46:08 -040045
46# Third-party imports
47import httplib2
John Asmuth864311d2014-04-24 15:46:08 -040048import uritemplate
49
50# Local imports
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -080051from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080052from googleapiclient import mimeparse
John Asmuth864311d2014-04-24 15:46:08 -040053from googleapiclient.errors import HttpError
54from googleapiclient.errors import InvalidJsonError
55from googleapiclient.errors import MediaUploadSizeError
56from googleapiclient.errors import UnacceptableMimeTypeError
57from googleapiclient.errors import UnknownApiNameOrVersion
58from googleapiclient.errors import UnknownFileType
Igor Maravić22435292017-01-19 22:28:22 +010059from googleapiclient.http import build_http
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -040060from googleapiclient.http import BatchHttpRequest
Kostyantyn Leschenkobe8b1cb2016-10-17 12:57:21 +030061from googleapiclient.http import HttpMock
62from googleapiclient.http import HttpMockSequence
John Asmuth864311d2014-04-24 15:46:08 -040063from googleapiclient.http import HttpRequest
64from googleapiclient.http import MediaFileUpload
65from googleapiclient.http import MediaUpload
66from googleapiclient.model import JsonModel
67from googleapiclient.model import MediaModel
68from googleapiclient.model import RawModel
69from googleapiclient.schema import Schemas
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070070
Helen Koikede13e3b2018-04-26 16:05:16 -030071from googleapiclient._helpers import _add_query_parameter
72from googleapiclient._helpers import positional
John Asmuth864311d2014-04-24 15:46:08 -040073
74
75# The client library requires a version of httplib2 that supports RETRIES.
76httplib2.RETRIES = 1
77
78logger = logging.getLogger(__name__)
79
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070080URITEMPLATE = re.compile("{[^}]*}")
81VARNAME = re.compile("[a-zA-Z0-9_-]+")
82DISCOVERY_URI = (
83 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
84)
Ethan Bao12b7cd32016-03-14 14:25:10 -070085V1_DISCOVERY_URI = DISCOVERY_URI
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070086V2_DISCOVERY_URI = (
87 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
88)
89DEFAULT_METHOD_DOC = "A description of how to use this function"
90HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
Igor Maravić22435292017-01-19 22:28:22 +010091
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070092_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
93BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
John Asmuth864311d2014-04-24 15:46:08 -040094MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070095 "description": (
96 "The filename of the media request body, or an instance "
97 "of a MediaUpload object."
98 ),
99 "type": "string",
100 "required": False,
John Asmuth864311d2014-04-24 15:46:08 -0400101}
Brian J. Watson38051ac2016-10-25 07:53:08 -0700102MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700103 "description": (
104 "The MIME type of the media request body, or an instance "
105 "of a MediaUpload object."
106 ),
107 "type": "string",
108 "required": False,
Brian J. Watson38051ac2016-10-25 07:53:08 -0700109}
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700110_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
John Asmuth864311d2014-04-24 15:46:08 -0400111
112# Parameters accepted by the stack, but not visible via discovery.
113# TODO(dhermes): Remove 'userip' in 'v2'.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700114STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
115STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
John Asmuth864311d2014-04-24 15:46:08 -0400116
117# Library-specific reserved words beyond Python keywords.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700118RESERVED_WORDS = frozenset(["body"])
John Asmuth864311d2014-04-24 15:46:08 -0400119
Phil Ruffwind26178fc2015-10-13 19:00:33 -0400120# patch _write_lines to avoid munging '\r' into '\n'
121# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
122class _BytesGenerator(BytesGenerator):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700123 _write_lines = BytesGenerator.write
124
John Asmuth864311d2014-04-24 15:46:08 -0400125
126def fix_method_name(name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700127 """Fix method names to avoid '$' characters and reserved word conflicts.
John Asmuth864311d2014-04-24 15:46:08 -0400128
129 Args:
130 name: string, method name.
131
132 Returns:
Bu Sun Kim84cbc072019-01-25 15:49:52 -0800133 The name with '_' appended if the name is a reserved word and '$'
134 replaced with '_'.
John Asmuth864311d2014-04-24 15:46:08 -0400135 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700136 name = name.replace("$", "_")
137 if keyword.iskeyword(name) or name in RESERVED_WORDS:
138 return name + "_"
139 else:
140 return name
John Asmuth864311d2014-04-24 15:46:08 -0400141
142
143def key2param(key):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700144 """Converts key names into parameter names.
John Asmuth864311d2014-04-24 15:46:08 -0400145
146 For example, converting "max-results" -> "max_results"
147
148 Args:
149 key: string, the method key name.
150
151 Returns:
152 A safe method name based on the key name.
153 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700154 result = []
155 key = list(key)
156 if not key[0].isalpha():
157 result.append("x")
158 for c in key:
159 if c.isalnum():
160 result.append(c)
161 else:
162 result.append("_")
John Asmuth864311d2014-04-24 15:46:08 -0400163
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700164 return "".join(result)
John Asmuth864311d2014-04-24 15:46:08 -0400165
166
167@positional(2)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700168def build(
169 serviceName,
170 version,
171 http=None,
172 discoveryServiceUrl=DISCOVERY_URI,
173 developerKey=None,
174 model=None,
175 requestBuilder=HttpRequest,
176 credentials=None,
177 cache_discovery=True,
178 cache=None,
179):
180 """Construct a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400181
182 Construct a Resource object for interacting with an API. The serviceName and
183 version are the names from the Discovery service.
184
185 Args:
186 serviceName: string, name of the service.
187 version: string, the version of the service.
188 http: httplib2.Http, An instance of httplib2.Http or something that acts
189 like it that HTTP requests will be made through.
190 discoveryServiceUrl: string, a URI Template that points to the location of
191 the discovery service. It should have two parameters {api} and
192 {apiVersion} that when filled in produce an absolute URI to the discovery
193 document for that service.
194 developerKey: string, key obtained from
195 https://code.google.com/apis/console.
196 model: googleapiclient.Model, converts to and from the wire format.
197 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
198 request.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800199 credentials: oauth2client.Credentials or
200 google.auth.credentials.Credentials, credentials to be used for
Orest Bolohane92c9002014-05-30 11:15:43 -0700201 authentication.
Takashi Matsuo30125122015-08-19 11:42:32 -0700202 cache_discovery: Boolean, whether or not to cache the discovery doc.
203 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
204 cache object for the discovery documents.
John Asmuth864311d2014-04-24 15:46:08 -0400205
206 Returns:
207 A Resource object with methods for interacting with the service.
208 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700209 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400210
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700211 if http is None:
212 discovery_http = build_http()
213 else:
214 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400215
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700216 for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI):
217 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400218
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700219 try:
220 content = _retrieve_discovery_doc(
221 requested_url, discovery_http, cache_discovery, cache, developerKey
222 )
223 return build_from_document(
224 content,
225 base=discovery_url,
226 http=http,
227 developerKey=developerKey,
228 model=model,
229 requestBuilder=requestBuilder,
230 credentials=credentials,
231 )
232 except HttpError as e:
233 if e.resp.status == http_client.NOT_FOUND:
234 continue
235 else:
236 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700237
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700238 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
Takashi Matsuo30125122015-08-19 11:42:32 -0700239
240
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700241def _retrieve_discovery_doc(url, http, cache_discovery, cache=None, developerKey=None):
242 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700243
244 Args:
245 url: string, the URL of the discovery document.
246 http: httplib2.Http, An instance of httplib2.Http or something that acts
247 like it through which HTTP requests will be made.
248 cache_discovery: Boolean, whether or not to cache the discovery doc.
249 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
250 object for the discovery documents.
251
252 Returns:
253 A unicode string representation of the discovery document.
254 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700255 if cache_discovery:
256 from . import discovery_cache
257 from .discovery_cache import base
Takashi Matsuo30125122015-08-19 11:42:32 -0700258
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700259 if cache is None:
260 cache = discovery_cache.autodetect()
261 if cache:
262 content = cache.get(url)
263 if content:
264 return content
John Asmuth864311d2014-04-24 15:46:08 -0400265
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700266 actual_url = url
267 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
268 # variable that contains the network address of the client sending the
269 # request. If it exists then add that to the request for the discovery
270 # document to avoid exceeding the quota on discovery requests.
271 if "REMOTE_ADDR" in os.environ:
272 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
273 if developerKey:
274 actual_url = _add_query_parameter(url, "key", developerKey)
275 logger.info("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400276
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700277 resp, content = http.request(actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400278
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700279 if resp.status >= 400:
280 raise HttpError(resp, content, uri=actual_url)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800281
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700282 try:
283 content = content.decode("utf-8")
284 except AttributeError:
285 pass
286
287 try:
288 service = json.loads(content)
289 except ValueError as e:
290 logger.error("Failed to parse as JSON: " + content)
291 raise InvalidJsonError()
292 if cache_discovery and cache:
293 cache.set(url, content)
294 return content
John Asmuth864311d2014-04-24 15:46:08 -0400295
296
297@positional(1)
298def build_from_document(
299 service,
300 base=None,
301 future=None,
302 http=None,
303 developerKey=None,
304 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700305 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700306 credentials=None,
307):
308 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400309
310 Same as `build()`, but constructs the Resource object from a discovery
311 document that is it given, as opposed to retrieving one over HTTP.
312
313 Args:
314 service: string or object, the JSON discovery document describing the API.
315 The value passed in may either be the JSON string or the deserialized
316 JSON.
317 base: string, base URI for all HTTP requests, usually the discovery URI.
318 This parameter is no longer used as rootUrl and servicePath are included
319 within the discovery document. (deprecated)
320 future: string, discovery document with future capabilities (deprecated).
321 http: httplib2.Http, An instance of httplib2.Http or something that acts
322 like it that HTTP requests will be made through.
323 developerKey: string, Key for controlling API usage, generated
324 from the API Console.
325 model: Model class instance that serializes and de-serializes requests and
326 responses.
327 requestBuilder: Takes an http request and packages it up to be executed.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800328 credentials: oauth2client.Credentials or
329 google.auth.credentials.Credentials, credentials to be used for
330 authentication.
John Asmuth864311d2014-04-24 15:46:08 -0400331
332 Returns:
333 A Resource object with methods for interacting with the service.
334 """
335
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700336 if http is not None and credentials is not None:
337 raise ValueError("Arguments http and credentials are mutually exclusive.")
John Asmuth864311d2014-04-24 15:46:08 -0400338
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700339 if isinstance(service, six.string_types):
340 service = json.loads(service)
341 elif isinstance(service, six.binary_type):
342 service = json.loads(service.decode("utf-8"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400343
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700344 if "rootUrl" not in service and (isinstance(http, (HttpMock, HttpMockSequence))):
345 logger.error(
346 "You are using HttpMock or HttpMockSequence without"
347 + "having the service discovery doc in cache. Try calling "
348 + "build() without mocking once first to populate the "
349 + "cache."
350 )
351 raise InvalidJsonError()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400352
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700353 base = urljoin(service["rootUrl"], service["servicePath"])
354 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400355
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700356 # If the http client is not specified, then we must construct an http client
357 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800358 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700359 if http is None:
360 # Does the service require scopes?
361 scopes = list(
362 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
363 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700364
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700365 # If so, then the we need to setup authentication if no developerKey is
366 # specified.
367 if scopes and not developerKey:
368 # If the user didn't pass in credentials, attempt to acquire application
369 # default credentials.
370 if credentials is None:
371 credentials = _auth.default_credentials()
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800372
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700373 # The credentials need to be scoped.
374 credentials = _auth.with_scopes(credentials, scopes)
375
376 # If credentials are provided, create an authorized http instance;
377 # otherwise, skip authentication.
378 if credentials:
379 http = _auth.authorized_http(credentials)
380
381 # If the service doesn't require scopes then there is no need for
382 # authentication.
383 else:
384 http = build_http()
385
386 if model is None:
387 features = service.get("features", [])
388 model = JsonModel("dataWrapper" in features)
389
390 return Resource(
391 http=http,
392 baseUrl=base,
393 model=model,
394 developerKey=developerKey,
395 requestBuilder=requestBuilder,
396 resourceDesc=service,
397 rootDesc=service,
398 schema=schema,
399 )
John Asmuth864311d2014-04-24 15:46:08 -0400400
401
402def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700403 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400404
405 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
406 JSON Schema.
407
408 Args:
409 value: any, the value to convert
410 schema_type: string, the type that value should be interpreted as
411
412 Returns:
413 A string representation of 'value' based on the schema_type.
414 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700415 if schema_type == "string":
416 if type(value) == type("") or type(value) == type(u""):
417 return value
418 else:
419 return str(value)
420 elif schema_type == "integer":
421 return str(int(value))
422 elif schema_type == "number":
423 return str(float(value))
424 elif schema_type == "boolean":
425 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400426 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700427 if type(value) == type("") or type(value) == type(u""):
428 return value
429 else:
430 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400431
432
433def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700434 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400435
436 Args:
437 maxSize: string, size as a string, such as 2MB or 7GB.
438
439 Returns:
440 The size as an integer value.
441 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700442 if len(maxSize) < 2:
443 return 0
444 units = maxSize[-2:].upper()
445 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
446 if bit_shift is not None:
447 return int(maxSize[:-2]) << bit_shift
448 else:
449 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400450
451
452def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700453 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400454
455 Constructed using the API root URI and service path from the discovery
456 document and the relative path for the API method.
457
458 Args:
459 root_desc: Dictionary; the entire original deserialized discovery document.
460 path_url: String; the relative URL for the API method. Relative to the API
461 root, which is specified in the discovery document.
462
463 Returns:
464 String; the absolute URI for media upload for the API method.
465 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700466 return "%(root)supload/%(service_path)s%(path)s" % {
467 "root": root_desc["rootUrl"],
468 "service_path": root_desc["servicePath"],
469 "path": path_url,
470 }
John Asmuth864311d2014-04-24 15:46:08 -0400471
472
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900473def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700474 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400475
476 Specifically, adds whatever global parameters are specified by the API to the
477 parameters for the individual method. Also adds parameters which don't
478 appear in the discovery document, but are available to all discovery based
479 APIs (these are listed in STACK_QUERY_PARAMETERS).
480
481 SIDE EFFECTS: This updates the parameters dictionary object in the method
482 description.
483
484 Args:
485 method_desc: Dictionary with metadata describing an API method. Value comes
486 from the dictionary of methods stored in the 'methods' key in the
487 deserialized discovery document.
488 root_desc: Dictionary; the entire original deserialized discovery document.
489 http_method: String; the HTTP method used to call the API method described
490 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900491 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400492
493 Returns:
494 The updated Dictionary stored in the 'parameters' key of the method
495 description dictionary.
496 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700497 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400498
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700499 # Add in the parameters common to all methods.
500 for name, description in six.iteritems(root_desc.get("parameters", {})):
501 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400502
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700503 # Add in undocumented query parameters.
504 for name in STACK_QUERY_PARAMETERS:
505 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400506
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700507 # Add 'body' (our own reserved word) to parameters if the method supports
508 # a request payload.
509 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
510 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
511 body.update(method_desc["request"])
512 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400513
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700514 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400515
516
517def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700518 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400519
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700520 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
521 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400522
523 Args:
524 method_desc: Dictionary with metadata describing an API method. Value comes
525 from the dictionary of methods stored in the 'methods' key in the
526 deserialized discovery document.
527 root_desc: Dictionary; the entire original deserialized discovery document.
528 path_url: String; the relative URL for the API method. Relative to the API
529 root, which is specified in the discovery document.
530 parameters: A dictionary describing method parameters for method described
531 in method_desc.
532
533 Returns:
534 Triple (accept, max_size, media_path_url) where:
535 - accept is a list of strings representing what content types are
536 accepted for media upload. Defaults to empty list if not in the
537 discovery document.
538 - max_size is a long representing the max size in bytes allowed for a
539 media upload. Defaults to 0L if not in the discovery document.
540 - media_path_url is a String; the absolute URI for media upload for the
541 API method. Constructed using the API root URI and service path from
542 the discovery document and the relative path for the API method. If
543 media upload is not supported, this is None.
544 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700545 media_upload = method_desc.get("mediaUpload", {})
546 accept = media_upload.get("accept", [])
547 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
548 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400549
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700550 if media_upload:
551 media_path_url = _media_path_url_from_info(root_desc, path_url)
552 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
553 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400554
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700555 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400556
557
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900558def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700559 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400560
561 SIDE EFFECTS: Changes the parameters dictionary in the method description with
562 extra parameters which are used locally.
563
564 Args:
565 method_desc: Dictionary with metadata describing an API method. Value comes
566 from the dictionary of methods stored in the 'methods' key in the
567 deserialized discovery document.
568 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900569 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400570
571 Returns:
572 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
573 where:
574 - path_url is a String; the relative URL for the API method. Relative to
575 the API root, which is specified in the discovery document.
576 - http_method is a String; the HTTP method used to call the API method
577 described in the method description.
578 - method_id is a String; the name of the RPC method associated with the
579 API method, and is in the method description in the 'id' key.
580 - accept is a list of strings representing what content types are
581 accepted for media upload. Defaults to empty list if not in the
582 discovery document.
583 - max_size is a long representing the max size in bytes allowed for a
584 media upload. Defaults to 0L if not in the discovery document.
585 - media_path_url is a String; the absolute URI for media upload for the
586 API method. Constructed using the API root URI and service path from
587 the discovery document and the relative path for the API method. If
588 media upload is not supported, this is None.
589 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700590 path_url = method_desc["path"]
591 http_method = method_desc["httpMethod"]
592 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400593
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700594 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
595 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
596 # 'parameters' key and needs to know if there is a 'body' parameter because it
597 # also sets a 'media_body' parameter.
598 accept, max_size, media_path_url = _fix_up_media_upload(
599 method_desc, root_desc, path_url, parameters
600 )
John Asmuth864311d2014-04-24 15:46:08 -0400601
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700602 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400603
604
Craig Citro7ee535d2015-02-23 10:11:14 -0800605def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700606 """Custom urljoin replacement supporting : before / in url."""
607 # In general, it's unsafe to simply join base and url. However, for
608 # the case of discovery documents, we know:
609 # * base will never contain params, query, or fragment
610 # * url will never contain a scheme or net_loc.
611 # In general, this means we can safely join on /; we just need to
612 # ensure we end up with precisely one / joining base and url. The
613 # exception here is the case of media uploads, where url will be an
614 # absolute url.
615 if url.startswith("http://") or url.startswith("https://"):
616 return urljoin(base, url)
617 new_base = base if base.endswith("/") else base + "/"
618 new_url = url[1:] if url.startswith("/") else url
619 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800620
621
John Asmuth864311d2014-04-24 15:46:08 -0400622# TODO(dhermes): Convert this class to ResourceMethod and make it callable
623class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700624 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400625
626 Attributes:
627 argmap: Map from method parameter name (string) to query parameter name
628 (string).
629 required_params: List of required parameters (represented by parameter
630 name as string).
631 repeated_params: List of repeated parameters (represented by parameter
632 name as string).
633 pattern_params: Map from method parameter name (string) to regular
634 expression (as a string). If the pattern is set for a parameter, the
635 value for that parameter must match the regular expression.
636 query_params: List of parameters (represented by parameter name as string)
637 that will be used in the query string.
638 path_params: Set of parameters (represented by parameter name as string)
639 that will be used in the base URL path.
640 param_types: Map from method parameter name (string) to parameter type. Type
641 can be any valid JSON schema type; valid values are 'any', 'array',
642 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
643 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
644 enum_params: Map from method parameter name (string) to list of strings,
645 where each list of strings is the list of acceptable enum values.
646 """
647
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700648 def __init__(self, method_desc):
649 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400650
651 Sets default values and defers to set_parameters to populate.
652
653 Args:
654 method_desc: Dictionary with metadata describing an API method. Value
655 comes from the dictionary of methods stored in the 'methods' key in
656 the deserialized discovery document.
657 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700658 self.argmap = {}
659 self.required_params = []
660 self.repeated_params = []
661 self.pattern_params = {}
662 self.query_params = []
663 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
664 # parsing is gotten rid of.
665 self.path_params = set()
666 self.param_types = {}
667 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400668
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700669 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400670
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700671 def set_parameters(self, method_desc):
672 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400673
674 Iterates through each parameter for the method and parses the values from
675 the parameter dictionary.
676
677 Args:
678 method_desc: Dictionary with metadata describing an API method. Value
679 comes from the dictionary of methods stored in the 'methods' key in
680 the deserialized discovery document.
681 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700682 for arg, desc in six.iteritems(method_desc.get("parameters", {})):
683 param = key2param(arg)
684 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400685
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700686 if desc.get("pattern"):
687 self.pattern_params[param] = desc["pattern"]
688 if desc.get("enum"):
689 self.enum_params[param] = desc["enum"]
690 if desc.get("required"):
691 self.required_params.append(param)
692 if desc.get("repeated"):
693 self.repeated_params.append(param)
694 if desc.get("location") == "query":
695 self.query_params.append(param)
696 if desc.get("location") == "path":
697 self.path_params.add(param)
698 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400699
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700700 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
701 # should have all path parameters already marked with
702 # 'location: path'.
703 for match in URITEMPLATE.finditer(method_desc["path"]):
704 for namematch in VARNAME.finditer(match.group(0)):
705 name = key2param(namematch.group(0))
706 self.path_params.add(name)
707 if name in self.query_params:
708 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400709
710
711def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700712 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400713
714 Args:
715 methodName: string, name of the method to use.
716 methodDesc: object, fragment of deserialized discovery document that
717 describes the method.
718 rootDesc: object, the entire deserialized discovery document.
719 schema: object, mapping of schema names to schema descriptions.
720 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700721 methodName = fix_method_name(methodName)
722 (
723 pathUrl,
724 httpMethod,
725 methodId,
726 accept,
727 maxSize,
728 mediaPathUrl,
729 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400730
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700731 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400732
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700733 def method(self, **kwargs):
734 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400735
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700736 for name in six.iterkeys(kwargs):
737 if name not in parameters.argmap:
738 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400739
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700740 # Remove args that have a value of None.
741 keys = list(kwargs.keys())
742 for name in keys:
743 if kwargs[name] is None:
744 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -0400745
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700746 for name in parameters.required_params:
747 if name not in kwargs:
748 # temporary workaround for non-paging methods incorrectly requiring
749 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
750 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
751 _methodProperties(methodDesc, schema, "response")
752 ):
753 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400754
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700755 for name, regex in six.iteritems(parameters.pattern_params):
756 if name in kwargs:
757 if isinstance(kwargs[name], six.string_types):
758 pvalues = [kwargs[name]]
759 else:
760 pvalues = kwargs[name]
761 for pvalue in pvalues:
762 if re.match(regex, pvalue) is None:
763 raise TypeError(
764 'Parameter "%s" value "%s" does not match the pattern "%s"'
765 % (name, pvalue, regex)
766 )
767
768 for name, enums in six.iteritems(parameters.enum_params):
769 if name in kwargs:
770 # We need to handle the case of a repeated enum
771 # name differently, since we want to handle both
772 # arg='value' and arg=['value1', 'value2']
773 if name in parameters.repeated_params and not isinstance(
774 kwargs[name], six.string_types
775 ):
776 values = kwargs[name]
777 else:
778 values = [kwargs[name]]
779 for value in values:
780 if value not in enums:
781 raise TypeError(
782 'Parameter "%s" value "%s" is not an allowed value in "%s"'
783 % (name, value, str(enums))
784 )
785
786 actual_query_params = {}
787 actual_path_params = {}
788 for key, value in six.iteritems(kwargs):
789 to_type = parameters.param_types.get(key, "string")
790 # For repeated parameters we cast each member of the list.
791 if key in parameters.repeated_params and type(value) == type([]):
792 cast_value = [_cast(x, to_type) for x in value]
793 else:
794 cast_value = _cast(value, to_type)
795 if key in parameters.query_params:
796 actual_query_params[parameters.argmap[key]] = cast_value
797 if key in parameters.path_params:
798 actual_path_params[parameters.argmap[key]] = cast_value
799 body_value = kwargs.get("body", None)
800 media_filename = kwargs.get("media_body", None)
801 media_mime_type = kwargs.get("media_mime_type", None)
802
803 if self._developerKey:
804 actual_query_params["key"] = self._developerKey
805
806 model = self._model
807 if methodName.endswith("_media"):
808 model = MediaModel()
809 elif "response" not in methodDesc:
810 model = RawModel()
811
812 headers = {}
813 headers, params, query, body = model.request(
814 headers, actual_path_params, actual_query_params, body_value
815 )
816
817 expanded_url = uritemplate.expand(pathUrl, params)
818 url = _urljoin(self._baseUrl, expanded_url + query)
819
820 resumable = None
821 multipart_boundary = ""
822
823 if media_filename:
824 # Ensure we end up with a valid MediaUpload object.
825 if isinstance(media_filename, six.string_types):
826 if media_mime_type is None:
827 logger.warning(
828 "media_mime_type argument not specified: trying to auto-detect for %s",
829 media_filename,
830 )
831 media_mime_type, _ = mimetypes.guess_type(media_filename)
832 if media_mime_type is None:
833 raise UnknownFileType(media_filename)
834 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
835 raise UnacceptableMimeTypeError(media_mime_type)
836 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
837 elif isinstance(media_filename, MediaUpload):
838 media_upload = media_filename
839 else:
840 raise TypeError("media_filename must be str or MediaUpload.")
841
842 # Check the maxSize
843 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
844 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
845
846 # Use the media path uri for media uploads
847 expanded_url = uritemplate.expand(mediaPathUrl, params)
848 url = _urljoin(self._baseUrl, expanded_url + query)
849 if media_upload.resumable():
850 url = _add_query_parameter(url, "uploadType", "resumable")
851
852 if media_upload.resumable():
853 # This is all we need to do for resumable, if the body exists it gets
854 # sent in the first request, otherwise an empty body is sent.
855 resumable = media_upload
856 else:
857 # A non-resumable upload
858 if body is None:
859 # This is a simple media upload
860 headers["content-type"] = media_upload.mimetype()
861 body = media_upload.getbytes(0, media_upload.size())
862 url = _add_query_parameter(url, "uploadType", "media")
863 else:
864 # This is a multipart/related upload.
865 msgRoot = MIMEMultipart("related")
866 # msgRoot should not write out it's own headers
867 setattr(msgRoot, "_write_headers", lambda self: None)
868
869 # attach the body as one part
870 msg = MIMENonMultipart(*headers["content-type"].split("/"))
871 msg.set_payload(body)
872 msgRoot.attach(msg)
873
874 # attach the media as the second part
875 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
876 msg["Content-Transfer-Encoding"] = "binary"
877
878 payload = media_upload.getbytes(0, media_upload.size())
879 msg.set_payload(payload)
880 msgRoot.attach(msg)
881 # encode the body: note that we can't use `as_string`, because
882 # it plays games with `From ` lines.
883 fp = BytesIO()
884 g = _BytesGenerator(fp, mangle_from_=False)
885 g.flatten(msgRoot, unixfrom=False)
886 body = fp.getvalue()
887
888 multipart_boundary = msgRoot.get_boundary()
889 headers["content-type"] = (
890 "multipart/related; " 'boundary="%s"'
891 ) % multipart_boundary
892 url = _add_query_parameter(url, "uploadType", "multipart")
893
894 logger.info("URL being requested: %s %s" % (httpMethod, url))
895 return self._requestBuilder(
896 self._http,
897 model.response,
898 url,
899 method=httpMethod,
900 body=body,
901 headers=headers,
902 methodId=methodId,
903 resumable=resumable,
904 )
905
906 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
907 if len(parameters.argmap) > 0:
908 docs.append("Args:\n")
909
910 # Skip undocumented params and params common to all methods.
911 skip_parameters = list(rootDesc.get("parameters", {}).keys())
912 skip_parameters.extend(STACK_QUERY_PARAMETERS)
913
914 all_args = list(parameters.argmap.keys())
915 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
916
917 # Move body to the front of the line.
918 if "body" in all_args:
919 args_ordered.append("body")
920
921 for name in all_args:
922 if name not in args_ordered:
923 args_ordered.append(name)
924
925 for arg in args_ordered:
926 if arg in skip_parameters:
927 continue
928
929 repeated = ""
930 if arg in parameters.repeated_params:
931 repeated = " (repeated)"
932 required = ""
933 if arg in parameters.required_params:
934 required = " (required)"
935 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
936 paramdoc = paramdesc.get("description", "A parameter")
937 if "$ref" in paramdesc:
938 docs.append(
939 (" %s: object, %s%s%s\n The object takes the" " form of:\n\n%s\n\n")
940 % (
941 arg,
942 paramdoc,
943 required,
944 repeated,
945 schema.prettyPrintByName(paramdesc["$ref"]),
946 )
947 )
John Asmuth864311d2014-04-24 15:46:08 -0400948 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700949 paramtype = paramdesc.get("type", "string")
950 docs.append(
951 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
952 )
953 enum = paramdesc.get("enum", [])
954 enumDesc = paramdesc.get("enumDescriptions", [])
955 if enum and enumDesc:
956 docs.append(" Allowed values\n")
957 for (name, desc) in zip(enum, enumDesc):
958 docs.append(" %s - %s\n" % (name, desc))
959 if "response" in methodDesc:
960 if methodName.endswith("_media"):
961 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -0400962 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700963 docs.append("\nReturns:\n An object of the form:\n\n ")
964 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -0400965
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700966 setattr(method, "__doc__", "".join(docs))
967 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -0400968
969
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700970def createNextMethod(
971 methodName,
972 pageTokenName="pageToken",
973 nextPageTokenName="nextPageToken",
974 isPageTokenParameter=True,
975):
976 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400977
978 The _next methods allow for easy iteration through list() responses.
979
980 Args:
981 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800982 pageTokenName: string, name of request page token field.
983 nextPageTokenName: string, name of response page token field.
984 isPageTokenParameter: Boolean, True if request page token is a query
985 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -0400986 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700987 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -0400988
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700989 def methodNext(self, previous_request, previous_response):
990 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -0400991
992Args:
993 previous_request: The request for the previous page. (required)
994 previous_response: The response from the request for the previous page. (required)
995
996Returns:
997 A request object that you can call 'execute()' on to request the next
998 page. Returns None if there are no more items in the collection.
999 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001000 # Retrieve nextPageToken from previous_response
1001 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001002
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001003 nextPageToken = previous_response.get(nextPageTokenName, None)
1004 if not nextPageToken:
1005 return None
John Asmuth864311d2014-04-24 15:46:08 -04001006
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001007 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001008
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001009 if isPageTokenParameter:
1010 # Replace pageToken value in URI
1011 request.uri = _add_query_parameter(
1012 request.uri, pageTokenName, nextPageToken
1013 )
1014 logger.info("Next page request URL: %s %s" % (methodName, request.uri))
1015 else:
1016 # Replace pageToken value in request body
1017 model = self._model
1018 body = model.deserialize(request.body)
1019 body[pageTokenName] = nextPageToken
1020 request.body = model.serialize(body)
1021 logger.info("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001022
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001023 return request
John Asmuth864311d2014-04-24 15:46:08 -04001024
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001025 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001026
1027
1028class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001029 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001030
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001031 def __init__(
1032 self,
1033 http,
1034 baseUrl,
1035 model,
1036 requestBuilder,
1037 developerKey,
1038 resourceDesc,
1039 rootDesc,
1040 schema,
1041 ):
1042 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001043
1044 Args:
1045 http: httplib2.Http, Object to make http requests with.
1046 baseUrl: string, base URL for the API. All requests are relative to this
1047 URI.
1048 model: googleapiclient.Model, converts to and from the wire format.
1049 requestBuilder: class or callable that instantiates an
1050 googleapiclient.HttpRequest object.
1051 developerKey: string, key obtained from
1052 https://code.google.com/apis/console
1053 resourceDesc: object, section of deserialized discovery document that
1054 describes a resource. Note that the top level discovery document
1055 is considered a resource.
1056 rootDesc: object, the entire deserialized discovery document.
1057 schema: object, mapping of schema names to schema descriptions.
1058 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001059 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001060
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001061 self._http = http
1062 self._baseUrl = baseUrl
1063 self._model = model
1064 self._developerKey = developerKey
1065 self._requestBuilder = requestBuilder
1066 self._resourceDesc = resourceDesc
1067 self._rootDesc = rootDesc
1068 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001069
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001070 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001071
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001072 def _set_dynamic_attr(self, attr_name, value):
1073 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001074
1075 Args:
1076 attr_name: string; The name of the attribute to be set
1077 value: The value being set on the object and tracked in the dynamic cache.
1078 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001079 self._dynamic_attrs.append(attr_name)
1080 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001081
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001082 def __getstate__(self):
1083 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001084
1085 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1086 will be wiped and restored on pickle serialization.
1087 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001088 state_dict = copy.copy(self.__dict__)
1089 for dynamic_attr in self._dynamic_attrs:
1090 del state_dict[dynamic_attr]
1091 del state_dict["_dynamic_attrs"]
1092 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001093
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001094 def __setstate__(self, state):
1095 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001096
1097 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1098 will be wiped and restored on pickle serialization.
1099 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001100 self.__dict__.update(state)
1101 self._dynamic_attrs = []
1102 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001103
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001104 def _set_service_methods(self):
1105 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1106 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1107 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001108
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001109 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1110 # If this is the root Resource, add a new_batch_http_request() method.
1111 if resourceDesc == rootDesc:
1112 batch_uri = "%s%s" % (
1113 rootDesc["rootUrl"],
1114 rootDesc.get("batchPath", "batch"),
1115 )
1116
1117 def new_batch_http_request(callback=None):
1118 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001119
1120 Args:
1121 callback: callable, A callback to be called for each response, of the
1122 form callback(id, response, exception). The first parameter is the
1123 request id, and the second is the deserialized response object. The
1124 third is an apiclient.errors.HttpError exception object if an HTTP
1125 error occurred while processing the request, or None if no error
1126 occurred.
1127
1128 Returns:
1129 A BatchHttpRequest object based on the discovery document.
1130 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001131 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001132
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001133 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001134
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001135 # Add basic methods to Resource
1136 if "methods" in resourceDesc:
1137 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1138 fixedMethodName, method = createMethod(
1139 methodName, methodDesc, rootDesc, schema
1140 )
1141 self._set_dynamic_attr(
1142 fixedMethodName, method.__get__(self, self.__class__)
1143 )
1144 # Add in _media methods. The functionality of the attached method will
1145 # change when it sees that the method name ends in _media.
1146 if methodDesc.get("supportsMediaDownload", False):
1147 fixedMethodName, method = createMethod(
1148 methodName + "_media", methodDesc, rootDesc, schema
1149 )
1150 self._set_dynamic_attr(
1151 fixedMethodName, method.__get__(self, self.__class__)
1152 )
John Asmuth864311d2014-04-24 15:46:08 -04001153
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001154 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1155 # Add in nested resources
1156 if "resources" in resourceDesc:
1157
1158 def createResourceMethod(methodName, methodDesc):
1159 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001160
1161 Args:
1162 methodName: string, name of the method to use.
1163 methodDesc: object, fragment of deserialized discovery document that
1164 describes the method.
1165 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001166 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001167
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001168 def methodResource(self):
1169 return Resource(
1170 http=self._http,
1171 baseUrl=self._baseUrl,
1172 model=self._model,
1173 developerKey=self._developerKey,
1174 requestBuilder=self._requestBuilder,
1175 resourceDesc=methodDesc,
1176 rootDesc=rootDesc,
1177 schema=schema,
1178 )
John Asmuth864311d2014-04-24 15:46:08 -04001179
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001180 setattr(methodResource, "__doc__", "A collection resource.")
1181 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001182
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001183 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001184
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001185 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1186 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1187 self._set_dynamic_attr(
1188 fixedMethodName, method.__get__(self, self.__class__)
1189 )
John Asmuth864311d2014-04-24 15:46:08 -04001190
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001191 def _add_next_methods(self, resourceDesc, schema):
1192 # Add _next() methods if and only if one of the names 'pageToken' or
1193 # 'nextPageToken' occurs among the fields of both the method's response
1194 # type either the method's request (query parameters) or request body.
1195 if "methods" not in resourceDesc:
1196 return
1197 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1198 nextPageTokenName = _findPageTokenName(
1199 _methodProperties(methodDesc, schema, "response")
1200 )
1201 if not nextPageTokenName:
1202 continue
1203 isPageTokenParameter = True
1204 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1205 if not pageTokenName:
1206 isPageTokenParameter = False
1207 pageTokenName = _findPageTokenName(
1208 _methodProperties(methodDesc, schema, "request")
1209 )
1210 if not pageTokenName:
1211 continue
1212 fixedMethodName, method = createNextMethod(
1213 methodName + "_next",
1214 pageTokenName,
1215 nextPageTokenName,
1216 isPageTokenParameter,
1217 )
1218 self._set_dynamic_attr(
1219 fixedMethodName, method.__get__(self, self.__class__)
1220 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001221
1222
1223def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001224 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001225
1226 Args:
1227 fields: container of string, names of fields.
1228
1229 Returns:
1230 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1231 otherwise None.
1232 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001233 return next(
1234 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1235 )
1236
Thomas Coffee20af04d2017-02-10 15:24:44 -08001237
1238def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001239 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001240
1241 Args:
1242 methodDesc: object, fragment of deserialized discovery document that
1243 describes the method.
1244 schema: object, mapping of schema names to schema descriptions.
1245 name: string, name of top-level field in method description.
1246
1247 Returns:
1248 Object representing fragment of deserialized discovery document
1249 corresponding to 'properties' field of object corresponding to named field
1250 in method description, if it exists, otherwise empty dict.
1251 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001252 desc = methodDesc.get(name, {})
1253 if "$ref" in desc:
1254 desc = schema.get(desc["$ref"], {})
1255 return desc.get("properties", {})