Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/googleapiclient/discovery.py: 57%

549 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

1# Copyright 2014 Google Inc. All Rights Reserved. 

2# 

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""" 

19from __future__ import absolute_import 

20 

21__author__ = "jcgregorio@google.com (Joe Gregorio)" 

22__all__ = ["build", "build_from_document", "fix_method_name", "key2param"] 

23 

24from collections import OrderedDict 

25import collections.abc 

26 

27# Standard library imports 

28import copy 

29from email.generator import BytesGenerator 

30from email.mime.multipart import MIMEMultipart 

31from email.mime.nonmultipart import MIMENonMultipart 

32import http.client as http_client 

33import io 

34import json 

35import keyword 

36import logging 

37import mimetypes 

38import os 

39import re 

40import urllib 

41 

42import google.api_core.client_options 

43from google.auth.exceptions import MutualTLSChannelError 

44from google.auth.transport import mtls 

45from google.oauth2 import service_account 

46 

47# Third-party imports 

48import httplib2 

49import uritemplate 

50 

51try: 

52 import google_auth_httplib2 

53except ImportError: # pragma: NO COVER 

54 google_auth_httplib2 = None 

55 

56# Local imports 

57from googleapiclient import _auth, mimeparse 

58from googleapiclient._helpers import _add_query_parameter, positional 

59from googleapiclient.errors import ( 

60 HttpError, 

61 InvalidJsonError, 

62 MediaUploadSizeError, 

63 UnacceptableMimeTypeError, 

64 UnknownApiNameOrVersion, 

65 UnknownFileType, 

66) 

67from googleapiclient.http import ( 

68 BatchHttpRequest, 

69 HttpMock, 

70 HttpMockSequence, 

71 HttpRequest, 

72 MediaFileUpload, 

73 MediaUpload, 

74 build_http, 

75) 

76from googleapiclient.model import JsonModel, MediaModel, RawModel 

77from googleapiclient.schema import Schemas 

78 

79# The client library requires a version of httplib2 that supports RETRIES. 

80httplib2.RETRIES = 1 

81 

82logger = logging.getLogger(__name__) 

83 

84URITEMPLATE = re.compile("{[^}]*}") 

85VARNAME = re.compile("[a-zA-Z0-9_-]+") 

86DISCOVERY_URI = ( 

87 "https://www.googleapis.com/discovery/v1/apis/" "{api}/{apiVersion}/rest" 

88) 

89V1_DISCOVERY_URI = DISCOVERY_URI 

90V2_DISCOVERY_URI = ( 

91 "https://{api}.googleapis.com/$discovery/rest?" "version={apiVersion}" 

92) 

93DEFAULT_METHOD_DOC = "A description of how to use this function" 

94HTTP_PAYLOAD_METHODS = frozenset(["PUT", "POST", "PATCH"]) 

95 

96_MEDIA_SIZE_BIT_SHIFTS = {"KB": 10, "MB": 20, "GB": 30, "TB": 40} 

97BODY_PARAMETER_DEFAULT_VALUE = {"description": "The request body.", "type": "object"} 

98MEDIA_BODY_PARAMETER_DEFAULT_VALUE = { 

99 "description": ( 

100 "The filename of the media request body, or an instance " 

101 "of a MediaUpload object." 

102 ), 

103 "type": "string", 

104 "required": False, 

105} 

106MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE = { 

107 "description": ( 

108 "The MIME type of the media request body, or an instance " 

109 "of a MediaUpload object." 

110 ), 

111 "type": "string", 

112 "required": False, 

113} 

114_PAGE_TOKEN_NAMES = ("pageToken", "nextPageToken") 

115 

116# Parameters controlling mTLS behavior. See https://google.aip.dev/auth/4114. 

117GOOGLE_API_USE_CLIENT_CERTIFICATE = "GOOGLE_API_USE_CLIENT_CERTIFICATE" 

118GOOGLE_API_USE_MTLS_ENDPOINT = "GOOGLE_API_USE_MTLS_ENDPOINT" 

119 

120# Parameters accepted by the stack, but not visible via discovery. 

121# TODO(dhermes): Remove 'userip' in 'v2'. 

122STACK_QUERY_PARAMETERS = frozenset(["trace", "pp", "userip", "strict"]) 

123STACK_QUERY_PARAMETER_DEFAULT_VALUE = {"type": "string", "location": "query"} 

124 

125# Library-specific reserved words beyond Python keywords. 

126RESERVED_WORDS = frozenset(["body"]) 

127 

128# patch _write_lines to avoid munging '\r' into '\n' 

129# ( https://bugs.python.org/issue18886 https://bugs.python.org/issue19003 ) 

130class _BytesGenerator(BytesGenerator): 

131 _write_lines = BytesGenerator.write 

132 

133 

134def fix_method_name(name): 

135 """Fix method names to avoid '$' characters and reserved word conflicts. 

136 

137 Args: 

138 name: string, method name. 

139 

140 Returns: 

141 The name with '_' appended if the name is a reserved word and '$' and '-' 

142 replaced with '_'. 

143 """ 

144 name = name.replace("$", "_").replace("-", "_") 

145 if keyword.iskeyword(name) or name in RESERVED_WORDS: 

146 return name + "_" 

147 else: 

148 return name 

149 

150 

151def key2param(key): 

152 """Converts key names into parameter names. 

153 

154 For example, converting "max-results" -> "max_results" 

155 

156 Args: 

157 key: string, the method key name. 

158 

159 Returns: 

160 A safe method name based on the key name. 

161 """ 

162 result = [] 

163 key = list(key) 

164 if not key[0].isalpha(): 

165 result.append("x") 

166 for c in key: 

167 if c.isalnum(): 

168 result.append(c) 

169 else: 

170 result.append("_") 

171 

172 return "".join(result) 

173 

174 

175@positional(2) 

176def build( 

177 serviceName, 

178 version, 

179 http=None, 

180 discoveryServiceUrl=None, 

181 developerKey=None, 

182 model=None, 

183 requestBuilder=HttpRequest, 

184 credentials=None, 

185 cache_discovery=True, 

186 cache=None, 

187 client_options=None, 

188 adc_cert_path=None, 

189 adc_key_path=None, 

190 num_retries=1, 

191 static_discovery=None, 

192 always_use_jwt_access=False, 

193): 

194 """Construct a Resource for interacting with an API. 

195 

196 Construct a Resource object for interacting with an API. The serviceName and 

197 version are the names from the Discovery service. 

198 

199 Args: 

200 serviceName: string, name of the service. 

201 version: string, the version of the service. 

202 http: httplib2.Http, An instance of httplib2.Http or something that acts 

203 like it that HTTP requests will be made through. 

204 discoveryServiceUrl: string, a URI Template that points to the location of 

205 the discovery service. It should have two parameters {api} and 

206 {apiVersion} that when filled in produce an absolute URI to the discovery 

207 document for that service. 

208 developerKey: string, key obtained from 

209 https://code.google.com/apis/console. 

210 model: googleapiclient.Model, converts to and from the wire format. 

211 requestBuilder: googleapiclient.http.HttpRequest, encapsulator for an HTTP 

212 request. 

213 credentials: oauth2client.Credentials or 

214 google.auth.credentials.Credentials, credentials to be used for 

215 authentication. 

216 cache_discovery: Boolean, whether or not to cache the discovery doc. 

217 cache: googleapiclient.discovery_cache.base.CacheBase, an optional 

218 cache object for the discovery documents. 

219 client_options: Mapping object or google.api_core.client_options, client 

220 options to set user options on the client. 

221 (1) The API endpoint should be set through client_options. If API endpoint 

222 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used 

223 to control which endpoint to use. 

224 (2) client_cert_source is not supported, client cert should be provided using 

225 client_encrypted_cert_source instead. In order to use the provided client 

226 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be 

227 set to `true`. 

228 More details on the environment variables are here: 

229 https://google.aip.dev/auth/4114 

230 adc_cert_path: str, client certificate file path to save the application 

231 default client certificate for mTLS. This field is required if you want to 

232 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE` 

233 environment variable must be set to `true` in order to use this field, 

234 otherwise this field doesn't nothing. 

235 More details on the environment variables are here: 

236 https://google.aip.dev/auth/4114 

237 adc_key_path: str, client encrypted private key file path to save the 

238 application default client encrypted private key for mTLS. This field is 

239 required if you want to use the default client certificate. 

240 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to 

241 `true` in order to use this field, otherwise this field doesn't nothing. 

242 More details on the environment variables are here: 

243 https://google.aip.dev/auth/4114 

244 num_retries: Integer, number of times to retry discovery with 

245 randomized exponential backoff in case of intermittent/connection issues. 

246 static_discovery: Boolean, whether or not to use the static discovery docs 

247 included in the library. The default value for `static_discovery` depends 

248 on the value of `discoveryServiceUrl`. `static_discovery` will default to 

249 `True` when `discoveryServiceUrl` is also not provided, otherwise it will 

250 default to `False`. 

251 always_use_jwt_access: Boolean, whether always use self signed JWT for service 

252 account credentials. This only applies to 

253 google.oauth2.service_account.Credentials. 

254 

255 Returns: 

256 A Resource object with methods for interacting with the service. 

257 

258 Raises: 

259 google.auth.exceptions.MutualTLSChannelError: if there are any problems 

260 setting up mutual TLS channel. 

261 """ 

262 params = {"api": serviceName, "apiVersion": version} 

263 

264 # The default value for `static_discovery` depends on the value of 

265 # `discoveryServiceUrl`. `static_discovery` will default to `True` when 

266 # `discoveryServiceUrl` is also not provided, otherwise it will default to 

267 # `False`. This is added for backwards compatability with 

268 # google-api-python-client 1.x which does not support the `static_discovery` 

269 # parameter. 

270 if static_discovery is None: 

271 if discoveryServiceUrl is None: 

272 static_discovery = True 

273 else: 

274 static_discovery = False 

275 

276 if http is None: 

277 discovery_http = build_http() 

278 else: 

279 discovery_http = http 

280 

281 service = None 

282 

283 for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version): 

284 requested_url = uritemplate.expand(discovery_url, params) 

285 

286 try: 

287 content = _retrieve_discovery_doc( 

288 requested_url, 

289 discovery_http, 

290 cache_discovery, 

291 serviceName, 

292 version, 

293 cache, 

294 developerKey, 

295 num_retries=num_retries, 

296 static_discovery=static_discovery, 

297 ) 

298 service = build_from_document( 

299 content, 

300 base=discovery_url, 

301 http=http, 

302 developerKey=developerKey, 

303 model=model, 

304 requestBuilder=requestBuilder, 

305 credentials=credentials, 

306 client_options=client_options, 

307 adc_cert_path=adc_cert_path, 

308 adc_key_path=adc_key_path, 

309 always_use_jwt_access=always_use_jwt_access, 

310 ) 

311 break # exit if a service was created 

312 except HttpError as e: 

313 if e.resp.status == http_client.NOT_FOUND: 

314 continue 

315 else: 

316 raise e 

317 

318 # If discovery_http was created by this function, we are done with it 

319 # and can safely close it 

320 if http is None: 

321 discovery_http.close() 

322 

323 if service is None: 

324 raise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version)) 

325 else: 

326 return service 

327 

328 

329def _discovery_service_uri_options(discoveryServiceUrl, version): 

330 """ 

331 Returns Discovery URIs to be used for attempting to build the API Resource. 

332 

333 Args: 

334 discoveryServiceUrl: 

335 string, the Original Discovery Service URL preferred by the customer. 

336 version: 

337 string, API Version requested 

338 

339 Returns: 

340 A list of URIs to be tried for the Service Discovery, in order. 

341 """ 

342 

343 if discoveryServiceUrl is not None: 

344 return [discoveryServiceUrl] 

345 if version is None: 

346 # V1 Discovery won't work if the requested version is None 

347 logger.warning( 

348 "Discovery V1 does not support empty versions. Defaulting to V2..." 

349 ) 

350 return [V2_DISCOVERY_URI] 

351 else: 

352 return [DISCOVERY_URI, V2_DISCOVERY_URI] 

353 

354 

355def _retrieve_discovery_doc( 

356 url, 

357 http, 

358 cache_discovery, 

359 serviceName, 

360 version, 

361 cache=None, 

362 developerKey=None, 

363 num_retries=1, 

364 static_discovery=True, 

365): 

366 """Retrieves the discovery_doc from cache or the internet. 

367 

368 Args: 

369 url: string, the URL of the discovery document. 

370 http: httplib2.Http, An instance of httplib2.Http or something that acts 

371 like it through which HTTP requests will be made. 

372 cache_discovery: Boolean, whether or not to cache the discovery doc. 

373 serviceName: string, name of the service. 

374 version: string, the version of the service. 

375 cache: googleapiclient.discovery_cache.base.Cache, an optional cache 

376 object for the discovery documents. 

377 developerKey: string, Key for controlling API usage, generated 

378 from the API Console. 

379 num_retries: Integer, number of times to retry discovery with 

380 randomized exponential backoff in case of intermittent/connection issues. 

381 static_discovery: Boolean, whether or not to use the static discovery docs 

382 included in the library. 

383 

384 Returns: 

385 A unicode string representation of the discovery document. 

386 """ 

387 from . import discovery_cache 

388 

389 if cache_discovery: 

390 if cache is None: 

391 cache = discovery_cache.autodetect() 

392 if cache: 

393 content = cache.get(url) 

394 if content: 

395 return content 

396 

397 # When `static_discovery=True`, use static discovery artifacts included 

398 # with the library 

399 if static_discovery: 

400 content = discovery_cache.get_static_doc(serviceName, version) 

401 if content: 

402 return content 

403 else: 

404 raise UnknownApiNameOrVersion( 

405 "name: %s version: %s" % (serviceName, version) 

406 ) 

407 

408 actual_url = url 

409 # REMOTE_ADDR is defined by the CGI spec [RFC3875] as the environment 

410 # variable that contains the network address of the client sending the 

411 # request. If it exists then add that to the request for the discovery 

412 # document to avoid exceeding the quota on discovery requests. 

413 if "REMOTE_ADDR" in os.environ: 

414 actual_url = _add_query_parameter(url, "userIp", os.environ["REMOTE_ADDR"]) 

415 if developerKey: 

416 actual_url = _add_query_parameter(url, "key", developerKey) 

417 logger.debug("URL being requested: GET %s", actual_url) 

418 

419 # Execute this request with retries build into HttpRequest 

420 # Note that it will already raise an error if we don't get a 2xx response 

421 req = HttpRequest(http, HttpRequest.null_postproc, actual_url) 

422 resp, content = req.execute(num_retries=num_retries) 

423 

424 try: 

425 content = content.decode("utf-8") 

426 except AttributeError: 

427 pass 

428 

429 try: 

430 service = json.loads(content) 

431 except ValueError as e: 

432 logger.error("Failed to parse as JSON: " + content) 

433 raise InvalidJsonError() 

434 if cache_discovery and cache: 

435 cache.set(url, content) 

436 return content 

437 

438 

439@positional(1) 

440def build_from_document( 

441 service, 

442 base=None, 

443 future=None, 

444 http=None, 

445 developerKey=None, 

446 model=None, 

447 requestBuilder=HttpRequest, 

448 credentials=None, 

449 client_options=None, 

450 adc_cert_path=None, 

451 adc_key_path=None, 

452 always_use_jwt_access=False, 

453): 

454 """Create a Resource for interacting with an API. 

455 

456 Same as `build()`, but constructs the Resource object from a discovery 

457 document that is it given, as opposed to retrieving one over HTTP. 

458 

459 Args: 

460 service: string or object, the JSON discovery document describing the API. 

461 The value passed in may either be the JSON string or the deserialized 

462 JSON. 

463 base: string, base URI for all HTTP requests, usually the discovery URI. 

464 This parameter is no longer used as rootUrl and servicePath are included 

465 within the discovery document. (deprecated) 

466 future: string, discovery document with future capabilities (deprecated). 

467 http: httplib2.Http, An instance of httplib2.Http or something that acts 

468 like it that HTTP requests will be made through. 

469 developerKey: string, Key for controlling API usage, generated 

470 from the API Console. 

471 model: Model class instance that serializes and de-serializes requests and 

472 responses. 

473 requestBuilder: Takes an http request and packages it up to be executed. 

474 credentials: oauth2client.Credentials or 

475 google.auth.credentials.Credentials, credentials to be used for 

476 authentication. 

477 client_options: Mapping object or google.api_core.client_options, client 

478 options to set user options on the client. 

479 (1) The API endpoint should be set through client_options. If API endpoint 

480 is not set, `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable can be used 

481 to control which endpoint to use. 

482 (2) client_cert_source is not supported, client cert should be provided using 

483 client_encrypted_cert_source instead. In order to use the provided client 

484 cert, `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be 

485 set to `true`. 

486 More details on the environment variables are here: 

487 https://google.aip.dev/auth/4114 

488 adc_cert_path: str, client certificate file path to save the application 

489 default client certificate for mTLS. This field is required if you want to 

490 use the default client certificate. `GOOGLE_API_USE_CLIENT_CERTIFICATE` 

491 environment variable must be set to `true` in order to use this field, 

492 otherwise this field doesn't nothing. 

493 More details on the environment variables are here: 

494 https://google.aip.dev/auth/4114 

495 adc_key_path: str, client encrypted private key file path to save the 

496 application default client encrypted private key for mTLS. This field is 

497 required if you want to use the default client certificate. 

498 `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable must be set to 

499 `true` in order to use this field, otherwise this field doesn't nothing. 

500 More details on the environment variables are here: 

501 https://google.aip.dev/auth/4114 

502 always_use_jwt_access: Boolean, whether always use self signed JWT for service 

503 account credentials. This only applies to 

504 google.oauth2.service_account.Credentials. 

505 

506 Returns: 

507 A Resource object with methods for interacting with the service. 

508 

509 Raises: 

510 google.auth.exceptions.MutualTLSChannelError: if there are any problems 

511 setting up mutual TLS channel. 

512 """ 

513 

514 if client_options is None: 

515 client_options = google.api_core.client_options.ClientOptions() 

516 if isinstance(client_options, collections.abc.Mapping): 

517 client_options = google.api_core.client_options.from_dict(client_options) 

518 

519 if http is not None: 

520 # if http is passed, the user cannot provide credentials 

521 banned_options = [ 

522 (credentials, "credentials"), 

523 (client_options.credentials_file, "client_options.credentials_file"), 

524 ] 

525 for option, name in banned_options: 

526 if option is not None: 

527 raise ValueError( 

528 "Arguments http and {} are mutually exclusive".format(name) 

529 ) 

530 

531 if isinstance(service, str): 

532 service = json.loads(service) 

533 elif isinstance(service, bytes): 

534 service = json.loads(service.decode("utf-8")) 

535 

536 if "rootUrl" not in service and isinstance(http, (HttpMock, HttpMockSequence)): 

537 logger.error( 

538 "You are using HttpMock or HttpMockSequence without" 

539 + "having the service discovery doc in cache. Try calling " 

540 + "build() without mocking once first to populate the " 

541 + "cache." 

542 ) 

543 raise InvalidJsonError() 

544 

545 # If an API Endpoint is provided on client options, use that as the base URL 

546 base = urllib.parse.urljoin(service["rootUrl"], service["servicePath"]) 

547 audience_for_self_signed_jwt = base 

548 if client_options.api_endpoint: 

549 base = client_options.api_endpoint 

550 

551 schema = Schemas(service) 

552 

553 # If the http client is not specified, then we must construct an http client 

554 # to make requests. If the service has scopes, then we also need to setup 

555 # authentication. 

556 if http is None: 

557 # Does the service require scopes? 

558 scopes = list( 

559 service.get("auth", {}).get("oauth2", {}).get("scopes", {}).keys() 

560 ) 

561 

562 # If so, then the we need to setup authentication if no developerKey is 

563 # specified. 

564 if scopes and not developerKey: 

565 # Make sure the user didn't pass multiple credentials 

566 if client_options.credentials_file and credentials: 

567 raise google.api_core.exceptions.DuplicateCredentialArgs( 

568 "client_options.credentials_file and credentials are mutually exclusive." 

569 ) 

570 # Check for credentials file via client options 

571 if client_options.credentials_file: 

572 credentials = _auth.credentials_from_file( 

573 client_options.credentials_file, 

574 scopes=client_options.scopes, 

575 quota_project_id=client_options.quota_project_id, 

576 ) 

577 # If the user didn't pass in credentials, attempt to acquire application 

578 # default credentials. 

579 if credentials is None: 

580 credentials = _auth.default_credentials( 

581 scopes=client_options.scopes, 

582 quota_project_id=client_options.quota_project_id, 

583 ) 

584 

585 # The credentials need to be scoped. 

586 # If the user provided scopes via client_options don't override them 

587 if not client_options.scopes: 

588 credentials = _auth.with_scopes(credentials, scopes) 

589 

590 # For google-auth service account credentials, enable self signed JWT if 

591 # always_use_jwt_access is true. 

592 if ( 

593 credentials 

594 and isinstance(credentials, service_account.Credentials) 

595 and always_use_jwt_access 

596 and hasattr(service_account.Credentials, "with_always_use_jwt_access") 

597 ): 

598 credentials = credentials.with_always_use_jwt_access(always_use_jwt_access) 

599 credentials._create_self_signed_jwt(audience_for_self_signed_jwt) 

600 

601 # If credentials are provided, create an authorized http instance; 

602 # otherwise, skip authentication. 

603 if credentials: 

604 http = _auth.authorized_http(credentials) 

605 

606 # If the service doesn't require scopes then there is no need for 

607 # authentication. 

608 else: 

609 http = build_http() 

610 

611 # Obtain client cert and create mTLS http channel if cert exists. 

612 client_cert_to_use = None 

613 use_client_cert = os.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE, "false") 

614 if not use_client_cert in ("true", "false"): 

615 raise MutualTLSChannelError( 

616 "Unsupported GOOGLE_API_USE_CLIENT_CERTIFICATE value. Accepted values: true, false" 

617 ) 

618 if client_options and client_options.client_cert_source: 

619 raise MutualTLSChannelError( 

620 "ClientOptions.client_cert_source is not supported, please use ClientOptions.client_encrypted_cert_source." 

621 ) 

622 if use_client_cert == "true": 

623 if ( 

624 client_options 

625 and hasattr(client_options, "client_encrypted_cert_source") 

626 and client_options.client_encrypted_cert_source 

627 ): 

628 client_cert_to_use = client_options.client_encrypted_cert_source 

629 elif ( 

630 adc_cert_path and adc_key_path and mtls.has_default_client_cert_source() 

631 ): 

632 client_cert_to_use = mtls.default_client_encrypted_cert_source( 

633 adc_cert_path, adc_key_path 

634 ) 

635 if client_cert_to_use: 

636 cert_path, key_path, passphrase = client_cert_to_use() 

637 

638 # The http object we built could be google_auth_httplib2.AuthorizedHttp 

639 # or httplib2.Http. In the first case we need to extract the wrapped 

640 # httplib2.Http object from google_auth_httplib2.AuthorizedHttp. 

641 http_channel = ( 

642 http.http 

643 if google_auth_httplib2 

644 and isinstance(http, google_auth_httplib2.AuthorizedHttp) 

645 else http 

646 ) 

647 http_channel.add_certificate(key_path, cert_path, "", passphrase) 

648 

649 # If user doesn't provide api endpoint via client options, decide which 

650 # api endpoint to use. 

651 if "mtlsRootUrl" in service and ( 

652 not client_options or not client_options.api_endpoint 

653 ): 

654 mtls_endpoint = urllib.parse.urljoin( 

655 service["mtlsRootUrl"], service["servicePath"] 

656 ) 

657 use_mtls_endpoint = os.getenv(GOOGLE_API_USE_MTLS_ENDPOINT, "auto") 

658 

659 if not use_mtls_endpoint in ("never", "auto", "always"): 

660 raise MutualTLSChannelError( 

661 "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted values: never, auto, always" 

662 ) 

663 

664 # Switch to mTLS endpoint, if environment variable is "always", or 

665 # environment varibable is "auto" and client cert exists. 

666 if use_mtls_endpoint == "always" or ( 

667 use_mtls_endpoint == "auto" and client_cert_to_use 

668 ): 

669 base = mtls_endpoint 

670 

671 if model is None: 

672 features = service.get("features", []) 

673 model = JsonModel("dataWrapper" in features) 

674 

675 return Resource( 

676 http=http, 

677 baseUrl=base, 

678 model=model, 

679 developerKey=developerKey, 

680 requestBuilder=requestBuilder, 

681 resourceDesc=service, 

682 rootDesc=service, 

683 schema=schema, 

684 ) 

685 

686 

687def _cast(value, schema_type): 

688 """Convert value to a string based on JSON Schema type. 

689 

690 See http://tools.ietf.org/html/draft-zyp-json-schema-03 for more details on 

691 JSON Schema. 

692 

693 Args: 

694 value: any, the value to convert 

695 schema_type: string, the type that value should be interpreted as 

696 

697 Returns: 

698 A string representation of 'value' based on the schema_type. 

699 """ 

700 if schema_type == "string": 

701 if type(value) == type("") or type(value) == type(""): 

702 return value 

703 else: 

704 return str(value) 

705 elif schema_type == "integer": 

706 return str(int(value)) 

707 elif schema_type == "number": 

708 return str(float(value)) 

709 elif schema_type == "boolean": 

710 return str(bool(value)).lower() 

711 else: 

712 if type(value) == type("") or type(value) == type(""): 

713 return value 

714 else: 

715 return str(value) 

716 

717 

718def _media_size_to_long(maxSize): 

719 """Convert a string media size, such as 10GB or 3TB into an integer. 

720 

721 Args: 

722 maxSize: string, size as a string, such as 2MB or 7GB. 

723 

724 Returns: 

725 The size as an integer value. 

726 """ 

727 if len(maxSize) < 2: 

728 return 0 

729 units = maxSize[-2:].upper() 

730 bit_shift = _MEDIA_SIZE_BIT_SHIFTS.get(units) 

731 if bit_shift is not None: 

732 return int(maxSize[:-2]) << bit_shift 

733 else: 

734 return int(maxSize) 

735 

736 

737def _media_path_url_from_info(root_desc, path_url): 

738 """Creates an absolute media path URL. 

739 

740 Constructed using the API root URI and service path from the discovery 

741 document and the relative path for the API method. 

742 

743 Args: 

744 root_desc: Dictionary; the entire original deserialized discovery document. 

745 path_url: String; the relative URL for the API method. Relative to the API 

746 root, which is specified in the discovery document. 

747 

748 Returns: 

749 String; the absolute URI for media upload for the API method. 

750 """ 

751 return "%(root)supload/%(service_path)s%(path)s" % { 

752 "root": root_desc["rootUrl"], 

753 "service_path": root_desc["servicePath"], 

754 "path": path_url, 

755 } 

756 

757 

758def _fix_up_parameters(method_desc, root_desc, http_method, schema): 

759 """Updates parameters of an API method with values specific to this library. 

760 

761 Specifically, adds whatever global parameters are specified by the API to the 

762 parameters for the individual method. Also adds parameters which don't 

763 appear in the discovery document, but are available to all discovery based 

764 APIs (these are listed in STACK_QUERY_PARAMETERS). 

765 

766 SIDE EFFECTS: This updates the parameters dictionary object in the method 

767 description. 

768 

769 Args: 

770 method_desc: Dictionary with metadata describing an API method. Value comes 

771 from the dictionary of methods stored in the 'methods' key in the 

772 deserialized discovery document. 

773 root_desc: Dictionary; the entire original deserialized discovery document. 

774 http_method: String; the HTTP method used to call the API method described 

775 in method_desc. 

776 schema: Object, mapping of schema names to schema descriptions. 

777 

778 Returns: 

779 The updated Dictionary stored in the 'parameters' key of the method 

780 description dictionary. 

781 """ 

782 parameters = method_desc.setdefault("parameters", {}) 

783 

784 # Add in the parameters common to all methods. 

785 for name, description in root_desc.get("parameters", {}).items(): 

786 parameters[name] = description 

787 

788 # Add in undocumented query parameters. 

789 for name in STACK_QUERY_PARAMETERS: 

790 parameters[name] = STACK_QUERY_PARAMETER_DEFAULT_VALUE.copy() 

791 

792 # Add 'body' (our own reserved word) to parameters if the method supports 

793 # a request payload. 

794 if http_method in HTTP_PAYLOAD_METHODS and "request" in method_desc: 

795 body = BODY_PARAMETER_DEFAULT_VALUE.copy() 

796 body.update(method_desc["request"]) 

797 parameters["body"] = body 

798 

799 return parameters 

800 

801 

802def _fix_up_media_upload(method_desc, root_desc, path_url, parameters): 

803 """Adds 'media_body' and 'media_mime_type' parameters if supported by method. 

804 

805 SIDE EFFECTS: If there is a 'mediaUpload' in the method description, adds 

806 'media_upload' key to parameters. 

807 

808 Args: 

809 method_desc: Dictionary with metadata describing an API method. Value comes 

810 from the dictionary of methods stored in the 'methods' key in the 

811 deserialized discovery document. 

812 root_desc: Dictionary; the entire original deserialized discovery document. 

813 path_url: String; the relative URL for the API method. Relative to the API 

814 root, which is specified in the discovery document. 

815 parameters: A dictionary describing method parameters for method described 

816 in method_desc. 

817 

818 Returns: 

819 Triple (accept, max_size, media_path_url) where: 

820 - accept is a list of strings representing what content types are 

821 accepted for media upload. Defaults to empty list if not in the 

822 discovery document. 

823 - max_size is a long representing the max size in bytes allowed for a 

824 media upload. Defaults to 0L if not in the discovery document. 

825 - media_path_url is a String; the absolute URI for media upload for the 

826 API method. Constructed using the API root URI and service path from 

827 the discovery document and the relative path for the API method. If 

828 media upload is not supported, this is None. 

829 """ 

830 media_upload = method_desc.get("mediaUpload", {}) 

831 accept = media_upload.get("accept", []) 

832 max_size = _media_size_to_long(media_upload.get("maxSize", "")) 

833 media_path_url = None 

834 

835 if media_upload: 

836 media_path_url = _media_path_url_from_info(root_desc, path_url) 

837 parameters["media_body"] = MEDIA_BODY_PARAMETER_DEFAULT_VALUE.copy() 

838 parameters["media_mime_type"] = MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE.copy() 

839 

840 return accept, max_size, media_path_url 

841 

842 

843def _fix_up_method_description(method_desc, root_desc, schema): 

844 """Updates a method description in a discovery document. 

845 

846 SIDE EFFECTS: Changes the parameters dictionary in the method description with 

847 extra parameters which are used locally. 

848 

849 Args: 

850 method_desc: Dictionary with metadata describing an API method. Value comes 

851 from the dictionary of methods stored in the 'methods' key in the 

852 deserialized discovery document. 

853 root_desc: Dictionary; the entire original deserialized discovery document. 

854 schema: Object, mapping of schema names to schema descriptions. 

855 

856 Returns: 

857 Tuple (path_url, http_method, method_id, accept, max_size, media_path_url) 

858 where: 

859 - path_url is a String; the relative URL for the API method. Relative to 

860 the API root, which is specified in the discovery document. 

861 - http_method is a String; the HTTP method used to call the API method 

862 described in the method description. 

863 - method_id is a String; the name of the RPC method associated with the 

864 API method, and is in the method description in the 'id' key. 

865 - accept is a list of strings representing what content types are 

866 accepted for media upload. Defaults to empty list if not in the 

867 discovery document. 

868 - max_size is a long representing the max size in bytes allowed for a 

869 media upload. Defaults to 0L if not in the discovery document. 

870 - media_path_url is a String; the absolute URI for media upload for the 

871 API method. Constructed using the API root URI and service path from 

872 the discovery document and the relative path for the API method. If 

873 media upload is not supported, this is None. 

874 """ 

875 path_url = method_desc["path"] 

876 http_method = method_desc["httpMethod"] 

877 method_id = method_desc["id"] 

878 

879 parameters = _fix_up_parameters(method_desc, root_desc, http_method, schema) 

880 # Order is important. `_fix_up_media_upload` needs `method_desc` to have a 

881 # 'parameters' key and needs to know if there is a 'body' parameter because it 

882 # also sets a 'media_body' parameter. 

883 accept, max_size, media_path_url = _fix_up_media_upload( 

884 method_desc, root_desc, path_url, parameters 

885 ) 

886 

887 return path_url, http_method, method_id, accept, max_size, media_path_url 

888 

889 

890def _fix_up_media_path_base_url(media_path_url, base_url): 

891 """ 

892 Update the media upload base url if its netloc doesn't match base url netloc. 

893 

894 This can happen in case the base url was overridden by 

895 client_options.api_endpoint. 

896 

897 Args: 

898 media_path_url: String; the absolute URI for media upload. 

899 base_url: string, base URL for the API. All requests are relative to this URI. 

900 

901 Returns: 

902 String; the absolute URI for media upload. 

903 """ 

904 parsed_media_url = urllib.parse.urlparse(media_path_url) 

905 parsed_base_url = urllib.parse.urlparse(base_url) 

906 if parsed_media_url.netloc == parsed_base_url.netloc: 

907 return media_path_url 

908 return urllib.parse.urlunparse( 

909 parsed_media_url._replace(netloc=parsed_base_url.netloc) 

910 ) 

911 

912 

913def _urljoin(base, url): 

914 """Custom urljoin replacement supporting : before / in url.""" 

915 # In general, it's unsafe to simply join base and url. However, for 

916 # the case of discovery documents, we know: 

917 # * base will never contain params, query, or fragment 

918 # * url will never contain a scheme or net_loc. 

919 # In general, this means we can safely join on /; we just need to 

920 # ensure we end up with precisely one / joining base and url. The 

921 # exception here is the case of media uploads, where url will be an 

922 # absolute url. 

923 if url.startswith("http://") or url.startswith("https://"): 

924 return urllib.parse.urljoin(base, url) 

925 new_base = base if base.endswith("/") else base + "/" 

926 new_url = url[1:] if url.startswith("/") else url 

927 return new_base + new_url 

928 

929 

930# TODO(dhermes): Convert this class to ResourceMethod and make it callable 

931class ResourceMethodParameters(object): 

932 """Represents the parameters associated with a method. 

933 

934 Attributes: 

935 argmap: Map from method parameter name (string) to query parameter name 

936 (string). 

937 required_params: List of required parameters (represented by parameter 

938 name as string). 

939 repeated_params: List of repeated parameters (represented by parameter 

940 name as string). 

941 pattern_params: Map from method parameter name (string) to regular 

942 expression (as a string). If the pattern is set for a parameter, the 

943 value for that parameter must match the regular expression. 

944 query_params: List of parameters (represented by parameter name as string) 

945 that will be used in the query string. 

946 path_params: Set of parameters (represented by parameter name as string) 

947 that will be used in the base URL path. 

948 param_types: Map from method parameter name (string) to parameter type. Type 

949 can be any valid JSON schema type; valid values are 'any', 'array', 

950 'boolean', 'integer', 'number', 'object', or 'string'. Reference: 

951 http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1 

952 enum_params: Map from method parameter name (string) to list of strings, 

953 where each list of strings is the list of acceptable enum values. 

954 """ 

955 

956 def __init__(self, method_desc): 

957 """Constructor for ResourceMethodParameters. 

958 

959 Sets default values and defers to set_parameters to populate. 

960 

961 Args: 

962 method_desc: Dictionary with metadata describing an API method. Value 

963 comes from the dictionary of methods stored in the 'methods' key in 

964 the deserialized discovery document. 

965 """ 

966 self.argmap = {} 

967 self.required_params = [] 

968 self.repeated_params = [] 

969 self.pattern_params = {} 

970 self.query_params = [] 

971 # TODO(dhermes): Change path_params to a list if the extra URITEMPLATE 

972 # parsing is gotten rid of. 

973 self.path_params = set() 

974 self.param_types = {} 

975 self.enum_params = {} 

976 

977 self.set_parameters(method_desc) 

978 

979 def set_parameters(self, method_desc): 

980 """Populates maps and lists based on method description. 

981 

982 Iterates through each parameter for the method and parses the values from 

983 the parameter dictionary. 

984 

985 Args: 

986 method_desc: Dictionary with metadata describing an API method. Value 

987 comes from the dictionary of methods stored in the 'methods' key in 

988 the deserialized discovery document. 

989 """ 

990 parameters = method_desc.get("parameters", {}) 

991 sorted_parameters = OrderedDict(sorted(parameters.items())) 

992 for arg, desc in sorted_parameters.items(): 

993 param = key2param(arg) 

994 self.argmap[param] = arg 

995 

996 if desc.get("pattern"): 

997 self.pattern_params[param] = desc["pattern"] 

998 if desc.get("enum"): 

999 self.enum_params[param] = desc["enum"] 

1000 if desc.get("required"): 

1001 self.required_params.append(param) 

1002 if desc.get("repeated"): 

1003 self.repeated_params.append(param) 

1004 if desc.get("location") == "query": 

1005 self.query_params.append(param) 

1006 if desc.get("location") == "path": 

1007 self.path_params.add(param) 

1008 self.param_types[param] = desc.get("type", "string") 

1009 

1010 # TODO(dhermes): Determine if this is still necessary. Discovery based APIs 

1011 # should have all path parameters already marked with 

1012 # 'location: path'. 

1013 for match in URITEMPLATE.finditer(method_desc["path"]): 

1014 for namematch in VARNAME.finditer(match.group(0)): 

1015 name = key2param(namematch.group(0)) 

1016 self.path_params.add(name) 

1017 if name in self.query_params: 

1018 self.query_params.remove(name) 

1019 

1020 

1021def createMethod(methodName, methodDesc, rootDesc, schema): 

1022 """Creates a method for attaching to a Resource. 

1023 

1024 Args: 

1025 methodName: string, name of the method to use. 

1026 methodDesc: object, fragment of deserialized discovery document that 

1027 describes the method. 

1028 rootDesc: object, the entire deserialized discovery document. 

1029 schema: object, mapping of schema names to schema descriptions. 

1030 """ 

1031 methodName = fix_method_name(methodName) 

1032 ( 

1033 pathUrl, 

1034 httpMethod, 

1035 methodId, 

1036 accept, 

1037 maxSize, 

1038 mediaPathUrl, 

1039 ) = _fix_up_method_description(methodDesc, rootDesc, schema) 

1040 

1041 parameters = ResourceMethodParameters(methodDesc) 

1042 

1043 def method(self, **kwargs): 

1044 # Don't bother with doc string, it will be over-written by createMethod. 

1045 

1046 for name in kwargs: 

1047 if name not in parameters.argmap: 

1048 raise TypeError("Got an unexpected keyword argument {}".format(name)) 

1049 

1050 # Remove args that have a value of None. 

1051 keys = list(kwargs.keys()) 

1052 for name in keys: 

1053 if kwargs[name] is None: 

1054 del kwargs[name] 

1055 

1056 for name in parameters.required_params: 

1057 if name not in kwargs: 

1058 # temporary workaround for non-paging methods incorrectly requiring 

1059 # page token parameter (cf. drive.changes.watch vs. drive.changes.list) 

1060 if name not in _PAGE_TOKEN_NAMES or _findPageTokenName( 

1061 _methodProperties(methodDesc, schema, "response") 

1062 ): 

1063 raise TypeError('Missing required parameter "%s"' % name) 

1064 

1065 for name, regex in parameters.pattern_params.items(): 

1066 if name in kwargs: 

1067 if isinstance(kwargs[name], str): 

1068 pvalues = [kwargs[name]] 

1069 else: 

1070 pvalues = kwargs[name] 

1071 for pvalue in pvalues: 

1072 if re.match(regex, pvalue) is None: 

1073 raise TypeError( 

1074 'Parameter "%s" value "%s" does not match the pattern "%s"' 

1075 % (name, pvalue, regex) 

1076 ) 

1077 

1078 for name, enums in parameters.enum_params.items(): 

1079 if name in kwargs: 

1080 # We need to handle the case of a repeated enum 

1081 # name differently, since we want to handle both 

1082 # arg='value' and arg=['value1', 'value2'] 

1083 if name in parameters.repeated_params and not isinstance( 

1084 kwargs[name], str 

1085 ): 

1086 values = kwargs[name] 

1087 else: 

1088 values = [kwargs[name]] 

1089 for value in values: 

1090 if value not in enums: 

1091 raise TypeError( 

1092 'Parameter "%s" value "%s" is not an allowed value in "%s"' 

1093 % (name, value, str(enums)) 

1094 ) 

1095 

1096 actual_query_params = {} 

1097 actual_path_params = {} 

1098 for key, value in kwargs.items(): 

1099 to_type = parameters.param_types.get(key, "string") 

1100 # For repeated parameters we cast each member of the list. 

1101 if key in parameters.repeated_params and type(value) == type([]): 

1102 cast_value = [_cast(x, to_type) for x in value] 

1103 else: 

1104 cast_value = _cast(value, to_type) 

1105 if key in parameters.query_params: 

1106 actual_query_params[parameters.argmap[key]] = cast_value 

1107 if key in parameters.path_params: 

1108 actual_path_params[parameters.argmap[key]] = cast_value 

1109 body_value = kwargs.get("body", None) 

1110 media_filename = kwargs.get("media_body", None) 

1111 media_mime_type = kwargs.get("media_mime_type", None) 

1112 

1113 if self._developerKey: 

1114 actual_query_params["key"] = self._developerKey 

1115 

1116 model = self._model 

1117 if methodName.endswith("_media"): 

1118 model = MediaModel() 

1119 elif "response" not in methodDesc: 

1120 model = RawModel() 

1121 

1122 headers = {} 

1123 headers, params, query, body = model.request( 

1124 headers, actual_path_params, actual_query_params, body_value 

1125 ) 

1126 

1127 expanded_url = uritemplate.expand(pathUrl, params) 

1128 url = _urljoin(self._baseUrl, expanded_url + query) 

1129 

1130 resumable = None 

1131 multipart_boundary = "" 

1132 

1133 if media_filename: 

1134 # Ensure we end up with a valid MediaUpload object. 

1135 if isinstance(media_filename, str): 

1136 if media_mime_type is None: 

1137 logger.warning( 

1138 "media_mime_type argument not specified: trying to auto-detect for %s", 

1139 media_filename, 

1140 ) 

1141 media_mime_type, _ = mimetypes.guess_type(media_filename) 

1142 if media_mime_type is None: 

1143 raise UnknownFileType(media_filename) 

1144 if not mimeparse.best_match([media_mime_type], ",".join(accept)): 

1145 raise UnacceptableMimeTypeError(media_mime_type) 

1146 media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type) 

1147 elif isinstance(media_filename, MediaUpload): 

1148 media_upload = media_filename 

1149 else: 

1150 raise TypeError("media_filename must be str or MediaUpload.") 

1151 

1152 # Check the maxSize 

1153 if media_upload.size() is not None and media_upload.size() > maxSize > 0: 

1154 raise MediaUploadSizeError("Media larger than: %s" % maxSize) 

1155 

1156 # Use the media path uri for media uploads 

1157 expanded_url = uritemplate.expand(mediaPathUrl, params) 

1158 url = _urljoin(self._baseUrl, expanded_url + query) 

1159 url = _fix_up_media_path_base_url(url, self._baseUrl) 

1160 if media_upload.resumable(): 

1161 url = _add_query_parameter(url, "uploadType", "resumable") 

1162 

1163 if media_upload.resumable(): 

1164 # This is all we need to do for resumable, if the body exists it gets 

1165 # sent in the first request, otherwise an empty body is sent. 

1166 resumable = media_upload 

1167 else: 

1168 # A non-resumable upload 

1169 if body is None: 

1170 # This is a simple media upload 

1171 headers["content-type"] = media_upload.mimetype() 

1172 body = media_upload.getbytes(0, media_upload.size()) 

1173 url = _add_query_parameter(url, "uploadType", "media") 

1174 else: 

1175 # This is a multipart/related upload. 

1176 msgRoot = MIMEMultipart("related") 

1177 # msgRoot should not write out it's own headers 

1178 setattr(msgRoot, "_write_headers", lambda self: None) 

1179 

1180 # attach the body as one part 

1181 msg = MIMENonMultipart(*headers["content-type"].split("/")) 

1182 msg.set_payload(body) 

1183 msgRoot.attach(msg) 

1184 

1185 # attach the media as the second part 

1186 msg = MIMENonMultipart(*media_upload.mimetype().split("/")) 

1187 msg["Content-Transfer-Encoding"] = "binary" 

1188 

1189 payload = media_upload.getbytes(0, media_upload.size()) 

1190 msg.set_payload(payload) 

1191 msgRoot.attach(msg) 

1192 # encode the body: note that we can't use `as_string`, because 

1193 # it plays games with `From ` lines. 

1194 fp = io.BytesIO() 

1195 g = _BytesGenerator(fp, mangle_from_=False) 

1196 g.flatten(msgRoot, unixfrom=False) 

1197 body = fp.getvalue() 

1198 

1199 multipart_boundary = msgRoot.get_boundary() 

1200 headers["content-type"] = ( 

1201 "multipart/related; " 'boundary="%s"' 

1202 ) % multipart_boundary 

1203 url = _add_query_parameter(url, "uploadType", "multipart") 

1204 

1205 logger.debug("URL being requested: %s %s" % (httpMethod, url)) 

1206 return self._requestBuilder( 

1207 self._http, 

1208 model.response, 

1209 url, 

1210 method=httpMethod, 

1211 body=body, 

1212 headers=headers, 

1213 methodId=methodId, 

1214 resumable=resumable, 

1215 ) 

1216 

1217 docs = [methodDesc.get("description", DEFAULT_METHOD_DOC), "\n\n"] 

1218 if len(parameters.argmap) > 0: 

1219 docs.append("Args:\n") 

1220 

1221 # Skip undocumented params and params common to all methods. 

1222 skip_parameters = list(rootDesc.get("parameters", {}).keys()) 

1223 skip_parameters.extend(STACK_QUERY_PARAMETERS) 

1224 

1225 all_args = list(parameters.argmap.keys()) 

1226 args_ordered = [key2param(s) for s in methodDesc.get("parameterOrder", [])] 

1227 

1228 # Move body to the front of the line. 

1229 if "body" in all_args: 

1230 args_ordered.append("body") 

1231 

1232 for name in sorted(all_args): 

1233 if name not in args_ordered: 

1234 args_ordered.append(name) 

1235 

1236 for arg in args_ordered: 

1237 if arg in skip_parameters: 

1238 continue 

1239 

1240 repeated = "" 

1241 if arg in parameters.repeated_params: 

1242 repeated = " (repeated)" 

1243 required = "" 

1244 if arg in parameters.required_params: 

1245 required = " (required)" 

1246 paramdesc = methodDesc["parameters"][parameters.argmap[arg]] 

1247 paramdoc = paramdesc.get("description", "A parameter") 

1248 if "$ref" in paramdesc: 

1249 docs.append( 

1250 (" %s: object, %s%s%s\n The object takes the form of:\n\n%s\n\n") 

1251 % ( 

1252 arg, 

1253 paramdoc, 

1254 required, 

1255 repeated, 

1256 schema.prettyPrintByName(paramdesc["$ref"]), 

1257 ) 

1258 ) 

1259 else: 

1260 paramtype = paramdesc.get("type", "string") 

1261 docs.append( 

1262 " %s: %s, %s%s%s\n" % (arg, paramtype, paramdoc, required, repeated) 

1263 ) 

1264 enum = paramdesc.get("enum", []) 

1265 enumDesc = paramdesc.get("enumDescriptions", []) 

1266 if enum and enumDesc: 

1267 docs.append(" Allowed values\n") 

1268 for (name, desc) in zip(enum, enumDesc): 

1269 docs.append(" %s - %s\n" % (name, desc)) 

1270 if "response" in methodDesc: 

1271 if methodName.endswith("_media"): 

1272 docs.append("\nReturns:\n The media object as a string.\n\n ") 

1273 else: 

1274 docs.append("\nReturns:\n An object of the form:\n\n ") 

1275 docs.append(schema.prettyPrintSchema(methodDesc["response"])) 

1276 

1277 setattr(method, "__doc__", "".join(docs)) 

1278 return (methodName, method) 

1279 

1280 

1281def createNextMethod( 

1282 methodName, 

1283 pageTokenName="pageToken", 

1284 nextPageTokenName="nextPageToken", 

1285 isPageTokenParameter=True, 

1286): 

1287 """Creates any _next methods for attaching to a Resource. 

1288 

1289 The _next methods allow for easy iteration through list() responses. 

1290 

1291 Args: 

1292 methodName: string, name of the method to use. 

1293 pageTokenName: string, name of request page token field. 

1294 nextPageTokenName: string, name of response page token field. 

1295 isPageTokenParameter: Boolean, True if request page token is a query 

1296 parameter, False if request page token is a field of the request body. 

1297 """ 

1298 methodName = fix_method_name(methodName) 

1299 

1300 def methodNext(self, previous_request, previous_response): 

1301 """Retrieves the next page of results. 

1302 

1303 Args: 

1304 previous_request: The request for the previous page. (required) 

1305 previous_response: The response from the request for the previous page. (required) 

1306 

1307 Returns: 

1308 A request object that you can call 'execute()' on to request the next 

1309 page. Returns None if there are no more items in the collection. 

1310 """ 

1311 # Retrieve nextPageToken from previous_response 

1312 # Use as pageToken in previous_request to create new request. 

1313 

1314 nextPageToken = previous_response.get(nextPageTokenName, None) 

1315 if not nextPageToken: 

1316 return None 

1317 

1318 request = copy.copy(previous_request) 

1319 

1320 if isPageTokenParameter: 

1321 # Replace pageToken value in URI 

1322 request.uri = _add_query_parameter( 

1323 request.uri, pageTokenName, nextPageToken 

1324 ) 

1325 logger.debug("Next page request URL: %s %s" % (methodName, request.uri)) 

1326 else: 

1327 # Replace pageToken value in request body 

1328 model = self._model 

1329 body = model.deserialize(request.body) 

1330 body[pageTokenName] = nextPageToken 

1331 request.body = model.serialize(body) 

1332 request.body_size = len(request.body) 

1333 if "content-length" in request.headers: 

1334 del request.headers["content-length"] 

1335 logger.debug("Next page request body: %s %s" % (methodName, body)) 

1336 

1337 return request 

1338 

1339 return (methodName, methodNext) 

1340 

1341 

1342class Resource(object): 

1343 """A class for interacting with a resource.""" 

1344 

1345 def __init__( 

1346 self, 

1347 http, 

1348 baseUrl, 

1349 model, 

1350 requestBuilder, 

1351 developerKey, 

1352 resourceDesc, 

1353 rootDesc, 

1354 schema, 

1355 ): 

1356 """Build a Resource from the API description. 

1357 

1358 Args: 

1359 http: httplib2.Http, Object to make http requests with. 

1360 baseUrl: string, base URL for the API. All requests are relative to this 

1361 URI. 

1362 model: googleapiclient.Model, converts to and from the wire format. 

1363 requestBuilder: class or callable that instantiates an 

1364 googleapiclient.HttpRequest object. 

1365 developerKey: string, key obtained from 

1366 https://code.google.com/apis/console 

1367 resourceDesc: object, section of deserialized discovery document that 

1368 describes a resource. Note that the top level discovery document 

1369 is considered a resource. 

1370 rootDesc: object, the entire deserialized discovery document. 

1371 schema: object, mapping of schema names to schema descriptions. 

1372 """ 

1373 self._dynamic_attrs = [] 

1374 

1375 self._http = http 

1376 self._baseUrl = baseUrl 

1377 self._model = model 

1378 self._developerKey = developerKey 

1379 self._requestBuilder = requestBuilder 

1380 self._resourceDesc = resourceDesc 

1381 self._rootDesc = rootDesc 

1382 self._schema = schema 

1383 

1384 self._set_service_methods() 

1385 

1386 def _set_dynamic_attr(self, attr_name, value): 

1387 """Sets an instance attribute and tracks it in a list of dynamic attributes. 

1388 

1389 Args: 

1390 attr_name: string; The name of the attribute to be set 

1391 value: The value being set on the object and tracked in the dynamic cache. 

1392 """ 

1393 self._dynamic_attrs.append(attr_name) 

1394 self.__dict__[attr_name] = value 

1395 

1396 def __getstate__(self): 

1397 """Trim the state down to something that can be pickled. 

1398 

1399 Uses the fact that the instance variable _dynamic_attrs holds attrs that 

1400 will be wiped and restored on pickle serialization. 

1401 """ 

1402 state_dict = copy.copy(self.__dict__) 

1403 for dynamic_attr in self._dynamic_attrs: 

1404 del state_dict[dynamic_attr] 

1405 del state_dict["_dynamic_attrs"] 

1406 return state_dict 

1407 

1408 def __setstate__(self, state): 

1409 """Reconstitute the state of the object from being pickled. 

1410 

1411 Uses the fact that the instance variable _dynamic_attrs holds attrs that 

1412 will be wiped and restored on pickle serialization. 

1413 """ 

1414 self.__dict__.update(state) 

1415 self._dynamic_attrs = [] 

1416 self._set_service_methods() 

1417 

1418 def __enter__(self): 

1419 return self 

1420 

1421 def __exit__(self, exc_type, exc, exc_tb): 

1422 self.close() 

1423 

1424 def close(self): 

1425 """Close httplib2 connections.""" 

1426 # httplib2 leaves sockets open by default. 

1427 # Cleanup using the `close` method. 

1428 # https://github.com/httplib2/httplib2/issues/148 

1429 self._http.close() 

1430 

1431 def _set_service_methods(self): 

1432 self._add_basic_methods(self._resourceDesc, self._rootDesc, self._schema) 

1433 self._add_nested_resources(self._resourceDesc, self._rootDesc, self._schema) 

1434 self._add_next_methods(self._resourceDesc, self._schema) 

1435 

1436 def _add_basic_methods(self, resourceDesc, rootDesc, schema): 

1437 # If this is the root Resource, add a new_batch_http_request() method. 

1438 if resourceDesc == rootDesc: 

1439 batch_uri = "%s%s" % ( 

1440 rootDesc["rootUrl"], 

1441 rootDesc.get("batchPath", "batch"), 

1442 ) 

1443 

1444 def new_batch_http_request(callback=None): 

1445 """Create a BatchHttpRequest object based on the discovery document. 

1446 

1447 Args: 

1448 callback: callable, A callback to be called for each response, of the 

1449 form callback(id, response, exception). The first parameter is the 

1450 request id, and the second is the deserialized response object. The 

1451 third is an apiclient.errors.HttpError exception object if an HTTP 

1452 error occurred while processing the request, or None if no error 

1453 occurred. 

1454 

1455 Returns: 

1456 A BatchHttpRequest object based on the discovery document. 

1457 """ 

1458 return BatchHttpRequest(callback=callback, batch_uri=batch_uri) 

1459 

1460 self._set_dynamic_attr("new_batch_http_request", new_batch_http_request) 

1461 

1462 # Add basic methods to Resource 

1463 if "methods" in resourceDesc: 

1464 for methodName, methodDesc in resourceDesc["methods"].items(): 

1465 fixedMethodName, method = createMethod( 

1466 methodName, methodDesc, rootDesc, schema 

1467 ) 

1468 self._set_dynamic_attr( 

1469 fixedMethodName, method.__get__(self, self.__class__) 

1470 ) 

1471 # Add in _media methods. The functionality of the attached method will 

1472 # change when it sees that the method name ends in _media. 

1473 if methodDesc.get("supportsMediaDownload", False): 

1474 fixedMethodName, method = createMethod( 

1475 methodName + "_media", methodDesc, rootDesc, schema 

1476 ) 

1477 self._set_dynamic_attr( 

1478 fixedMethodName, method.__get__(self, self.__class__) 

1479 ) 

1480 

1481 def _add_nested_resources(self, resourceDesc, rootDesc, schema): 

1482 # Add in nested resources 

1483 if "resources" in resourceDesc: 

1484 

1485 def createResourceMethod(methodName, methodDesc): 

1486 """Create a method on the Resource to access a nested Resource. 

1487 

1488 Args: 

1489 methodName: string, name of the method to use. 

1490 methodDesc: object, fragment of deserialized discovery document that 

1491 describes the method. 

1492 """ 

1493 methodName = fix_method_name(methodName) 

1494 

1495 def methodResource(self): 

1496 return Resource( 

1497 http=self._http, 

1498 baseUrl=self._baseUrl, 

1499 model=self._model, 

1500 developerKey=self._developerKey, 

1501 requestBuilder=self._requestBuilder, 

1502 resourceDesc=methodDesc, 

1503 rootDesc=rootDesc, 

1504 schema=schema, 

1505 ) 

1506 

1507 setattr(methodResource, "__doc__", "A collection resource.") 

1508 setattr(methodResource, "__is_resource__", True) 

1509 

1510 return (methodName, methodResource) 

1511 

1512 for methodName, methodDesc in resourceDesc["resources"].items(): 

1513 fixedMethodName, method = createResourceMethod(methodName, methodDesc) 

1514 self._set_dynamic_attr( 

1515 fixedMethodName, method.__get__(self, self.__class__) 

1516 ) 

1517 

1518 def _add_next_methods(self, resourceDesc, schema): 

1519 # Add _next() methods if and only if one of the names 'pageToken' or 

1520 # 'nextPageToken' occurs among the fields of both the method's response 

1521 # type either the method's request (query parameters) or request body. 

1522 if "methods" not in resourceDesc: 

1523 return 

1524 for methodName, methodDesc in resourceDesc["methods"].items(): 

1525 nextPageTokenName = _findPageTokenName( 

1526 _methodProperties(methodDesc, schema, "response") 

1527 ) 

1528 if not nextPageTokenName: 

1529 continue 

1530 isPageTokenParameter = True 

1531 pageTokenName = _findPageTokenName(methodDesc.get("parameters", {})) 

1532 if not pageTokenName: 

1533 isPageTokenParameter = False 

1534 pageTokenName = _findPageTokenName( 

1535 _methodProperties(methodDesc, schema, "request") 

1536 ) 

1537 if not pageTokenName: 

1538 continue 

1539 fixedMethodName, method = createNextMethod( 

1540 methodName + "_next", 

1541 pageTokenName, 

1542 nextPageTokenName, 

1543 isPageTokenParameter, 

1544 ) 

1545 self._set_dynamic_attr( 

1546 fixedMethodName, method.__get__(self, self.__class__) 

1547 ) 

1548 

1549 

1550def _findPageTokenName(fields): 

1551 """Search field names for one like a page token. 

1552 

1553 Args: 

1554 fields: container of string, names of fields. 

1555 

1556 Returns: 

1557 First name that is either 'pageToken' or 'nextPageToken' if one exists, 

1558 otherwise None. 

1559 """ 

1560 return next( 

1561 (tokenName for tokenName in _PAGE_TOKEN_NAMES if tokenName in fields), None 

1562 ) 

1563 

1564 

1565def _methodProperties(methodDesc, schema, name): 

1566 """Get properties of a field in a method description. 

1567 

1568 Args: 

1569 methodDesc: object, fragment of deserialized discovery document that 

1570 describes the method. 

1571 schema: object, mapping of schema names to schema descriptions. 

1572 name: string, name of top-level field in method description. 

1573 

1574 Returns: 

1575 Object representing fragment of deserialized discovery document 

1576 corresponding to 'properties' field of object corresponding to named field 

1577 in method description, if it exists, otherwise empty dict. 

1578 """ 

1579 desc = methodDesc.get(name, {}) 

1580 if "$ref" in desc: 

1581 desc = schema.get(desc["$ref"], {}) 

1582 return desc.get("properties", {})