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 Tuple 

22from typing import Union 

23 

24import google.api_core.client_options 

25import google.api_core.exceptions 

26import google.auth 

27from google.auth import environment_vars 

28import google.auth.credentials 

29import google.auth.transport.requests 

30from google.cloud._helpers import _determine_default_project 

31from google.oauth2 import service_account 

32 

33try: 

34 import google.auth.api_key 

35 

36 HAS_GOOGLE_AUTH_API_KEY = True 

37except ImportError: # pragma: NO COVER 

38 HAS_GOOGLE_AUTH_API_KEY = False # pragma: NO COVER 

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

40 

41 

42_GOOGLE_AUTH_CREDENTIALS_HELP = ( 

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

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

45 "for help on authentication with this library." 

46) 

47 

48# Default timeout for auth requests. 

49_CREDENTIALS_REFRESH_TIMEOUT = 300 

50 

51 

52class _ClientFactoryMixin(object): 

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

54 

55 .. note:: 

56 

57 This class is virtual. 

58 """ 

59 

60 _SET_PROJECT = False 

61 

62 @classmethod 

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

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

65 

66 :type info: dict 

67 :param info: 

68 The JSON object with a private key and other credentials 

69 information (downloaded from the Google APIs console). 

70 

71 :type args: tuple 

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

73 

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

75 

76 :rtype: :class:`_ClientFactoryMixin` 

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

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

79 and the credentials created by the factory. 

80 """ 

81 if "credentials" in kwargs: 

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

83 

84 credentials = service_account.Credentials.from_service_account_info(info) 

85 if cls._SET_PROJECT: 

86 if "project" not in kwargs: 

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

88 

89 kwargs["credentials"] = credentials 

90 return cls(*args, **kwargs) 

91 

92 @classmethod 

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

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

95 

96 :type json_credentials_path: str 

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

98 was given to you when you created the 

99 service account). This file must contain 

100 a JSON object with a private key and 

101 other credentials information (downloaded 

102 from the Google APIs console). 

103 

104 :type args: tuple 

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

106 

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

108 

109 :rtype: :class:`_ClientFactoryMixin` 

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

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

112 and the credentials created by the factory. 

113 """ 

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

115 credentials_info = json.load(json_fi) 

116 

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

118 

119 

120class Client(_ClientFactoryMixin): 

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

122 

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

124 can pass them along to a connection class. 

125 

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

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

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

129 

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

131 ``credentials`` to sign data. 

132 

133 Args: 

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

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

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

137 default inferred from the environment. 

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

139 (Optional) Custom options for the client. 

140 _http (requests.Session): 

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

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

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

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

145 current object. 

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

147 the future. 

148 

149 Raises: 

150 google.auth.exceptions.DefaultCredentialsError: 

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

152 to acquire default credentials. 

153 """ 

154 

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

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

157 

158 Needs to be set by subclasses. 

159 """ 

160 

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

162 if isinstance(client_options, dict): 

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

164 if client_options is None: 

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

166 

167 if credentials and client_options.credentials_file: 

168 raise google.api_core.exceptions.DuplicateCredentialArgs( 

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

170 ) 

171 

172 if ( 

173 HAS_GOOGLE_AUTH_API_KEY 

174 and client_options.api_key 

175 and (credentials or client_options.credentials_file) 

176 ): 

177 raise google.api_core.exceptions.DuplicateCredentialArgs( 

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

179 ) 

180 

181 if credentials and not isinstance( 

182 credentials, google.auth.credentials.Credentials 

183 ): 

184 raise ValueError(_GOOGLE_AUTH_CREDENTIALS_HELP) 

185 

186 scopes = client_options.scopes or self.SCOPE 

187 

188 # if no http is provided, credentials must exist 

189 if not _http and credentials is None: 

190 if client_options.credentials_file: 

191 credentials, _ = google.auth.load_credentials_from_file( 

192 client_options.credentials_file, scopes=scopes 

193 ) 

194 elif HAS_GOOGLE_AUTH_API_KEY and client_options.api_key is not None: 

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

196 else: 

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

198 

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

200 credentials, scopes=scopes 

201 ) 

202 

203 if client_options.quota_project_id: 

204 self._credentials = self._credentials.with_quota_project( 

205 client_options.quota_project_id 

206 ) 

207 

208 self._http_internal = _http 

209 self._client_cert_source = client_options.client_cert_source 

210 

211 def __getstate__(self): 

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

213 raise PicklingError( 

214 "\n".join( 

215 [ 

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

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

218 ] 

219 ) 

220 ) 

221 

222 @property 

223 def _http(self): 

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

225 

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

227 :returns: An HTTP object. 

228 """ 

229 if self._http_internal is None: 

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

231 self._credentials, 

232 refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT, 

233 ) 

234 self._http_internal.configure_mtls_channel(self._client_cert_source) 

235 return self._http_internal 

236 

237 def close(self): 

238 """Clean up transport, if set. 

239 

240 Suggested use: 

241 

242 .. code-block:: python 

243 

244 import contextlib 

245 

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

247 do_something_with(client) 

248 """ 

249 if self._http_internal is not None: 

250 self._http_internal.close() 

251 

252 

253class _ClientProjectMixin(object): 

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

255 

256 :type project: str 

257 :param project: 

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

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

260 

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

262 :param credentials: 

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

264 

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

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

267 if the project value is invalid. 

268 """ 

269 

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

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

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

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

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

275 if project is None: 

276 project = os.getenv( 

277 environment_vars.PROJECT, 

278 os.getenv(environment_vars.LEGACY_PROJECT), 

279 ) 

280 

281 # Project set on explicit credentials overrides discovery from 

282 # SDK / GAE / GCE. 

283 if project is None and credentials is not None: 

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

285 

286 if project is None: 

287 project = self._determine_default(project) 

288 

289 if project is None: 

290 raise EnvironmentError( 

291 "Project was not passed and could not be " 

292 "determined from the environment." 

293 ) 

294 

295 if isinstance(project, bytes): 

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

297 

298 if not isinstance(project, str): 

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

300 

301 self.project = project 

302 

303 @staticmethod 

304 def _determine_default(project): 

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

306 return _determine_default_project(project) 

307 

308 

309class ClientWithProject(Client, _ClientProjectMixin): 

310 """Client that also stores a project. 

311 

312 :type project: str 

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

314 passed falls back to the default inferred from the 

315 environment. 

316 

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

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

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

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

321 environment. 

322 

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

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

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

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

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

328 ``credentials`` for the current object. 

329 This parameter should be considered private, and could 

330 change in the future. 

331 

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

333 set in the environment. 

334 """ 

335 

336 _SET_PROJECT = True # Used by from_service_account_json() 

337 

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

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

340 Client.__init__( 

341 self, credentials=credentials, client_options=client_options, _http=_http 

342 )