cloud-sql-python-connector 1.17.0.dev0__py3-none-any.whl → 1.18.0__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.
- {cloud_sql_python_connector-1.17.0.dev0.dist-info → cloud_sql_python_connector-1.18.0.dist-info}/METADATA +42 -4
- cloud_sql_python_connector-1.18.0.dist-info/RECORD +25 -0
- {cloud_sql_python_connector-1.17.0.dev0.dist-info → cloud_sql_python_connector-1.18.0.dist-info}/WHEEL +1 -1
- google/cloud/sql/connector/__init__.py +1 -1
- google/cloud/sql/connector/client.py +17 -4
- google/cloud/sql/connector/connection_info.py +22 -0
- google/cloud/sql/connector/connection_name.py +14 -0
- google/cloud/sql/connector/connector.py +50 -15
- google/cloud/sql/connector/exceptions.py +7 -0
- google/cloud/sql/connector/instance.py +12 -1
- google/cloud/sql/connector/lazy.py +13 -2
- google/cloud/sql/connector/monitored_cache.py +146 -0
- google/cloud/sql/connector/pg8000.py +3 -12
- google/cloud/sql/connector/pymysql.py +3 -11
- google/cloud/sql/connector/pytds.py +3 -11
- google/cloud/sql/connector/resolver.py +11 -2
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.17.0.dev0.dist-info/RECORD +0 -24
- {cloud_sql_python_connector-1.17.0.dev0.dist-info → cloud_sql_python_connector-1.18.0.dist-info/licenses}/LICENSE +0 -0
- {cloud_sql_python_connector-1.17.0.dev0.dist-info → cloud_sql_python_connector-1.18.0.dist-info}/top_level.txt +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.18.0
|
|
4
4
|
Summary: Google Cloud SQL Python Connector library
|
|
5
5
|
Author-email: Google LLC <googleapis-packages@google.com>
|
|
6
|
-
License: Apache
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/GoogleCloudPlatform/cloud-sql-python-connector
|
|
8
8
|
Project-URL: Repository, https://github.com/GoogleCloudPlatform/cloud-sql-python-connector.git
|
|
9
9
|
Project-URL: Issues, https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/issues
|
|
10
10
|
Project-URL: Changelog, https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/blob/main/CHANGELOG.md
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
13
|
Classifier: Programming Language :: Python
|
|
15
14
|
Classifier: Programming Language :: Python :: 3
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -36,6 +35,7 @@ Provides-Extra: pytds
|
|
|
36
35
|
Requires-Dist: python-tds>=1.15.0; extra == "pytds"
|
|
37
36
|
Provides-Extra: asyncpg
|
|
38
37
|
Requires-Dist: asyncpg>=0.30.0; extra == "asyncpg"
|
|
38
|
+
Dynamic: license-file
|
|
39
39
|
|
|
40
40
|
<p align="center">
|
|
41
41
|
<a href="https://cloud.google.com/blog/topics/developers-practitioners/how-connect-cloud-sql-using-python-easy-way">
|
|
@@ -467,6 +467,44 @@ with Connector(resolver=DnsResolver) as connector:
|
|
|
467
467
|
# ... use SQLAlchemy engine normally
|
|
468
468
|
```
|
|
469
469
|
|
|
470
|
+
### Automatic failover using DNS domain names
|
|
471
|
+
|
|
472
|
+
> [!NOTE]
|
|
473
|
+
>
|
|
474
|
+
> Usage of the `asyncpg` driver does not currently support automatic failover.
|
|
475
|
+
|
|
476
|
+
When the connector is configured using a domain name, the connector will
|
|
477
|
+
periodically check if the DNS record for an instance changes. When the connector
|
|
478
|
+
detects that the domain name refers to a different instance, the connector will
|
|
479
|
+
close all open connections to the old instance. Subsequent connection attempts
|
|
480
|
+
will be directed to the new instance.
|
|
481
|
+
|
|
482
|
+
For example: suppose application is configured to connect using the
|
|
483
|
+
domain name `prod-db.mycompany.example.com`. Initially the private DNS
|
|
484
|
+
zone has a TXT record with the value `my-project:region:my-instance`. The
|
|
485
|
+
application establishes connections to the `my-project:region:my-instance`
|
|
486
|
+
Cloud SQL instance.
|
|
487
|
+
|
|
488
|
+
Then, to reconfigure the application to use a different database
|
|
489
|
+
instance, change the value of the `prod-db.mycompany.example.com` DNS record
|
|
490
|
+
from `my-project:region:my-instance` to `my-project:other-region:my-instance-2`
|
|
491
|
+
|
|
492
|
+
The connector inside the application detects the change to this
|
|
493
|
+
DNS record. Now, when the application connects to its database using the
|
|
494
|
+
domain name `prod-db.mycompany.example.com`, it will connect to the
|
|
495
|
+
`my-project:other-region:my-instance-2` Cloud SQL instance.
|
|
496
|
+
|
|
497
|
+
The connector will automatically close all existing connections to
|
|
498
|
+
`my-project:region:my-instance`. This will force the connection pools to
|
|
499
|
+
establish new connections. Also, it may cause database queries in progress
|
|
500
|
+
to fail.
|
|
501
|
+
|
|
502
|
+
The connector will poll for changes to the DNS name every 30 seconds by default.
|
|
503
|
+
You may configure the frequency of the connections using the Connector's
|
|
504
|
+
`failover_period` argument (i.e. `Connector(failover_period=60`). When this is
|
|
505
|
+
set to 0, the connector will disable polling and only check if the DNS record
|
|
506
|
+
changed when it is creating a new connection.
|
|
507
|
+
|
|
470
508
|
### Using the Python Connector with Python Web Frameworks
|
|
471
509
|
|
|
472
510
|
The Python Connector can be used alongside popular Python web frameworks such
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
cloud_sql_python_connector-1.18.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
2
|
+
google/cloud/sql/connector/__init__.py,sha256=w6w35V-m2QlpcL-ShKd2BvNF-xXOwJ2FoidE7npPE7c,1148
|
|
3
|
+
google/cloud/sql/connector/asyncpg.py,sha256=HUOB0rRhr2-zMbygDtdWTDNlYqEPkN0ADqCIpM3waDc,1839
|
|
4
|
+
google/cloud/sql/connector/client.py,sha256=ITZNJsIEAeCSKgH2B-cFklg_zVEPaskIzo7t_Q85F4M,13124
|
|
5
|
+
google/cloud/sql/connector/connection_info.py,sha256=BSuW-ovtEUYTIvDzQkaE3GofCs3Xoi0TtztFaAq4Kvo,4868
|
|
6
|
+
google/cloud/sql/connector/connection_name.py,sha256=bI4M3Qa2mtBHyD08oF41FF5W9FBOXWu4dGGg50Na_A4,2606
|
|
7
|
+
google/cloud/sql/connector/connector.py,sha256=jukyYD75FElLvKgSHg4o91Wug21jIn87nJWNbeoTZUc,24183
|
|
8
|
+
google/cloud/sql/connector/enums.py,sha256=dVBS9eEWNrHtzze0AJlmtbdUogG3EocNWOQcI_8qPd0,2937
|
|
9
|
+
google/cloud/sql/connector/exceptions.py,sha256=QdQBzZ3t1RE5V744wW4sGaucWD2cEedB-dWNCkh8SAA,1974
|
|
10
|
+
google/cloud/sql/connector/instance.py,sha256=jDEQf-OXFzqKO9a4bnzRQ0qUyO3_xKuiEIRWc_7wpyE,8694
|
|
11
|
+
google/cloud/sql/connector/lazy.py,sha256=oB6F-O2fk8ZguY4J7Vrts91bqbVb6P5wj72Lf6UJIQA,4948
|
|
12
|
+
google/cloud/sql/connector/monitored_cache.py,sha256=LFYjX8hsdN_Dt0cMwLSWgAhWWQpZ35qA0CLlpc6xhsE,5587
|
|
13
|
+
google/cloud/sql/connector/pg8000.py,sha256=3p4CJixaiYrjUtG3DWTQrh6qCkmUEbQyoX25SQSvyX4,1594
|
|
14
|
+
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
google/cloud/sql/connector/pymysql.py,sha256=U1HEf0jBtnMiLjIuhp6ZGMJDg8IKQH24m2IRsy3k3tw,1886
|
|
16
|
+
google/cloud/sql/connector/pytds.py,sha256=s3_LvC-CfNwz7VKmmMVd0MwaC3vQigDLszsre3xLZC4,2233
|
|
17
|
+
google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
|
|
18
|
+
google/cloud/sql/connector/refresh_utils.py,sha256=HfuuLdlqg2IxusB4T3BMj-BJlGsXtHsrCX_Hp-eJ_j0,5063
|
|
19
|
+
google/cloud/sql/connector/resolver.py,sha256=1KYN0E-oTca8wRyW2nqCw56XRHtq-QGYpPvkk-UhjnY,3290
|
|
20
|
+
google/cloud/sql/connector/utils.py,sha256=7g1fX-JsZc7xOgTuxrAiroFHQIQk_BEJZNYaKOinwQY,3508
|
|
21
|
+
google/cloud/sql/connector/version.py,sha256=t8RiJsR1VPKxNkKjjeAvtxSWJtJ2z_lMfWNUWDFzRk8,598
|
|
22
|
+
cloud_sql_python_connector-1.18.0.dist-info/METADATA,sha256=pFnUd-5DK_0daO_QGMMb4zWVI0EGFMGNrMoprHhVVw8,32331
|
|
23
|
+
cloud_sql_python_connector-1.18.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
|
24
|
+
cloud_sql_python_connector-1.18.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
25
|
+
cloud_sql_python_connector-1.18.0.dist-info/RECORD,,
|
|
@@ -156,10 +156,23 @@ class CloudSQLClient:
|
|
|
156
156
|
# resolve dnsName into IP address for PSC
|
|
157
157
|
# Note that we have to check for PSC enablement also because CAS
|
|
158
158
|
# instances also set the dnsName field.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
if ret_dict.get("pscEnabled"):
|
|
160
|
+
# Find PSC instance DNS name in the dns_names field
|
|
161
|
+
psc_dns_names = [
|
|
162
|
+
d["name"]
|
|
163
|
+
for d in ret_dict.get("dnsNames", [])
|
|
164
|
+
if d["connectionType"] == "PRIVATE_SERVICE_CONNECT"
|
|
165
|
+
and d["dnsScope"] == "INSTANCE"
|
|
166
|
+
]
|
|
167
|
+
dns_name = psc_dns_names[0] if psc_dns_names else None
|
|
168
|
+
|
|
169
|
+
# Fall back do dns_name field if dns_names is not set
|
|
170
|
+
if dns_name is None:
|
|
171
|
+
dns_name = ret_dict.get("dnsName", None)
|
|
172
|
+
|
|
173
|
+
# Remove trailing period from DNS name. Required for SSL in Python
|
|
174
|
+
if dns_name:
|
|
175
|
+
ip_addresses["PSC"] = dns_name.rstrip(".")
|
|
163
176
|
|
|
164
177
|
return {
|
|
165
178
|
"ip_addresses": ip_addresses,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import abc
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
import logging
|
|
19
20
|
import ssl
|
|
@@ -34,6 +35,27 @@ if TYPE_CHECKING:
|
|
|
34
35
|
logger = logging.getLogger(name=__name__)
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
class ConnectionInfoCache(abc.ABC):
|
|
39
|
+
"""Abstract class for Connector connection info caches."""
|
|
40
|
+
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
async def connect_info(self) -> ConnectionInfo:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abc.abstractmethod
|
|
46
|
+
async def force_refresh(self) -> None:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abc.abstractmethod
|
|
50
|
+
async def close(self) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def closed(self) -> bool:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
37
59
|
@dataclass
|
|
38
60
|
class ConnectionInfo:
|
|
39
61
|
"""Contains all necessary information to connect securely to the
|
|
@@ -19,6 +19,10 @@ import re
|
|
|
19
19
|
# Additionally, we have to support legacy "domain-scoped" projects
|
|
20
20
|
# (e.g. "google.com:PROJECT")
|
|
21
21
|
CONN_NAME_REGEX = re.compile(("([^:]+(:[^:]+)?):([^:]+):([^:]+)"))
|
|
22
|
+
# The domain name pattern in accordance with RFC 1035, RFC 1123 and RFC 2181.
|
|
23
|
+
DOMAIN_NAME_REGEX = re.compile(
|
|
24
|
+
r"^(?:[_a-z0-9](?:[_a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?)?$"
|
|
25
|
+
)
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
@dataclass
|
|
@@ -38,6 +42,16 @@ class ConnectionName:
|
|
|
38
42
|
return f"{self.domain_name} -> {self.project}:{self.region}:{self.instance_name}"
|
|
39
43
|
return f"{self.project}:{self.region}:{self.instance_name}"
|
|
40
44
|
|
|
45
|
+
def get_connection_string(self) -> str:
|
|
46
|
+
"""Get the instance connection string for the Cloud SQL instance."""
|
|
47
|
+
return f"{self.project}:{self.region}:{self.instance_name}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _is_valid_domain(domain_name: str) -> bool:
|
|
51
|
+
if DOMAIN_NAME_REGEX.fullmatch(domain_name) is None:
|
|
52
|
+
return False
|
|
53
|
+
return True
|
|
54
|
+
|
|
41
55
|
|
|
42
56
|
def _parse_connection_name(connection_name: str) -> ConnectionName:
|
|
43
57
|
return _parse_connection_name_with_domain_name(connection_name, "")
|
|
@@ -20,9 +20,10 @@ import asyncio
|
|
|
20
20
|
from functools import partial
|
|
21
21
|
import logging
|
|
22
22
|
import os
|
|
23
|
+
import socket
|
|
23
24
|
from threading import Thread
|
|
24
25
|
from types import TracebackType
|
|
25
|
-
from typing import Any, Optional, Union
|
|
26
|
+
from typing import Any, Callable, Optional, Union
|
|
26
27
|
|
|
27
28
|
import google.auth
|
|
28
29
|
from google.auth.credentials import Credentials
|
|
@@ -35,6 +36,7 @@ from google.cloud.sql.connector.enums import IPTypes
|
|
|
35
36
|
from google.cloud.sql.connector.enums import RefreshStrategy
|
|
36
37
|
from google.cloud.sql.connector.instance import RefreshAheadCache
|
|
37
38
|
from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
39
|
+
from google.cloud.sql.connector.monitored_cache import MonitoredCache
|
|
38
40
|
import google.cloud.sql.connector.pg8000 as pg8000
|
|
39
41
|
import google.cloud.sql.connector.pymysql as pymysql
|
|
40
42
|
import google.cloud.sql.connector.pytds as pytds
|
|
@@ -46,6 +48,7 @@ from google.cloud.sql.connector.utils import generate_keys
|
|
|
46
48
|
logger = logging.getLogger(name=__name__)
|
|
47
49
|
|
|
48
50
|
ASYNC_DRIVERS = ["asyncpg"]
|
|
51
|
+
SERVER_PROXY_PORT = 3307
|
|
49
52
|
_DEFAULT_SCHEME = "https://"
|
|
50
53
|
_DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
|
|
51
54
|
_SQLADMIN_HOST_TEMPLATE = "sqladmin.{universe_domain}"
|
|
@@ -67,6 +70,7 @@ class Connector:
|
|
|
67
70
|
universe_domain: Optional[str] = None,
|
|
68
71
|
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
69
72
|
resolver: type[DefaultResolver] | type[DnsResolver] = DefaultResolver,
|
|
73
|
+
failover_period: int = 30,
|
|
70
74
|
) -> None:
|
|
71
75
|
"""Initializes a Connector instance.
|
|
72
76
|
|
|
@@ -114,6 +118,11 @@ class Connector:
|
|
|
114
118
|
name. To resolve a DNS record to an instance connection name, use
|
|
115
119
|
DnsResolver.
|
|
116
120
|
Default: DefaultResolver
|
|
121
|
+
|
|
122
|
+
failover_period (int): The time interval in seconds between each
|
|
123
|
+
attempt to check if a failover has occured for a given instance.
|
|
124
|
+
Must be used with `resolver=DnsResolver` to have any effect.
|
|
125
|
+
Default: 30
|
|
117
126
|
"""
|
|
118
127
|
# if refresh_strategy is str, convert to RefreshStrategy enum
|
|
119
128
|
if isinstance(refresh_strategy, str):
|
|
@@ -143,9 +152,7 @@ class Connector:
|
|
|
143
152
|
)
|
|
144
153
|
# initialize dict to store caches, key is a tuple consisting of instance
|
|
145
154
|
# connection name string and enable_iam_auth boolean flag
|
|
146
|
-
self._cache: dict[
|
|
147
|
-
tuple[str, bool], Union[RefreshAheadCache, LazyRefreshCache]
|
|
148
|
-
] = {}
|
|
155
|
+
self._cache: dict[tuple[str, bool], MonitoredCache] = {}
|
|
149
156
|
self._client: Optional[CloudSQLClient] = None
|
|
150
157
|
|
|
151
158
|
# initialize credentials
|
|
@@ -167,6 +174,7 @@ class Connector:
|
|
|
167
174
|
self._enable_iam_auth = enable_iam_auth
|
|
168
175
|
self._user_agent = user_agent
|
|
169
176
|
self._resolver = resolver()
|
|
177
|
+
self._failover_period = failover_period
|
|
170
178
|
# if ip_type is str, convert to IPTypes enum
|
|
171
179
|
if isinstance(ip_type, str):
|
|
172
180
|
ip_type = IPTypes._from_str(ip_type)
|
|
@@ -285,15 +293,19 @@ class Connector:
|
|
|
285
293
|
driver=driver,
|
|
286
294
|
)
|
|
287
295
|
enable_iam_auth = kwargs.pop("enable_iam_auth", self._enable_iam_auth)
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
|
|
297
|
+
conn_name = await self._resolver.resolve(instance_connection_string)
|
|
298
|
+
# Cache entry must exist and not be closed
|
|
299
|
+
if (str(conn_name), enable_iam_auth) in self._cache and not self._cache[
|
|
300
|
+
(str(conn_name), enable_iam_auth)
|
|
301
|
+
].closed:
|
|
302
|
+
monitored_cache = self._cache[(str(conn_name), enable_iam_auth)]
|
|
290
303
|
else:
|
|
291
|
-
conn_name = await self._resolver.resolve(instance_connection_string)
|
|
292
304
|
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
293
305
|
logger.debug(
|
|
294
306
|
f"['{conn_name}']: Refresh strategy is set to lazy refresh"
|
|
295
307
|
)
|
|
296
|
-
cache = LazyRefreshCache(
|
|
308
|
+
cache: Union[LazyRefreshCache, RefreshAheadCache] = LazyRefreshCache(
|
|
297
309
|
conn_name,
|
|
298
310
|
self._client,
|
|
299
311
|
self._keys,
|
|
@@ -309,8 +321,14 @@ class Connector:
|
|
|
309
321
|
self._keys,
|
|
310
322
|
enable_iam_auth,
|
|
311
323
|
)
|
|
324
|
+
# wrap cache as a MonitoredCache
|
|
325
|
+
monitored_cache = MonitoredCache(
|
|
326
|
+
cache,
|
|
327
|
+
self._failover_period,
|
|
328
|
+
self._resolver,
|
|
329
|
+
)
|
|
312
330
|
logger.debug(f"['{conn_name}']: Connection info added to cache")
|
|
313
|
-
self._cache[(
|
|
331
|
+
self._cache[(str(conn_name), enable_iam_auth)] = monitored_cache
|
|
314
332
|
|
|
315
333
|
connect_func = {
|
|
316
334
|
"pymysql": pymysql.connect,
|
|
@@ -321,7 +339,7 @@ class Connector:
|
|
|
321
339
|
|
|
322
340
|
# only accept supported database drivers
|
|
323
341
|
try:
|
|
324
|
-
connector = connect_func[driver]
|
|
342
|
+
connector: Callable = connect_func[driver] # type: ignore
|
|
325
343
|
except KeyError:
|
|
326
344
|
raise KeyError(f"Driver '{driver}' is not supported.")
|
|
327
345
|
|
|
@@ -339,14 +357,14 @@ class Connector:
|
|
|
339
357
|
|
|
340
358
|
# attempt to get connection info for Cloud SQL instance
|
|
341
359
|
try:
|
|
342
|
-
conn_info = await
|
|
360
|
+
conn_info = await monitored_cache.connect_info()
|
|
343
361
|
# validate driver matches intended database engine
|
|
344
362
|
DriverMapping.validate_engine(driver, conn_info.database_version)
|
|
345
363
|
ip_address = conn_info.get_preferred_ip(ip_type)
|
|
346
364
|
except Exception:
|
|
347
365
|
# with an error from Cloud SQL Admin API call or IP type, invalidate
|
|
348
366
|
# the cache and re-raise the error
|
|
349
|
-
await self._remove_cached(
|
|
367
|
+
await self._remove_cached(str(conn_name), enable_iam_auth)
|
|
350
368
|
raise
|
|
351
369
|
logger.debug(f"['{conn_info.conn_name}']: Connecting to {ip_address}:3307")
|
|
352
370
|
# format `user` param for automatic IAM database authn
|
|
@@ -367,18 +385,28 @@ class Connector:
|
|
|
367
385
|
await conn_info.create_ssl_context(enable_iam_auth),
|
|
368
386
|
**kwargs,
|
|
369
387
|
)
|
|
370
|
-
#
|
|
388
|
+
# Create socket with SSLContext for sync drivers
|
|
389
|
+
ctx = await conn_info.create_ssl_context(enable_iam_auth)
|
|
390
|
+
sock = ctx.wrap_socket(
|
|
391
|
+
socket.create_connection((ip_address, SERVER_PROXY_PORT)),
|
|
392
|
+
server_hostname=ip_address,
|
|
393
|
+
)
|
|
394
|
+
# If this connection was opened using a domain name, then store it
|
|
395
|
+
# for later in case we need to forcibly close it on failover.
|
|
396
|
+
if conn_info.conn_name.domain_name:
|
|
397
|
+
monitored_cache.sockets.append(sock)
|
|
398
|
+
# Synchronous drivers are blocking and run using executor
|
|
371
399
|
connect_partial = partial(
|
|
372
400
|
connector,
|
|
373
401
|
ip_address,
|
|
374
|
-
|
|
402
|
+
sock,
|
|
375
403
|
**kwargs,
|
|
376
404
|
)
|
|
377
405
|
return await self._loop.run_in_executor(None, connect_partial)
|
|
378
406
|
|
|
379
407
|
except Exception:
|
|
380
408
|
# with any exception, we attempt a force refresh, then throw the error
|
|
381
|
-
await
|
|
409
|
+
await monitored_cache.force_refresh()
|
|
382
410
|
raise
|
|
383
411
|
|
|
384
412
|
async def _remove_cached(
|
|
@@ -456,6 +484,7 @@ async def create_async_connector(
|
|
|
456
484
|
universe_domain: Optional[str] = None,
|
|
457
485
|
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
458
486
|
resolver: type[DefaultResolver] | type[DnsResolver] = DefaultResolver,
|
|
487
|
+
failover_period: int = 30,
|
|
459
488
|
) -> Connector:
|
|
460
489
|
"""Helper function to create Connector object for asyncio connections.
|
|
461
490
|
|
|
@@ -507,6 +536,11 @@ async def create_async_connector(
|
|
|
507
536
|
DnsResolver.
|
|
508
537
|
Default: DefaultResolver
|
|
509
538
|
|
|
539
|
+
failover_period (int): The time interval in seconds between each
|
|
540
|
+
attempt to check if a failover has occured for a given instance.
|
|
541
|
+
Must be used with `resolver=DnsResolver` to have any effect.
|
|
542
|
+
Default: 30
|
|
543
|
+
|
|
510
544
|
Returns:
|
|
511
545
|
A Connector instance configured with running event loop.
|
|
512
546
|
"""
|
|
@@ -525,4 +559,5 @@ async def create_async_connector(
|
|
|
525
559
|
universe_domain=universe_domain,
|
|
526
560
|
refresh_strategy=refresh_strategy,
|
|
527
561
|
resolver=resolver,
|
|
562
|
+
failover_period=failover_period,
|
|
528
563
|
)
|
|
@@ -77,3 +77,10 @@ class DnsResolutionError(Exception):
|
|
|
77
77
|
Exception to be raised when an instance connection name can not be resolved
|
|
78
78
|
from a DNS record.
|
|
79
79
|
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CacheClosedError(Exception):
|
|
83
|
+
"""
|
|
84
|
+
Exception to be raised when a ConnectionInfoCache can not be accessed after
|
|
85
|
+
it is closed.
|
|
86
|
+
"""
|
|
@@ -24,6 +24,7 @@ import logging
|
|
|
24
24
|
|
|
25
25
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
26
26
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
27
|
+
from google.cloud.sql.connector.connection_info import ConnectionInfoCache
|
|
27
28
|
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
28
29
|
from google.cloud.sql.connector.exceptions import RefreshNotValidError
|
|
29
30
|
from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
|
|
@@ -35,7 +36,7 @@ logger = logging.getLogger(name=__name__)
|
|
|
35
36
|
APPLICATION_NAME = "cloud-sql-python-connector"
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
class RefreshAheadCache:
|
|
39
|
+
class RefreshAheadCache(ConnectionInfoCache):
|
|
39
40
|
"""Cache that refreshes connection info in the background prior to expiration.
|
|
40
41
|
|
|
41
42
|
Background tasks are used to schedule refresh attempts to get a new
|
|
@@ -74,6 +75,15 @@ class RefreshAheadCache:
|
|
|
74
75
|
self._refresh_in_progress = asyncio.locks.Event()
|
|
75
76
|
self._current: asyncio.Task = self._schedule_refresh(0)
|
|
76
77
|
self._next: asyncio.Task = self._current
|
|
78
|
+
self._closed = False
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def conn_name(self) -> ConnectionName:
|
|
82
|
+
return self._conn_name
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def closed(self) -> bool:
|
|
86
|
+
return self._closed
|
|
77
87
|
|
|
78
88
|
async def force_refresh(self) -> None:
|
|
79
89
|
"""
|
|
@@ -212,3 +222,4 @@ class RefreshAheadCache:
|
|
|
212
222
|
# gracefully wait for tasks to cancel
|
|
213
223
|
tasks = asyncio.gather(self._current, self._next, return_exceptions=True)
|
|
214
224
|
await asyncio.wait_for(tasks, timeout=2.0)
|
|
225
|
+
self._closed = True
|
|
@@ -21,13 +21,14 @@ 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_info import ConnectionInfoCache
|
|
24
25
|
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
25
26
|
from google.cloud.sql.connector.refresh_utils import _refresh_buffer
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(name=__name__)
|
|
28
29
|
|
|
29
30
|
|
|
30
|
-
class LazyRefreshCache:
|
|
31
|
+
class LazyRefreshCache(ConnectionInfoCache):
|
|
31
32
|
"""Cache that refreshes connection info when a caller requests a connection.
|
|
32
33
|
|
|
33
34
|
Only refreshes the cache when a new connection is requested and the current
|
|
@@ -62,6 +63,15 @@ class LazyRefreshCache:
|
|
|
62
63
|
self._lock = asyncio.Lock()
|
|
63
64
|
self._cached: Optional[ConnectionInfo] = None
|
|
64
65
|
self._needs_refresh = False
|
|
66
|
+
self._closed = False
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def conn_name(self) -> ConnectionName:
|
|
70
|
+
return self._conn_name
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def closed(self) -> bool:
|
|
74
|
+
return self._closed
|
|
65
75
|
|
|
66
76
|
async def force_refresh(self) -> None:
|
|
67
77
|
"""
|
|
@@ -121,4 +131,5 @@ class LazyRefreshCache:
|
|
|
121
131
|
"""Close is a no-op and provided purely for a consistent interface with
|
|
122
132
|
other cache types.
|
|
123
133
|
"""
|
|
124
|
-
|
|
134
|
+
self._closed = True
|
|
135
|
+
return
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
import logging
|
|
17
|
+
import ssl
|
|
18
|
+
from typing import Any, Callable, Optional, Union
|
|
19
|
+
|
|
20
|
+
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
21
|
+
from google.cloud.sql.connector.connection_info import ConnectionInfoCache
|
|
22
|
+
from google.cloud.sql.connector.exceptions import CacheClosedError
|
|
23
|
+
from google.cloud.sql.connector.instance import RefreshAheadCache
|
|
24
|
+
from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
25
|
+
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
26
|
+
from google.cloud.sql.connector.resolver import DnsResolver
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(name=__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MonitoredCache(ConnectionInfoCache):
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
cache: Union[RefreshAheadCache, LazyRefreshCache],
|
|
35
|
+
failover_period: int,
|
|
36
|
+
resolver: Union[DefaultResolver, DnsResolver],
|
|
37
|
+
) -> None:
|
|
38
|
+
self.resolver = resolver
|
|
39
|
+
self.cache = cache
|
|
40
|
+
self.domain_name_ticker: Optional[asyncio.Task] = None
|
|
41
|
+
self.sockets: list[ssl.SSLSocket] = []
|
|
42
|
+
|
|
43
|
+
# If domain name is configured for instance and failover period is set,
|
|
44
|
+
# poll for DNS record changes.
|
|
45
|
+
if self.cache.conn_name.domain_name and failover_period > 0:
|
|
46
|
+
self.domain_name_ticker = asyncio.create_task(
|
|
47
|
+
ticker(failover_period, self._check_domain_name)
|
|
48
|
+
)
|
|
49
|
+
logger.debug(
|
|
50
|
+
f"['{self.cache.conn_name}']: Configured polling of domain "
|
|
51
|
+
f"name with failover period of {failover_period} seconds."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def closed(self) -> bool:
|
|
56
|
+
return self.cache.closed
|
|
57
|
+
|
|
58
|
+
def _purge_closed_sockets(self) -> None:
|
|
59
|
+
"""Remove closed sockets from monitored cache.
|
|
60
|
+
|
|
61
|
+
If a socket is closed by the database driver we should remove it from
|
|
62
|
+
list of sockets.
|
|
63
|
+
"""
|
|
64
|
+
open_sockets = []
|
|
65
|
+
for socket in self.sockets:
|
|
66
|
+
# Check fileno for if socket is closed. Will return
|
|
67
|
+
# -1 on failure, which will be used to signal socket closed.
|
|
68
|
+
if socket.fileno() != -1:
|
|
69
|
+
open_sockets.append(socket)
|
|
70
|
+
self.sockets = open_sockets
|
|
71
|
+
|
|
72
|
+
async def _check_domain_name(self) -> None:
|
|
73
|
+
# remove any closed connections from cache
|
|
74
|
+
self._purge_closed_sockets()
|
|
75
|
+
try:
|
|
76
|
+
# Resolve domain name and see if Cloud SQL instance connection name
|
|
77
|
+
# has changed. If it has, close all connections.
|
|
78
|
+
new_conn_name = await self.resolver.resolve(
|
|
79
|
+
self.cache.conn_name.domain_name
|
|
80
|
+
)
|
|
81
|
+
if new_conn_name != self.cache.conn_name:
|
|
82
|
+
logger.debug(
|
|
83
|
+
f"['{self.cache.conn_name}']: Cloud SQL instance changed "
|
|
84
|
+
f"from {self.cache.conn_name.get_connection_string()} to "
|
|
85
|
+
f"{new_conn_name.get_connection_string()}, closing all "
|
|
86
|
+
"connections!"
|
|
87
|
+
)
|
|
88
|
+
await self.close()
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
# Domain name checks should not be fatal, log error and continue.
|
|
92
|
+
logger.debug(
|
|
93
|
+
f"['{self.cache.conn_name}']: Unable to check domain name, "
|
|
94
|
+
f"domain name {self.cache.conn_name.domain_name} did not "
|
|
95
|
+
f"resolve: {e}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def connect_info(self) -> ConnectionInfo:
|
|
99
|
+
if self.closed:
|
|
100
|
+
raise CacheClosedError(
|
|
101
|
+
"Can not get connection info, cache has already been closed."
|
|
102
|
+
)
|
|
103
|
+
return await self.cache.connect_info()
|
|
104
|
+
|
|
105
|
+
async def force_refresh(self) -> None:
|
|
106
|
+
# if cache is closed do not refresh
|
|
107
|
+
if self.closed:
|
|
108
|
+
return
|
|
109
|
+
return await self.cache.force_refresh()
|
|
110
|
+
|
|
111
|
+
async def close(self) -> None:
|
|
112
|
+
# Cancel domain name ticker task.
|
|
113
|
+
if self.domain_name_ticker:
|
|
114
|
+
self.domain_name_ticker.cancel()
|
|
115
|
+
try:
|
|
116
|
+
await self.domain_name_ticker
|
|
117
|
+
except asyncio.CancelledError:
|
|
118
|
+
logger.debug(
|
|
119
|
+
f"['{self.cache.conn_name}']: Cancelled domain name polling task."
|
|
120
|
+
)
|
|
121
|
+
finally:
|
|
122
|
+
self.domain_name_ticker = None
|
|
123
|
+
# If cache is already closed, no further work.
|
|
124
|
+
if self.closed:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Close underyling ConnectionInfoCache
|
|
128
|
+
await self.cache.close()
|
|
129
|
+
|
|
130
|
+
# Close any still open sockets
|
|
131
|
+
for socket in self.sockets:
|
|
132
|
+
# Check fileno for if socket is closed. Will return
|
|
133
|
+
# -1 on failure, which will be used to signal socket closed.
|
|
134
|
+
if socket.fileno() != -1:
|
|
135
|
+
socket.close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def ticker(interval: int, function: Callable, *args: Any, **kwargs: Any) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Ticker function to sleep for specified interval and then schedule call
|
|
141
|
+
to given function.
|
|
142
|
+
"""
|
|
143
|
+
while True:
|
|
144
|
+
# Sleep for interval and then schedule task
|
|
145
|
+
await asyncio.sleep(interval)
|
|
146
|
+
asyncio.create_task(function(*args, **kwargs))
|
|
@@ -14,18 +14,15 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
import socket
|
|
18
17
|
import ssl
|
|
19
18
|
from typing import Any, TYPE_CHECKING
|
|
20
19
|
|
|
21
|
-
SERVER_PROXY_PORT = 3307
|
|
22
|
-
|
|
23
20
|
if TYPE_CHECKING:
|
|
24
21
|
import pg8000
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
def connect(
|
|
28
|
-
ip_address: str,
|
|
25
|
+
ip_address: str, sock: ssl.SSLSocket, **kwargs: Any
|
|
29
26
|
) -> "pg8000.dbapi.Connection":
|
|
30
27
|
"""Helper function to create a pg8000 DB-API connection object.
|
|
31
28
|
|
|
@@ -33,8 +30,8 @@ def connect(
|
|
|
33
30
|
:param ip_address: A string containing an IP address for the Cloud SQL
|
|
34
31
|
instance.
|
|
35
32
|
|
|
36
|
-
:type
|
|
37
|
-
:param
|
|
33
|
+
:type sock: ssl.SSLSocket
|
|
34
|
+
:param sock: An SSLSocket object created from the Cloud SQL server CA
|
|
38
35
|
cert and ephemeral cert.
|
|
39
36
|
|
|
40
37
|
|
|
@@ -48,12 +45,6 @@ def connect(
|
|
|
48
45
|
'Unable to import module "pg8000." Please install and try again.'
|
|
49
46
|
)
|
|
50
47
|
|
|
51
|
-
# Create socket and wrap with context.
|
|
52
|
-
sock = ctx.wrap_socket(
|
|
53
|
-
socket.create_connection((ip_address, SERVER_PROXY_PORT)),
|
|
54
|
-
server_hostname=ip_address,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
48
|
user = kwargs.pop("user")
|
|
58
49
|
db = kwargs.pop("db")
|
|
59
50
|
passwd = kwargs.pop("password", None)
|
|
@@ -14,18 +14,15 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
import socket
|
|
18
17
|
import ssl
|
|
19
18
|
from typing import Any, TYPE_CHECKING
|
|
20
19
|
|
|
21
|
-
SERVER_PROXY_PORT = 3307
|
|
22
|
-
|
|
23
20
|
if TYPE_CHECKING:
|
|
24
21
|
import pymysql
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
def connect(
|
|
28
|
-
ip_address: str,
|
|
25
|
+
ip_address: str, sock: ssl.SSLSocket, **kwargs: Any
|
|
29
26
|
) -> "pymysql.connections.Connection":
|
|
30
27
|
"""Helper function to create a pymysql DB-API connection object.
|
|
31
28
|
|
|
@@ -33,8 +30,8 @@ def connect(
|
|
|
33
30
|
:param ip_address: A string containing an IP address for the Cloud SQL
|
|
34
31
|
instance.
|
|
35
32
|
|
|
36
|
-
:type
|
|
37
|
-
:param
|
|
33
|
+
:type sock: ssl.SSLSocket
|
|
34
|
+
:param sock: An SSLSocket object created from the Cloud SQL server CA
|
|
38
35
|
cert and ephemeral cert.
|
|
39
36
|
|
|
40
37
|
:rtype: pymysql.Connection
|
|
@@ -50,11 +47,6 @@ def connect(
|
|
|
50
47
|
# allow automatic IAM database authentication to not require password
|
|
51
48
|
kwargs["password"] = kwargs["password"] if "password" in kwargs else None
|
|
52
49
|
|
|
53
|
-
# Create socket and wrap with context.
|
|
54
|
-
sock = ctx.wrap_socket(
|
|
55
|
-
socket.create_connection((ip_address, SERVER_PROXY_PORT)),
|
|
56
|
-
server_hostname=ip_address,
|
|
57
|
-
)
|
|
58
50
|
# pop timeout as timeout arg is called 'connect_timeout' for pymysql
|
|
59
51
|
timeout = kwargs.pop("timeout")
|
|
60
52
|
kwargs["connect_timeout"] = kwargs.get("connect_timeout", timeout)
|
|
@@ -15,27 +15,24 @@ limitations under the License.
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import platform
|
|
18
|
-
import socket
|
|
19
18
|
import ssl
|
|
20
19
|
from typing import Any, TYPE_CHECKING
|
|
21
20
|
|
|
22
21
|
from google.cloud.sql.connector.exceptions import PlatformNotSupportedError
|
|
23
22
|
|
|
24
|
-
SERVER_PROXY_PORT = 3307
|
|
25
|
-
|
|
26
23
|
if TYPE_CHECKING:
|
|
27
24
|
import pytds
|
|
28
25
|
|
|
29
26
|
|
|
30
|
-
def connect(ip_address: str,
|
|
27
|
+
def connect(ip_address: str, sock: ssl.SSLSocket, **kwargs: Any) -> "pytds.Connection":
|
|
31
28
|
"""Helper function to create a pytds DB-API connection object.
|
|
32
29
|
|
|
33
30
|
:type ip_address: str
|
|
34
31
|
:param ip_address: A string containing an IP address for the Cloud SQL
|
|
35
32
|
instance.
|
|
36
33
|
|
|
37
|
-
:type
|
|
38
|
-
:param
|
|
34
|
+
:type sock: ssl.SSLSocket
|
|
35
|
+
:param sock: An SSLSocket object created from the Cloud SQL server CA
|
|
39
36
|
cert and ephemeral cert.
|
|
40
37
|
|
|
41
38
|
|
|
@@ -51,11 +48,6 @@ def connect(ip_address: str, ctx: ssl.SSLContext, **kwargs: Any) -> "pytds.Conne
|
|
|
51
48
|
|
|
52
49
|
db = kwargs.pop("db", None)
|
|
53
50
|
|
|
54
|
-
# Create socket and wrap with context.
|
|
55
|
-
sock = ctx.wrap_socket(
|
|
56
|
-
socket.create_connection((ip_address, SERVER_PROXY_PORT)),
|
|
57
|
-
server_hostname=ip_address,
|
|
58
|
-
)
|
|
59
51
|
if kwargs.pop("active_directory_auth", False):
|
|
60
52
|
if platform.system() == "Windows":
|
|
61
53
|
# Ignore username and password if using active directory auth
|
|
@@ -17,6 +17,7 @@ import dns.asyncresolver
|
|
|
17
17
|
from google.cloud.sql.connector.connection_name import (
|
|
18
18
|
_parse_connection_name_with_domain_name,
|
|
19
19
|
)
|
|
20
|
+
from google.cloud.sql.connector.connection_name import _is_valid_domain
|
|
20
21
|
from google.cloud.sql.connector.connection_name import _parse_connection_name
|
|
21
22
|
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
22
23
|
from google.cloud.sql.connector.exceptions import DnsResolutionError
|
|
@@ -40,8 +41,16 @@ class DnsResolver(dns.asyncresolver.Resolver):
|
|
|
40
41
|
conn_name = _parse_connection_name(dns)
|
|
41
42
|
except ValueError:
|
|
42
43
|
# The connection name was not project:region:instance format.
|
|
43
|
-
#
|
|
44
|
-
|
|
44
|
+
# Check if connection name is a valid DNS domain name
|
|
45
|
+
if _is_valid_domain(dns):
|
|
46
|
+
# Attempt to query a TXT record to get connection name.
|
|
47
|
+
conn_name = await self.query_dns(dns)
|
|
48
|
+
else:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"Arg `instance_connection_string` must have "
|
|
51
|
+
"format: PROJECT:REGION:INSTANCE or be a valid DNS domain "
|
|
52
|
+
f"name, got {dns}."
|
|
53
|
+
)
|
|
45
54
|
return conn_name
|
|
46
55
|
|
|
47
56
|
async def query_dns(self, dns: str) -> ConnectionName:
|
|
@@ -1,24 +0,0 @@
|
|
|
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=emahfH9ZSrbooFBV-m6gcwGJ7yYQwaUJuCJPz0DHfHA,22547
|
|
7
|
-
google/cloud/sql/connector/enums.py,sha256=dVBS9eEWNrHtzze0AJlmtbdUogG3EocNWOQcI_8qPd0,2937
|
|
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=YfNpSkpcPhRwrFt2dV7ut5xGe0KjgvNinweha483X2w,603
|
|
20
|
-
cloud_sql_python_connector-1.17.0.dev0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
21
|
-
cloud_sql_python_connector-1.17.0.dev0.dist-info/METADATA,sha256=qlls3nyZF0ZxHsLxEvorF9ukUEyY5pQpWUhvbQ0H2tk,30494
|
|
22
|
-
cloud_sql_python_connector-1.17.0.dev0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
23
|
-
cloud_sql_python_connector-1.17.0.dev0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
24
|
-
cloud_sql_python_connector-1.17.0.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|