cloud-sql-python-connector 1.9.2__py2.py3-none-any.whl → 1.10.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-sql-python-connector
3
- Version: 1.9.2
3
+ Version: 1.10.0
4
4
  Summary: The Cloud SQL Python Connector is a library that can be used alongside a database driver to allow users with sufficient permissions to connect to a Cloud SQL database without having to manually allowlist IPs or manage SSL certificates.
5
5
  Home-page: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector
6
6
  Author: Google LLC
@@ -207,9 +207,10 @@ To close the `Connector` object's background resources, call its `close()` metho
207
207
  connector.close()
208
208
  ```
209
209
 
210
- **Note**: For more examples of using SQLAlchemy to manage connection pooling with the connector, please see [Cloud SQL SQLAlchemy Samples](https://cloud.google.com/sql/docs/postgres/connect-connectors#python_1).
211
-
212
- **Note for SQL Server users**: If your SQL Server instance requires SSL, you need to download the CA certificate for your instance and include `cafile={path to downloaded certificate}` and `validate_host=False`. This is a workaround for a [known issue](https://issuetracker.google.com/184867147).
210
+ > [!NOTE]
211
+ >
212
+ > For more examples of using SQLAlchemy to manage connection pooling with the connector,
213
+ > please see [Cloud SQL SQLAlchemy Samples](https://cloud.google.com/sql/docs/postgres/connect-connectors#python_1).
213
214
 
214
215
  ### Configuring the Connector
215
216
 
@@ -218,14 +219,15 @@ defaults for each connection to make, you can initialize a
218
219
  `Connector` object as follows:
219
220
 
220
221
  ```python
221
- from google.cloud.sql.connector import Connector, IPTypes
222
+ from google.cloud.sql.connector import Connector
222
223
 
223
224
  # Note: all parameters below are optional
224
225
  connector = Connector(
225
226
  ip_type="public", # can also be "private" or "psc"
226
227
  enable_iam_auth=False,
227
228
  timeout=30,
228
- credentials=custom_creds # google.auth.credentials.Credentials
229
+ credentials=custom_creds, # google.auth.credentials.Credentials
230
+ refresh_strategy="lazy", # can be "lazy" or "background"
229
231
  )
230
232
  ```
231
233
 
@@ -287,6 +289,21 @@ with Connector() as connector:
287
289
  print(row)
288
290
  ```
289
291
 
292
+ ### Configuring a Lazy Refresh (Cloud Run, Cloud Functions etc.)
293
+
294
+ The Connector's `refresh_strategy` argument can be set to `"lazy"` to configure
295
+ the Python Connector to retrieve connection info lazily and as-needed.
296
+ Otherwise, a background refresh cycle runs to retrive the connection info
297
+ periodically. This setting is useful in environments where the CPU may be
298
+ throttled outside of a request context, e.g., Cloud Run, Cloud Functions, etc.
299
+
300
+ To set the refresh strategy, set the `refresh_strategy` keyword argument when
301
+ initializing a `Connector`:
302
+
303
+ ```python
304
+ connector = Connector(refresh_strategy="lazy")
305
+ ```
306
+
290
307
  ### Specifying IP Address Type
291
308
 
292
309
  The Cloud SQL Python Connector can be used to connect to Cloud SQL instances
@@ -309,9 +326,11 @@ conn = connector.connect(
309
326
  )
310
327
  ```
311
328
 
312
- Note: If specifying Private IP or Private Service Connect, your application must be
313
- attached to the proper VPC network to connect to your Cloud SQL instance. For most
314
- applications this will require the use of a [VPC Connector][vpc-connector].
329
+ > [!IMPORTANT]
330
+ >
331
+ > If specifying Private IP or Private Service Connect (PSC), your application must be
332
+ > attached to the proper VPC network to connect to your Cloud SQL instance. For most
333
+ > applications this will require the use of a [VPC Connector][vpc-connector].
315
334
 
316
335
  [psc]: https://cloud.google.com/vpc/docs/private-service-connect
317
336
  [vpc-connector]: https://cloud.google.com/vpc/docs/configure-serverless-vpc-access#create-connector
@@ -340,9 +359,20 @@ conn = connector.connect(
340
359
  )
341
360
  ```
342
361
 
343
- ### SQL Server Active Directory Authentication
362
+ ### SQL Server (MSSQL)
363
+
364
+ > [!IMPORTANT]
365
+ >
366
+ > If your SQL Server instance is set to [enforce SSL connections](https://cloud.google.com/sql/docs/sqlserver/configure-ssl-instance#enforcing-ssl),
367
+ > you need to download the CA certificate for your instance and include `cafile={path to downloaded certificate}`
368
+ > and `validate_host=False`. This is a workaround for a [known issue](https://issuetracker.google.com/184867147).
369
+
370
+ #### Active Directory Authentication
344
371
 
345
- Active Directory authentication for SQL Server instances is currently only supported on Windows. First, make sure to follow [these steps](https://cloud.google.com/blog/topics/developers-practitioners/creating-sql-server-instance-integrated-active-directory-using-google-cloud-sql) to set up a Managed AD domain and join your Cloud SQL instance to the domain. [See here for more info on Cloud SQL Active Directory integration](https://cloud.google.com/sql/docs/sqlserver/ad).
372
+ Active Directory authentication for SQL Server instances is currently only supported on Windows.
373
+ First, make sure to follow [these steps](https://cloud.google.com/blog/topics/developers-practitioners/creating-sql-server-instance-integrated-active-directory-using-google-cloud-sql)
374
+ to set up a Managed AD domain and join your Cloud SQL instance to the domain.
375
+ [See here for more info on Cloud SQL Active Directory integration](https://cloud.google.com/sql/docs/sqlserver/ad).
346
376
 
347
377
  Once you have followed the steps linked above, you can run the following code to return a connection object:
348
378
 
@@ -375,6 +405,14 @@ The Python Connector can be used alongside popular Python web frameworks such
375
405
  as Flask, FastAPI, etc, to integrate Cloud SQL databases within your
376
406
  web applications.
377
407
 
408
+ > [!NOTE]
409
+ >
410
+ > For serverless environments such as Cloud Functions, Cloud Run, etc, it may be
411
+ > beneficial to initialize the `Connector` with the lazy refresh strategy.
412
+ > i.e. `Connector(refresh_strategy="lazy")`
413
+ >
414
+ > See [Configuring a Lazy Refresh](#configuring-a-lazy-refresh-cloud-run-cloud-functions-etc)
415
+
378
416
  #### Flask-SQLAlchemy
379
417
 
380
418
  [Flask-SQLAlchemy](https://flask-sqlalchemy.palletsprojects.com/en/2.x/)
@@ -555,8 +593,10 @@ An alternative to using the `create_async_connector` function is initializing
555
593
  a `Connector` as an async context manager, removing the need for explicit
556
594
  calls to `connector.close_async()` to cleanup resources.
557
595
 
558
- **Note:** This alternative requires that the running event loop be
559
- passed in as the `loop` argument to `Connector()`.
596
+ > [!NOTE]
597
+ >
598
+ > This alternative requires that the running event loop be
599
+ > passed in as the `loop` argument to `Connector()`.
560
600
 
561
601
  ```python
562
602
  import asyncio
@@ -603,6 +643,28 @@ async def main():
603
643
  await pool.dispose()
604
644
  ```
605
645
 
646
+ ### Debug Logging
647
+
648
+ The Cloud SQL Python Connector uses the standard [Python logging module][python-logging]
649
+ for debug logging support.
650
+
651
+ Add the below code to your application to enable debug logging with the Cloud SQL
652
+ Python Connector:
653
+
654
+ ```python
655
+ import logging
656
+
657
+ logging.basicConfig(format="%(asctime)s [%(levelname)s]: %(message)s")
658
+ logger = logging.getLogger(name="google.cloud.sql.connector")
659
+ logger.setLevel(logging.DEBUG)
660
+ ```
661
+
662
+ For more details on configuring logging, please refer to the
663
+ [Python logging docs][configure-logging].
664
+
665
+ [python-logging]: https://docs.python.org/3/library/logging.html
666
+ [configure-logging]: https://docs.python.org/3/howto/logging.html#configuring-logging
667
+
606
668
  ## Support policy
607
669
 
608
670
  ### Major version lifecycle
@@ -0,0 +1,22 @@
1
+ google/cloud/sql/connector/__init__.py,sha256=x8ui7u3gMlMI-LLJkKTtUU9yvzZvQkTJ-yzr2FH4VTU,983
2
+ google/cloud/sql/connector/asyncpg.py,sha256=c4lAlvEgGf_eBq0G8MVNPRAJQQvo6DcwybiXDW36qkU,1838
3
+ google/cloud/sql/connector/client.py,sha256=VvfDgJVZ1V2X54m1P6X3GCL7HHUoSvvAzEl1PKDDQ1I,11119
4
+ google/cloud/sql/connector/connection_info.py,sha256=esasEjExqkHt3V9tgx7jqeGgSPJMCuTZQmSMEFqAvWY,4309
5
+ google/cloud/sql/connector/connector.py,sha256=PtRRQ0n-OAM4d3bDmLZErPRikSudDFmd9xY9SbCVfcc,21761
6
+ google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
7
+ google/cloud/sql/connector/exceptions.py,sha256=wRK3gKXomzi9FUs9qju3WN1FDUTuwozzZEkucTe4wKI,1860
8
+ google/cloud/sql/connector/instance.py,sha256=v3x936OddIaqcGD8aIRi-bzcpOncTExwZTKu_4MpzBo,10137
9
+ google/cloud/sql/connector/lazy.py,sha256=og4UPiJxeuLgN7tyqrFAnQF3895Yqdf6kkd4HZSzVJA,5093
10
+ google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
11
+ google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
13
+ google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
14
+ google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
15
+ google/cloud/sql/connector/refresh_utils.py,sha256=P2ye3qDymdK1iOlGCXby4VIaqwvHdc9GEmuUsMcufxY,3680
16
+ google/cloud/sql/connector/utils.py,sha256=FQs7RgoViJLIFgJIynVT-_DwAVWnpyXIKu5WZ4m10Ts,3448
17
+ google/cloud/sql/connector/version.py,sha256=4BE16mU-VOqsOw52ua4QjeLiTwW8fwyo0zXrk3YHwF0,598
18
+ cloud_sql_python_connector-1.10.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
19
+ cloud_sql_python_connector-1.10.0.dist-info/METADATA,sha256=M8VvudvgqHr2ETZrjHR_G7qsOOw-yK8FlzmT4VwZ93w,25925
20
+ cloud_sql_python_connector-1.10.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
21
+ cloud_sql_python_connector-1.10.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
22
+ cloud_sql_python_connector-1.10.0.dist-info/RECORD,,
@@ -16,7 +16,14 @@ limitations under the License.
16
16
 
17
17
  from google.cloud.sql.connector.connector import Connector
18
18
  from google.cloud.sql.connector.connector import create_async_connector
19
- from google.cloud.sql.connector.instance import IPTypes
19
+ from google.cloud.sql.connector.enums import IPTypes
20
+ from google.cloud.sql.connector.enums import RefreshStrategy
20
21
  from google.cloud.sql.connector.version import __version__
21
22
 
22
- __all__ = ["__version__", "create_async_connector", "Connector", "IPTypes"]
23
+ __all__ = [
24
+ "__version__",
25
+ "create_async_connector",
26
+ "Connector",
27
+ "IPTypes",
28
+ "RefreshStrategy",
29
+ ]
@@ -14,6 +14,7 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import asyncio
17
18
  import datetime
18
19
  import logging
19
20
  from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
@@ -21,8 +22,11 @@ from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
21
22
  import aiohttp
22
23
  from cryptography.hazmat.backends import default_backend
23
24
  from cryptography.x509 import load_pem_x509_certificate
24
- import google.auth.transport.requests
25
+ from google.auth.credentials import TokenState
26
+ from google.auth.transport import requests
25
27
 
28
+ from google.cloud.sql.connector.connection_info import ConnectionInfo
29
+ from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
26
30
  from google.cloud.sql.connector.refresh_utils import _downscope_credentials
27
31
  from google.cloud.sql.connector.version import __version__ as version
28
32
 
@@ -113,9 +117,6 @@ class CloudSQLClient:
113
117
  addresses and their type and a string representing the
114
118
  certificate authority.
115
119
  """
116
- if not self._credentials.valid:
117
- request = google.auth.transport.requests.Request()
118
- self._credentials.refresh(request)
119
120
 
120
121
  headers = {
121
122
  "Authorization": f"Bearer {self._credentials.token}",
@@ -123,8 +124,6 @@ class CloudSQLClient:
123
124
 
124
125
  url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/projects/{project}/instances/{instance}/connectSettings"
125
126
 
126
- logger.debug(f"['{instance}']: Requesting metadata")
127
-
128
127
  resp = await self._client.get(url, headers=headers, raise_for_status=True)
129
128
  ret_dict = await resp.json()
130
129
 
@@ -174,13 +173,6 @@ class CloudSQLClient:
174
173
  :returns: An ephemeral certificate from the Cloud SQL instance that allows
175
174
  authorized connections to the instance.
176
175
  """
177
-
178
- logger.debug(f"['{instance}']: Requesting ephemeral certificate")
179
-
180
- if not self._credentials.valid:
181
- request = google.auth.transport.requests.Request()
182
- self._credentials.refresh(request)
183
-
184
176
  headers = {
185
177
  "Authorization": f"Bearer {self._credentials.token}",
186
178
  }
@@ -220,6 +212,84 @@ class CloudSQLClient:
220
212
  expiration = token_expiration
221
213
  return ephemeral_cert, expiration
222
214
 
215
+ async def get_connection_info(
216
+ self,
217
+ project: str,
218
+ region: str,
219
+ instance: str,
220
+ keys: asyncio.Future,
221
+ enable_iam_auth: bool,
222
+ ) -> ConnectionInfo:
223
+ """Immediately performs a full refresh operation using the Cloud SQL
224
+ Admin API.
225
+
226
+ Args:
227
+ project (str): The name of the project the Cloud SQL instance is
228
+ located in.
229
+ region (str): The region the Cloud SQL instance is located in.
230
+ instance (str): Name of the Cloud SQL instance.
231
+ keys (asyncio.Future): A future to the client's public-private key
232
+ pair.
233
+ enable_iam_auth (bool): Whether an automatic IAM database
234
+ authentication connection is being requested (Postgres and MySQL).
235
+
236
+ Returns:
237
+ ConnectionInfo: All the information required to connect securely to
238
+ the Cloud SQL instance.
239
+ Raises:
240
+ AutoIAMAuthNotSupported: Database engine does not support automatic
241
+ IAM authentication.
242
+ """
243
+ priv_key, pub_key = await keys
244
+ # before making Cloud SQL Admin API calls, refresh creds if required
245
+ if not self._credentials.token_state == TokenState.FRESH:
246
+ self._credentials.refresh(requests.Request())
247
+
248
+ metadata_task = asyncio.create_task(
249
+ self._get_metadata(
250
+ project,
251
+ region,
252
+ instance,
253
+ )
254
+ )
255
+
256
+ ephemeral_task = asyncio.create_task(
257
+ self._get_ephemeral(
258
+ project,
259
+ instance,
260
+ pub_key,
261
+ enable_iam_auth,
262
+ )
263
+ )
264
+ try:
265
+ metadata = await metadata_task
266
+ # check if automatic IAM database authn is supported for database engine
267
+ if enable_iam_auth and not metadata["database_version"].startswith(
268
+ ("POSTGRES", "MYSQL")
269
+ ):
270
+ raise AutoIAMAuthNotSupported(
271
+ f"'{metadata['database_version']}' does not support "
272
+ "automatic IAM authentication. It is only supported with "
273
+ "Cloud SQL Postgres or MySQL instances."
274
+ )
275
+ except Exception:
276
+ # cancel ephemeral cert task if exception occurs before it is awaited
277
+ ephemeral_task.cancel()
278
+ raise
279
+
280
+ ephemeral_cert, expiration = await ephemeral_task
281
+
282
+ return ConnectionInfo(
283
+ ephemeral_cert,
284
+ metadata["server_ca_cert"],
285
+ priv_key,
286
+ metadata["ip_addresses"],
287
+ metadata["database_version"],
288
+ expiration,
289
+ )
290
+
223
291
  async def close(self) -> None:
224
292
  """Close CloudSQLClient gracefully."""
293
+ logger.debug("Waiting for Connector's http client to close")
225
294
  await self._client.close()
295
+ logger.debug("Closed Connector's http client")
@@ -0,0 +1,105 @@
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 dataclasses import dataclass
18
+ import logging
19
+ import ssl
20
+ from tempfile import TemporaryDirectory
21
+ from typing import Any, Dict, Optional, TYPE_CHECKING
22
+
23
+ from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
24
+ from google.cloud.sql.connector.exceptions import TLSVersionError
25
+ from google.cloud.sql.connector.utils import write_to_file
26
+
27
+ if TYPE_CHECKING:
28
+ import datetime
29
+
30
+ from google.cloud.sql.connector.enums import IPTypes
31
+
32
+ logger = logging.getLogger(name=__name__)
33
+
34
+
35
+ @dataclass
36
+ class ConnectionInfo:
37
+ """Contains all necessary information to connect securely to the
38
+ server-side Proxy running on a Cloud SQL instance."""
39
+
40
+ client_cert: str
41
+ server_ca_cert: str
42
+ private_key: bytes
43
+ ip_addrs: Dict[str, Any]
44
+ database_version: str
45
+ expiration: datetime.datetime
46
+ context: Optional[ssl.SSLContext] = None
47
+
48
+ def create_ssl_context(self, enable_iam_auth: bool = False) -> ssl.SSLContext:
49
+ """Constructs a SSL/TLS context for the given connection info.
50
+
51
+ Cache the SSL context to ensure we don't read from disk repeatedly when
52
+ configuring a secure connection.
53
+ """
54
+ # if SSL context is cached, use it
55
+ if self.context is not None:
56
+ return self.context
57
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
58
+
59
+ # update ssl.PROTOCOL_TLS_CLIENT default
60
+ context.check_hostname = False
61
+
62
+ # TODO: remove if/else when Python 3.10 is min version. PEP 644 has been
63
+ # implemented. The ssl module requires OpenSSL 1.1.1 or newer.
64
+ # verify OpenSSL version supports TLSv1.3
65
+ if ssl.HAS_TLSv1_3:
66
+ # force TLSv1.3 if supported by client
67
+ context.minimum_version = ssl.TLSVersion.TLSv1_3
68
+ # fallback to TLSv1.2 for older versions of OpenSSL
69
+ else:
70
+ if enable_iam_auth:
71
+ raise TLSVersionError(
72
+ f"Your current version of OpenSSL ({ssl.OPENSSL_VERSION}) does not "
73
+ "support TLSv1.3, which is required to use IAM Authentication.\n"
74
+ "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
75
+ )
76
+ logger.warning(
77
+ "TLSv1.3 is not supported with your version of OpenSSL "
78
+ f"({ssl.OPENSSL_VERSION}), falling back to TLSv1.2\n"
79
+ "Upgrade your OpenSSL version to 1.1.1 for TLSv1.3 support."
80
+ )
81
+ context.minimum_version = ssl.TLSVersion.TLSv1_2
82
+
83
+ # tmpdir and its contents are automatically deleted after the CA cert
84
+ # and ephemeral cert are loaded into the SSLcontext. The values
85
+ # need to be written to files in order to be loaded by the SSLContext
86
+ with TemporaryDirectory() as tmpdir:
87
+ ca_filename, cert_filename, key_filename = write_to_file(
88
+ tmpdir, self.server_ca_cert, self.client_cert, self.private_key
89
+ )
90
+ context.load_cert_chain(cert_filename, keyfile=key_filename)
91
+ context.load_verify_locations(cafile=ca_filename)
92
+ # set class attribute to cache context for subsequent calls
93
+ self.context = context
94
+ return context
95
+
96
+ def get_preferred_ip(self, ip_type: IPTypes) -> str:
97
+ """Returns the first IP address for the instance, according to the preference
98
+ supplied by ip_type. If no IP addressess with the given preference are found,
99
+ an error is raised."""
100
+ if ip_type.value in self.ip_addrs:
101
+ return self.ip_addrs[ip_type.value]
102
+ raise CloudSQLIPTypeError(
103
+ "Cloud SQL instance does not have any IP addresses matching "
104
+ f"preference: {ip_type.value})"
105
+ )
@@ -22,7 +22,7 @@ import logging
22
22
  import socket
23
23
  from threading import Thread
24
24
  from types import TracebackType
25
- from typing import Any, Dict, Optional, Type
25
+ from typing import Any, Dict, Optional, Type, Union
26
26
 
27
27
  import google.auth
28
28
  from google.auth.credentials import Credentials
@@ -30,10 +30,13 @@ from google.auth.credentials import with_scopes_if_required
30
30
 
31
31
  import google.cloud.sql.connector.asyncpg as asyncpg
32
32
  from google.cloud.sql.connector.client import CloudSQLClient
33
+ from google.cloud.sql.connector.enums import DriverMapping
34
+ from google.cloud.sql.connector.enums import IPTypes
35
+ from google.cloud.sql.connector.enums import RefreshStrategy
33
36
  from google.cloud.sql.connector.exceptions import ConnectorLoopError
34
37
  from google.cloud.sql.connector.exceptions import DnsNameResolutionError
35
- from google.cloud.sql.connector.instance import IPTypes
36
38
  from google.cloud.sql.connector.instance import RefreshAheadCache
39
+ from google.cloud.sql.connector.lazy import LazyRefreshCache
37
40
  import google.cloud.sql.connector.pg8000 as pg8000
38
41
  import google.cloud.sql.connector.pymysql as pymysql
39
42
  import google.cloud.sql.connector.pytds as pytds
@@ -62,6 +65,7 @@ class Connector:
62
65
  sqladmin_api_endpoint: Optional[str] = None,
63
66
  user_agent: Optional[str] = None,
64
67
  universe_domain: Optional[str] = None,
68
+ refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
65
69
  ) -> None:
66
70
  """Initializes a Connector instance.
67
71
 
@@ -98,22 +102,39 @@ class Connector:
98
102
  universe_domain (str): The universe domain for Cloud SQL API calls.
99
103
  Default: "googleapis.com".
100
104
 
105
+ refresh_strategy (str | RefreshStrategy): The default refresh strategy
106
+ used to refresh SSL/TLS cert and instance metadata. Can be one
107
+ of the following: RefreshStrategy.LAZY ("LAZY") or
108
+ RefreshStrategy.BACKGROUND ("BACKGROUND").
109
+ Default: RefreshStrategy.BACKGROUND
101
110
  """
111
+ # if refresh_strategy is str, convert to RefreshStrategy enum
112
+ if isinstance(refresh_strategy, str):
113
+ refresh_strategy = RefreshStrategy._from_str(refresh_strategy)
114
+ self._refresh_strategy = refresh_strategy
102
115
  # if event loop is given, use for background tasks
103
116
  if loop:
104
117
  self._loop: asyncio.AbstractEventLoop = loop
105
118
  self._thread: Optional[Thread] = None
106
- self._keys: asyncio.Future = loop.create_task(generate_keys())
119
+ # if lazy refresh is specified we should lazy init keys
120
+ if self._refresh_strategy == RefreshStrategy.LAZY:
121
+ self._keys: Optional[asyncio.Future] = None
122
+ else:
123
+ self._keys = loop.create_task(generate_keys())
107
124
  # if no event loop is given, spin up new loop in background thread
108
125
  else:
109
126
  self._loop = asyncio.new_event_loop()
110
127
  self._thread = Thread(target=self._loop.run_forever, daemon=True)
111
128
  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] = {}
129
+ # if lazy refresh is specified we should lazy init keys
130
+ if self._refresh_strategy == RefreshStrategy.LAZY:
131
+ self._keys = None
132
+ else:
133
+ self._keys = asyncio.wrap_future(
134
+ asyncio.run_coroutine_threadsafe(generate_keys(), self._loop),
135
+ loop=self._loop,
136
+ )
137
+ self._cache: Dict[str, Union[RefreshAheadCache, LazyRefreshCache]] = {}
117
138
  self._client: Optional[CloudSQLClient] = None
118
139
 
119
140
  # initialize credentials
@@ -245,6 +266,8 @@ class Connector:
245
266
  DnsNameResolutionError: Could not resolve PSC IP address from DNS
246
267
  host name.
247
268
  """
269
+ if self._keys is None:
270
+ self._keys = asyncio.create_task(generate_keys())
248
271
  if self._client is None:
249
272
  # lazy init client as it has to be initialized in async context
250
273
  self._client = CloudSQLClient(
@@ -265,11 +288,30 @@ class Connector:
265
288
  "connector.Connector object."
266
289
  )
267
290
  else:
268
- cache = RefreshAheadCache(
269
- instance_connection_string,
270
- self._client,
271
- self._keys,
272
- enable_iam_auth,
291
+ if self._refresh_strategy == RefreshStrategy.LAZY:
292
+ logger.debug(
293
+ f"['{instance_connection_string}']: Refresh strategy is set"
294
+ " to lazy refresh"
295
+ )
296
+ cache = LazyRefreshCache(
297
+ instance_connection_string,
298
+ self._client,
299
+ self._keys,
300
+ enable_iam_auth,
301
+ )
302
+ else:
303
+ logger.debug(
304
+ f"['{instance_connection_string}']: Refresh strategy is set"
305
+ " to backgound refresh"
306
+ )
307
+ cache = RefreshAheadCache(
308
+ instance_connection_string,
309
+ self._client,
310
+ self._keys,
311
+ enable_iam_auth,
312
+ )
313
+ logger.debug(
314
+ f"['{instance_connection_string}']: Connection info added to cache"
273
315
  )
274
316
  self._cache[instance_connection_string] = cache
275
317
 
@@ -300,7 +342,10 @@ class Connector:
300
342
 
301
343
  # attempt to make connection to Cloud SQL instance
302
344
  try:
303
- instance_data, ip_address = await cache.connect_info(ip_type)
345
+ conn_info = await cache.connect_info()
346
+ # validate driver matches intended database engine
347
+ DriverMapping.validate_engine(driver, conn_info.database_version)
348
+ ip_address = conn_info.get_preferred_ip(ip_type)
304
349
  # resolve DNS name into IP address for PSC
305
350
  if ip_type.value == "PSC":
306
351
  addr_info = await self._loop.getaddrinfo(
@@ -316,11 +361,13 @@ class Connector:
316
361
  raise DnsNameResolutionError(
317
362
  f"['{instance_connection_string}']: DNS name could not be resolved into IP address"
318
363
  ) from e
319
-
364
+ logger.debug(
365
+ f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
366
+ )
320
367
  # format `user` param for automatic IAM database authn
321
368
  if enable_iam_auth:
322
369
  formatted_user = format_database_user(
323
- instance_data.database_version, kwargs["user"]
370
+ conn_info.database_version, kwargs["user"]
324
371
  )
325
372
  if formatted_user != kwargs["user"]:
326
373
  logger.debug(
@@ -330,10 +377,17 @@ class Connector:
330
377
 
331
378
  # async drivers are unblocking and can be awaited directly
332
379
  if driver in ASYNC_DRIVERS:
333
- return await connector(ip_address, instance_data.context, **kwargs)
380
+ return await connector(
381
+ ip_address,
382
+ conn_info.create_ssl_context(enable_iam_auth),
383
+ **kwargs,
384
+ )
334
385
  # synchronous drivers are blocking and run using executor
335
386
  connect_partial = partial(
336
- connector, ip_address, instance_data.context, **kwargs
387
+ connector,
388
+ ip_address,
389
+ conn_info.create_ssl_context(enable_iam_auth),
390
+ **kwargs,
337
391
  )
338
392
  return await self._loop.run_in_executor(None, connect_partial)
339
393
 
@@ -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
+ )
@@ -62,3 +62,18 @@ class DnsNameResolutionError(Exception):
62
62
  Exception to be raised when the DnsName of a PSC connection to a
63
63
  Cloud SQL instance can not be resolved to a proper IP address.
64
64
  """
65
+
66
+
67
+ class RefreshNotValidError(Exception):
68
+ """
69
+ Exception to be raised when the task returned from refresh is not valid.
70
+ """
71
+
72
+ pass
73
+
74
+
75
+ class IncompatibleDriverError(Exception):
76
+ """
77
+ Exception to be raised when the database driver given is for the wrong
78
+ database engine. (i.e. asyncpg for a MySQL database)
79
+ """
@@ -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
153
  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."
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)
@@ -0,0 +1,132 @@
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
+ import asyncio
16
+ from datetime import datetime
17
+ from datetime import timedelta
18
+ from datetime import timezone
19
+ import logging
20
+ from typing import Optional
21
+
22
+ from google.cloud.sql.connector.client import CloudSQLClient
23
+ from google.cloud.sql.connector.connection_info import ConnectionInfo
24
+ from google.cloud.sql.connector.instance import _parse_instance_connection_name
25
+ from google.cloud.sql.connector.refresh_utils import _refresh_buffer
26
+
27
+ logger = logging.getLogger(name=__name__)
28
+
29
+
30
+ class LazyRefreshCache:
31
+ """Cache that refreshes connection info when a caller requests a connection.
32
+
33
+ Only refreshes the cache when a new connection is requested and the current
34
+ certificate is close to or already expired.
35
+
36
+ This is the recommended option for serverless environments.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ instance_connection_string: str,
42
+ client: CloudSQLClient,
43
+ keys: asyncio.Future,
44
+ enable_iam_auth: bool = False,
45
+ ) -> None:
46
+ """Initializes a LazyRefreshCache instance.
47
+
48
+ Args:
49
+ instance_connection_string (str): The Cloud SQL Instance's
50
+ connection string (also known as an instance connection name).
51
+ client (CloudSQLClient): The Cloud SQL Client instance.
52
+ keys (asyncio.Future): A future to the client's public-private key
53
+ pair.
54
+ enable_iam_auth (bool): Enables automatic IAM database authentication
55
+ (Postgres and MySQL) as the default authentication method for all
56
+ connections.
57
+ """
58
+ # validate and parse instance connection name
59
+ self._project, self._region, self._instance = _parse_instance_connection_name(
60
+ instance_connection_string
61
+ )
62
+ self._instance_connection_string = instance_connection_string
63
+
64
+ self._enable_iam_auth = enable_iam_auth
65
+ self._keys = keys
66
+ self._client = client
67
+ self._lock = asyncio.Lock()
68
+ self._cached: Optional[ConnectionInfo] = None
69
+ self._needs_refresh = False
70
+
71
+ async def force_refresh(self) -> None:
72
+ """
73
+ Invalidates the cache and configures the next call to
74
+ connect_info() to retrieve a fresh ConnectionInfo instance.
75
+ """
76
+ async with self._lock:
77
+ self._needs_refresh = True
78
+
79
+ async def connect_info(self) -> ConnectionInfo:
80
+ """Retrieves ConnectionInfo instance for establishing a secure
81
+ connection to the Cloud SQL instance.
82
+ """
83
+ async with self._lock:
84
+ # If connection info is cached, check expiration.
85
+ # Pad expiration with a buffer to give the client plenty of time to
86
+ # establish a connection to the server with the certificate.
87
+ if (
88
+ self._cached
89
+ and not self._needs_refresh
90
+ and datetime.now(timezone.utc)
91
+ < (self._cached.expiration - timedelta(seconds=_refresh_buffer))
92
+ ):
93
+ logger.debug(
94
+ f"['{self._instance_connection_string}']: Connection info "
95
+ "is still valid, using cached info"
96
+ )
97
+ return self._cached
98
+ logger.debug(
99
+ f"['{self._instance_connection_string}']: Connection info "
100
+ "refresh operation started"
101
+ )
102
+ try:
103
+ conn_info = await self._client.get_connection_info(
104
+ self._project,
105
+ self._region,
106
+ self._instance,
107
+ self._keys,
108
+ self._enable_iam_auth,
109
+ )
110
+ except Exception as e:
111
+ logger.debug(
112
+ f"['{self._instance_connection_string}']: Connection info "
113
+ f"refresh operation failed: {str(e)}"
114
+ )
115
+ raise
116
+ logger.debug(
117
+ f"['{self._instance_connection_string}']: Connection info "
118
+ "refresh operation completed successfully"
119
+ )
120
+ logger.debug(
121
+ f"['{self._instance_connection_string}']: Current certificate "
122
+ f"expiration = {str(conn_info.expiration)}"
123
+ )
124
+ self._cached = conn_info
125
+ self._needs_refresh = False
126
+ return conn_info
127
+
128
+ async def close(self) -> None:
129
+ """Close is a no-op and provided purely for a consistent interface with
130
+ other cache types.
131
+ """
132
+ pass
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "1.9.2"
15
+ __version__ = "1.10.0"
@@ -1,19 +0,0 @@
1
- google/cloud/sql/connector/__init__.py,sha256=Z3AkPOQwjNF1ogo2DR18GN-CnYvI4MTmhsCe8hURKUo,883
2
- google/cloud/sql/connector/asyncpg.py,sha256=c4lAlvEgGf_eBq0G8MVNPRAJQQvo6DcwybiXDW36qkU,1838
3
- google/cloud/sql/connector/client.py,sha256=NZZnPqE_f13S04tpRCs0UGF-RxR4EL-_vH7ebQBXTaA,8494
4
- google/cloud/sql/connector/connector.py,sha256=q1P0lqDjU5O5U8FQjNa9punrdExwcjvVz7SodbaVmSw,19147
5
- google/cloud/sql/connector/exceptions.py,sha256=rTBtbCoiZ3U5IFVsyXPw5baHbCODixx2hPDQeXUzgzs,1524
6
- google/cloud/sql/connector/instance.py,sha256=Oed1kFTaVolce74DzsSRbooj6mUthwZtt75OamTfOQs,14966
7
- google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
8
- google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
10
- google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
11
- google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
12
- google/cloud/sql/connector/refresh_utils.py,sha256=P2ye3qDymdK1iOlGCXby4VIaqwvHdc9GEmuUsMcufxY,3680
13
- google/cloud/sql/connector/utils.py,sha256=FQs7RgoViJLIFgJIynVT-_DwAVWnpyXIKu5WZ4m10Ts,3448
14
- google/cloud/sql/connector/version.py,sha256=x6crtIS26FuBXZ_fZ636i8UD16NlwWdlYJf5SEIqLIc,597
15
- cloud_sql_python_connector-1.9.2.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
16
- cloud_sql_python_connector-1.9.2.dist-info/METADATA,sha256=5ufJWE8PP34QHa8_1oh1vPJmzaOY-Byqfso7o_BbCnw,24113
17
- cloud_sql_python_connector-1.9.2.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
18
- cloud_sql_python_connector-1.9.2.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
19
- cloud_sql_python_connector-1.9.2.dist-info/RECORD,,