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

89 statements  

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

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 

33 

34_GOOGLE_AUTH_CREDENTIALS_HELP = ( 

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

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

37 "for help on authentication with this library." 

38) 

39 

40# Default timeout for auth requests. 

41_CREDENTIALS_REFRESH_TIMEOUT = 300 

42 

43 

44class _ClientFactoryMixin(object): 

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

46 

47 .. note:: 

48 

49 This class is virtual. 

50 """ 

51 

52 _SET_PROJECT = False 

53 

54 @classmethod 

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

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

57 

58 :type info: dict 

59 :param info: 

60 The JSON object with a private key and other credentials 

61 information (downloaded from the Google APIs console). 

62 

63 :type args: tuple 

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

65 

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

67 

68 :rtype: :class:`_ClientFactoryMixin` 

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

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

71 and the credentials created by the factory. 

72 """ 

73 if "credentials" in kwargs: 

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

75 

76 credentials = service_account.Credentials.from_service_account_info(info) 

77 if cls._SET_PROJECT: 

78 if "project" not in kwargs: 

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

80 

81 kwargs["credentials"] = credentials 

82 return cls(*args, **kwargs) 

83 

84 @classmethod 

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

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

87 

88 :type json_credentials_path: str 

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

90 was given to you when you created the 

91 service account). This file must contain 

92 a JSON object with a private key and 

93 other credentials information (downloaded 

94 from the Google APIs console). 

95 

96 :type args: tuple 

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

98 

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

100 

101 :rtype: :class:`_ClientFactoryMixin` 

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

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

104 and the credentials created by the factory. 

105 """ 

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

107 credentials_info = json.load(json_fi) 

108 

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

110 

111 

112class Client(_ClientFactoryMixin): 

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

114 

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

116 can pass them along to a connection class. 

117 

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

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

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

121 

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

123 ``credentials`` to sign data. 

124 

125 Args: 

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

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

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

129 default inferred from the environment. 

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

131 (Optional) Custom options for the client. 

132 _http (requests.Session): 

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

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

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

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

137 current object. 

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

139 the future. 

140 

141 Raises: 

142 google.auth.exceptions.DefaultCredentialsError: 

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

144 to acquire default credentials. 

145 """ 

146 

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

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

149 

150 Needs to be set by subclasses. 

151 """ 

152 

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

154 if isinstance(client_options, dict): 

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

156 if client_options is None: 

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

158 

159 if credentials and client_options.credentials_file: 

160 raise google.api_core.exceptions.DuplicateCredentialArgs( 

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

162 ) 

163 

164 if credentials and not isinstance( 

165 credentials, google.auth.credentials.Credentials 

166 ): 

167 raise ValueError(_GOOGLE_AUTH_CREDENTIALS_HELP) 

168 

169 scopes = client_options.scopes or self.SCOPE 

170 

171 # if no http is provided, credentials must exist 

172 if not _http and credentials is None: 

173 if client_options.credentials_file: 

174 credentials, _ = google.auth.load_credentials_from_file( 

175 client_options.credentials_file, scopes=scopes 

176 ) 

177 else: 

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

179 

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

181 credentials, scopes=scopes 

182 ) 

183 

184 if client_options.quota_project_id: 

185 self._credentials = self._credentials.with_quota_project( 

186 client_options.quota_project_id 

187 ) 

188 

189 self._http_internal = _http 

190 self._client_cert_source = client_options.client_cert_source 

191 

192 def __getstate__(self): 

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

194 raise PicklingError( 

195 "\n".join( 

196 [ 

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

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

199 ] 

200 ) 

201 ) 

202 

203 @property 

204 def _http(self): 

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

206 

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

208 :returns: An HTTP object. 

209 """ 

210 if self._http_internal is None: 

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

212 self._credentials, 

213 refresh_timeout=_CREDENTIALS_REFRESH_TIMEOUT, 

214 ) 

215 self._http_internal.configure_mtls_channel(self._client_cert_source) 

216 return self._http_internal 

217 

218 def close(self): 

219 """Clean up transport, if set. 

220 

221 Suggested use: 

222 

223 .. code-block:: python 

224 

225 import contextlib 

226 

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

228 do_something_with(client) 

229 """ 

230 if self._http_internal is not None: 

231 self._http_internal.close() 

232 

233 

234class _ClientProjectMixin(object): 

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

236 

237 :type project: str 

238 :param project: 

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

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

241 

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

243 :param credentials: 

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

245 

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

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

248 if the project value is invalid. 

249 """ 

250 

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

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

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

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

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

256 if project is None: 

257 project = os.getenv( 

258 environment_vars.PROJECT, 

259 os.getenv(environment_vars.LEGACY_PROJECT), 

260 ) 

261 

262 # Project set on explicit credentials overrides discovery from 

263 # SDK / GAE / GCE. 

264 if project is None and credentials is not None: 

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

266 

267 if project is None: 

268 project = self._determine_default(project) 

269 

270 if project is None: 

271 raise EnvironmentError( 

272 "Project was not passed and could not be " 

273 "determined from the environment." 

274 ) 

275 

276 if isinstance(project, bytes): 

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

278 

279 if not isinstance(project, str): 

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

281 

282 self.project = project 

283 

284 @staticmethod 

285 def _determine_default(project): 

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

287 return _determine_default_project(project) 

288 

289 

290class ClientWithProject(Client, _ClientProjectMixin): 

291 """Client that also stores a project. 

292 

293 :type project: str 

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

295 passed falls back to the default inferred from the 

296 environment. 

297 

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

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

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

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

302 environment. 

303 

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

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

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

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

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

309 ``credentials`` for the current object. 

310 This parameter should be considered private, and could 

311 change in the future. 

312 

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

314 set in the environment. 

315 """ 

316 

317 _SET_PROJECT = True # Used by from_service_account_json() 

318 

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

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

321 Client.__init__( 

322 self, credentials=credentials, client_options=client_options, _http=_http 

323 )