cloud-sql-python-connector 1.14.0__tar.gz → 1.15.0__tar.gz

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.
Files changed (30) hide show
  1. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/PKG-INFO +65 -1
  2. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/README.md +63 -0
  3. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/cloud_sql_python_connector.egg-info/PKG-INFO +65 -1
  4. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/cloud_sql_python_connector.egg-info/SOURCES.txt +1 -0
  5. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/cloud_sql_python_connector.egg-info/requires.txt +1 -0
  6. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/__init__.py +4 -0
  7. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/client.py +36 -16
  8. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/connection_info.py +2 -0
  9. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/connection_name.py +11 -1
  10. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/connector.py +17 -12
  11. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/exceptions.py +7 -0
  12. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/instance.py +7 -27
  13. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/lazy.py +5 -15
  14. cloud_sql_python_connector-1.15.0/google/cloud/sql/connector/resolver.py +70 -0
  15. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/version.py +1 -1
  16. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/setup.py +1 -0
  17. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/LICENSE +0 -0
  18. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/cloud_sql_python_connector.egg-info/dependency_links.txt +0 -0
  19. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/cloud_sql_python_connector.egg-info/not-zip-safe +0 -0
  20. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/cloud_sql_python_connector.egg-info/top_level.txt +0 -0
  21. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/asyncpg.py +0 -0
  22. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/enums.py +0 -0
  23. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/pg8000.py +0 -0
  24. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/py.typed +0 -0
  25. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/pymysql.py +0 -0
  26. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/pytds.py +0 -0
  27. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/rate_limiter.py +0 -0
  28. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/refresh_utils.py +0 -0
  29. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/google/cloud/sql/connector/utils.py +0 -0
  30. {cloud_sql_python_connector-1.14.0 → cloud_sql_python_connector-1.15.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-sql-python-connector
3
- Version: 1.14.0
3
+ Version: 1.15.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
@@ -22,6 +22,7 @@ License-File: LICENSE
22
22
  Requires-Dist: aiofiles
23
23
  Requires-Dist: aiohttp
24
24
  Requires-Dist: cryptography>=42.0.0
25
+ Requires-Dist: dnspython>=2.0.0
25
26
  Requires-Dist: Requests
26
27
  Requires-Dist: google-auth>=2.28.0
27
28
  Provides-Extra: pymysql
@@ -400,6 +401,69 @@ conn = connector.connect(
400
401
  )
401
402
  ```
402
403
 
404
+ ### Using DNS domain names to identify instances
405
+
406
+ The connector can be configured to use DNS to look up an instance. This would
407
+ allow you to configure your application to connect to a database instance, and
408
+ centrally configure which instance in your DNS zone.
409
+
410
+ #### Configure your DNS Records
411
+
412
+ Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server
413
+ or a private Google Cloud DNS Zone used by your application.
414
+
415
+ > [!NOTE]
416
+ >
417
+ > You are strongly discouraged from adding DNS records for your
418
+ > Cloud SQL instances to a public DNS server. This would allow anyone on the
419
+ > internet to discover the Cloud SQL instance name.
420
+
421
+ For example: suppose you wanted to use the domain name
422
+ `prod-db.mycompany.example.com` to connect to your database instance
423
+ `my-project:region:my-instance`. You would create the following DNS record:
424
+
425
+ * Record type: `TXT`
426
+ * Name: `prod-db.mycompany.example.com` – This is the domain name used by the application
427
+ * Value: `my-project:my-region:my-instance` – This is the Cloud SQL instance connection name
428
+
429
+ #### Configure the connector
430
+
431
+ Configure the connector to resolve DNS names by initializing it with
432
+ `resolver=DnsResolver` and replacing the instance connection name with the DNS
433
+ name in `connector.connect`:
434
+
435
+ ```python
436
+ from google.cloud.sql.connector import Connector, DnsResolver
437
+ import pymysql
438
+ import sqlalchemy
439
+
440
+ # helper function to return SQLAlchemy connection pool
441
+ def init_connection_pool(connector: Connector) -> sqlalchemy.engine.Engine:
442
+ # function used to generate database connection
443
+ def getconn() -> pymysql.connections.Connection:
444
+ conn = connector.connect(
445
+ "prod-db.mycompany.example.com", # using DNS name
446
+ "pymysql",
447
+ user="my-user",
448
+ password="my-password",
449
+ db="my-db-name"
450
+ )
451
+ return conn
452
+
453
+ # create connection pool
454
+ pool = sqlalchemy.create_engine(
455
+ "mysql+pymysql://",
456
+ creator=getconn,
457
+ )
458
+ return pool
459
+
460
+ # initialize Cloud SQL Python Connector with `resolver=DnsResolver`
461
+ with Connector(resolver=DnsResolver) as connector:
462
+ # initialize connection pool
463
+ pool = init_connection_pool(connector)
464
+ # ... use SQLAlchemy engine normally
465
+ ```
466
+
403
467
  ### Using the Python Connector with Python Web Frameworks
404
468
 
405
469
  The Python Connector can be used alongside popular Python web frameworks such
@@ -365,6 +365,69 @@ conn = connector.connect(
365
365
  )
366
366
  ```
367
367
 
368
+ ### Using DNS domain names to identify instances
369
+
370
+ The connector can be configured to use DNS to look up an instance. This would
371
+ allow you to configure your application to connect to a database instance, and
372
+ centrally configure which instance in your DNS zone.
373
+
374
+ #### Configure your DNS Records
375
+
376
+ Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server
377
+ or a private Google Cloud DNS Zone used by your application.
378
+
379
+ > [!NOTE]
380
+ >
381
+ > You are strongly discouraged from adding DNS records for your
382
+ > Cloud SQL instances to a public DNS server. This would allow anyone on the
383
+ > internet to discover the Cloud SQL instance name.
384
+
385
+ For example: suppose you wanted to use the domain name
386
+ `prod-db.mycompany.example.com` to connect to your database instance
387
+ `my-project:region:my-instance`. You would create the following DNS record:
388
+
389
+ * Record type: `TXT`
390
+ * Name: `prod-db.mycompany.example.com` – This is the domain name used by the application
391
+ * Value: `my-project:my-region:my-instance` – This is the Cloud SQL instance connection name
392
+
393
+ #### Configure the connector
394
+
395
+ Configure the connector to resolve DNS names by initializing it with
396
+ `resolver=DnsResolver` and replacing the instance connection name with the DNS
397
+ name in `connector.connect`:
398
+
399
+ ```python
400
+ from google.cloud.sql.connector import Connector, DnsResolver
401
+ import pymysql
402
+ import sqlalchemy
403
+
404
+ # helper function to return SQLAlchemy connection pool
405
+ def init_connection_pool(connector: Connector) -> sqlalchemy.engine.Engine:
406
+ # function used to generate database connection
407
+ def getconn() -> pymysql.connections.Connection:
408
+ conn = connector.connect(
409
+ "prod-db.mycompany.example.com", # using DNS name
410
+ "pymysql",
411
+ user="my-user",
412
+ password="my-password",
413
+ db="my-db-name"
414
+ )
415
+ return conn
416
+
417
+ # create connection pool
418
+ pool = sqlalchemy.create_engine(
419
+ "mysql+pymysql://",
420
+ creator=getconn,
421
+ )
422
+ return pool
423
+
424
+ # initialize Cloud SQL Python Connector with `resolver=DnsResolver`
425
+ with Connector(resolver=DnsResolver) as connector:
426
+ # initialize connection pool
427
+ pool = init_connection_pool(connector)
428
+ # ... use SQLAlchemy engine normally
429
+ ```
430
+
368
431
  ### Using the Python Connector with Python Web Frameworks
369
432
 
370
433
  The Python Connector can be used alongside popular Python web frameworks such
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-sql-python-connector
3
- Version: 1.14.0
3
+ Version: 1.15.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
@@ -22,6 +22,7 @@ License-File: LICENSE
22
22
  Requires-Dist: aiofiles
23
23
  Requires-Dist: aiohttp
24
24
  Requires-Dist: cryptography>=42.0.0
25
+ Requires-Dist: dnspython>=2.0.0
25
26
  Requires-Dist: Requests
26
27
  Requires-Dist: google-auth>=2.28.0
27
28
  Provides-Extra: pymysql
@@ -400,6 +401,69 @@ conn = connector.connect(
400
401
  )
401
402
  ```
402
403
 
404
+ ### Using DNS domain names to identify instances
405
+
406
+ The connector can be configured to use DNS to look up an instance. This would
407
+ allow you to configure your application to connect to a database instance, and
408
+ centrally configure which instance in your DNS zone.
409
+
410
+ #### Configure your DNS Records
411
+
412
+ Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server
413
+ or a private Google Cloud DNS Zone used by your application.
414
+
415
+ > [!NOTE]
416
+ >
417
+ > You are strongly discouraged from adding DNS records for your
418
+ > Cloud SQL instances to a public DNS server. This would allow anyone on the
419
+ > internet to discover the Cloud SQL instance name.
420
+
421
+ For example: suppose you wanted to use the domain name
422
+ `prod-db.mycompany.example.com` to connect to your database instance
423
+ `my-project:region:my-instance`. You would create the following DNS record:
424
+
425
+ * Record type: `TXT`
426
+ * Name: `prod-db.mycompany.example.com` – This is the domain name used by the application
427
+ * Value: `my-project:my-region:my-instance` – This is the Cloud SQL instance connection name
428
+
429
+ #### Configure the connector
430
+
431
+ Configure the connector to resolve DNS names by initializing it with
432
+ `resolver=DnsResolver` and replacing the instance connection name with the DNS
433
+ name in `connector.connect`:
434
+
435
+ ```python
436
+ from google.cloud.sql.connector import Connector, DnsResolver
437
+ import pymysql
438
+ import sqlalchemy
439
+
440
+ # helper function to return SQLAlchemy connection pool
441
+ def init_connection_pool(connector: Connector) -> sqlalchemy.engine.Engine:
442
+ # function used to generate database connection
443
+ def getconn() -> pymysql.connections.Connection:
444
+ conn = connector.connect(
445
+ "prod-db.mycompany.example.com", # using DNS name
446
+ "pymysql",
447
+ user="my-user",
448
+ password="my-password",
449
+ db="my-db-name"
450
+ )
451
+ return conn
452
+
453
+ # create connection pool
454
+ pool = sqlalchemy.create_engine(
455
+ "mysql+pymysql://",
456
+ creator=getconn,
457
+ )
458
+ return pool
459
+
460
+ # initialize Cloud SQL Python Connector with `resolver=DnsResolver`
461
+ with Connector(resolver=DnsResolver) as connector:
462
+ # initialize connection pool
463
+ pool = init_connection_pool(connector)
464
+ # ... use SQLAlchemy engine normally
465
+ ```
466
+
403
467
  ### Using the Python Connector with Python Web Frameworks
404
468
 
405
469
  The Python Connector can be used alongside popular Python web frameworks such
@@ -24,5 +24,6 @@ google/cloud/sql/connector/pymysql.py
24
24
  google/cloud/sql/connector/pytds.py
25
25
  google/cloud/sql/connector/rate_limiter.py
26
26
  google/cloud/sql/connector/refresh_utils.py
27
+ google/cloud/sql/connector/resolver.py
27
28
  google/cloud/sql/connector/utils.py
28
29
  google/cloud/sql/connector/version.py
@@ -1,6 +1,7 @@
1
1
  aiofiles
2
2
  aiohttp
3
3
  cryptography>=42.0.0
4
+ dnspython>=2.0.0
4
5
  Requests
5
6
  google-auth>=2.28.0
6
7
 
@@ -18,12 +18,16 @@ from google.cloud.sql.connector.connector import Connector
18
18
  from google.cloud.sql.connector.connector import create_async_connector
19
19
  from google.cloud.sql.connector.enums import IPTypes
20
20
  from google.cloud.sql.connector.enums import RefreshStrategy
21
+ from google.cloud.sql.connector.resolver import DefaultResolver
22
+ from google.cloud.sql.connector.resolver import DnsResolver
21
23
  from google.cloud.sql.connector.version import __version__
22
24
 
23
25
  __all__ = [
24
26
  "__version__",
25
27
  "create_async_connector",
26
28
  "Connector",
29
+ "DefaultResolver",
30
+ "DnsResolver",
27
31
  "IPTypes",
28
32
  "RefreshStrategy",
29
33
  ]
@@ -26,6 +26,7 @@ from google.auth.credentials import TokenState
26
26
  from google.auth.transport import requests
27
27
 
28
28
  from google.cloud.sql.connector.connection_info import ConnectionInfo
29
+ from google.cloud.sql.connector.connection_name import ConnectionName
29
30
  from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
30
31
  from google.cloud.sql.connector.refresh_utils import _downscope_credentials
31
32
  from google.cloud.sql.connector.refresh_utils import retry_50x
@@ -128,8 +129,19 @@ class CloudSQLClient:
128
129
  resp = await self._client.get(url, headers=headers)
129
130
  if resp.status >= 500:
130
131
  resp = await retry_50x(self._client.get, url, headers=headers)
131
- resp.raise_for_status()
132
- ret_dict = await resp.json()
132
+ # try to get response json for better error message
133
+ try:
134
+ ret_dict = await resp.json()
135
+ if resp.status >= 400:
136
+ # if detailed error message is in json response, use as error message
137
+ message = ret_dict.get("error", {}).get("message")
138
+ if message:
139
+ resp.reason = message
140
+ # skip, raise_for_status will catch all errors in finally block
141
+ except Exception:
142
+ pass
143
+ finally:
144
+ resp.raise_for_status()
133
145
 
134
146
  if ret_dict["region"] != region:
135
147
  raise ValueError(
@@ -198,8 +210,19 @@ class CloudSQLClient:
198
210
  resp = await self._client.post(url, headers=headers, json=data)
199
211
  if resp.status >= 500:
200
212
  resp = await retry_50x(self._client.post, url, headers=headers, json=data)
201
- resp.raise_for_status()
202
- ret_dict = await resp.json()
213
+ # try to get response json for better error message
214
+ try:
215
+ ret_dict = await resp.json()
216
+ if resp.status >= 400:
217
+ # if detailed error message is in json response, use as error message
218
+ message = ret_dict.get("error", {}).get("message")
219
+ if message:
220
+ resp.reason = message
221
+ # skip, raise_for_status will catch all errors in finally block
222
+ except Exception:
223
+ pass
224
+ finally:
225
+ resp.raise_for_status()
203
226
 
204
227
  ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
205
228
 
@@ -223,9 +246,7 @@ class CloudSQLClient:
223
246
 
224
247
  async def get_connection_info(
225
248
  self,
226
- project: str,
227
- region: str,
228
- instance: str,
249
+ conn_name: ConnectionName,
229
250
  keys: asyncio.Future,
230
251
  enable_iam_auth: bool,
231
252
  ) -> ConnectionInfo:
@@ -233,10 +254,8 @@ class CloudSQLClient:
233
254
  Admin API.
234
255
 
235
256
  Args:
236
- project (str): The name of the project the Cloud SQL instance is
237
- located in.
238
- region (str): The region the Cloud SQL instance is located in.
239
- instance (str): Name of the Cloud SQL instance.
257
+ conn_name (ConnectionName): The Cloud SQL instance's
258
+ connection name.
240
259
  keys (asyncio.Future): A future to the client's public-private key
241
260
  pair.
242
261
  enable_iam_auth (bool): Whether an automatic IAM database
@@ -256,16 +275,16 @@ class CloudSQLClient:
256
275
 
257
276
  metadata_task = asyncio.create_task(
258
277
  self._get_metadata(
259
- project,
260
- region,
261
- instance,
278
+ conn_name.project,
279
+ conn_name.region,
280
+ conn_name.instance_name,
262
281
  )
263
282
  )
264
283
 
265
284
  ephemeral_task = asyncio.create_task(
266
285
  self._get_ephemeral(
267
- project,
268
- instance,
286
+ conn_name.project,
287
+ conn_name.instance_name,
269
288
  pub_key,
270
289
  enable_iam_auth,
271
290
  )
@@ -289,6 +308,7 @@ class CloudSQLClient:
289
308
  ephemeral_cert, expiration = await ephemeral_task
290
309
 
291
310
  return ConnectionInfo(
311
+ conn_name,
292
312
  ephemeral_cert,
293
313
  metadata["server_ca_cert"],
294
314
  priv_key,
@@ -21,6 +21,7 @@ from typing import Any, Optional, TYPE_CHECKING
21
21
 
22
22
  from aiofiles.tempfile import TemporaryDirectory
23
23
 
24
+ from google.cloud.sql.connector.connection_name import ConnectionName
24
25
  from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
25
26
  from google.cloud.sql.connector.exceptions import TLSVersionError
26
27
  from google.cloud.sql.connector.utils import write_to_file
@@ -38,6 +39,7 @@ class ConnectionInfo:
38
39
  """Contains all necessary information to connect securely to the
39
40
  server-side Proxy running on a Cloud SQL instance."""
40
41
 
42
+ conn_name: ConnectionName
41
43
  client_cert: str
42
44
  server_ca_cert: str
43
45
  private_key: bytes
@@ -31,12 +31,21 @@ class ConnectionName:
31
31
  project: str
32
32
  region: str
33
33
  instance_name: str
34
+ domain_name: str = ""
34
35
 
35
36
  def __str__(self) -> str:
37
+ if self.domain_name:
38
+ return f"{self.domain_name} -> {self.project}:{self.region}:{self.instance_name}"
36
39
  return f"{self.project}:{self.region}:{self.instance_name}"
37
40
 
38
41
 
39
- def _parse_instance_connection_name(connection_name: str) -> ConnectionName:
42
+ def _parse_connection_name(connection_name: str) -> ConnectionName:
43
+ return _parse_connection_name_with_domain_name(connection_name, "")
44
+
45
+
46
+ def _parse_connection_name_with_domain_name(
47
+ connection_name: str, domain_name: str
48
+ ) -> ConnectionName:
40
49
  if CONN_NAME_REGEX.fullmatch(connection_name) is None:
41
50
  raise ValueError(
42
51
  "Arg `instance_connection_string` must have "
@@ -48,4 +57,5 @@ def _parse_instance_connection_name(connection_name: str) -> ConnectionName:
48
57
  connection_name_split[1],
49
58
  connection_name_split[3],
50
59
  connection_name_split[4],
60
+ domain_name,
51
61
  )
@@ -37,6 +37,8 @@ from google.cloud.sql.connector.lazy import LazyRefreshCache
37
37
  import google.cloud.sql.connector.pg8000 as pg8000
38
38
  import google.cloud.sql.connector.pymysql as pymysql
39
39
  import google.cloud.sql.connector.pytds as pytds
40
+ from google.cloud.sql.connector.resolver import DefaultResolver
41
+ from google.cloud.sql.connector.resolver import DnsResolver
40
42
  from google.cloud.sql.connector.utils import format_database_user
41
43
  from google.cloud.sql.connector.utils import generate_keys
42
44
 
@@ -63,6 +65,7 @@ class Connector:
63
65
  user_agent: Optional[str] = None,
64
66
  universe_domain: Optional[str] = None,
65
67
  refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
68
+ resolver: Type[DefaultResolver] | Type[DnsResolver] = DefaultResolver,
66
69
  ) -> None:
67
70
  """Initializes a Connector instance.
68
71
 
@@ -104,6 +107,12 @@ class Connector:
104
107
  of the following: RefreshStrategy.LAZY ("LAZY") or
105
108
  RefreshStrategy.BACKGROUND ("BACKGROUND").
106
109
  Default: RefreshStrategy.BACKGROUND
110
+
111
+ resolver (DefaultResolver | DnsResolver): The class name of the
112
+ resolver to use for resolving the Cloud SQL instance connection
113
+ name. To resolve a DNS record to an instance connection name, use
114
+ DnsResolver.
115
+ Default: DefaultResolver
107
116
  """
108
117
  # if refresh_strategy is str, convert to RefreshStrategy enum
109
118
  if isinstance(refresh_strategy, str):
@@ -157,6 +166,7 @@ class Connector:
157
166
  self._enable_iam_auth = enable_iam_auth
158
167
  self._quota_project = quota_project
159
168
  self._user_agent = user_agent
169
+ self._resolver = resolver()
160
170
  # if ip_type is str, convert to IPTypes enum
161
171
  if isinstance(ip_type, str):
162
172
  ip_type = IPTypes._from_str(ip_type)
@@ -269,31 +279,28 @@ class Connector:
269
279
  if (instance_connection_string, enable_iam_auth) in self._cache:
270
280
  cache = self._cache[(instance_connection_string, enable_iam_auth)]
271
281
  else:
282
+ conn_name = await self._resolver.resolve(instance_connection_string)
272
283
  if self._refresh_strategy == RefreshStrategy.LAZY:
273
284
  logger.debug(
274
- f"['{instance_connection_string}']: Refresh strategy is set"
275
- " to lazy refresh"
285
+ f"['{conn_name}']: Refresh strategy is set to lazy refresh"
276
286
  )
277
287
  cache = LazyRefreshCache(
278
- instance_connection_string,
288
+ conn_name,
279
289
  self._client,
280
290
  self._keys,
281
291
  enable_iam_auth,
282
292
  )
283
293
  else:
284
294
  logger.debug(
285
- f"['{instance_connection_string}']: Refresh strategy is set"
286
- " to backgound refresh"
295
+ f"['{conn_name}']: Refresh strategy is set to backgound refresh"
287
296
  )
288
297
  cache = RefreshAheadCache(
289
- instance_connection_string,
298
+ conn_name,
290
299
  self._client,
291
300
  self._keys,
292
301
  enable_iam_auth,
293
302
  )
294
- logger.debug(
295
- f"['{instance_connection_string}']: Connection info added to cache"
296
- )
303
+ logger.debug(f"['{conn_name}']: Connection info added to cache")
297
304
  self._cache[(instance_connection_string, enable_iam_auth)] = cache
298
305
 
299
306
  connect_func = {
@@ -332,9 +339,7 @@ class Connector:
332
339
  # the cache and re-raise the error
333
340
  await self._remove_cached(instance_connection_string, enable_iam_auth)
334
341
  raise
335
- logger.debug(
336
- f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
337
- )
342
+ logger.debug(f"['{conn_info.conn_name}']: Connecting to {ip_address}:3307")
338
343
  # format `user` param for automatic IAM database authn
339
344
  if enable_iam_auth:
340
345
  formatted_user = format_database_user(
@@ -70,3 +70,10 @@ class IncompatibleDriverError(Exception):
70
70
  Exception to be raised when the database driver given is for the wrong
71
71
  database engine. (i.e. asyncpg for a MySQL database)
72
72
  """
73
+
74
+
75
+ class DnsResolutionError(Exception):
76
+ """
77
+ Exception to be raised when an instance connection name can not be resolved
78
+ from a DNS record.
79
+ """
@@ -22,11 +22,9 @@ from datetime import timedelta
22
22
  from datetime import timezone
23
23
  import logging
24
24
 
25
- import aiohttp
26
-
27
25
  from google.cloud.sql.connector.client import CloudSQLClient
28
26
  from google.cloud.sql.connector.connection_info import ConnectionInfo
29
- from google.cloud.sql.connector.connection_name import _parse_instance_connection_name
27
+ from google.cloud.sql.connector.connection_name import ConnectionName
30
28
  from google.cloud.sql.connector.exceptions import RefreshNotValidError
31
29
  from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
32
30
  from google.cloud.sql.connector.refresh_utils import _is_valid
@@ -47,7 +45,7 @@ class RefreshAheadCache:
47
45
 
48
46
  def __init__(
49
47
  self,
50
- instance_connection_string: str,
48
+ conn_name: ConnectionName,
51
49
  client: CloudSQLClient,
52
50
  keys: asyncio.Future,
53
51
  enable_iam_auth: bool = False,
@@ -55,8 +53,8 @@ class RefreshAheadCache:
55
53
  """Initializes a RefreshAheadCache instance.
56
54
 
57
55
  Args:
58
- instance_connection_string (str): The Cloud SQL Instance's
59
- connection string (also known as an instance connection name).
56
+ conn_name (ConnectionName): The Cloud SQL instance's
57
+ connection name.
60
58
  client (CloudSQLClient): The Cloud SQL Client instance.
61
59
  keys (asyncio.Future): A future to the client's public-private key
62
60
  pair.
@@ -64,13 +62,6 @@ class RefreshAheadCache:
64
62
  (Postgres and MySQL) as the default authentication method for all
65
63
  connections.
66
64
  """
67
- # validate and parse instance connection name
68
- conn_name = _parse_instance_connection_name(instance_connection_string)
69
- self._project, self._region, self._instance = (
70
- conn_name.project,
71
- conn_name.region,
72
- conn_name.instance_name,
73
- )
74
65
  self._conn_name = conn_name
75
66
 
76
67
  self._enable_iam_auth = enable_iam_auth
@@ -108,35 +99,24 @@ class RefreshAheadCache:
108
99
  """
109
100
  self._refresh_in_progress.set()
110
101
  logger.debug(
111
- f"['{self._conn_name}']: Connection info refresh " "operation started"
102
+ f"['{self._conn_name}']: Connection info refresh operation started"
112
103
  )
113
104
 
114
105
  try:
115
106
  await self._refresh_rate_limiter.acquire()
116
107
  connection_info = await self._client.get_connection_info(
117
- self._project,
118
- self._region,
119
- self._instance,
108
+ self._conn_name,
120
109
  self._keys,
121
110
  self._enable_iam_auth,
122
111
  )
123
112
  logger.debug(
124
- f"['{self._conn_name}']: Connection info " "refresh operation complete"
113
+ f"['{self._conn_name}']: Connection info refresh operation complete"
125
114
  )
126
115
  logger.debug(
127
116
  f"['{self._conn_name}']: Current certificate "
128
117
  f"expiration = {connection_info.expiration.isoformat()}"
129
118
  )
130
119
 
131
- except aiohttp.ClientResponseError as e:
132
- logger.debug(
133
- f"['{self._conn_name}']: Connection info "
134
- f"refresh operation failed: {str(e)}"
135
- )
136
- if e.status == 403:
137
- 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."
138
- raise
139
-
140
120
  except Exception as e:
141
121
  logger.debug(
142
122
  f"['{self._conn_name}']: Connection info "
@@ -21,7 +21,7 @@ from typing import Optional
21
21
 
22
22
  from google.cloud.sql.connector.client import CloudSQLClient
23
23
  from google.cloud.sql.connector.connection_info import ConnectionInfo
24
- from google.cloud.sql.connector.connection_name import _parse_instance_connection_name
24
+ from google.cloud.sql.connector.connection_name import ConnectionName
25
25
  from google.cloud.sql.connector.refresh_utils import _refresh_buffer
26
26
 
27
27
  logger = logging.getLogger(name=__name__)
@@ -38,7 +38,7 @@ class LazyRefreshCache:
38
38
 
39
39
  def __init__(
40
40
  self,
41
- instance_connection_string: str,
41
+ conn_name: ConnectionName,
42
42
  client: CloudSQLClient,
43
43
  keys: asyncio.Future,
44
44
  enable_iam_auth: bool = False,
@@ -46,8 +46,8 @@ class LazyRefreshCache:
46
46
  """Initializes a LazyRefreshCache instance.
47
47
 
48
48
  Args:
49
- instance_connection_string (str): The Cloud SQL Instance's
50
- connection string (also known as an instance connection name).
49
+ conn_name (ConnectionName): The Cloud SQL instance's
50
+ connection name.
51
51
  client (CloudSQLClient): The Cloud SQL Client instance.
52
52
  keys (asyncio.Future): A future to the client's public-private key
53
53
  pair.
@@ -55,15 +55,7 @@ class LazyRefreshCache:
55
55
  (Postgres and MySQL) as the default authentication method for all
56
56
  connections.
57
57
  """
58
- # validate and parse instance connection name
59
- conn_name = _parse_instance_connection_name(instance_connection_string)
60
- self._project, self._region, self._instance = (
61
- conn_name.project,
62
- conn_name.region,
63
- conn_name.instance_name,
64
- )
65
58
  self._conn_name = conn_name
66
-
67
59
  self._enable_iam_auth = enable_iam_auth
68
60
  self._keys = keys
69
61
  self._client = client
@@ -103,9 +95,7 @@ class LazyRefreshCache:
103
95
  )
104
96
  try:
105
97
  conn_info = await self._client.get_connection_info(
106
- self._project,
107
- self._region,
108
- self._instance,
98
+ self._conn_name,
109
99
  self._keys,
110
100
  self._enable_iam_auth,
111
101
  )
@@ -0,0 +1,70 @@
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 dns.asyncresolver
16
+
17
+ from google.cloud.sql.connector.connection_name import (
18
+ _parse_connection_name_with_domain_name,
19
+ )
20
+ from google.cloud.sql.connector.connection_name import _parse_connection_name
21
+ from google.cloud.sql.connector.connection_name import ConnectionName
22
+ from google.cloud.sql.connector.exceptions import DnsResolutionError
23
+
24
+
25
+ class DefaultResolver:
26
+ """DefaultResolver simply validates and parses instance connection name."""
27
+
28
+ async def resolve(self, connection_name: str) -> ConnectionName:
29
+ return _parse_connection_name(connection_name)
30
+
31
+
32
+ class DnsResolver(dns.asyncresolver.Resolver):
33
+ """
34
+ DnsResolver resolves domain names into instance connection names using
35
+ TXT records in DNS.
36
+ """
37
+
38
+ async def resolve(self, dns: str) -> ConnectionName: # type: ignore
39
+ try:
40
+ conn_name = _parse_connection_name(dns)
41
+ except ValueError:
42
+ # The connection name was not project:region:instance format.
43
+ # Attempt to query a TXT record to get connection name.
44
+ conn_name = await self.query_dns(dns)
45
+ return conn_name
46
+
47
+ async def query_dns(self, dns: str) -> ConnectionName:
48
+ try:
49
+ # Attempt to query the TXT records.
50
+ records = await super().resolve(dns, "TXT", raise_on_no_answer=True)
51
+ # Sort the TXT record values alphabetically, strip quotes as record
52
+ # values can be returned as raw strings
53
+ rdata = [record.to_text().strip('"') for record in records]
54
+ rdata.sort()
55
+ # Attempt to parse records, returning the first valid record.
56
+ for record in rdata:
57
+ try:
58
+ conn_name = _parse_connection_name_with_domain_name(record, dns)
59
+ return conn_name
60
+ except Exception:
61
+ continue
62
+ # If all records failed to parse, throw error
63
+ raise DnsResolutionError(
64
+ f"Unable to parse TXT record for `{dns}` -> `{rdata[0]}`"
65
+ )
66
+ # Don't override above DnsResolutionError
67
+ except DnsResolutionError:
68
+ raise
69
+ except Exception as e:
70
+ raise DnsResolutionError(f"Unable to resolve TXT record for `{dns}`") from e
@@ -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.14.0"
15
+ __version__ = "1.15.0"
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "aiofiles",
29
29
  "aiohttp",
30
30
  "cryptography>=42.0.0",
31
+ "dnspython>=2.0.0",
31
32
  "Requests",
32
33
  "google-auth>=2.28.0",
33
34
  ]