blob: 66d4927aaeb881555384cce3146fdc18506f7dee [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
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -070049import google.api_core.client_options
John Asmuth864311d2014-04-24 15:46:08 -040050
51# Local imports
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -080052from googleapiclient import _auth
Pat Ferateb240c172015-03-03 16:23:51 -080053from googleapiclient import mimeparse
John Asmuth864311d2014-04-24 15:46:08 -040054from googleapiclient.errors import HttpError
55from googleapiclient.errors import InvalidJsonError
56from googleapiclient.errors import MediaUploadSizeError
57from googleapiclient.errors import UnacceptableMimeTypeError
58from googleapiclient.errors import UnknownApiNameOrVersion
59from googleapiclient.errors import UnknownFileType
Igor Maravić22435292017-01-19 22:28:22 +010060from googleapiclient.http import build_http
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -040061from googleapiclient.http import BatchHttpRequest
Kostyantyn Leschenkobe8b1cb2016-10-17 12:57:21 +030062from googleapiclient.http import HttpMock
63from googleapiclient.http import HttpMockSequence
John Asmuth864311d2014-04-24 15:46:08 -040064from googleapiclient.http import HttpRequest
65from googleapiclient.http import MediaFileUpload
66from googleapiclient.http import MediaUpload
67from googleapiclient.model import JsonModel
68from googleapiclient.model import MediaModel
69from googleapiclient.model import RawModel
70from googleapiclient.schema import Schemas
Jon Wayne Parrott6755f612016-08-15 10:52:26 -070071
Helen Koikede13e3b2018-04-26 16:05:16 -030072from googleapiclient._helpers import _add_query_parameter
73from googleapiclient._helpers import positional
John Asmuth864311d2014-04-24 15:46:08 -040074
75
76# The client library requires a version of httplib2 that supports RETRIES.
77httplib2.RETRIES = 1
78
79logger = logging.getLogger(__name__)
80
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070081URITEMPLATE = re.compile("{[^}]*}")
82VARNAME = re.compile("[a-zA-Z0-9_-]+")
83DISCOVERY_URI = (
84 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest"
85)
Ethan Bao12b7cd32016-03-14 14:25:10 -070086V1_DISCOVERY_URI = DISCOVERY_URI
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070087V2_DISCOVERY_URI = (
88 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}"
89)
90DEFAULT_METHOD_DOC = "A description of how to use this function"
91HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"])
Igor Maravić22435292017-01-19 22:28:22 +010092
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070093_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40}
94BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"}
John Asmuth864311d2014-04-24 15:46:08 -040095MEDIA_BODY_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070096 "description": (
97 "The filename of the media request body, or an instance "
98 "of a MediaUpload object."
99 ),
100 "type": "string",
101 "required": False,
John Asmuth864311d2014-04-24 15:46:08 -0400102}
Brian J. Watson38051ac2016-10-25 07:53:08 -0700103MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = {
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700104 "description": (
105 "The MIME type of the media request body, or an instance "
106 "of a MediaUpload object."
107 ),
108 "type": "string",
109 "required": False,
Brian J. Watson38051ac2016-10-25 07:53:08 -0700110}
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700111_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken")
John Asmuth864311d2014-04-24 15:46:08 -0400112
113# Parameters accepted by the stack, but not visible via discovery.
114# TODO(dhermes): Remove 'userip' in 'v2'.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700115STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"])
116STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"}
John Asmuth864311d2014-04-24 15:46:08 -0400117
118# Library-specific reserved words beyond Python keywords.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700119RESERVED_WORDS = frozenset(["body"])
John Asmuth864311d2014-04-24 15:46:08 -0400120
Phil Ruffwind26178fc2015-10-13 19:00:33 -0400121# patch _write_lines to avoid munging '\r' into '\n'
122# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 )
123class _BytesGenerator(BytesGenerator):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700124 _write_lines = BytesGenerator.write
125
John Asmuth864311d2014-04-24 15:46:08 -0400126
127def fix_method_name(name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700128 """Fix method names to avoid '$' characters and reserved word conflicts.
John Asmuth864311d2014-04-24 15:46:08 -0400129
130 Args:
131 name: string, method name.
132
133 Returns:
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700134 The name with '_' appended if the name is a reserved word and '$' and '-'
Bu Sun Kim84cbc072019-01-25 15:49:52 -0800135 replaced with '_'.
John Asmuth864311d2014-04-24 15:46:08 -0400136 """
Bu Sun Kim8ed729f2020-04-17 10:23:27 -0700137 name = name.replace("$", "_").replace("-", "_")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700138 if keyword.iskeyword(name) or name in RESERVED_WORDS:
139 return name + "_"
140 else:
141 return name
John Asmuth864311d2014-04-24 15:46:08 -0400142
143
144def key2param(key):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700145 """Converts key names into parameter names.
John Asmuth864311d2014-04-24 15:46:08 -0400146
147 For example, converting "max-results" -> "max_results"
148
149 Args:
150 key: string, the method key name.
151
152 Returns:
153 A safe method name based on the key name.
154 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700155 result = []
156 key = list(key)
157 if not key[0].isalpha():
158 result.append("x")
159 for c in key:
160 if c.isalnum():
161 result.append(c)
162 else:
163 result.append("_")
John Asmuth864311d2014-04-24 15:46:08 -0400164
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700165 return "".join(result)
John Asmuth864311d2014-04-24 15:46:08 -0400166
167
168@positional(2)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700169def build(
170 serviceName,
171 version,
172 http=None,
173 discoveryServiceUrl=DISCOVERY_URI,
174 developerKey=None,
175 model=None,
176 requestBuilder=HttpRequest,
177 credentials=None,
178 cache_discovery=True,
179 cache=None,
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700180 client_options=None,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700181):
182 """Construct a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400183
184 Construct a Resource object for interacting with an API. The serviceName and
185 version are the names from the Discovery service.
186
187 Args:
188 serviceName: string, name of the service.
189 version: string, the version of the service.
190 http: httplib2.Http, An instance of httplib2.Http or something that acts
191 like it that HTTP requests will be made through.
192 discoveryServiceUrl: string, a URI Template that points to the location of
193 the discovery service. It should have two parameters {api} and
194 {apiVersion} that when filled in produce an absolute URI to the discovery
195 document for that service.
196 developerKey: string, key obtained from
197 https://code.google.com/apis/console.
198 model: googleapiclient.Model, converts to and from the wire format.
199 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP
200 request.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800201 credentials: oauth2client.Credentials or
202 google.auth.credentials.Credentials, credentials to be used for
Orest Bolohane92c9002014-05-30 11:15:43 -0700203 authentication.
Takashi Matsuo30125122015-08-19 11:42:32 -0700204 cache_discovery: Boolean, whether or not to cache the discovery doc.
205 cache: googleapiclient.discovery_cache.base.CacheBase, an optional
206 cache object for the discovery documents.
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700207 client_options: Dictionary or google.api_core.client_options, Client options to set user
208 options on the client. API endpoint should be set through client_options.
John Asmuth864311d2014-04-24 15:46:08 -0400209
210 Returns:
211 A Resource object with methods for interacting with the service.
212 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700213 params = {"api": serviceName, "apiVersion": version}
John Asmuth864311d2014-04-24 15:46:08 -0400214
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700215 if http is None:
216 discovery_http = build_http()
217 else:
218 discovery_http = http
John Asmuth864311d2014-04-24 15:46:08 -0400219
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700220 for discovery_url in (discoveryServiceUrl, V2_DISCOVERY_URI):
221 requested_url = uritemplate.expand(discovery_url, params)
John Asmuth864311d2014-04-24 15:46:08 -0400222
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700223 try:
224 content = _retrieve_discovery_doc(
225 requested_url, discovery_http, cache_discovery, cache, developerKey
226 )
227 return build_from_document(
228 content,
229 base=discovery_url,
230 http=http,
231 developerKey=developerKey,
232 model=model,
233 requestBuilder=requestBuilder,
234 credentials=credentials,
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700235 client_options=client_options
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700236 )
237 except HttpError as e:
238 if e.resp.status == http_client.NOT_FOUND:
239 continue
240 else:
241 raise e
Takashi Matsuo30125122015-08-19 11:42:32 -0700242
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700243 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))
Takashi Matsuo30125122015-08-19 11:42:32 -0700244
245
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700246def _retrieve_discovery_doc(url, http, cache_discovery, cache=None, developerKey=None):
247 """Retrieves the discovery_doc from cache or the internet.
Takashi Matsuo30125122015-08-19 11:42:32 -0700248
249 Args:
250 url: string, the URL of the discovery document.
251 http: httplib2.Http, An instance of httplib2.Http or something that acts
252 like it through which HTTP requests will be made.
253 cache_discovery: Boolean, whether or not to cache the discovery doc.
254 cache: googleapiclient.discovery_cache.base.Cache, an optional cache
255 object for the discovery documents.
256
257 Returns:
258 A unicode string representation of the discovery document.
259 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700260 if cache_discovery:
261 from . import discovery_cache
262 from .discovery_cache import base
Takashi Matsuo30125122015-08-19 11:42:32 -0700263
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700264 if cache is None:
265 cache = discovery_cache.autodetect()
266 if cache:
267 content = cache.get(url)
268 if content:
269 return content
John Asmuth864311d2014-04-24 15:46:08 -0400270
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700271 actual_url = url
272 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment
273 # variable that contains the network address of the client sending the
274 # request. If it exists then add that to the request for the discovery
275 # document to avoid exceeding the quota on discovery requests.
276 if "REMOTE_ADDR" in os.environ:
277 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"])
278 if developerKey:
279 actual_url = _add_query_parameter(url, "key", developerKey)
280 logger.info("URL being requested: GET %s", actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400281
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700282 resp, content = http.request(actual_url)
John Asmuth864311d2014-04-24 15:46:08 -0400283
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700284 if resp.status >= 400:
285 raise HttpError(resp, content, uri=actual_url)
Pat Ferate9b0452c2015-03-03 17:59:56 -0800286
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700287 try:
288 content = content.decode("utf-8")
289 except AttributeError:
290 pass
291
292 try:
293 service = json.loads(content)
294 except ValueError as e:
295 logger.error("Failed to parse as JSON: " + content)
296 raise InvalidJsonError()
297 if cache_discovery and cache:
298 cache.set(url, content)
299 return content
John Asmuth864311d2014-04-24 15:46:08 -0400300
301
302@positional(1)
303def build_from_document(
304 service,
305 base=None,
306 future=None,
307 http=None,
308 developerKey=None,
309 model=None,
Orest Bolohane92c9002014-05-30 11:15:43 -0700310 requestBuilder=HttpRequest,
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700311 credentials=None,
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700312 client_options=None
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700313):
314 """Create a Resource for interacting with an API.
John Asmuth864311d2014-04-24 15:46:08 -0400315
316 Same as `build()`, but constructs the Resource object from a discovery
317 document that is it given, as opposed to retrieving one over HTTP.
318
319 Args:
320 service: string or object, the JSON discovery document describing the API.
321 The value passed in may either be the JSON string or the deserialized
322 JSON.
323 base: string, base URI for all HTTP requests, usually the discovery URI.
324 This parameter is no longer used as rootUrl and servicePath are included
325 within the discovery document. (deprecated)
326 future: string, discovery document with future capabilities (deprecated).
327 http: httplib2.Http, An instance of httplib2.Http or something that acts
328 like it that HTTP requests will be made through.
329 developerKey: string, Key for controlling API usage, generated
330 from the API Console.
331 model: Model class instance that serializes and de-serializes requests and
332 responses.
333 requestBuilder: Takes an http request and packages it up to be executed.
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800334 credentials: oauth2client.Credentials or
335 google.auth.credentials.Credentials, credentials to be used for
336 authentication.
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700337 client_options: Dictionary or google.api_core.client_options, Client options to set user
338 options on the client. API endpoint should be set through client_options.
John Asmuth864311d2014-04-24 15:46:08 -0400339
340 Returns:
341 A Resource object with methods for interacting with the service.
342 """
343
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700344 if http is not None and credentials is not None:
345 raise ValueError("Arguments http and credentials are mutually exclusive.")
John Asmuth864311d2014-04-24 15:46:08 -0400346
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700347 if isinstance(service, six.string_types):
348 service = json.loads(service)
349 elif isinstance(service, six.binary_type):
350 service = json.loads(service.decode("utf-8"))
Christian Ternuse469a9f2016-08-16 12:44:03 -0400351
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700352 if "rootUrl" not in service and (isinstance(http, (HttpMock, HttpMockSequence))):
353 logger.error(
354 "You are using HttpMock or HttpMockSequence without"
355 + "having the service discovery doc in cache. Try calling "
356 + "build() without mocking once first to populate the "
357 + "cache."
358 )
359 raise InvalidJsonError()
Christian Ternuse469a9f2016-08-16 12:44:03 -0400360
Bu Sun Kim1cf3cbc2020-03-12 12:38:23 -0700361 # If an API Endpoint is provided on client options, use that as the base URL
362 base = urljoin(service['rootUrl'], service["servicePath"])
363 if client_options:
364 if type(client_options) == dict:
365 client_options = google.api_core.client_options.from_dict(
366 client_options
367 )
368 if client_options.api_endpoint:
369 base = client_options.api_endpoint
370
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700371 schema = Schemas(service)
John Asmuth864311d2014-04-24 15:46:08 -0400372
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700373 # If the http client is not specified, then we must construct an http client
374 # to make requests. If the service has scopes, then we also need to setup
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800375 # authentication.
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700376 if http is None:
377 # Does the service require scopes?
378 scopes = list(
379 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys()
380 )
Orest Bolohane92c9002014-05-30 11:15:43 -0700381
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700382 # If so, then the we need to setup authentication if no developerKey is
383 # specified.
384 if scopes and not developerKey:
385 # If the user didn't pass in credentials, attempt to acquire application
386 # default credentials.
387 if credentials is None:
388 credentials = _auth.default_credentials()
Jon Wayne Parrott85c2c6d2017-01-05 12:34:49 -0800389
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700390 # The credentials need to be scoped.
391 credentials = _auth.with_scopes(credentials, scopes)
392
393 # If credentials are provided, create an authorized http instance;
394 # otherwise, skip authentication.
395 if credentials:
396 http = _auth.authorized_http(credentials)
397
398 # If the service doesn't require scopes then there is no need for
399 # authentication.
400 else:
401 http = build_http()
402
403 if model is None:
404 features = service.get("features", [])
405 model = JsonModel("dataWrapper" in features)
406
407 return Resource(
408 http=http,
409 baseUrl=base,
410 model=model,
411 developerKey=developerKey,
412 requestBuilder=requestBuilder,
413 resourceDesc=service,
414 rootDesc=service,
415 schema=schema,
416 )
John Asmuth864311d2014-04-24 15:46:08 -0400417
418
419def _cast(value, schema_type):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700420 """Convert value to a string based on JSON Schema type.
John Asmuth864311d2014-04-24 15:46:08 -0400421
422 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on
423 JSON Schema.
424
425 Args:
426 value: any, the value to convert
427 schema_type: string, the type that value should be interpreted as
428
429 Returns:
430 A string representation of 'value' based on the schema_type.
431 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700432 if schema_type == "string":
433 if type(value) == type("") or type(value) == type(u""):
434 return value
435 else:
436 return str(value)
437 elif schema_type == "integer":
438 return str(int(value))
439 elif schema_type == "number":
440 return str(float(value))
441 elif schema_type == "boolean":
442 return str(bool(value)).lower()
John Asmuth864311d2014-04-24 15:46:08 -0400443 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700444 if type(value) == type("") or type(value) == type(u""):
445 return value
446 else:
447 return str(value)
John Asmuth864311d2014-04-24 15:46:08 -0400448
449
450def _media_size_to_long(maxSize):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700451 """Convert a string media size, such as 10GB or 3TB into an integer.
John Asmuth864311d2014-04-24 15:46:08 -0400452
453 Args:
454 maxSize: string, size as a string, such as 2MB or 7GB.
455
456 Returns:
457 The size as an integer value.
458 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700459 if len(maxSize) < 2:
460 return 0
461 units = maxSize[-2:].upper()
462 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units)
463 if bit_shift is not None:
464 return int(maxSize[:-2]) << bit_shift
465 else:
466 return int(maxSize)
John Asmuth864311d2014-04-24 15:46:08 -0400467
468
469def _media_path_url_from_info(root_desc, path_url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700470 """Creates an absolute media path URL.
John Asmuth864311d2014-04-24 15:46:08 -0400471
472 Constructed using the API root URI and service path from the discovery
473 document and the relative path for the API method.
474
475 Args:
476 root_desc: Dictionary; the entire original deserialized discovery document.
477 path_url: String; the relative URL for the API method. Relative to the API
478 root, which is specified in the discovery document.
479
480 Returns:
481 String; the absolute URI for media upload for the API method.
482 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700483 return "%(root)supload/%(service_path)s%(path)s" % {
484 "root": root_desc["rootUrl"],
485 "service_path": root_desc["servicePath"],
486 "path": path_url,
487 }
John Asmuth864311d2014-04-24 15:46:08 -0400488
489
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900490def _fix_up_parameters(method_desc, root_desc, http_method, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700491 """Updates parameters of an API method with values specific to this library.
John Asmuth864311d2014-04-24 15:46:08 -0400492
493 Specifically, adds whatever global parameters are specified by the API to the
494 parameters for the individual method. Also adds parameters which don't
495 appear in the discovery document, but are available to all discovery based
496 APIs (these are listed in STACK_QUERY_PARAMETERS).
497
498 SIDE EFFECTS: This updates the parameters dictionary object in the method
499 description.
500
501 Args:
502 method_desc: Dictionary with metadata describing an API method. Value comes
503 from the dictionary of methods stored in the 'methods' key in the
504 deserialized discovery document.
505 root_desc: Dictionary; the entire original deserialized discovery document.
506 http_method: String; the HTTP method used to call the API method described
507 in method_desc.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900508 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400509
510 Returns:
511 The updated Dictionary stored in the 'parameters' key of the method
512 description dictionary.
513 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700514 parameters = method_desc.setdefault("parameters", {})
John Asmuth864311d2014-04-24 15:46:08 -0400515
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700516 # Add in the parameters common to all methods.
517 for name, description in six.iteritems(root_desc.get("parameters", {})):
518 parameters[name] = description
John Asmuth864311d2014-04-24 15:46:08 -0400519
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700520 # Add in undocumented query parameters.
521 for name in STACK_QUERY_PARAMETERS:
522 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400523
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700524 # Add 'body' (our own reserved word) to parameters if the method supports
525 # a request payload.
526 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc:
527 body = BODY_PARAMETER_DEFAULT_VALUE.copy()
528 body.update(method_desc["request"])
529 parameters["body"] = body
John Asmuth864311d2014-04-24 15:46:08 -0400530
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700531 return parameters
John Asmuth864311d2014-04-24 15:46:08 -0400532
533
534def _fix_up_media_upload(method_desc, root_desc, path_url, parameters):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700535 """Adds 'media_body' and 'media_mime_type' parameters if supported by method.
John Asmuth864311d2014-04-24 15:46:08 -0400536
Bu Sun Kimb854ff12019-07-16 17:46:08 -0700537 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds
538 'media_upload' key to parameters.
John Asmuth864311d2014-04-24 15:46:08 -0400539
540 Args:
541 method_desc: Dictionary with metadata describing an API method. Value comes
542 from the dictionary of methods stored in the 'methods' key in the
543 deserialized discovery document.
544 root_desc: Dictionary; the entire original deserialized discovery document.
545 path_url: String; the relative URL for the API method. Relative to the API
546 root, which is specified in the discovery document.
547 parameters: A dictionary describing method parameters for method described
548 in method_desc.
549
550 Returns:
551 Triple (accept, max_size, media_path_url) where:
552 - accept is a list of strings representing what content types are
553 accepted for media upload. Defaults to empty list if not in the
554 discovery document.
555 - max_size is a long representing the max size in bytes allowed for a
556 media upload. Defaults to 0L if not in the discovery document.
557 - media_path_url is a String; the absolute URI for media upload for the
558 API method. Constructed using the API root URI and service path from
559 the discovery document and the relative path for the API method. If
560 media upload is not supported, this is None.
561 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700562 media_upload = method_desc.get("mediaUpload", {})
563 accept = media_upload.get("accept", [])
564 max_size = _media_size_to_long(media_upload.get("maxSize", ""))
565 media_path_url = None
John Asmuth864311d2014-04-24 15:46:08 -0400566
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700567 if media_upload:
568 media_path_url = _media_path_url_from_info(root_desc, path_url)
569 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy()
570 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy()
John Asmuth864311d2014-04-24 15:46:08 -0400571
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700572 return accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400573
574
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900575def _fix_up_method_description(method_desc, root_desc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700576 """Updates a method description in a discovery document.
John Asmuth864311d2014-04-24 15:46:08 -0400577
578 SIDE EFFECTS: Changes the parameters dictionary in the method description with
579 extra parameters which are used locally.
580
581 Args:
582 method_desc: Dictionary with metadata describing an API method. Value comes
583 from the dictionary of methods stored in the 'methods' key in the
584 deserialized discovery document.
585 root_desc: Dictionary; the entire original deserialized discovery document.
Jean-Loup Roussel-Clouet0c0c8972018-04-28 05:42:43 +0900586 schema: Object, mapping of schema names to schema descriptions.
John Asmuth864311d2014-04-24 15:46:08 -0400587
588 Returns:
589 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url)
590 where:
591 - path_url is a String; the relative URL for the API method. Relative to
592 the API root, which is specified in the discovery document.
593 - http_method is a String; the HTTP method used to call the API method
594 described in the method description.
595 - method_id is a String; the name of the RPC method associated with the
596 API method, and is in the method description in the 'id' key.
597 - accept is a list of strings representing what content types are
598 accepted for media upload. Defaults to empty list if not in the
599 discovery document.
600 - max_size is a long representing the max size in bytes allowed for a
601 media upload. Defaults to 0L if not in the discovery document.
602 - media_path_url is a String; the absolute URI for media upload for the
603 API method. Constructed using the API root URI and service path from
604 the discovery document and the relative path for the API method. If
605 media upload is not supported, this is None.
606 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700607 path_url = method_desc["path"]
608 http_method = method_desc["httpMethod"]
609 method_id = method_desc["id"]
John Asmuth864311d2014-04-24 15:46:08 -0400610
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700611 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema)
612 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a
613 # 'parameters' key and needs to know if there is a 'body' parameter because it
614 # also sets a 'media_body' parameter.
615 accept, max_size, media_path_url = _fix_up_media_upload(
616 method_desc, root_desc, path_url, parameters
617 )
John Asmuth864311d2014-04-24 15:46:08 -0400618
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700619 return path_url, http_method, method_id, accept, max_size, media_path_url
John Asmuth864311d2014-04-24 15:46:08 -0400620
621
Craig Citro7ee535d2015-02-23 10:11:14 -0800622def _urljoin(base, url):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700623 """Custom urljoin replacement supporting : before / in url."""
624 # In general, it's unsafe to simply join base and url. However, for
625 # the case of discovery documents, we know:
626 # * base will never contain params, query, or fragment
627 # * url will never contain a scheme or net_loc.
628 # In general, this means we can safely join on /; we just need to
629 # ensure we end up with precisely one / joining base and url. The
630 # exception here is the case of media uploads, where url will be an
631 # absolute url.
632 if url.startswith("http://") or url.startswith("https://"):
633 return urljoin(base, url)
634 new_base = base if base.endswith("/") else base + "/"
635 new_url = url[1:] if url.startswith("/") else url
636 return new_base + new_url
Craig Citro7ee535d2015-02-23 10:11:14 -0800637
638
John Asmuth864311d2014-04-24 15:46:08 -0400639# TODO(dhermes): Convert this class to ResourceMethod and make it callable
640class ResourceMethodParameters(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700641 """Represents the parameters associated with a method.
John Asmuth864311d2014-04-24 15:46:08 -0400642
643 Attributes:
644 argmap: Map from method parameter name (string) to query parameter name
645 (string).
646 required_params: List of required parameters (represented by parameter
647 name as string).
648 repeated_params: List of repeated parameters (represented by parameter
649 name as string).
650 pattern_params: Map from method parameter name (string) to regular
651 expression (as a string). If the pattern is set for a parameter, the
652 value for that parameter must match the regular expression.
653 query_params: List of parameters (represented by parameter name as string)
654 that will be used in the query string.
655 path_params: Set of parameters (represented by parameter name as string)
656 that will be used in the base URL path.
657 param_types: Map from method parameter name (string) to parameter type. Type
658 can be any valid JSON schema type; valid values are 'any', 'array',
659 'boolean', 'integer', 'number', 'object', or 'string'. Reference:
660 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1
661 enum_params: Map from method parameter name (string) to list of strings,
662 where each list of strings is the list of acceptable enum values.
663 """
664
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700665 def __init__(self, method_desc):
666 """Constructor for ResourceMethodParameters.
John Asmuth864311d2014-04-24 15:46:08 -0400667
668 Sets default values and defers to set_parameters to populate.
669
670 Args:
671 method_desc: Dictionary with metadata describing an API method. Value
672 comes from the dictionary of methods stored in the 'methods' key in
673 the deserialized discovery document.
674 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700675 self.argmap = {}
676 self.required_params = []
677 self.repeated_params = []
678 self.pattern_params = {}
679 self.query_params = []
680 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE
681 # parsing is gotten rid of.
682 self.path_params = set()
683 self.param_types = {}
684 self.enum_params = {}
John Asmuth864311d2014-04-24 15:46:08 -0400685
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700686 self.set_parameters(method_desc)
John Asmuth864311d2014-04-24 15:46:08 -0400687
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700688 def set_parameters(self, method_desc):
689 """Populates maps and lists based on method description.
John Asmuth864311d2014-04-24 15:46:08 -0400690
691 Iterates through each parameter for the method and parses the values from
692 the parameter dictionary.
693
694 Args:
695 method_desc: Dictionary with metadata describing an API method. Value
696 comes from the dictionary of methods stored in the 'methods' key in
697 the deserialized discovery document.
698 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700699 for arg, desc in six.iteritems(method_desc.get("parameters", {})):
700 param = key2param(arg)
701 self.argmap[param] = arg
John Asmuth864311d2014-04-24 15:46:08 -0400702
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700703 if desc.get("pattern"):
704 self.pattern_params[param] = desc["pattern"]
705 if desc.get("enum"):
706 self.enum_params[param] = desc["enum"]
707 if desc.get("required"):
708 self.required_params.append(param)
709 if desc.get("repeated"):
710 self.repeated_params.append(param)
711 if desc.get("location") == "query":
712 self.query_params.append(param)
713 if desc.get("location") == "path":
714 self.path_params.add(param)
715 self.param_types[param] = desc.get("type", "string")
John Asmuth864311d2014-04-24 15:46:08 -0400716
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700717 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs
718 # should have all path parameters already marked with
719 # 'location: path'.
720 for match in URITEMPLATE.finditer(method_desc["path"]):
721 for namematch in VARNAME.finditer(match.group(0)):
722 name = key2param(namematch.group(0))
723 self.path_params.add(name)
724 if name in self.query_params:
725 self.query_params.remove(name)
John Asmuth864311d2014-04-24 15:46:08 -0400726
727
728def createMethod(methodName, methodDesc, rootDesc, schema):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700729 """Creates a method for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400730
731 Args:
732 methodName: string, name of the method to use.
733 methodDesc: object, fragment of deserialized discovery document that
734 describes the method.
735 rootDesc: object, the entire deserialized discovery document.
736 schema: object, mapping of schema names to schema descriptions.
737 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700738 methodName = fix_method_name(methodName)
739 (
740 pathUrl,
741 httpMethod,
742 methodId,
743 accept,
744 maxSize,
745 mediaPathUrl,
746 ) = _fix_up_method_description(methodDesc, rootDesc, schema)
John Asmuth864311d2014-04-24 15:46:08 -0400747
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700748 parameters = ResourceMethodParameters(methodDesc)
John Asmuth864311d2014-04-24 15:46:08 -0400749
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700750 def method(self, **kwargs):
751 # Don't bother with doc string, it will be over-written by createMethod.
John Asmuth864311d2014-04-24 15:46:08 -0400752
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700753 for name in six.iterkeys(kwargs):
754 if name not in parameters.argmap:
755 raise TypeError('Got an unexpected keyword argument "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400756
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700757 # Remove args that have a value of None.
758 keys = list(kwargs.keys())
759 for name in keys:
760 if kwargs[name] is None:
761 del kwargs[name]
John Asmuth864311d2014-04-24 15:46:08 -0400762
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700763 for name in parameters.required_params:
764 if name not in kwargs:
765 # temporary workaround for non-paging methods incorrectly requiring
766 # page token parameter (cf. drive.changes.watch vs. drive.changes.list)
767 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName(
768 _methodProperties(methodDesc, schema, "response")
769 ):
770 raise TypeError('Missing required parameter "%s"' % name)
John Asmuth864311d2014-04-24 15:46:08 -0400771
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700772 for name, regex in six.iteritems(parameters.pattern_params):
773 if name in kwargs:
774 if isinstance(kwargs[name], six.string_types):
775 pvalues = [kwargs[name]]
776 else:
777 pvalues = kwargs[name]
778 for pvalue in pvalues:
779 if re.match(regex, pvalue) is None:
780 raise TypeError(
781 'Parameter "%s" value "%s" does not match the pattern "%s"'
782 % (name, pvalue, regex)
783 )
784
785 for name, enums in six.iteritems(parameters.enum_params):
786 if name in kwargs:
787 # We need to handle the case of a repeated enum
788 # name differently, since we want to handle both
789 # arg='value' and arg=['value1', 'value2']
790 if name in parameters.repeated_params and not isinstance(
791 kwargs[name], six.string_types
792 ):
793 values = kwargs[name]
794 else:
795 values = [kwargs[name]]
796 for value in values:
797 if value not in enums:
798 raise TypeError(
799 'Parameter "%s" value "%s" is not an allowed value in "%s"'
800 % (name, value, str(enums))
801 )
802
803 actual_query_params = {}
804 actual_path_params = {}
805 for key, value in six.iteritems(kwargs):
806 to_type = parameters.param_types.get(key, "string")
807 # For repeated parameters we cast each member of the list.
808 if key in parameters.repeated_params and type(value) == type([]):
809 cast_value = [_cast(x, to_type) for x in value]
810 else:
811 cast_value = _cast(value, to_type)
812 if key in parameters.query_params:
813 actual_query_params[parameters.argmap[key]] = cast_value
814 if key in parameters.path_params:
815 actual_path_params[parameters.argmap[key]] = cast_value
816 body_value = kwargs.get("body", None)
817 media_filename = kwargs.get("media_body", None)
818 media_mime_type = kwargs.get("media_mime_type", None)
819
820 if self._developerKey:
821 actual_query_params["key"] = self._developerKey
822
823 model = self._model
824 if methodName.endswith("_media"):
825 model = MediaModel()
826 elif "response" not in methodDesc:
827 model = RawModel()
828
829 headers = {}
830 headers, params, query, body = model.request(
831 headers, actual_path_params, actual_query_params, body_value
832 )
833
834 expanded_url = uritemplate.expand(pathUrl, params)
835 url = _urljoin(self._baseUrl, expanded_url + query)
836
837 resumable = None
838 multipart_boundary = ""
839
840 if media_filename:
841 # Ensure we end up with a valid MediaUpload object.
842 if isinstance(media_filename, six.string_types):
843 if media_mime_type is None:
844 logger.warning(
845 "media_mime_type argument not specified: trying to auto-detect for %s",
846 media_filename,
847 )
848 media_mime_type, _ = mimetypes.guess_type(media_filename)
849 if media_mime_type is None:
850 raise UnknownFileType(media_filename)
851 if not mimeparse.best_match([media_mime_type], ",".join(accept)):
852 raise UnacceptableMimeTypeError(media_mime_type)
853 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type)
854 elif isinstance(media_filename, MediaUpload):
855 media_upload = media_filename
856 else:
857 raise TypeError("media_filename must be str or MediaUpload.")
858
859 # Check the maxSize
860 if media_upload.size() is not None and media_upload.size() > maxSize > 0:
861 raise MediaUploadSizeError("Media larger than: %s" % maxSize)
862
863 # Use the media path uri for media uploads
864 expanded_url = uritemplate.expand(mediaPathUrl, params)
865 url = _urljoin(self._baseUrl, expanded_url + query)
866 if media_upload.resumable():
867 url = _add_query_parameter(url, "uploadType", "resumable")
868
869 if media_upload.resumable():
870 # This is all we need to do for resumable, if the body exists it gets
871 # sent in the first request, otherwise an empty body is sent.
872 resumable = media_upload
873 else:
874 # A non-resumable upload
875 if body is None:
876 # This is a simple media upload
877 headers["content-type"] = media_upload.mimetype()
878 body = media_upload.getbytes(0, media_upload.size())
879 url = _add_query_parameter(url, "uploadType", "media")
880 else:
881 # This is a multipart/related upload.
882 msgRoot = MIMEMultipart("related")
883 # msgRoot should not write out it's own headers
884 setattr(msgRoot, "_write_headers", lambda self: None)
885
886 # attach the body as one part
887 msg = MIMENonMultipart(*headers["content-type"].split("/"))
888 msg.set_payload(body)
889 msgRoot.attach(msg)
890
891 # attach the media as the second part
892 msg = MIMENonMultipart(*media_upload.mimetype().split("/"))
893 msg["Content-Transfer-Encoding"] = "binary"
894
895 payload = media_upload.getbytes(0, media_upload.size())
896 msg.set_payload(payload)
897 msgRoot.attach(msg)
898 # encode the body: note that we can't use `as_string`, because
899 # it plays games with `From ` lines.
900 fp = BytesIO()
901 g = _BytesGenerator(fp, mangle_from_=False)
902 g.flatten(msgRoot, unixfrom=False)
903 body = fp.getvalue()
904
905 multipart_boundary = msgRoot.get_boundary()
906 headers["content-type"] = (
907 "multipart/related; " 'boundary="%s"'
908 ) % multipart_boundary
909 url = _add_query_parameter(url, "uploadType", "multipart")
910
911 logger.info("URL being requested: %s %s" % (httpMethod, url))
912 return self._requestBuilder(
913 self._http,
914 model.response,
915 url,
916 method=httpMethod,
917 body=body,
918 headers=headers,
919 methodId=methodId,
920 resumable=resumable,
921 )
922
923 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"]
924 if len(parameters.argmap) > 0:
925 docs.append("Args:\n")
926
927 # Skip undocumented params and params common to all methods.
928 skip_parameters = list(rootDesc.get("parameters", {}).keys())
929 skip_parameters.extend(STACK_QUERY_PARAMETERS)
930
931 all_args = list(parameters.argmap.keys())
932 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])]
933
934 # Move body to the front of the line.
935 if "body" in all_args:
936 args_ordered.append("body")
937
938 for name in all_args:
939 if name not in args_ordered:
940 args_ordered.append(name)
941
942 for arg in args_ordered:
943 if arg in skip_parameters:
944 continue
945
946 repeated = ""
947 if arg in parameters.repeated_params:
948 repeated = " (repeated)"
949 required = ""
950 if arg in parameters.required_params:
951 required = " (required)"
952 paramdesc = methodDesc["parameters"][parameters.argmap[arg]]
953 paramdoc = paramdesc.get("description", "A parameter")
954 if "$ref" in paramdesc:
955 docs.append(
956 (" %s: object, %s%s%s\n The object takes the" " form of:\n\n%s\n\n")
957 % (
958 arg,
959 paramdoc,
960 required,
961 repeated,
962 schema.prettyPrintByName(paramdesc["$ref"]),
963 )
964 )
John Asmuth864311d2014-04-24 15:46:08 -0400965 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700966 paramtype = paramdesc.get("type", "string")
967 docs.append(
968 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated)
969 )
970 enum = paramdesc.get("enum", [])
971 enumDesc = paramdesc.get("enumDescriptions", [])
972 if enum and enumDesc:
973 docs.append(" Allowed values\n")
974 for (name, desc) in zip(enum, enumDesc):
975 docs.append(" %s - %s\n" % (name, desc))
976 if "response" in methodDesc:
977 if methodName.endswith("_media"):
978 docs.append("\nReturns:\n The media object as a string.\n\n ")
John Asmuth864311d2014-04-24 15:46:08 -0400979 else:
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700980 docs.append("\nReturns:\n An object of the form:\n\n ")
981 docs.append(schema.prettyPrintSchema(methodDesc["response"]))
John Asmuth864311d2014-04-24 15:46:08 -0400982
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700983 setattr(method, "__doc__", "".join(docs))
984 return (methodName, method)
John Asmuth864311d2014-04-24 15:46:08 -0400985
986
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700987def createNextMethod(
988 methodName,
989 pageTokenName="pageToken",
990 nextPageTokenName="nextPageToken",
991 isPageTokenParameter=True,
992):
993 """Creates any _next methods for attaching to a Resource.
John Asmuth864311d2014-04-24 15:46:08 -0400994
995 The _next methods allow for easy iteration through list() responses.
996
997 Args:
998 methodName: string, name of the method to use.
Thomas Coffee20af04d2017-02-10 15:24:44 -0800999 pageTokenName: string, name of request page token field.
1000 nextPageTokenName: string, name of response page token field.
1001 isPageTokenParameter: Boolean, True if request page token is a query
1002 parameter, False if request page token is a field of the request body.
John Asmuth864311d2014-04-24 15:46:08 -04001003 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001004 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001005
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001006 def methodNext(self, previous_request, previous_response):
1007 """Retrieves the next page of results.
John Asmuth864311d2014-04-24 15:46:08 -04001008
1009Args:
1010 previous_request: The request for the previous page. (required)
1011 previous_response: The response from the request for the previous page. (required)
1012
1013Returns:
1014 A request object that you can call 'execute()' on to request the next
1015 page. Returns None if there are no more items in the collection.
1016 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001017 # Retrieve nextPageToken from previous_response
1018 # Use as pageToken in previous_request to create new request.
John Asmuth864311d2014-04-24 15:46:08 -04001019
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001020 nextPageToken = previous_response.get(nextPageTokenName, None)
1021 if not nextPageToken:
1022 return None
John Asmuth864311d2014-04-24 15:46:08 -04001023
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001024 request = copy.copy(previous_request)
John Asmuth864311d2014-04-24 15:46:08 -04001025
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001026 if isPageTokenParameter:
1027 # Replace pageToken value in URI
1028 request.uri = _add_query_parameter(
1029 request.uri, pageTokenName, nextPageToken
1030 )
1031 logger.info("Next page request URL: %s %s" % (methodName, request.uri))
1032 else:
1033 # Replace pageToken value in request body
1034 model = self._model
1035 body = model.deserialize(request.body)
1036 body[pageTokenName] = nextPageToken
1037 request.body = model.serialize(body)
1038 logger.info("Next page request body: %s %s" % (methodName, body))
John Asmuth864311d2014-04-24 15:46:08 -04001039
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001040 return request
John Asmuth864311d2014-04-24 15:46:08 -04001041
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001042 return (methodName, methodNext)
John Asmuth864311d2014-04-24 15:46:08 -04001043
1044
1045class Resource(object):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001046 """A class for interacting with a resource."""
John Asmuth864311d2014-04-24 15:46:08 -04001047
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001048 def __init__(
1049 self,
1050 http,
1051 baseUrl,
1052 model,
1053 requestBuilder,
1054 developerKey,
1055 resourceDesc,
1056 rootDesc,
1057 schema,
1058 ):
1059 """Build a Resource from the API description.
John Asmuth864311d2014-04-24 15:46:08 -04001060
1061 Args:
1062 http: httplib2.Http, Object to make http requests with.
1063 baseUrl: string, base URL for the API. All requests are relative to this
1064 URI.
1065 model: googleapiclient.Model, converts to and from the wire format.
1066 requestBuilder: class or callable that instantiates an
1067 googleapiclient.HttpRequest object.
1068 developerKey: string, key obtained from
1069 https://code.google.com/apis/console
1070 resourceDesc: object, section of deserialized discovery document that
1071 describes a resource. Note that the top level discovery document
1072 is considered a resource.
1073 rootDesc: object, the entire deserialized discovery document.
1074 schema: object, mapping of schema names to schema descriptions.
1075 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001076 self._dynamic_attrs = []
John Asmuth864311d2014-04-24 15:46:08 -04001077
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001078 self._http = http
1079 self._baseUrl = baseUrl
1080 self._model = model
1081 self._developerKey = developerKey
1082 self._requestBuilder = requestBuilder
1083 self._resourceDesc = resourceDesc
1084 self._rootDesc = rootDesc
1085 self._schema = schema
John Asmuth864311d2014-04-24 15:46:08 -04001086
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001087 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001088
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001089 def _set_dynamic_attr(self, attr_name, value):
1090 """Sets an instance attribute and tracks it in a list of dynamic attributes.
John Asmuth864311d2014-04-24 15:46:08 -04001091
1092 Args:
1093 attr_name: string; The name of the attribute to be set
1094 value: The value being set on the object and tracked in the dynamic cache.
1095 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001096 self._dynamic_attrs.append(attr_name)
1097 self.__dict__[attr_name] = value
John Asmuth864311d2014-04-24 15:46:08 -04001098
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001099 def __getstate__(self):
1100 """Trim the state down to something that can be pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001101
1102 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1103 will be wiped and restored on pickle serialization.
1104 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001105 state_dict = copy.copy(self.__dict__)
1106 for dynamic_attr in self._dynamic_attrs:
1107 del state_dict[dynamic_attr]
1108 del state_dict["_dynamic_attrs"]
1109 return state_dict
John Asmuth864311d2014-04-24 15:46:08 -04001110
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001111 def __setstate__(self, state):
1112 """Reconstitute the state of the object from being pickled.
John Asmuth864311d2014-04-24 15:46:08 -04001113
1114 Uses the fact that the instance variable _dynamic_attrs holds attrs that
1115 will be wiped and restored on pickle serialization.
1116 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001117 self.__dict__.update(state)
1118 self._dynamic_attrs = []
1119 self._set_service_methods()
John Asmuth864311d2014-04-24 15:46:08 -04001120
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001121 def _set_service_methods(self):
1122 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema)
1123 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema)
1124 self._add_next_methods(self._resourceDesc, self._schema)
John Asmuth864311d2014-04-24 15:46:08 -04001125
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001126 def _add_basic_methods(self, resourceDesc, rootDesc, schema):
1127 # If this is the root Resource, add a new_batch_http_request() method.
1128 if resourceDesc == rootDesc:
1129 batch_uri = "%s%s" % (
1130 rootDesc["rootUrl"],
1131 rootDesc.get("batchPath", "batch"),
1132 )
1133
1134 def new_batch_http_request(callback=None):
1135 """Create a BatchHttpRequest object based on the discovery document.
Pepper Lebeck-Jobe56052ec2015-06-13 14:07:14 -04001136
1137 Args:
1138 callback: callable, A callback to be called for each response, of the
1139 form callback(id, response, exception). The first parameter is the
1140 request id, and the second is the deserialized response object. The
1141 third is an apiclient.errors.HttpError exception object if an HTTP
1142 error occurred while processing the request, or None if no error
1143 occurred.
1144
1145 Returns:
1146 A BatchHttpRequest object based on the discovery document.
1147 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001148 return BatchHttpRequest(callback=callback, batch_uri=batch_uri)
Pepper Lebeck-Jobe860836f2015-06-12 20:42:23 -04001149
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001150 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request)
John Asmuth864311d2014-04-24 15:46:08 -04001151
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001152 # Add basic methods to Resource
1153 if "methods" in resourceDesc:
1154 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1155 fixedMethodName, method = createMethod(
1156 methodName, methodDesc, rootDesc, schema
1157 )
1158 self._set_dynamic_attr(
1159 fixedMethodName, method.__get__(self, self.__class__)
1160 )
1161 # Add in _media methods. The functionality of the attached method will
1162 # change when it sees that the method name ends in _media.
1163 if methodDesc.get("supportsMediaDownload", False):
1164 fixedMethodName, method = createMethod(
1165 methodName + "_media", methodDesc, rootDesc, schema
1166 )
1167 self._set_dynamic_attr(
1168 fixedMethodName, method.__get__(self, self.__class__)
1169 )
John Asmuth864311d2014-04-24 15:46:08 -04001170
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001171 def _add_nested_resources(self, resourceDesc, rootDesc, schema):
1172 # Add in nested resources
1173 if "resources" in resourceDesc:
1174
1175 def createResourceMethod(methodName, methodDesc):
1176 """Create a method on the Resource to access a nested Resource.
John Asmuth864311d2014-04-24 15:46:08 -04001177
1178 Args:
1179 methodName: string, name of the method to use.
1180 methodDesc: object, fragment of deserialized discovery document that
1181 describes the method.
1182 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001183 methodName = fix_method_name(methodName)
John Asmuth864311d2014-04-24 15:46:08 -04001184
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001185 def methodResource(self):
1186 return Resource(
1187 http=self._http,
1188 baseUrl=self._baseUrl,
1189 model=self._model,
1190 developerKey=self._developerKey,
1191 requestBuilder=self._requestBuilder,
1192 resourceDesc=methodDesc,
1193 rootDesc=rootDesc,
1194 schema=schema,
1195 )
John Asmuth864311d2014-04-24 15:46:08 -04001196
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001197 setattr(methodResource, "__doc__", "A collection resource.")
1198 setattr(methodResource, "__is_resource__", True)
John Asmuth864311d2014-04-24 15:46:08 -04001199
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001200 return (methodName, methodResource)
John Asmuth864311d2014-04-24 15:46:08 -04001201
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001202 for methodName, methodDesc in six.iteritems(resourceDesc["resources"]):
1203 fixedMethodName, method = createResourceMethod(methodName, methodDesc)
1204 self._set_dynamic_attr(
1205 fixedMethodName, method.__get__(self, self.__class__)
1206 )
John Asmuth864311d2014-04-24 15:46:08 -04001207
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001208 def _add_next_methods(self, resourceDesc, schema):
1209 # Add _next() methods if and only if one of the names 'pageToken' or
1210 # 'nextPageToken' occurs among the fields of both the method's response
1211 # type either the method's request (query parameters) or request body.
1212 if "methods" not in resourceDesc:
1213 return
1214 for methodName, methodDesc in six.iteritems(resourceDesc["methods"]):
1215 nextPageTokenName = _findPageTokenName(
1216 _methodProperties(methodDesc, schema, "response")
1217 )
1218 if not nextPageTokenName:
1219 continue
1220 isPageTokenParameter = True
1221 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {}))
1222 if not pageTokenName:
1223 isPageTokenParameter = False
1224 pageTokenName = _findPageTokenName(
1225 _methodProperties(methodDesc, schema, "request")
1226 )
1227 if not pageTokenName:
1228 continue
1229 fixedMethodName, method = createNextMethod(
1230 methodName + "_next",
1231 pageTokenName,
1232 nextPageTokenName,
1233 isPageTokenParameter,
1234 )
1235 self._set_dynamic_attr(
1236 fixedMethodName, method.__get__(self, self.__class__)
1237 )
Thomas Coffee20af04d2017-02-10 15:24:44 -08001238
1239
1240def _findPageTokenName(fields):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001241 """Search field names for one like a page token.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001242
1243 Args:
1244 fields: container of string, names of fields.
1245
1246 Returns:
1247 First name that is either 'pageToken' or 'nextPageToken' if one exists,
1248 otherwise None.
1249 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001250 return next(
1251 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None
1252 )
1253
Thomas Coffee20af04d2017-02-10 15:24:44 -08001254
1255def _methodProperties(methodDesc, schema, name):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001256 """Get properties of a field in a method description.
Thomas Coffee20af04d2017-02-10 15:24:44 -08001257
1258 Args:
1259 methodDesc: object, fragment of deserialized discovery document that
1260 describes the method.
1261 schema: object, mapping of schema names to schema descriptions.
1262 name: string, name of top-level field in method description.
1263
1264 Returns:
1265 Object representing fragment of deserialized discovery document
1266 corresponding to 'properties' field of object corresponding to named field
1267 in method description, if it exists, otherwise empty dict.
1268 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -07001269 desc = methodDesc.get(name, {})
1270 if "$ref" in desc:
1271 desc = schema.get(desc["$ref"], {})
1272 return desc.get("properties", {})