Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/_http/__init__.py: 24%

115 statements  

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

1# Copyright 2014 Google LLC 

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"""Shared implementation of connections to API servers.""" 

16 

17import collections 

18import collections.abc 

19import json 

20import os 

21import platform 

22from typing import Optional 

23from urllib.parse import urlencode 

24import warnings 

25 

26from google.api_core.client_info import ClientInfo 

27from google.cloud import exceptions 

28from google.cloud import version 

29 

30 

31API_BASE_URL = "https://www.googleapis.com" 

32"""The base of the API call URL.""" 

33 

34DEFAULT_USER_AGENT = "gcloud-python/{0}".format(version.__version__) 

35"""The user agent for google-cloud-python requests.""" 

36 

37CLIENT_INFO_HEADER = "X-Goog-API-Client" 

38CLIENT_INFO_TEMPLATE = "gl-python/" + platform.python_version() + " gccl/{}" 

39 

40_USER_AGENT_ALL_CAPS_DEPRECATED = """\ 

41The 'USER_AGENT' class-level attribute is deprecated. Please use 

42'user_agent' instead. 

43""" 

44 

45_EXTRA_HEADERS_ALL_CAPS_DEPRECATED = """\ 

46The '_EXTRA_HEADERS' class-level attribute is deprecated. Please use 

47'extra_headers' instead. 

48""" 

49 

50_DEFAULT_TIMEOUT = 60 # in seconds 

51 

52 

53class Connection(object): 

54 """A generic connection to Google Cloud Platform. 

55 

56 :type client: :class:`~google.cloud.client.Client` 

57 :param client: The client that owns the current connection. 

58 

59 :type client_info: :class:`~google.api_core.client_info.ClientInfo` 

60 :param client_info: (Optional) instance used to generate user agent. 

61 """ 

62 

63 _user_agent = DEFAULT_USER_AGENT 

64 

65 def __init__(self, client, client_info=None): 

66 self._client = client 

67 

68 if client_info is None: 

69 client_info = ClientInfo() 

70 

71 self._client_info = client_info 

72 self._extra_headers = {} 

73 

74 @property 

75 def USER_AGENT(self): 

76 """Deprecated: get / set user agent sent by connection. 

77 

78 :rtype: str 

79 :returns: user agent 

80 """ 

81 warnings.warn(_USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2) 

82 return self.user_agent 

83 

84 @USER_AGENT.setter 

85 def USER_AGENT(self, value): 

86 warnings.warn(_USER_AGENT_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2) 

87 self.user_agent = value 

88 

89 @property 

90 def user_agent(self): 

91 """Get / set user agent sent by connection. 

92 

93 :rtype: str 

94 :returns: user agent 

95 """ 

96 return self._client_info.to_user_agent() 

97 

98 @user_agent.setter 

99 def user_agent(self, value): 

100 self._client_info.user_agent = value 

101 

102 @property 

103 def _EXTRA_HEADERS(self): 

104 """Deprecated: get / set extra headers sent by connection. 

105 

106 :rtype: dict 

107 :returns: header keys / values 

108 """ 

109 warnings.warn( 

110 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2 

111 ) 

112 return self.extra_headers 

113 

114 @_EXTRA_HEADERS.setter 

115 def _EXTRA_HEADERS(self, value): 

116 warnings.warn( 

117 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2 

118 ) 

119 self.extra_headers = value 

120 

121 @property 

122 def extra_headers(self): 

123 """Get / set extra headers sent by connection. 

124 

125 :rtype: dict 

126 :returns: header keys / values 

127 """ 

128 return self._extra_headers 

129 

130 @extra_headers.setter 

131 def extra_headers(self, value): 

132 self._extra_headers = value 

133 

134 @property 

135 def credentials(self): 

136 """Getter for current credentials. 

137 

138 :rtype: :class:`google.auth.credentials.Credentials` or 

139 :class:`NoneType` 

140 :returns: The credentials object associated with this connection. 

141 """ 

142 return self._client._credentials 

143 

144 @property 

145 def http(self): 

146 """A getter for the HTTP transport used in talking to the API. 

147 

148 Returns: 

149 google.auth.transport.requests.AuthorizedSession: 

150 A :class:`requests.Session` instance. 

151 """ 

152 return self._client._http 

153 

154 

155class JSONConnection(Connection): 

156 """A connection to a Google JSON-based API. 

157 

158 These APIs are discovery based. For reference: 

159 

160 https://developers.google.com/discovery/ 

161 

162 This defines :meth:`api_request` for making a generic JSON 

163 API request and API requests are created elsewhere. 

164 

165 * :attr:`API_BASE_URL` 

166 * :attr:`API_VERSION` 

167 * :attr:`API_URL_TEMPLATE` 

168 

169 must be updated by subclasses. 

170 """ 

171 

172 API_BASE_URL: Optional[str] = None 

173 """The base of the API call URL.""" 

174 

175 API_BASE_MTLS_URL: Optional[str] = None 

176 """The base of the API call URL for mutual TLS.""" 

177 

178 ALLOW_AUTO_SWITCH_TO_MTLS_URL = False 

179 """Indicates if auto switch to mTLS url is allowed.""" 

180 

181 API_VERSION: Optional[str] = None 

182 """The version of the API, used in building the API call's URL.""" 

183 

184 API_URL_TEMPLATE: Optional[str] = None 

185 """A template for the URL of a particular API call.""" 

186 

187 def get_api_base_url_for_mtls(self, api_base_url=None): 

188 """Return the api base url for mutual TLS. 

189 

190 Typically, you shouldn't need to use this method. 

191 

192 The logic is as follows: 

193 

194 If `api_base_url` is provided, just return this value; otherwise, the 

195 return value depends `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable 

196 value. 

197 

198 If the environment variable value is "always", return `API_BASE_MTLS_URL`. 

199 If the environment variable value is "never", return `API_BASE_URL`. 

200 Otherwise, if `ALLOW_AUTO_SWITCH_TO_MTLS_URL` is True and the underlying 

201 http is mTLS, then return `API_BASE_MTLS_URL`; otherwise return `API_BASE_URL`. 

202 

203 :type api_base_url: str 

204 :param api_base_url: User provided api base url. It takes precedence over 

205 `API_BASE_URL` and `API_BASE_MTLS_URL`. 

206 

207 :rtype: str 

208 :returns: The api base url used for mTLS. 

209 """ 

210 if api_base_url: 

211 return api_base_url 

212 

213 env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") 

214 if env == "always": 

215 url_to_use = self.API_BASE_MTLS_URL 

216 elif env == "never": 

217 url_to_use = self.API_BASE_URL 

218 else: 

219 if self.ALLOW_AUTO_SWITCH_TO_MTLS_URL: 

220 url_to_use = ( 

221 self.API_BASE_MTLS_URL if self.http.is_mtls else self.API_BASE_URL 

222 ) 

223 else: 

224 url_to_use = self.API_BASE_URL 

225 return url_to_use 

226 

227 def build_api_url( 

228 self, path, query_params=None, api_base_url=None, api_version=None 

229 ): 

230 """Construct an API url given a few components, some optional. 

231 

232 Typically, you shouldn't need to use this method. 

233 

234 :type path: str 

235 :param path: The path to the resource (ie, ``'/b/bucket-name'``). 

236 

237 :type query_params: dict or list 

238 :param query_params: A dictionary of keys and values (or list of 

239 key-value pairs) to insert into the query 

240 string of the URL. 

241 

242 :type api_base_url: str 

243 :param api_base_url: The base URL for the API endpoint. 

244 Typically you won't have to provide this. 

245 

246 :type api_version: str 

247 :param api_version: The version of the API to call. 

248 Typically you shouldn't provide this and instead 

249 use the default for the library. 

250 

251 :rtype: str 

252 :returns: The URL assembled from the pieces provided. 

253 """ 

254 url = self.API_URL_TEMPLATE.format( 

255 api_base_url=self.get_api_base_url_for_mtls(api_base_url), 

256 api_version=(api_version or self.API_VERSION), 

257 path=path, 

258 ) 

259 

260 query_params = query_params or {} 

261 

262 if isinstance(query_params, collections.abc.Mapping): 

263 query_params = query_params.copy() 

264 else: 

265 query_params_dict = collections.defaultdict(list) 

266 for key, value in query_params: 

267 query_params_dict[key].append(value) 

268 query_params = query_params_dict 

269 

270 query_params.setdefault("prettyPrint", "false") 

271 

272 url += "?" + urlencode(query_params, doseq=True) 

273 

274 return url 

275 

276 def _make_request( 

277 self, 

278 method, 

279 url, 

280 data=None, 

281 content_type=None, 

282 headers=None, 

283 target_object=None, 

284 timeout=_DEFAULT_TIMEOUT, 

285 extra_api_info=None, 

286 ): 

287 """A low level method to send a request to the API. 

288 

289 Typically, you shouldn't need to use this method. 

290 

291 :type method: str 

292 :param method: The HTTP method to use in the request. 

293 

294 :type url: str 

295 :param url: The URL to send the request to. 

296 

297 :type data: str 

298 :param data: The data to send as the body of the request. 

299 

300 :type content_type: str 

301 :param content_type: The proper MIME type of the data provided. 

302 

303 :type headers: dict 

304 :param headers: (Optional) A dictionary of HTTP headers to send with 

305 the request. If passed, will be modified directly 

306 here with added headers. 

307 

308 :type target_object: object 

309 :param target_object: 

310 (Optional) Argument to be used by library callers. This can allow 

311 custom behavior, for example, to defer an HTTP request and complete 

312 initialization of the object at a later time. 

313 

314 :type timeout: float or tuple 

315 :param timeout: (optional) The amount of time, in seconds, to wait 

316 for the server response. 

317 

318 Can also be passed as a tuple (connect_timeout, read_timeout). 

319 See :meth:`requests.Session.request` documentation for details. 

320 

321 :type extra_api_info: string 

322 :param extra_api_info: (optional) Extra api info to be appended to 

323 the X-Goog-API-Client header 

324 

325 :rtype: :class:`requests.Response` 

326 :returns: The HTTP response. 

327 """ 

328 headers = headers or {} 

329 headers.update(self.extra_headers) 

330 headers["Accept-Encoding"] = "gzip" 

331 

332 if content_type: 

333 headers["Content-Type"] = content_type 

334 

335 if extra_api_info: 

336 headers[CLIENT_INFO_HEADER] = f"{self.user_agent} {extra_api_info}" 

337 else: 

338 headers[CLIENT_INFO_HEADER] = self.user_agent 

339 headers["User-Agent"] = self.user_agent 

340 

341 return self._do_request( 

342 method, url, headers, data, target_object, timeout=timeout 

343 ) 

344 

345 def _do_request( 

346 self, method, url, headers, data, target_object, timeout=_DEFAULT_TIMEOUT 

347 ): # pylint: disable=unused-argument 

348 """Low-level helper: perform the actual API request over HTTP. 

349 

350 Allows batch context managers to override and defer a request. 

351 

352 :type method: str 

353 :param method: The HTTP method to use in the request. 

354 

355 :type url: str 

356 :param url: The URL to send the request to. 

357 

358 :type headers: dict 

359 :param headers: A dictionary of HTTP headers to send with the request. 

360 

361 :type data: str 

362 :param data: The data to send as the body of the request. 

363 

364 :type target_object: object 

365 :param target_object: 

366 (Optional) Unused ``target_object`` here but may be used by a 

367 superclass. 

368 

369 :type timeout: float or tuple 

370 :param timeout: (optional) The amount of time, in seconds, to wait 

371 for the server response. 

372 

373 Can also be passed as a tuple (connect_timeout, read_timeout). 

374 See :meth:`requests.Session.request` documentation for details. 

375 

376 :rtype: :class:`requests.Response` 

377 :returns: The HTTP response. 

378 """ 

379 return self.http.request( 

380 url=url, method=method, headers=headers, data=data, timeout=timeout 

381 ) 

382 

383 def api_request( 

384 self, 

385 method, 

386 path, 

387 query_params=None, 

388 data=None, 

389 content_type=None, 

390 headers=None, 

391 api_base_url=None, 

392 api_version=None, 

393 expect_json=True, 

394 _target_object=None, 

395 timeout=_DEFAULT_TIMEOUT, 

396 extra_api_info=None, 

397 ): 

398 """Make a request over the HTTP transport to the API. 

399 

400 You shouldn't need to use this method, but if you plan to 

401 interact with the API using these primitives, this is the 

402 correct one to use. 

403 

404 :type method: str 

405 :param method: The HTTP method name (ie, ``GET``, ``POST``, etc). 

406 Required. 

407 

408 :type path: str 

409 :param path: The path to the resource (ie, ``'/b/bucket-name'``). 

410 Required. 

411 

412 :type query_params: dict or list 

413 :param query_params: A dictionary of keys and values (or list of 

414 key-value pairs) to insert into the query 

415 string of the URL. 

416 

417 :type data: str 

418 :param data: The data to send as the body of the request. Default is 

419 the empty string. 

420 

421 :type content_type: str 

422 :param content_type: The proper MIME type of the data provided. Default 

423 is None. 

424 

425 :type headers: dict 

426 :param headers: extra HTTP headers to be sent with the request. 

427 

428 :type api_base_url: str 

429 :param api_base_url: The base URL for the API endpoint. 

430 Typically you won't have to provide this. 

431 Default is the standard API base URL. 

432 

433 :type api_version: str 

434 :param api_version: The version of the API to call. Typically 

435 you shouldn't provide this and instead use 

436 the default for the library. Default is the 

437 latest API version supported by 

438 google-cloud-python. 

439 

440 :type expect_json: bool 

441 :param expect_json: If True, this method will try to parse the 

442 response as JSON and raise an exception if 

443 that cannot be done. Default is True. 

444 

445 :type _target_object: :class:`object` 

446 :param _target_object: 

447 (Optional) Protected argument to be used by library callers. This 

448 can allow custom behavior, for example, to defer an HTTP request 

449 and complete initialization of the object at a later time. 

450 

451 :type timeout: float or tuple 

452 :param timeout: (optional) The amount of time, in seconds, to wait 

453 for the server response. 

454 

455 Can also be passed as a tuple (connect_timeout, read_timeout). 

456 See :meth:`requests.Session.request` documentation for details. 

457 

458 :type extra_api_info: string 

459 :param extra_api_info: (optional) Extra api info to be appended to 

460 the X-Goog-API-Client header 

461 

462 :raises ~google.cloud.exceptions.GoogleCloudError: if the response code 

463 is not 200 OK. 

464 :raises ValueError: if the response content type is not JSON. 

465 :rtype: dict or str 

466 :returns: The API response payload, either as a raw string or 

467 a dictionary if the response is valid JSON. 

468 """ 

469 url = self.build_api_url( 

470 path=path, 

471 query_params=query_params, 

472 api_base_url=api_base_url, 

473 api_version=api_version, 

474 ) 

475 

476 # Making the executive decision that any dictionary 

477 # data will be sent properly as JSON. 

478 if data and isinstance(data, dict): 

479 data = json.dumps(data) 

480 content_type = "application/json" 

481 

482 response = self._make_request( 

483 method=method, 

484 url=url, 

485 data=data, 

486 content_type=content_type, 

487 headers=headers, 

488 target_object=_target_object, 

489 timeout=timeout, 

490 extra_api_info=extra_api_info, 

491 ) 

492 

493 if not 200 <= response.status_code < 300: 

494 raise exceptions.from_http_response(response) 

495 

496 if expect_json and response.content: 

497 return response.json() 

498 else: 

499 return response.content