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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

124 statements  

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, Set 

23from urllib.parse import urlencode 

24import warnings 

25 

26# PEP 0810: Explicit Lazy Imports 

27# Python 3.15+ natively intercepts and defers these imports. 

28# Developers can disable this behavior and force eager imports. 

29# For more information, see: 

30# https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports_filter 

31# Older Python versions safely ignore this variable. 

32# NOTE: We statically define all modules here to ensure static analysis tools 

33# (mypy, pyright, Ruff) can easily parse them. If support is not present, the 

34# imports are ignored, making their presence safe. 

35__lazy_modules__: Set[str] = { 

36 "google.api_core.client_info", 

37 "google.cloud.exceptions", 

38 "google.cloud.version", 

39} 

40 

41from google.api_core.client_info import ClientInfo 

42from google.cloud import exceptions 

43from google.cloud import version 

44 

45 

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

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

48 

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

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

51 

52CLIENT_INFO_HEADER = "x-goog-api-client" 

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

54 

55_USER_AGENT_ALL_CAPS_DEPRECATED = """\ 

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

57'user_agent' instead. 

58""" 

59 

60_EXTRA_HEADERS_ALL_CAPS_DEPRECATED = """\ 

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

62'extra_headers' instead. 

63""" 

64 

65_DEFAULT_TIMEOUT = 60 # in seconds 

66 

67 

68class Connection(object): 

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

70 

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

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

73 

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

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

76 """ 

77 

78 _user_agent = DEFAULT_USER_AGENT 

79 

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

81 self._client = client 

82 

83 if client_info is None: 

84 client_info = ClientInfo() 

85 

86 self._client_info = client_info 

87 self._extra_headers = {} 

88 

89 @property 

90 def USER_AGENT(self): 

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

92 

93 :rtype: str 

94 :returns: user agent 

95 """ 

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

97 return self.user_agent 

98 

99 @USER_AGENT.setter 

100 def USER_AGENT(self, value): 

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

102 self.user_agent = value 

103 

104 @property 

105 def user_agent(self): 

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

107 

108 :rtype: str 

109 :returns: user agent 

110 """ 

111 return self._client_info.to_user_agent() 

112 

113 @user_agent.setter 

114 def user_agent(self, value): 

115 self._client_info.user_agent = value 

116 

117 @property 

118 def _EXTRA_HEADERS(self): 

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

120 

121 :rtype: dict 

122 :returns: header keys / values 

123 """ 

124 warnings.warn( 

125 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2 

126 ) 

127 return self.extra_headers 

128 

129 @_EXTRA_HEADERS.setter 

130 def _EXTRA_HEADERS(self, value): 

131 warnings.warn( 

132 _EXTRA_HEADERS_ALL_CAPS_DEPRECATED, DeprecationWarning, stacklevel=2 

133 ) 

134 self.extra_headers = value 

135 

136 @property 

137 def extra_headers(self): 

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

139 

140 :rtype: dict 

141 :returns: header keys / values 

142 """ 

143 return self._extra_headers 

144 

145 @extra_headers.setter 

146 def extra_headers(self, value): 

147 self._extra_headers = value 

148 

149 @property 

150 def credentials(self): 

151 """Getter for current credentials. 

152 

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

154 :class:`NoneType` 

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

156 """ 

157 return self._client._credentials 

158 

159 @property 

160 def http(self): 

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

162 

163 Returns: 

164 google.auth.transport.requests.AuthorizedSession: 

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

166 """ 

167 return self._client._http 

168 

169 

170class JSONConnection(Connection): 

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

172 

173 These APIs are discovery based. For reference: 

174 

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

176 

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

178 API request and API requests are created elsewhere. 

179 

180 * :attr:`API_BASE_URL` 

181 * :attr:`API_VERSION` 

182 * :attr:`API_URL_TEMPLATE` 

183 

184 must be updated by subclasses. 

185 """ 

186 

187 API_BASE_URL: Optional[str] = None 

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

189 

190 API_BASE_MTLS_URL: Optional[str] = None 

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

192 

193 ALLOW_AUTO_SWITCH_TO_MTLS_URL = False 

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

195 

196 API_VERSION: Optional[str] = None 

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

198 

199 API_URL_TEMPLATE: Optional[str] = None 

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

201 

202 def get_api_base_url_for_mtls(self, api_base_url=None): 

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

204 

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

206 

207 The logic is as follows: 

208 

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

210 return value depends `GOOGLE_API_USE_MTLS_ENDPOINT` environment variable 

211 value. 

212 

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

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

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

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

217 

218 :type api_base_url: str 

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

220 `API_BASE_URL` and `API_BASE_MTLS_URL`. 

221 

222 :rtype: str 

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

224 """ 

225 if api_base_url: 

226 return api_base_url 

227 

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

229 if env == "always": 

230 url_to_use = self.API_BASE_MTLS_URL 

231 elif env == "never": 

232 url_to_use = self.API_BASE_URL 

233 else: 

234 if self.ALLOW_AUTO_SWITCH_TO_MTLS_URL: 

235 url_to_use = ( 

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

237 ) 

238 else: 

239 url_to_use = self.API_BASE_URL 

240 return url_to_use 

241 

242 def build_api_url( 

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

244 ): 

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

246 

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

248 

249 :type path: str 

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

251 

252 :type query_params: dict or list 

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

254 key-value pairs) to insert into the query 

255 string of the URL. 

256 

257 :type api_base_url: str 

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

259 Typically you won't have to provide this. 

260 

261 :type api_version: str 

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

263 Typically you shouldn't provide this and instead 

264 use the default for the library. 

265 

266 :rtype: str 

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

268 """ 

269 url = self.API_URL_TEMPLATE.format( 

270 api_base_url=self.get_api_base_url_for_mtls(api_base_url), 

271 api_version=(api_version or self.API_VERSION), 

272 path=path, 

273 ) 

274 

275 query_params = query_params or {} 

276 

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

278 query_params = query_params.copy() 

279 else: 

280 query_params_dict = collections.defaultdict(list) 

281 for key, value in query_params: 

282 query_params_dict[key].append(value) 

283 query_params = query_params_dict 

284 

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

286 

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

288 

289 return url 

290 

291 def _make_request( 

292 self, 

293 method, 

294 url, 

295 data=None, 

296 content_type=None, 

297 headers=None, 

298 target_object=None, 

299 timeout=_DEFAULT_TIMEOUT, 

300 extra_api_info=None, 

301 ): 

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

303 

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

305 

306 :type method: str 

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

308 

309 :type url: str 

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

311 

312 :type data: str 

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

314 

315 :type content_type: str 

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

317 

318 :type headers: dict 

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

320 the request. If passed, will be modified directly 

321 here with added headers. 

322 

323 :type target_object: object 

324 :param target_object: 

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

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

327 initialization of the object at a later time. 

328 

329 :type timeout: float or tuple 

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

331 for the server response. 

332 

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

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

335 

336 :type extra_api_info: string 

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

338 the X-Goog-API-Client header 

339 

340 :rtype: :class:`requests.Response` 

341 :returns: The HTTP response. 

342 """ 

343 headers = headers or {} 

344 headers.update(self.extra_headers) 

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

346 

347 if content_type: 

348 headers["Content-Type"] = content_type 

349 

350 if extra_api_info: 

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

352 else: 

353 headers[CLIENT_INFO_HEADER] = self.user_agent 

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

355 

356 return self._do_request( 

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

358 ) 

359 

360 def _do_request( 

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

362 ): # pylint: disable=unused-argument 

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

364 

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

366 

367 :type method: str 

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

369 

370 :type url: str 

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

372 

373 :type headers: dict 

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

375 

376 :type data: str 

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

378 

379 :type target_object: object 

380 :param target_object: 

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

382 superclass. 

383 

384 :type timeout: float or tuple 

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

386 for the server response. 

387 

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

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

390 

391 :rtype: :class:`requests.Response` 

392 :returns: The HTTP response. 

393 """ 

394 return self.http.request( 

395 url=url, method=method, headers=headers, data=data, timeout=timeout 

396 ) 

397 

398 def api_request( 

399 self, 

400 method, 

401 path, 

402 query_params=None, 

403 data=None, 

404 content_type=None, 

405 headers=None, 

406 api_base_url=None, 

407 api_version=None, 

408 expect_json=True, 

409 _target_object=None, 

410 timeout=_DEFAULT_TIMEOUT, 

411 extra_api_info=None, 

412 ): 

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

414 

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

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

417 correct one to use. 

418 

419 :type method: str 

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

421 Required. 

422 

423 :type path: str 

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

425 Required. 

426 

427 :type query_params: dict or list 

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

429 key-value pairs) to insert into the query 

430 string of the URL. 

431 

432 :type data: str 

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

434 the empty string. 

435 

436 :type content_type: str 

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

438 is None. 

439 

440 :type headers: dict 

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

442 

443 :type api_base_url: str 

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

445 Typically you won't have to provide this. 

446 Default is the standard API base URL. 

447 

448 :type api_version: str 

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

450 you shouldn't provide this and instead use 

451 the default for the library. Default is the 

452 latest API version supported by 

453 google-cloud-python. 

454 

455 :type expect_json: bool 

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

457 response as JSON and raise an exception if 

458 that cannot be done. Default is True. 

459 

460 :type _target_object: :class:`object` 

461 :param _target_object: 

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

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

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

465 

466 :type timeout: float or tuple 

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

468 for the server response. 

469 

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

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

472 

473 :type extra_api_info: string 

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

475 the X-Goog-API-Client header 

476 

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

478 is not 200 OK. 

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

480 :rtype: dict or str 

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

482 a dictionary if the response is valid JSON. 

483 """ 

484 url = self.build_api_url( 

485 path=path, 

486 query_params=query_params, 

487 api_base_url=api_base_url, 

488 api_version=api_version, 

489 ) 

490 

491 # Making the executive decision that any dictionary 

492 # data will be sent properly as JSON. 

493 if data and isinstance(data, dict): 

494 data = json.dumps(data) 

495 content_type = "application/json" 

496 

497 response = self._make_request( 

498 method=method, 

499 url=url, 

500 data=data, 

501 content_type=content_type, 

502 headers=headers, 

503 target_object=_target_object, 

504 timeout=timeout, 

505 extra_api_info=extra_api_info, 

506 ) 

507 

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

509 raise exceptions.from_http_response(response) 

510 

511 if expect_json and response.content: 

512 return response.json() 

513 else: 

514 return response.content