cloud-sql-python-connector 1.13.0__py2.py3-none-any.whl → 1.15.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.13.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
@@ -21,17 +21,18 @@ Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
22
  Requires-Dist: aiofiles
23
23
  Requires-Dist: aiohttp
24
- Requires-Dist: cryptography (>=42.0.0)
24
+ Requires-Dist: cryptography>=42.0.0
25
+ Requires-Dist: dnspython>=2.0.0
25
26
  Requires-Dist: Requests
26
- Requires-Dist: google-auth (>=2.28.0)
27
+ Requires-Dist: google-auth>=2.28.0
27
28
  Provides-Extra: asyncpg
28
- Requires-Dist: asyncpg (>=0.29.0) ; extra == 'asyncpg'
29
+ Requires-Dist: asyncpg>=0.30.0; extra == "asyncpg"
29
30
  Provides-Extra: pg8000
30
- Requires-Dist: pg8000 (>=1.31.1) ; extra == 'pg8000'
31
+ Requires-Dist: pg8000>=1.31.1; extra == "pg8000"
31
32
  Provides-Extra: pymysql
32
- Requires-Dist: PyMySQL (>=1.1.0) ; extra == 'pymysql'
33
+ Requires-Dist: PyMySQL>=1.1.0; extra == "pymysql"
33
34
  Provides-Extra: pytds
34
- Requires-Dist: python-tds (>=1.15.0) ; extra == 'pytds'
35
+ Requires-Dist: python-tds>=1.15.0; extra == "pytds"
35
36
 
36
37
  <p align="center">
37
38
  <a href="https://cloud.google.com/blog/topics/developers-practitioners/how-connect-cloud-sql-using-python-easy-way">
@@ -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
@@ -537,6 +601,42 @@ The `create_async_connector` allows all the same input arguments as the
537
601
  Once a `Connector` object is returned by `create_async_connector` you can call
538
602
  its `connect_async` method, just as you would the `connect` method:
539
603
 
604
+ #### Asyncpg Connection Pool
605
+
606
+ ```python
607
+ import asyncpg
608
+ from google.cloud.sql.connector import Connector, create_async_connector
609
+
610
+ async def main():
611
+ # initialize Connector object for connections to Cloud SQL
612
+ connector = create_async_connector()
613
+
614
+ # creation function to generate asyncpg connections as the 'connect' arg
615
+ async def getconn(instance_connection_name, **kwargs) -> asyncpg.Connection:
616
+ return await connector.connect_async(
617
+ instance_connection_name,
618
+ "asyncpg",
619
+ user="my-user",
620
+ password="my-password",
621
+ db="my-db",
622
+ **kwargs, # ... additional asyncpg args
623
+ )
624
+
625
+ # initialize connection pool
626
+ pool = await asyncpg.create_pool(
627
+ "my-project:my-region:my-instance", connect=getconn
628
+ )
629
+
630
+ # acquire connection and query Cloud SQL database
631
+ async with pool.acquire() as conn:
632
+ res = await conn.fetch("SELECT NOW()")
633
+
634
+ # close Connector
635
+ await connector.close_async()
636
+ ```
637
+
638
+ #### SQLAlchemy Async Engine
639
+
540
640
  ```python
541
641
  import asyncpg
542
642
 
@@ -546,7 +646,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
546
646
  from google.cloud.sql.connector import Connector, create_async_connector
547
647
 
548
648
  async def init_connection_pool(connector: Connector) -> AsyncEngine:
549
- # initialize Connector object for connections to Cloud SQL
649
+ # creation function to generate asyncpg connections as 'async_creator' arg
550
650
  async def getconn() -> asyncpg.Connection:
551
651
  conn: asyncpg.Connection = await connector.connect_async(
552
652
  "project:region:instance", # Cloud SQL instance connection name
@@ -599,6 +699,40 @@ calls to `connector.close_async()` to cleanup resources.
599
699
  > This alternative requires that the running event loop be
600
700
  > passed in as the `loop` argument to `Connector()`.
601
701
 
702
+ #### Asyncpg Connection Pool
703
+
704
+ ```python
705
+ import asyncpg
706
+ from google.cloud.sql.connector import Connector, create_async_connector
707
+
708
+ async def main():
709
+ # initialize Connector object for connections to Cloud SQL
710
+ loop = asyncio.get_running_loop()
711
+ async with Connector(loop=loop) as connector:
712
+
713
+ # creation function to generate asyncpg connections as the 'connect' arg
714
+ async def getconn(instance_connection_name, **kwargs) -> asyncpg.Connection:
715
+ return await connector.connect_async(
716
+ instance_connection_name,
717
+ "asyncpg",
718
+ user="my-user",
719
+ password="my-password",
720
+ db="my-db",
721
+ **kwargs, # ... additional asyncpg args
722
+ )
723
+
724
+ # create connection pool
725
+ pool = await asyncpg.create_pool(
726
+ "my-project:my-region:my-instance", connect=getconn
727
+ )
728
+
729
+ # acquire connection and query Cloud SQL database
730
+ async with pool.acquire() as conn:
731
+ res = await conn.fetch("SELECT NOW()")
732
+ ```
733
+
734
+ #### SQLAlchemy Async Engine
735
+
602
736
  ```python
603
737
  import asyncio
604
738
  import asyncpg
@@ -609,17 +743,17 @@ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
609
743
  from google.cloud.sql.connector import Connector
610
744
 
611
745
  async def init_connection_pool(connector: Connector) -> AsyncEngine:
612
- # initialize Connector object for connections to Cloud SQL
746
+ # creation function to generate asyncpg connections as 'async_creator' arg
613
747
  async def getconn() -> asyncpg.Connection:
614
- conn: asyncpg.Connection = await connector.connect_async(
615
- "project:region:instance", # Cloud SQL instance connection name
616
- "asyncpg",
617
- user="my-user",
618
- password="my-password",
619
- db="my-db-name"
620
- # ... additional database driver args
621
- )
622
- return conn
748
+ conn: asyncpg.Connection = await connector.connect_async(
749
+ "project:region:instance", # Cloud SQL instance connection name
750
+ "asyncpg",
751
+ user="my-user",
752
+ password="my-password",
753
+ db="my-db-name"
754
+ # ... additional database driver args
755
+ )
756
+ return conn
623
757
 
624
758
  # The Cloud SQL Python Connector can be used along with SQLAlchemy using the
625
759
  # 'async_creator' argument to 'create_async_engine'
@@ -0,0 +1,24 @@
1
+ google/cloud/sql/connector/__init__.py,sha256=kSGB2q6hQwkFLD1eI7RjY4BvPLvg2FD3wfPBmbnsjDE,1149
2
+ google/cloud/sql/connector/asyncpg.py,sha256=HUOB0rRhr2-zMbygDtdWTDNlYqEPkN0ADqCIpM3waDc,1839
3
+ google/cloud/sql/connector/client.py,sha256=ocUN1nrgZyJRnxgh6Ym3w0c_qKb8hqKCmR1OiJ2KUpM,12622
4
+ google/cloud/sql/connector/connection_info.py,sha256=cFNOeHJ0lQODf7q5Hc25LXsNWzWgzYcFMrBuNdZ1y5k,4430
5
+ google/cloud/sql/connector/connection_name.py,sha256=4F5XhgmSWGEeF-3TJk2E61YPbxMAecPGw56nPTZmQ2o,2067
6
+ google/cloud/sql/connector/connector.py,sha256=9t2iz_Tx1NluGgaM8JlZUfb7HWHgsWjmLnAL8WvKp4E,21157
7
+ google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
8
+ google/cloud/sql/connector/exceptions.py,sha256=eyflYggkKZx3t944FTPqQrX-R75lZgtpOwC4uVLPhG0,1823
9
+ google/cloud/sql/connector/instance.py,sha256=nRzhE7t_M6AhjiYvEZHOjkBHx3d9wGRCC6RekgDApFI,8379
10
+ google/cloud/sql/connector/lazy.py,sha256=p1wLte49xE_aHiY5shjMbxwytgW-ycS0Hd67C34gaDY,4631
11
+ google/cloud/sql/connector/pg8000.py,sha256=gZisjfXP4rNIjyxOQS75fvr0Xe6eLrFo4mIhVjoMLTw,1815
12
+ google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ google/cloud/sql/connector/pymysql.py,sha256=82DwSJUhZHH2AW-vO7qhkljyShwj6Kmd17WCbOGZH74,2106
14
+ google/cloud/sql/connector/pytds.py,sha256=h-1pDhqLDkhviKBBOJ29gnIeYHKWw7Dtj91VLu1iNUI,2453
15
+ google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
16
+ google/cloud/sql/connector/refresh_utils.py,sha256=HfuuLdlqg2IxusB4T3BMj-BJlGsXtHsrCX_Hp-eJ_j0,5063
17
+ google/cloud/sql/connector/resolver.py,sha256=cNDJMhmS_0CIAmz8kCABKHJNLNm1wiwIVoPEN-gWzzY,2850
18
+ google/cloud/sql/connector/utils.py,sha256=7g1fX-JsZc7xOgTuxrAiroFHQIQk_BEJZNYaKOinwQY,3508
19
+ google/cloud/sql/connector/version.py,sha256=l3R3usCjpr4Wwp3w2gpWFcrGmsLi8Y1gAAvZC15RLv0,598
20
+ cloud_sql_python_connector-1.15.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
21
+ cloud_sql_python_connector-1.15.0.dist-info/METADATA,sha256=Qszn2k_QqcVlX92c_1A-7MurYUoojTIMbx5M13aPZnI,30314
22
+ cloud_sql_python_connector-1.15.0.dist-info/WHEEL,sha256=OpXWERl2xLPRHTvd2ZXo_iluPEQd8uSbYkJ53NAER_Y,109
23
+ cloud_sql_python_connector-1.15.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
24
+ cloud_sql_python_connector-1.15.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -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
  ]
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
+
16
17
  import ssl
17
18
  from typing import Any, TYPE_CHECKING
18
19
 
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
  import asyncio
18
18
  import datetime
19
19
  import logging
20
- from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
20
+ from typing import Any, Optional, TYPE_CHECKING
21
21
 
22
22
  import aiohttp
23
23
  from cryptography.hazmat.backends import default_backend
@@ -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
@@ -98,7 +99,7 @@ class CloudSQLClient:
98
99
  project: str,
99
100
  region: str,
100
101
  instance: str,
101
- ) -> Dict[str, Any]:
102
+ ) -> dict[str, Any]:
102
103
  """Requests metadata from the Cloud SQL Instance
103
104
  and returns a dictionary containing the IP addresses and certificate
104
105
  authority of the Cloud SQL Instance.
@@ -113,7 +114,7 @@ class CloudSQLClient:
113
114
  :type instance: str
114
115
  :param instance: A string representing the name of the instance.
115
116
 
116
- :rtype: Dict[str: Union[Dict, str]]
117
+ :rtype: dict[str: Union[dict, str]]
117
118
  :returns: Returns a dictionary containing a dictionary of all IP
118
119
  addresses and their type and a string representing the
119
120
  certificate authority.
@@ -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(
@@ -161,7 +173,7 @@ class CloudSQLClient:
161
173
  instance: str,
162
174
  pub_key: str,
163
175
  enable_iam_auth: bool = False,
164
- ) -> Tuple[str, datetime.datetime]:
176
+ ) -> tuple[str, datetime.datetime]:
165
177
  """Asynchronously requests an ephemeral certificate from the Cloud SQL Instance.
166
178
 
167
179
  :type project: str
@@ -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,
@@ -17,10 +17,11 @@ from __future__ import annotations
17
17
  from dataclasses import dataclass
18
18
  import logging
19
19
  import ssl
20
- from typing import Any, Dict, Optional, TYPE_CHECKING
20
+ 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,10 +39,11 @@ 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
44
- ip_addrs: Dict[str, Any]
46
+ ip_addrs: dict[str, Any]
45
47
  database_version: str
46
48
  expiration: datetime.datetime
47
49
  context: Optional[ssl.SSLContext] = None
@@ -0,0 +1,61 @@
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 dataclasses import dataclass
16
+ import re
17
+
18
+ # Instance connection name is the format <PROJECT>:<REGION>:<INSTANCE_NAME>
19
+ # Additionally, we have to support legacy "domain-scoped" projects
20
+ # (e.g. "google.com:PROJECT")
21
+ CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)"))
22
+
23
+
24
+ @dataclass
25
+ class ConnectionName:
26
+ """ConnectionName represents a Cloud SQL instance's "instance connection name".
27
+
28
+ Takes the format "<PROJECT>:<REGION>:<INSTANCE_NAME>".
29
+ """
30
+
31
+ project: str
32
+ region: str
33
+ instance_name: str
34
+ domain_name: str = ""
35
+
36
+ def __str__(self) -> str:
37
+ if self.domain_name:
38
+ return f"{self.domain_name} -> {self.project}:{self.region}:{self.instance_name}"
39
+ return f"{self.project}:{self.region}:{self.instance_name}"
40
+
41
+
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:
49
+ if CONN_NAME_REGEX.fullmatch(connection_name) is None:
50
+ raise ValueError(
51
+ "Arg `instance_connection_string` must have "
52
+ "format: PROJECT:REGION:INSTANCE, "
53
+ f"got {connection_name}."
54
+ )
55
+ connection_name_split = CONN_NAME_REGEX.split(connection_name)
56
+ return ConnectionName(
57
+ connection_name_split[1],
58
+ connection_name_split[3],
59
+ connection_name_split[4],
60
+ domain_name,
61
+ )
@@ -21,7 +21,7 @@ from functools import partial
21
21
  import logging
22
22
  from threading import Thread
23
23
  from types import TracebackType
24
- from typing import Any, Dict, Optional, Tuple, Type, Union
24
+ from typing import Any, Optional, Type, Union
25
25
 
26
26
  import google.auth
27
27
  from google.auth.credentials import Credentials
@@ -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):
@@ -133,8 +142,8 @@ class Connector:
133
142
  )
134
143
  # initialize dict to store caches, key is a tuple consisting of instance
135
144
  # connection name string and enable_iam_auth boolean flag
136
- self._cache: Dict[
137
- Tuple[str, bool], Union[RefreshAheadCache, LazyRefreshCache]
145
+ self._cache: dict[
146
+ tuple[str, bool], Union[RefreshAheadCache, LazyRefreshCache]
138
147
  ] = {}
139
148
  self._client: Optional[CloudSQLClient] = None
140
149
 
@@ -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
+ """
@@ -21,13 +21,10 @@ from datetime import datetime
21
21
  from datetime import timedelta
22
22
  from datetime import timezone
23
23
  import logging
24
- import re
25
- from typing import Tuple
26
-
27
- import aiohttp
28
24
 
29
25
  from google.cloud.sql.connector.client import CloudSQLClient
30
26
  from google.cloud.sql.connector.connection_info import ConnectionInfo
27
+ from google.cloud.sql.connector.connection_name import ConnectionName
31
28
  from google.cloud.sql.connector.exceptions import RefreshNotValidError
32
29
  from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
33
30
  from google.cloud.sql.connector.refresh_utils import _is_valid
@@ -37,22 +34,6 @@ logger = logging.getLogger(name=__name__)
37
34
 
38
35
  APPLICATION_NAME = "cloud-sql-python-connector"
39
36
 
40
- # Instance connection name is the format <PROJECT>:<REGION>:<INSTANCE>
41
- # Additionally, we have to support legacy "domain-scoped" projects
42
- # (e.g. "google.com:PROJECT")
43
- CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)"))
44
-
45
-
46
- def _parse_instance_connection_name(connection_name: str) -> Tuple[str, str, str]:
47
- if CONN_NAME_REGEX.fullmatch(connection_name) is None:
48
- raise ValueError(
49
- "Arg `instance_connection_string` must have "
50
- "format: PROJECT:REGION:INSTANCE, "
51
- f"got {connection_name}."
52
- )
53
- connection_name_split = CONN_NAME_REGEX.split(connection_name)
54
- return connection_name_split[1], connection_name_split[3], connection_name_split[4]
55
-
56
37
 
57
38
  class RefreshAheadCache:
58
39
  """Cache that refreshes connection info in the background prior to expiration.
@@ -64,7 +45,7 @@ class RefreshAheadCache:
64
45
 
65
46
  def __init__(
66
47
  self,
67
- instance_connection_string: str,
48
+ conn_name: ConnectionName,
68
49
  client: CloudSQLClient,
69
50
  keys: asyncio.Future,
70
51
  enable_iam_auth: bool = False,
@@ -72,8 +53,8 @@ class RefreshAheadCache:
72
53
  """Initializes a RefreshAheadCache instance.
73
54
 
74
55
  Args:
75
- instance_connection_string (str): The Cloud SQL Instance's
76
- connection string (also known as an instance connection name).
56
+ conn_name (ConnectionName): The Cloud SQL instance's
57
+ connection name.
77
58
  client (CloudSQLClient): The Cloud SQL Client instance.
78
59
  keys (asyncio.Future): A future to the client's public-private key
79
60
  pair.
@@ -81,11 +62,7 @@ class RefreshAheadCache:
81
62
  (Postgres and MySQL) as the default authentication method for all
82
63
  connections.
83
64
  """
84
- # validate and parse instance connection name
85
- self._project, self._region, self._instance = _parse_instance_connection_name(
86
- instance_connection_string
87
- )
88
- self._instance_connection_string = instance_connection_string
65
+ self._conn_name = conn_name
89
66
 
90
67
  self._enable_iam_auth = enable_iam_auth
91
68
  self._keys = keys
@@ -122,40 +99,27 @@ class RefreshAheadCache:
122
99
  """
123
100
  self._refresh_in_progress.set()
124
101
  logger.debug(
125
- f"['{self._instance_connection_string}']: Connection info refresh "
126
- "operation started"
102
+ f"['{self._conn_name}']: Connection info refresh operation started"
127
103
  )
128
104
 
129
105
  try:
130
106
  await self._refresh_rate_limiter.acquire()
131
107
  connection_info = await self._client.get_connection_info(
132
- self._project,
133
- self._region,
134
- self._instance,
108
+ self._conn_name,
135
109
  self._keys,
136
110
  self._enable_iam_auth,
137
111
  )
138
112
  logger.debug(
139
- f"['{self._instance_connection_string}']: Connection info "
140
- "refresh operation complete"
113
+ f"['{self._conn_name}']: Connection info refresh operation complete"
141
114
  )
142
115
  logger.debug(
143
- f"['{self._instance_connection_string}']: Current certificate "
116
+ f"['{self._conn_name}']: Current certificate "
144
117
  f"expiration = {connection_info.expiration.isoformat()}"
145
118
  )
146
119
 
147
- except aiohttp.ClientResponseError as e:
148
- logger.debug(
149
- f"['{self._instance_connection_string}']: Connection info "
150
- f"refresh operation failed: {str(e)}"
151
- )
152
- if e.status == 403:
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."
154
- raise
155
-
156
120
  except Exception as e:
157
121
  logger.debug(
158
- f"['{self._instance_connection_string}']: Connection info "
122
+ f"['{self._conn_name}']: Connection info "
159
123
  f"refresh operation failed: {str(e)}"
160
124
  )
161
125
  raise
@@ -189,18 +153,17 @@ class RefreshAheadCache:
189
153
  # check that refresh is valid
190
154
  if not await _is_valid(refresh_task):
191
155
  raise RefreshNotValidError(
192
- f"['{self._instance_connection_string}']: Invalid refresh operation. Certficate appears to be expired."
156
+ f"['{self._conn_name}']: Invalid refresh operation. Certficate appears to be expired."
193
157
  )
194
158
  except asyncio.CancelledError:
195
159
  logger.debug(
196
- f"['{self._instance_connection_string}']: Scheduled refresh"
197
- " operation cancelled"
160
+ f"['{self._conn_name}']: Scheduled refresh" " operation cancelled"
198
161
  )
199
162
  raise
200
163
  # bad refresh attempt
201
164
  except Exception as e:
202
165
  logger.exception(
203
- f"['{self._instance_connection_string}']: "
166
+ f"['{self._conn_name}']: "
204
167
  "An error occurred while performing refresh. "
205
168
  "Scheduling another refresh attempt immediately",
206
169
  exc_info=e,
@@ -217,7 +180,7 @@ class RefreshAheadCache:
217
180
  # calculate refresh delay based on certificate expiration
218
181
  delay = _seconds_until_refresh(refresh_data.expiration)
219
182
  logger.debug(
220
- f"['{self._instance_connection_string}']: Connection info refresh"
183
+ f"['{self._conn_name}']: Connection info refresh"
221
184
  " operation scheduled for "
222
185
  f"{(datetime.now(timezone.utc) + timedelta(seconds=delay)).isoformat(timespec='seconds')} "
223
186
  f"(now + {timedelta(seconds=delay)})"
@@ -241,7 +204,7 @@ class RefreshAheadCache:
241
204
  graceful exit.
242
205
  """
243
206
  logger.debug(
244
- f"['{self._instance_connection_string}']: Canceling connection info "
207
+ f"['{self._conn_name}']: Canceling connection info "
245
208
  "refresh operation tasks"
246
209
  )
247
210
  self._current.cancel()
@@ -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.instance 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,12 +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
- self._project, self._region, self._instance = _parse_instance_connection_name(
60
- instance_connection_string
61
- )
62
- self._instance_connection_string = instance_connection_string
63
-
58
+ self._conn_name = conn_name
64
59
  self._enable_iam_auth = enable_iam_auth
65
60
  self._keys = keys
66
61
  self._client = client
@@ -91,34 +86,31 @@ class LazyRefreshCache:
91
86
  < (self._cached.expiration - timedelta(seconds=_refresh_buffer))
92
87
  ):
93
88
  logger.debug(
94
- f"['{self._instance_connection_string}']: Connection info "
89
+ f"['{self._conn_name}']: Connection info "
95
90
  "is still valid, using cached info"
96
91
  )
97
92
  return self._cached
98
93
  logger.debug(
99
- f"['{self._instance_connection_string}']: Connection info "
100
- "refresh operation started"
94
+ f"['{self._conn_name}']: Connection info " "refresh operation started"
101
95
  )
102
96
  try:
103
97
  conn_info = await self._client.get_connection_info(
104
- self._project,
105
- self._region,
106
- self._instance,
98
+ self._conn_name,
107
99
  self._keys,
108
100
  self._enable_iam_auth,
109
101
  )
110
102
  except Exception as e:
111
103
  logger.debug(
112
- f"['{self._instance_connection_string}']: Connection info "
104
+ f"['{self._conn_name}']: Connection info "
113
105
  f"refresh operation failed: {str(e)}"
114
106
  )
115
107
  raise
116
108
  logger.debug(
117
- f"['{self._instance_connection_string}']: Connection info "
109
+ f"['{self._conn_name}']: Connection info "
118
110
  "refresh operation completed successfully"
119
111
  )
120
112
  logger.debug(
121
- f"['{self._instance_connection_string}']: Current certificate "
113
+ f"['{self._conn_name}']: Current certificate "
122
114
  f"expiration = {str(conn_info.expiration)}"
123
115
  )
124
116
  self._cached = conn_info
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
+
16
17
  import socket
17
18
  import ssl
18
19
  from typing import Any, TYPE_CHECKING
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
+
16
17
  import socket
17
18
  import ssl
18
19
  from typing import Any, TYPE_CHECKING
@@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
+
16
17
  import platform
17
18
  import socket
18
19
  import ssl
@@ -21,7 +21,7 @@ import copy
21
21
  import datetime
22
22
  import logging
23
23
  import random
24
- from typing import Any, Callable, List
24
+ from typing import Any, Callable
25
25
 
26
26
  import aiohttp
27
27
  from google.auth.credentials import Credentials
@@ -77,7 +77,7 @@ async def _is_valid(task: asyncio.Task) -> bool:
77
77
 
78
78
  def _downscope_credentials(
79
79
  credentials: Credentials,
80
- scopes: List[str] = ["https://www.googleapis.com/auth/sqlservice.login"],
80
+ scopes: list[str] = ["https://www.googleapis.com/auth/sqlservice.login"],
81
81
  ) -> Credentials:
82
82
  """Generate a down-scoped credential.
83
83
 
@@ -85,7 +85,7 @@ def _downscope_credentials(
85
85
  :param credentials
86
86
  Credentials object used to generate down-scoped credentials.
87
87
 
88
- :type scopes: List[str]
88
+ :type scopes: list[str]
89
89
  :param scopes
90
90
  List of Google scopes to include in down-scoped credentials object.
91
91
 
@@ -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
@@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
16
 
17
- from typing import Tuple
18
-
19
17
  import aiofiles
20
18
  from cryptography.hazmat.backends import default_backend
21
19
  from cryptography.hazmat.primitives import serialization
22
20
  from cryptography.hazmat.primitives.asymmetric import rsa
23
21
 
24
22
 
25
- async def generate_keys() -> Tuple[bytes, str]:
23
+ async def generate_keys() -> tuple[bytes, str]:
26
24
  """A helper function to generate the private and public keys.
27
25
 
28
26
  backend - The value specified is default_backend(). This is because the
@@ -61,7 +59,7 @@ async def generate_keys() -> Tuple[bytes, str]:
61
59
 
62
60
  async def write_to_file(
63
61
  dir_path: str, serverCaCert: str, ephemeralCert: str, priv_key: bytes
64
- ) -> Tuple[str, str, str]:
62
+ ) -> tuple[str, str, str]:
65
63
  """
66
64
  Helper function to write the serverCaCert, ephemeral certificate and
67
65
  private key to .pem files in a given directory
@@ -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.13.0"
15
+ __version__ = "1.15.0"
@@ -1,22 +0,0 @@
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=dqUYJdUdusV_s2Xs0uWFmKqqoOU4mpHkrgl0qqbx9Vw,11713
4
- google/cloud/sql/connector/connection_info.py,sha256=exxhtb0-rk8J3OIotf8drEPMx8pke17hAX_Mbt7gOts,4336
5
- google/cloud/sql/connector/connector.py,sha256=NWyuwUoAsCuPxxzjB9OiD19tD1LdcQtqfbxJOLlS5-A,20731
6
- google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
7
- google/cloud/sql/connector/exceptions.py,sha256=OVenqBkkn98xFeytHuOXFTWa9R5bCmemBtbSIm8caoM,1665
8
- google/cloud/sql/connector/instance.py,sha256=EySszNNHMFjAFskf6-G6Upikvb-tYjEwXx9A_OJBl14,10136
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=CajL-MOcQBBRUkywowdHB3GNOr0WpQn37DXG7dRpkMk,5069
16
- google/cloud/sql/connector/utils.py,sha256=nQVZVfEijbO3XEj1oU68cR-u8Nx99rRM2a8aXaygOg0,3534
17
- google/cloud/sql/connector/version.py,sha256=EWwYfhVJ-gfn9Us5uJphJ-6hyY5hIvZ1lMkYVSAinqo,598
18
- cloud_sql_python_connector-1.13.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
19
- cloud_sql_python_connector-1.13.0.dist-info/METADATA,sha256=4D0iy9DJ08BNblMe2HaliEjMi6Wee_eQVQY_JT7nb4o,25950
20
- cloud_sql_python_connector-1.13.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
21
- cloud_sql_python_connector-1.13.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
22
- cloud_sql_python_connector-1.13.0.dist-info/RECORD,,