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

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

98 statements  

1# Copyright 2015 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"""Base classes for client used to interact with Google Cloud APIs.""" 

16 

17import io 

18import json 

19import os 

20from pickle import PicklingError 

21from typing import Set, Tuple, Union 

22 

23# PEP 0810: Explicit Lazy Imports 

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

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

26# For more information, see: 

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

28# Older Python versions safely ignore this variable. 

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

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

31# imports are ignored, making their presence safe. 

32__lazy_modules__: Set[str] = { 

33 "google.api_core.client_options", 

34 "google.api_core.exceptions", 

35 "google.auth", 

36 "google.auth.api_key", 

37 "google.auth.environment_vars", 

38 "google.auth.credentials", 

39 "google.auth.transport.requests", 

40 "google.cloud._helpers", 

41 "google.oauth2", 

42 "google.oauth2.service_account", 

43} 

44 

45import google.api_core.client_options 

46import google.api_core.exceptions 

47import google.auth 

48from google.auth import environment_vars 

49import google.auth.credentials 

50import google.auth.transport.requests 

51from google.cloud._helpers import _determine_default_project 

52from google.oauth2 import service_account 

53 

54try: 

55 import google.auth.api_key 

56 

57 HAS_GOOGLE_AUTH_API_KEY = True 

58except ImportError: # pragma: NO COVER 

59 HAS_GOOGLE_AUTH_API_KEY = False # pragma: NO COVER 

60 # TODO: Investigate adding a test for google.auth.api_key ImportError (https://github.com/googleapis/python-cloud-core/issues/334) 

61 

62 

63_GOOGLE_AUTH_CREDENTIALS_HELP = ( 

64 "This library only supports credentials from google-auth-library-python. " 

65 "See https://google-auth.readthedocs.io/en/latest/ " 

66 "for help on authentication with this library." 

67) 

68 

69# Default timeout for auth requests. 

70_CREDENTIALS_REFRESH_TIMEOUT = 300 

71 

72 

73class _ClientFactoryMixin(object): 

74 """Mixin to allow factories that create credentials. 

75 

76 .. note:: 

77 

78 This class is virtual. 

79 """ 

80 

81 _SET_PROJECT = False 

82 

83 @classmethod 

84 def from_service_account_info(cls, info, *args, **kwargs): 

85 """Factory to retrieve JSON credentials while creating client. 

86 

87 :type info: dict 

88 :param info: 

89 The JSON object with a private key and other credentials 

90 information (downloaded from the Google APIs console). 

91 

92 :type args: tuple 

93 :param args: Remaining positional arguments to pass to constructor. 

94 

95 :param kwargs: Remaining keyword arguments to pass to constructor. 

96 

97 :rtype: :class:`_ClientFactoryMixin` 

98 :returns: The client created with the retrieved JSON credentials. 

99 :raises TypeError: if there is a conflict with the kwargs 

100 and the credentials created by the factory. 

101 """ 

102 if "credentials" in kwargs: 

103 raise TypeError("credentials must not be in keyword arguments") 

104 

105 credentials = service_account.Credentials.from_service_account_info(info) 

106 if cls._SET_PROJECT: 

107 if "project" not in kwargs: 

108 kwargs["project"] = info.get("project_id") 

109 

110 kwargs["credentials"] = credentials 

111 return cls(*args, **kwargs) 

112 

113 @classmethod 

114 def from_service_account_json(cls, json_credentials_path, *args, **kwargs): 

115 """Factory to retrieve JSON credentials while creating client. 

116 

117 :type json_credentials_path: str 

118 :param json_credentials_path: The path to a private key file (this file 

119 was given to you when you created the 

120 service account). This file must contain 

121 a JSON object with a private key and 

122 other credentials information (downloaded 

123 from the Google APIs console). 

124 

125 :type args: tuple 

126 :param args: Remaining positional arguments to pass to constructor. 

127 

128 :param kwargs: Remaining keyword arguments to pass to constructor. 

129 

130 :rtype: :class:`_ClientFactoryMixin` 

131 :returns: The client created with the retrieved JSON credentials. 

132 :raises TypeError: if there is a conflict with the kwargs 

133 and the credentials created by the factory. 

134 """ 

135 with io.open(json_credentials_path, "r", encoding="utf-8") as json_fi: 

136 credentials_info = json.load(json_fi) 

137 

138 return cls.from_service_account_info(credentials_info, *args, **kwargs) 

139 

140 

141class Client(_ClientFactoryMixin): 

142 """Client to bundle configuration needed for API requests. 

143 

144 Stores ``credentials`` and an HTTP object so that subclasses 

145 can pass them along to a connection class. 

146 

147 If no value is passed in for ``_http``, a :class:`requests.Session` object 

148 will be created and authorized with the ``credentials``. If not, the 

149 ``credentials`` and ``_http`` need not be related. 

150 

151 Callers and subclasses may seek to use the private key from 

152 ``credentials`` to sign data. 

153 

154 Args: 

155 credentials (google.auth.credentials.Credentials): 

156 (Optional) The OAuth2 Credentials to use for this client. If not 

157 passed (and if no ``_http`` object is passed), falls back to the 

158 default inferred from the environment. 

159 client_options (google.api_core.client_options.ClientOptions): 

160 (Optional) Custom options for the client. 

161 _http (requests.Session): 

162 (Optional) HTTP object to make requests. Can be any object that 

163 defines ``request()`` with the same interface as 

164 :meth:`requests.Session.request`. If not passed, an ``_http`` 

165 object is created that is bound to the ``credentials`` for the 

166 current object. 

167 This parameter should be considered private, and could change in 

168 the future. 

169 

170 Raises: 

171 google.auth.exceptions.DefaultCredentialsError: 

172 Raised if ``credentials`` is not specified and the library fails 

173 to acquire default credentials. 

174 """ 

175 

176 SCOPE: Union[Tuple[str, ...], None] = None 

177 """The scopes required for authenticating with a service. 

178 

179 Needs to be set by subclasses. 

180 """ 

181 

182 def __init__(self, credentials=None, _http=None, client_options=None): 

183 if isinstance(client_options, dict): 

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

185 if client_options is None: 

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

187 

188 if credentials and client_options.credentials_file: 

189 raise google.api_core.exceptions.DuplicateCredentialArgs( 

190 "'credentials' and 'client_options.credentials_file' are mutually exclusive." 

191 ) 

192 

193 if ( 

194 HAS_GOOGLE_AUTH_API_KEY 

195 and client_options.api_key 

196 and (credentials or client_options.credentials_file) 

197 ): 

198 raise google.api_core.exceptions.DuplicateCredentialArgs( 

199 "'client_options.api_key' is mutually exclusive with 'credentials' and 'client_options.credentials_file'." 

200 ) 

201 

202 if credentials and not isinstance( 

203 credentials, google.auth.credentials.Credentials 

204 ): 

205 raise ValueError(_GOOGLE_AUTH_CREDENTIALS_HELP) 

206 

207 scopes = client_options.scopes or self.SCOPE 

208 

209 # if no http is provided, credentials must exist 

210 if not _http and credentials is None: 

211 if client_options.credentials_file: 

212 credentials, _ = google.auth.load_credentials_from_file( 

213 client_options.credentials_file, scopes=scopes 

214 ) 

215 elif HAS_GOOGLE_AUTH_API_KEY and client_options.api_key is not None: 

216 credentials = google.auth.api_key.Credentials(client_options.api_key) 

217 else: 

218 credentials, _ = google.auth.default(scopes=scopes) 

219 

220 self._credentials = google.auth.credentials.with_scopes_if_required( 

221 credentials, scopes=scopes 

222 ) 

223 

224 if client_options.quota_project_id: 

225 self._credentials = self._credentials.with_quota_project( 

226 client_options.quota_project_id 

227 ) 

228 

229 self._http_internal = _http 

230 self._client_cert_source = client_options.client_cert_source 

231 

232 def __getstate__(self): 

233 """Explicitly state that clients are not pickleable.""" 

234 raise PicklingError( 

235 "\n".join( 

236 [ 

237 "Pickling client objects is explicitly not supported.", 

238 "Clients have non-trivial state that is local and unpickleable.", 

239 ] 

240 ) 

241 ) 

242 

243 @property 

244 def _http(self): 

245 """Getter for object used for HTTP transport. 

246 

247 :rtype: :class:`~requests.Session` 

248 :returns: An HTTP object. 

249 """ 

250 if self._http_internal is None: 

251 self._http_internal = google.auth.transport.requests.AuthorizedSession( 

252 self._credentials, 

253 refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT, 

254 ) 

255 self._http_internal.configure_mtls_channel(self._client_cert_source) 

256 return self._http_internal 

257 

258 def close(self): 

259 """Clean up transport, if set. 

260 

261 Suggested use: 

262 

263 .. code-block:: python 

264 

265 import contextlib 

266 

267 with contextlib.closing(client): # closes on exit 

268 do_something_with(client) 

269 """ 

270 if self._http_internal is not None: 

271 self._http_internal.close() 

272 

273 

274class _ClientProjectMixin(object): 

275 """Mixin to allow setting the project on the client. 

276 

277 :type project: str 

278 :param project: 

279 (Optional) the project which the client acts on behalf of. If not 

280 passed, falls back to the default inferred from the environment. 

281 

282 :type credentials: :class:`google.auth.credentials.Credentials` 

283 :param credentials: 

284 (Optional) credentials used to discover a project, if not passed. 

285 

286 :raises: :class:`EnvironmentError` if the project is neither passed in nor 

287 set on the credentials or in the environment. :class:`ValueError` 

288 if the project value is invalid. 

289 """ 

290 

291 def __init__(self, project=None, credentials=None): 

292 # This test duplicates the one from `google.auth.default`, but earlier, 

293 # for backward compatibility: we want the environment variable to 

294 # override any project set on the credentials. See: 

295 # https://github.com/googleapis/python-cloud-core/issues/27 

296 if project is None: 

297 project = os.getenv( 

298 environment_vars.PROJECT, 

299 os.getenv(environment_vars.LEGACY_PROJECT), 

300 ) 

301 

302 # Project set on explicit credentials overrides discovery from 

303 # SDK / GAE / GCE. 

304 if project is None and credentials is not None: 

305 project = getattr(credentials, "project_id", None) 

306 

307 if project is None: 

308 project = self._determine_default(project) 

309 

310 if project is None: 

311 raise EnvironmentError( 

312 "Project was not passed and could not be " 

313 "determined from the environment." 

314 ) 

315 

316 if isinstance(project, bytes): 

317 project = project.decode("utf-8") 

318 

319 if not isinstance(project, str): 

320 raise ValueError("Project must be a string.") 

321 

322 self.project = project 

323 

324 @staticmethod 

325 def _determine_default(project): 

326 """Helper: use default project detection.""" 

327 return _determine_default_project(project) 

328 

329 

330class ClientWithProject(Client, _ClientProjectMixin): 

331 """Client that also stores a project. 

332 

333 :type project: str 

334 :param project: the project which the client acts on behalf of. If not 

335 passed falls back to the default inferred from the 

336 environment. 

337 

338 :type credentials: :class:`~google.auth.credentials.Credentials` 

339 :param credentials: (Optional) The OAuth2 Credentials to use for this 

340 client. If not passed (and if no ``_http`` object is 

341 passed), falls back to the default inferred from the 

342 environment. 

343 

344 :type _http: :class:`~requests.Session` 

345 :param _http: (Optional) HTTP object to make requests. Can be any object 

346 that defines ``request()`` with the same interface as 

347 :meth:`~requests.Session.request`. If not passed, an 

348 ``_http`` object is created that is bound to the 

349 ``credentials`` for the current object. 

350 This parameter should be considered private, and could 

351 change in the future. 

352 

353 :raises: :class:`ValueError` if the project is neither passed in nor 

354 set in the environment. 

355 """ 

356 

357 _SET_PROJECT = True # Used by from_service_account_json() 

358 

359 def __init__(self, project=None, credentials=None, client_options=None, _http=None): 

360 _ClientProjectMixin.__init__(self, project=project, credentials=credentials) 

361 Client.__init__( 

362 self, credentials=credentials, client_options=client_options, _http=_http 

363 )