cloud-sql-python-connector 1.9.2__py2.py3-none-any.whl → 1.11.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,10 +19,9 @@ from __future__ import annotations
19
19
  import asyncio
20
20
  from functools import partial
21
21
  import logging
22
- import socket
23
22
  from threading import Thread
24
23
  from types import TracebackType
25
- from typing import Any, Dict, Optional, Type
24
+ from typing import Any, Dict, Optional, Type, Union
26
25
 
27
26
  import google.auth
28
27
  from google.auth.credentials import Credentials
@@ -30,10 +29,12 @@ from google.auth.credentials import with_scopes_if_required
30
29
 
31
30
  import google.cloud.sql.connector.asyncpg as asyncpg
32
31
  from google.cloud.sql.connector.client import CloudSQLClient
32
+ from google.cloud.sql.connector.enums import DriverMapping
33
+ from google.cloud.sql.connector.enums import IPTypes
34
+ from google.cloud.sql.connector.enums import RefreshStrategy
33
35
  from google.cloud.sql.connector.exceptions import ConnectorLoopError
34
- from google.cloud.sql.connector.exceptions import DnsNameResolutionError
35
- from google.cloud.sql.connector.instance import IPTypes
36
36
  from google.cloud.sql.connector.instance import RefreshAheadCache
37
+ from google.cloud.sql.connector.lazy import LazyRefreshCache
37
38
  import google.cloud.sql.connector.pg8000 as pg8000
38
39
  import google.cloud.sql.connector.pymysql as pymysql
39
40
  import google.cloud.sql.connector.pytds as pytds
@@ -62,6 +63,7 @@ class Connector:
62
63
  sqladmin_api_endpoint: Optional[str] = None,
63
64
  user_agent: Optional[str] = None,
64
65
  universe_domain: Optional[str] = None,
66
+ refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
65
67
  ) -> None:
66
68
  """Initializes a Connector instance.
67
69
 
@@ -98,22 +100,39 @@ class Connector:
98
100
  universe_domain (str): The universe domain for Cloud SQL API calls.
99
101
  Default: "googleapis.com".
100
102
 
103
+ refresh_strategy (str | RefreshStrategy): The default refresh strategy
104
+ used to refresh SSL/TLS cert and instance metadata. Can be one
105
+ of the following: RefreshStrategy.LAZY ("LAZY") or
106
+ RefreshStrategy.BACKGROUND ("BACKGROUND").
107
+ Default: RefreshStrategy.BACKGROUND
101
108
  """
109
+ # if refresh_strategy is str, convert to RefreshStrategy enum
110
+ if isinstance(refresh_strategy, str):
111
+ refresh_strategy = RefreshStrategy._from_str(refresh_strategy)
112
+ self._refresh_strategy = refresh_strategy
102
113
  # if event loop is given, use for background tasks
103
114
  if loop:
104
115
  self._loop: asyncio.AbstractEventLoop = loop
105
116
  self._thread: Optional[Thread] = None
106
- self._keys: asyncio.Future = loop.create_task(generate_keys())
117
+ # if lazy refresh is specified we should lazy init keys
118
+ if self._refresh_strategy == RefreshStrategy.LAZY:
119
+ self._keys: Optional[asyncio.Future] = None
120
+ else:
121
+ self._keys = loop.create_task(generate_keys())
107
122
  # if no event loop is given, spin up new loop in background thread
108
123
  else:
109
124
  self._loop = asyncio.new_event_loop()
110
125
  self._thread = Thread(target=self._loop.run_forever, daemon=True)
111
126
  self._thread.start()
112
- self._keys = asyncio.wrap_future(
113
- asyncio.run_coroutine_threadsafe(generate_keys(), self._loop),
114
- loop=self._loop,
115
- )
116
- self._cache: Dict[str, RefreshAheadCache] = {}
127
+ # if lazy refresh is specified we should lazy init keys
128
+ if self._refresh_strategy == RefreshStrategy.LAZY:
129
+ self._keys = None
130
+ else:
131
+ self._keys = asyncio.wrap_future(
132
+ asyncio.run_coroutine_threadsafe(generate_keys(), self._loop),
133
+ loop=self._loop,
134
+ )
135
+ self._cache: Dict[str, Union[RefreshAheadCache, LazyRefreshCache]] = {}
117
136
  self._client: Optional[CloudSQLClient] = None
118
137
 
119
138
  # initialize credentials
@@ -242,9 +261,9 @@ class Connector:
242
261
  and then subsequent attempt with IAM database authentication.
243
262
  KeyError: Unsupported database driver Must be one of pymysql, asyncpg,
244
263
  pg8000, and pytds.
245
- DnsNameResolutionError: Could not resolve PSC IP address from DNS
246
- host name.
247
264
  """
265
+ if self._keys is None:
266
+ self._keys = asyncio.create_task(generate_keys())
248
267
  if self._client is None:
249
268
  # lazy init client as it has to be initialized in async context
250
269
  self._client = CloudSQLClient(
@@ -265,11 +284,30 @@ class Connector:
265
284
  "connector.Connector object."
266
285
  )
267
286
  else:
268
- cache = RefreshAheadCache(
269
- instance_connection_string,
270
- self._client,
271
- self._keys,
272
- enable_iam_auth,
287
+ if self._refresh_strategy == RefreshStrategy.LAZY:
288
+ logger.debug(
289
+ f"['{instance_connection_string}']: Refresh strategy is set"
290
+ " to lazy refresh"
291
+ )
292
+ cache = LazyRefreshCache(
293
+ instance_connection_string,
294
+ self._client,
295
+ self._keys,
296
+ enable_iam_auth,
297
+ )
298
+ else:
299
+ logger.debug(
300
+ f"['{instance_connection_string}']: Refresh strategy is set"
301
+ " to backgound refresh"
302
+ )
303
+ cache = RefreshAheadCache(
304
+ instance_connection_string,
305
+ self._client,
306
+ self._keys,
307
+ enable_iam_auth,
308
+ )
309
+ logger.debug(
310
+ f"['{instance_connection_string}']: Connection info added to cache"
273
311
  )
274
312
  self._cache[instance_connection_string] = cache
275
313
 
@@ -298,42 +336,44 @@ class Connector:
298
336
  kwargs.pop("ssl", None)
299
337
  kwargs.pop("port", None)
300
338
 
301
- # attempt to make connection to Cloud SQL instance
339
+ # attempt to get connection info for Cloud SQL instance
302
340
  try:
303
- instance_data, ip_address = await cache.connect_info(ip_type)
304
- # resolve DNS name into IP address for PSC
305
- if ip_type.value == "PSC":
306
- addr_info = await self._loop.getaddrinfo(
307
- ip_address, None, family=socket.AF_INET, type=socket.SOCK_STREAM
308
- )
309
- # getaddrinfo returns a list of 5-tuples that contain socket
310
- # connection info in the form
311
- # (family, type, proto, canonname, sockaddr), where sockaddr is a
312
- # 2-tuple in the form (ip_address, port)
313
- try:
314
- ip_address = addr_info[0][4][0]
315
- except IndexError as e:
316
- raise DnsNameResolutionError(
317
- f"['{instance_connection_string}']: DNS name could not be resolved into IP address"
318
- ) from e
319
-
320
- # format `user` param for automatic IAM database authn
321
- if enable_iam_auth:
322
- formatted_user = format_database_user(
323
- instance_data.database_version, kwargs["user"]
341
+ conn_info = await cache.connect_info()
342
+ # validate driver matches intended database engine
343
+ DriverMapping.validate_engine(driver, conn_info.database_version)
344
+ ip_address = conn_info.get_preferred_ip(ip_type)
345
+ except Exception:
346
+ # with an error from Cloud SQL Admin API call or IP type, invalidate
347
+ # the cache and re-raise the error
348
+ await self._remove_cached(instance_connection_string)
349
+ raise
350
+ logger.debug(
351
+ f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
352
+ )
353
+ # format `user` param for automatic IAM database authn
354
+ if enable_iam_auth:
355
+ formatted_user = format_database_user(
356
+ conn_info.database_version, kwargs["user"]
357
+ )
358
+ if formatted_user != kwargs["user"]:
359
+ logger.debug(
360
+ f"['{instance_connection_string}']: Truncated IAM database username from {kwargs['user']} to {formatted_user}"
324
361
  )
325
- if formatted_user != kwargs["user"]:
326
- logger.debug(
327
- f"['{instance_connection_string}']: Truncated IAM database username from {kwargs['user']} to {formatted_user}"
328
- )
329
- kwargs["user"] = formatted_user
330
-
362
+ kwargs["user"] = formatted_user
363
+ try:
331
364
  # async drivers are unblocking and can be awaited directly
332
365
  if driver in ASYNC_DRIVERS:
333
- return await connector(ip_address, instance_data.context, **kwargs)
366
+ return await connector(
367
+ ip_address,
368
+ conn_info.create_ssl_context(enable_iam_auth),
369
+ **kwargs,
370
+ )
334
371
  # synchronous drivers are blocking and run using executor
335
372
  connect_partial = partial(
336
- connector, ip_address, instance_data.context, **kwargs
373
+ connector,
374
+ ip_address,
375
+ conn_info.create_ssl_context(enable_iam_auth),
376
+ **kwargs,
337
377
  )
338
378
  return await self._loop.run_in_executor(None, connect_partial)
339
379
 
@@ -342,6 +382,17 @@ class Connector:
342
382
  await cache.force_refresh()
343
383
  raise
344
384
 
385
+ async def _remove_cached(self, instance_connection_string: str) -> None:
386
+ """Stops all background refreshes and deletes the connection
387
+ info cache from the map of caches.
388
+ """
389
+ logger.debug(
390
+ f"['{instance_connection_string}']: Removing connection info from cache"
391
+ )
392
+ # remove cache from stored caches and close it
393
+ cache = self._cache.pop(instance_connection_string)
394
+ await cache.close()
395
+
345
396
  def __enter__(self) -> Any:
346
397
  """Enter context manager by returning Connector object"""
347
398
  return self
@@ -0,0 +1,85 @@
1
+ # Copyright 2024 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
+ from __future__ import annotations
16
+
17
+ from enum import Enum
18
+
19
+ from google.cloud.sql.connector.exceptions import IncompatibleDriverError
20
+
21
+
22
+ class RefreshStrategy(Enum):
23
+ LAZY: str = "LAZY"
24
+ BACKGROUND: str = "BACKGROUND"
25
+
26
+ @classmethod
27
+ def _missing_(cls, value: object) -> None:
28
+ raise ValueError(
29
+ f"Incorrect value for refresh_strategy, got '{value}'. Want one of: "
30
+ f"{', '.join([repr(m.value) for m in cls])}."
31
+ )
32
+
33
+ @classmethod
34
+ def _from_str(cls, refresh_strategy: str) -> RefreshStrategy:
35
+ """Convert refresh strategy from a str into RefreshStrategy."""
36
+ return cls(refresh_strategy.upper())
37
+
38
+
39
+ class IPTypes(Enum):
40
+ PUBLIC: str = "PRIMARY"
41
+ PRIVATE: str = "PRIVATE"
42
+ PSC: str = "PSC"
43
+
44
+ @classmethod
45
+ def _missing_(cls, value: object) -> None:
46
+ raise ValueError(
47
+ f"Incorrect value for ip_type, got '{value}'. Want one of: "
48
+ f"{', '.join([repr(m.value) for m in cls])}, 'PUBLIC'."
49
+ )
50
+
51
+ @classmethod
52
+ def _from_str(cls, ip_type_str: str) -> IPTypes:
53
+ """Convert IP type from a str into IPTypes."""
54
+ if ip_type_str.upper() == "PUBLIC":
55
+ ip_type_str = "PRIMARY"
56
+ return cls(ip_type_str.upper())
57
+
58
+
59
+ class DriverMapping(Enum):
60
+ """Maps a given database driver to it's corresponding database engine."""
61
+
62
+ ASYNCPG = "POSTGRES"
63
+ PG8000 = "POSTGRES"
64
+ PYMYSQL = "MYSQL"
65
+ PYTDS = "SQLSERVER"
66
+
67
+ @staticmethod
68
+ def validate_engine(driver: str, engine_version: str) -> None:
69
+ """Validate that the given driver is compatible with the given engine.
70
+
71
+ Args:
72
+ driver (str): Database driver being used. (i.e. "pg8000")
73
+ engine_version (str): Database engine version. (i.e. "POSTGRES_16")
74
+
75
+ Raises:
76
+ IncompatibleDriverError: If the given driver is not compatible with
77
+ the given engine.
78
+ """
79
+ mapping = DriverMapping[driver.upper()]
80
+ if not engine_version.startswith(mapping.value):
81
+ raise IncompatibleDriverError(
82
+ f"Database driver '{driver}' is incompatible with database "
83
+ f"version '{engine_version}'. Given driver can "
84
+ f"only be used with Cloud SQL {mapping.value} databases."
85
+ )
@@ -57,8 +57,16 @@ class AutoIAMAuthNotSupported(Exception):
57
57
  pass
58
58
 
59
59
 
60
- class DnsNameResolutionError(Exception):
60
+ class RefreshNotValidError(Exception):
61
61
  """
62
- Exception to be raised when the DnsName of a PSC connection to a
63
- Cloud SQL instance can not be resolved to a proper IP address.
62
+ Exception to be raised when the task returned from refresh is not valid.
63
+ """
64
+
65
+ pass
66
+
67
+
68
+ class IncompatibleDriverError(Exception):
69
+ """
70
+ Exception to be raised when the database driver given is for the wrong
71
+ database engine. (i.e. asyncpg for a MySQL database)
64
72
  """
@@ -17,26 +17,21 @@ limitations under the License.
17
17
  from __future__ import annotations
18
18
 
19
19
  import asyncio
20
- from enum import Enum
20
+ from datetime import datetime
21
+ from datetime import timedelta
22
+ from datetime import timezone
21
23
  import logging
22
24
  import re
23
- import ssl
24
- from tempfile import TemporaryDirectory
25
- from typing import Any, Dict, Tuple, TYPE_CHECKING
25
+ from typing import Tuple
26
26
 
27
27
  import aiohttp
28
28
 
29
29
  from google.cloud.sql.connector.client import CloudSQLClient
30
- from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
31
- from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
32
- from google.cloud.sql.connector.exceptions import TLSVersionError
30
+ from google.cloud.sql.connector.connection_info import ConnectionInfo
31
+ from google.cloud.sql.connector.exceptions import RefreshNotValidError
33
32
  from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
34
33
  from google.cloud.sql.connector.refresh_utils import _is_valid
35
34
  from google.cloud.sql.connector.refresh_utils import _seconds_until_refresh
36
- from google.cloud.sql.connector.utils import write_to_file
37
-
38
- if TYPE_CHECKING:
39
- import datetime
40
35
 
41
36
  logger = logging.getLogger(name=__name__)
42
37
 
@@ -59,91 +54,6 @@ def _parse_instance_connection_name(connection_name: str) -> Tuple[str, str, str
59
54
  return connection_name_split[1], connection_name_split[3], connection_name_split[4]
60
55
 
61
56
 
62
- class IPTypes(Enum):
63
- PUBLIC: str = "PRIMARY"
64
- PRIVATE: str = "PRIVATE"
65
- PSC: str = "PSC"
66
-
67
- @classmethod
68
- def _missing_(cls, value: object) -> None:
69
- raise ValueError(
70
- f"Incorrect value for ip_type, got '{value}'. Want one of: "
71
- f"{', '.join([repr(m.value) for m in cls])}, 'PUBLIC'."
72
- )
73
-
74
- @classmethod
75
- def _from_str(cls, ip_type_str: str) -> IPTypes:
76
- """Convert IP type from a str into IPTypes."""
77
- if ip_type_str.upper() == "PUBLIC":
78
- ip_type_str = "PRIMARY"
79
- return cls(ip_type_str.upper())
80
-
81
-
82
- class ConnectionInfo:
83
- ip_addrs: Dict[str, Any]
84
- context: ssl.SSLContext
85
- database_version: str
86
- expiration: datetime.datetime
87
-
88
- def __init__(
89
- self,
90
- ephemeral_cert: str,
91
- database_version: str,
92
- ip_addrs: Dict[str, Any],
93
- private_key: bytes,
94
- server_ca_cert: str,
95
- expiration: datetime.datetime,
96
- enable_iam_auth: bool,
97
- ) -> None:
98
- self.ip_addrs = ip_addrs
99
- self.database_version = database_version
100
- self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
101
-
102
- # update ssl.PROTOCOL_TLS_CLIENT default
103
- self.context.check_hostname = False
104
-
105
- # verify OpenSSL version supports TLSv1.3
106
- if ssl.HAS_TLSv1_3:
107
- # force TLSv1.3 if supported by client
108
- self.context.minimum_version = ssl.TLSVersion.TLSv1_3
109
- # fallback to TLSv1.2 for older versions of OpenSSL
110
- else:
111
- if enable_iam_auth:
112
- raise TLSVersionError(
113
- f"Your current version of OpenSSL ({ssl.OPENSSL_VERSION}) does not "
114
- "support TLSv1.3, which is required to use IAM Authentication.\n"
115
- "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
116
- )
117
- logger.warning(
118
- "TLSv1.3 is not supported with your version of OpenSSL "
119
- f"({ssl.OPENSSL_VERSION}), falling back to TLSv1.2\n"
120
- "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
121
- )
122
- self.context.minimum_version = ssl.TLSVersion.TLSv1_2
123
- self.expiration = expiration
124
-
125
- # tmpdir and its contents are automatically deleted after the CA cert
126
- # and ephemeral cert are loaded into the SSLcontext. The values
127
- # need to be written to files in order to be loaded by the SSLContext
128
- with TemporaryDirectory() as tmpdir:
129
- ca_filename, cert_filename, key_filename = write_to_file(
130
- tmpdir, server_ca_cert, ephemeral_cert, private_key
131
- )
132
- self.context.load_cert_chain(cert_filename, keyfile=key_filename)
133
- self.context.load_verify_locations(cafile=ca_filename)
134
-
135
- def get_preferred_ip(self, ip_type: IPTypes) -> str:
136
- """Returns the first IP address for the instance, according to the preference
137
- supplied by ip_type. If no IP addressess with the given preference are found,
138
- an error is raised."""
139
- if ip_type.value in self.ip_addrs:
140
- return self.ip_addrs[ip_type.value]
141
- raise CloudSQLIPTypeError(
142
- "Cloud SQL instance does not have any IP addresses matching "
143
- f"preference: {ip_type.value})"
144
- )
145
-
146
-
147
57
  class RefreshAheadCache:
148
58
  """Cache that refreshes connection info in the background prior to expiration.
149
59
 
@@ -212,73 +122,47 @@ class RefreshAheadCache:
212
122
  """
213
123
  self._refresh_in_progress.set()
214
124
  logger.debug(
215
- f"['{self._instance_connection_string}']: Entered _perform_refresh"
125
+ f"['{self._instance_connection_string}']: Connection info refresh "
126
+ "operation started"
216
127
  )
217
128
 
218
129
  try:
219
130
  await self._refresh_rate_limiter.acquire()
220
- priv_key, pub_key = await self._keys
221
-
222
- logger.debug(f"['{self._instance_connection_string}']: Creating context")
223
-
224
- metadata_task = asyncio.create_task(
225
- self._client._get_metadata(
226
- self._project,
227
- self._region,
228
- self._instance,
229
- )
131
+ connection_info = await self._client.get_connection_info(
132
+ self._project,
133
+ self._region,
134
+ self._instance,
135
+ self._keys,
136
+ self._enable_iam_auth,
230
137
  )
231
-
232
- ephemeral_task = asyncio.create_task(
233
- self._client._get_ephemeral(
234
- self._project,
235
- self._instance,
236
- pub_key,
237
- self._enable_iam_auth,
238
- )
138
+ logger.debug(
139
+ f"['{self._instance_connection_string}']: Connection info "
140
+ "refresh operation complete"
141
+ )
142
+ logger.debug(
143
+ f"['{self._instance_connection_string}']: Current certificate "
144
+ f"expiration = {connection_info.expiration.isoformat()}"
239
145
  )
240
- try:
241
- metadata = await metadata_task
242
- # check if automatic IAM database authn is supported for database engine
243
- if self._enable_iam_auth and not metadata[
244
- "database_version"
245
- ].startswith(("POSTGRES", "MYSQL")):
246
- raise AutoIAMAuthNotSupported(
247
- f"'{metadata['database_version']}' does not support automatic IAM authentication. It is only supported with Cloud SQL Postgres or MySQL instances."
248
- )
249
- except Exception:
250
- # cancel ephemeral cert task if exception occurs before it is awaited
251
- ephemeral_task.cancel()
252
- raise
253
-
254
- ephemeral_cert, expiration = await ephemeral_task
255
146
 
256
147
  except aiohttp.ClientResponseError as e:
257
148
  logger.debug(
258
- f"['{self._instance_connection_string}']: Error occurred during _perform_refresh."
149
+ f"['{self._instance_connection_string}']: Connection info "
150
+ f"refresh operation failed: {str(e)}"
259
151
  )
260
152
  if e.status == 403:
261
- e.message = "Forbidden: Authenticated IAM principal does not seeem authorized to make API request. Verify 'Cloud SQL Admin API' is enabled within your GCP project and 'Cloud SQL Client' role has been granted to IAM principal."
153
+ e.message = "Forbidden: Authenticated IAM principal does not seem authorized to make API request. Verify 'Cloud SQL Admin API' is enabled within your GCP project and 'Cloud SQL Client' role has been granted to IAM principal."
262
154
  raise
263
155
 
264
- except Exception:
156
+ except Exception as e:
265
157
  logger.debug(
266
- f"['{self._instance_connection_string}']: Error occurred during _perform_refresh."
158
+ f"['{self._instance_connection_string}']: Connection info "
159
+ f"refresh operation failed: {str(e)}"
267
160
  )
268
161
  raise
269
162
 
270
163
  finally:
271
164
  self._refresh_in_progress.clear()
272
-
273
- return ConnectionInfo(
274
- ephemeral_cert,
275
- metadata["database_version"],
276
- metadata["ip_addresses"],
277
- priv_key,
278
- metadata["server_ca_cert"],
279
- expiration,
280
- self._enable_iam_auth,
281
- )
165
+ return connection_info
282
166
 
283
167
  def _schedule_refresh(self, delay: int) -> asyncio.Task:
284
168
  """
@@ -298,14 +182,19 @@ class RefreshAheadCache:
298
182
  """
299
183
  refresh_task: asyncio.Task
300
184
  try:
301
- logger.debug(f"['{self._instance_connection_string}']: Entering sleep")
302
185
  if delay > 0:
303
186
  await asyncio.sleep(delay)
304
187
  refresh_task = asyncio.create_task(self._perform_refresh())
305
188
  refresh_data = await refresh_task
189
+ # check that refresh is valid
190
+ if not await _is_valid(refresh_task):
191
+ raise RefreshNotValidError(
192
+ f"['{self._instance_connection_string}']: Invalid refresh operation. Certficate appears to be expired."
193
+ )
306
194
  except asyncio.CancelledError:
307
195
  logger.debug(
308
- f"['{self._instance_connection_string}']: Schedule refresh task cancelled."
196
+ f"['{self._instance_connection_string}']: Scheduled refresh"
197
+ " operation cancelled"
309
198
  )
310
199
  raise
311
200
  # bad refresh attempt
@@ -327,6 +216,12 @@ class RefreshAheadCache:
327
216
  self._current = refresh_task
328
217
  # calculate refresh delay based on certificate expiration
329
218
  delay = _seconds_until_refresh(refresh_data.expiration)
219
+ logger.debug(
220
+ f"['{self._instance_connection_string}']: Connection info refresh"
221
+ " operation scheduled for "
222
+ f"{(datetime.now(timezone.utc) + timedelta(seconds=delay)).isoformat(timespec='seconds')} "
223
+ f"(now + {timedelta(seconds=delay)})"
224
+ )
330
225
  self._next = self._schedule_refresh(delay)
331
226
 
332
227
  return refresh_data
@@ -335,47 +230,22 @@ class RefreshAheadCache:
335
230
  scheduled_task = asyncio.create_task(_refresh_task(self, delay))
336
231
  return scheduled_task
337
232
 
338
- async def connect_info(
339
- self,
340
- ip_type: IPTypes,
341
- ) -> Tuple[ConnectionInfo, str]:
342
- """Retrieve instance metadata and ip address required
343
- for making connection to Cloud SQL instance.
344
-
345
- Args:
346
- ip_type (IPTypes): Enum specifying type of IP address to lookup and
347
- use for connection.
348
-
349
- Returns:
350
- A tuple with the first item being the ConnectionInfo instance for
351
- establishing the connection, and the second item being the IP
352
- address of the Cloud SQL instance matching the specified IP type.
233
+ async def connect_info(self) -> ConnectionInfo:
234
+ """Retrieves ConnectionInfo instance for establishing a secure
235
+ connection to the Cloud SQL instance.
353
236
  """
354
- logger.debug(
355
- f"['{self._instance_connection_string}']: Entered connect_info method"
356
- )
357
-
358
- instance_data: ConnectionInfo
359
-
360
- instance_data = await self._current
361
- ip_address: str = instance_data.get_preferred_ip(ip_type)
362
- return instance_data, ip_address
237
+ return await self._current
363
238
 
364
239
  async def close(self) -> None:
365
- """Cleanup function to make sure ClientSession is closed and tasks have
366
- finished to have a graceful exit.
240
+ """Cleanup function to make sure tasks have finished to have a
241
+ graceful exit.
367
242
  """
368
243
  logger.debug(
369
- f"['{self._instance_connection_string}']: Waiting for _current to be cancelled"
244
+ f"['{self._instance_connection_string}']: Canceling connection info "
245
+ "refresh operation tasks"
370
246
  )
371
247
  self._current.cancel()
372
- logger.debug(
373
- f"['{self._instance_connection_string}']: Waiting for _next to be cancelled"
374
- )
375
248
  self._next.cancel()
376
- logger.debug(
377
- f"['{self._instance_connection_string}']: Waiting for _client_session to close"
378
- )
379
249
  # gracefully wait for tasks to cancel
380
250
  tasks = asyncio.gather(self._current, self._next, return_exceptions=True)
381
251
  await asyncio.wait_for(tasks, timeout=2.0)