cloud-sql-python-connector 1.17.0.dev0__py3-none-any.whl → 1.18.1__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.1.dist-info}/METADATA +103 -130
- cloud_sql_python_connector-1.18.1.dist-info/RECORD +25 -0
- {cloud_sql_python_connector-1.17.0.dev0.dist-info → cloud_sql_python_connector-1.18.1.dist-info}/WHEEL +1 -1
- google/cloud/sql/connector/__init__.py +1 -1
- google/cloud/sql/connector/asyncpg.py +14 -13
- google/cloud/sql/connector/client.py +41 -37
- 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 +12 -19
- google/cloud/sql/connector/pymysql.py +11 -17
- google/cloud/sql/connector/pytds.py +11 -18
- google/cloud/sql/connector/rate_limiter.py +9 -17
- google/cloud/sql/connector/refresh_utils.py +12 -11
- google/cloud/sql/connector/resolver.py +11 -2
- google/cloud/sql/connector/utils.py +6 -8
- 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.1.dist-info/licenses}/LICENSE +0 -0
- {cloud_sql_python_connector-1.17.0.dev0.dist-info → cloud_sql_python_connector-1.18.1.dist-info}/top_level.txt +0 -0
|
@@ -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,32 +14,31 @@ 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
|
|
|
32
|
-
:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
cert and ephemeral cert.
|
|
29
|
+
Args:
|
|
30
|
+
ip_address (str): A string containing an IP address for the Cloud SQL
|
|
31
|
+
instance.
|
|
32
|
+
sock (ssl.SSLSocket): An SSLSocket object created from the Cloud SQL
|
|
33
|
+
server CA cert and ephemeral cert.
|
|
34
|
+
kwargs: Additional arguments to pass to the pg8000 connect method.
|
|
39
35
|
|
|
36
|
+
Returns:
|
|
37
|
+
pg8000.dbapi.Connection: A pg8000 connection to the Cloud SQL
|
|
38
|
+
instance.
|
|
40
39
|
|
|
41
|
-
:
|
|
42
|
-
|
|
40
|
+
Raises:
|
|
41
|
+
ImportError: The pg8000 module cannot be imported.
|
|
43
42
|
"""
|
|
44
43
|
try:
|
|
45
44
|
import pg8000
|
|
@@ -48,12 +47,6 @@ def connect(
|
|
|
48
47
|
'Unable to import module "pg8000." Please install and try again.'
|
|
49
48
|
)
|
|
50
49
|
|
|
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
50
|
user = kwargs.pop("user")
|
|
58
51
|
db = kwargs.pop("db")
|
|
59
52
|
passwd = kwargs.pop("password", None)
|
|
@@ -14,31 +14,30 @@ 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
|
|
|
32
|
-
:
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
Args:
|
|
30
|
+
ip_address (str): A string containing an IP address for the Cloud SQL
|
|
31
|
+
instance.
|
|
32
|
+
sock (ssl.SSLSocket): An SSLSocket object created from the Cloud SQL
|
|
33
|
+
server CA cert and ephemeral cert.
|
|
35
34
|
|
|
36
|
-
:
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
Returns:
|
|
36
|
+
pymysql.connections.Connection: A pymysql connection to the Cloud SQL
|
|
37
|
+
instance.
|
|
39
38
|
|
|
40
|
-
:
|
|
41
|
-
|
|
39
|
+
Raises:
|
|
40
|
+
ImportError: The pymysql module cannot be imported.
|
|
42
41
|
"""
|
|
43
42
|
try:
|
|
44
43
|
import pymysql
|
|
@@ -50,11 +49,6 @@ def connect(
|
|
|
50
49
|
# allow automatic IAM database authentication to not require password
|
|
51
50
|
kwargs["password"] = kwargs["password"] if "password" in kwargs else None
|
|
52
51
|
|
|
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
52
|
# pop timeout as timeout arg is called 'connect_timeout' for pymysql
|
|
59
53
|
timeout = kwargs.pop("timeout")
|
|
60
54
|
kwargs["connect_timeout"] = kwargs.get("connect_timeout", timeout)
|
|
@@ -15,32 +15,30 @@ 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
|
-
:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
:param ctx: An SSLContext object created from the Cloud SQL server CA
|
|
39
|
-
cert and ephemeral cert.
|
|
30
|
+
Args:
|
|
31
|
+
ip_address (str): A string containing an IP address for the Cloud SQL
|
|
32
|
+
instance.
|
|
33
|
+
sock (ssl.SSLSocket): An SSLSocket object created from the Cloud SQL
|
|
34
|
+
server CA cert and ephemeral cert.
|
|
40
35
|
|
|
36
|
+
Returns:
|
|
37
|
+
pytds.Connection: A pytds connection to the Cloud SQL
|
|
38
|
+
instance.
|
|
41
39
|
|
|
42
|
-
:
|
|
43
|
-
|
|
40
|
+
Raises:
|
|
41
|
+
ImportError: The pytds module cannot be imported.
|
|
44
42
|
"""
|
|
45
43
|
try:
|
|
46
44
|
import pytds
|
|
@@ -51,11 +49,6 @@ def connect(ip_address: str, ctx: ssl.SSLContext, **kwargs: Any) -> "pytds.Conne
|
|
|
51
49
|
|
|
52
50
|
db = kwargs.pop("db", None)
|
|
53
51
|
|
|
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
52
|
if kwargs.pop("active_directory_auth", False):
|
|
60
53
|
if platform.system() == "Windows":
|
|
61
54
|
# Ignore username and password if using active directory auth
|
|
@@ -19,24 +19,16 @@ from typing import Optional
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class AsyncRateLimiter(object):
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:type max_capacity: int
|
|
27
|
-
:param: max_capacity:
|
|
28
|
-
The maximum capacity of tokens the bucket will store at any one time.
|
|
29
|
-
Default: 1
|
|
30
|
-
|
|
31
|
-
:type rate: float
|
|
32
|
-
:param: rate:
|
|
33
|
-
The number of tokens that should be added per second.
|
|
34
|
-
|
|
35
|
-
:type loop: asyncio.AbstractEventLoop
|
|
36
|
-
:param: loop:
|
|
37
|
-
The event loop to use. If not provided, the default event loop will be used.
|
|
38
|
-
|
|
22
|
+
"""An asyncio-compatible rate limiter which uses the Token Bucket algorithm
|
|
23
|
+
(https://en.wikipedia.org/wiki/Token_bucket) to limit the number
|
|
24
|
+
of function calls over a time interval using an event queue.
|
|
39
25
|
|
|
26
|
+
Args:
|
|
27
|
+
max_capacity (int): The maximum capacity of tokens the bucket
|
|
28
|
+
will store at any one time. Default: 1
|
|
29
|
+
rate (float): The number of tokens that should be added per second.
|
|
30
|
+
loop (asyncio.AbstractEventLoop): The event loop to use.
|
|
31
|
+
If not provided, the default event loop will be used.
|
|
40
32
|
"""
|
|
41
33
|
|
|
42
34
|
def __init__(
|
|
@@ -44,8 +44,11 @@ def _seconds_until_refresh(
|
|
|
44
44
|
Usually the duration will be half of the time until certificate
|
|
45
45
|
expiration.
|
|
46
46
|
|
|
47
|
-
:
|
|
48
|
-
|
|
47
|
+
Args:
|
|
48
|
+
expiration (datetime.datetime): The expiration time of the certificate.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
int: Time in seconds to wait before performing next refresh.
|
|
49
52
|
"""
|
|
50
53
|
|
|
51
54
|
duration = int(
|
|
@@ -81,16 +84,14 @@ def _downscope_credentials(
|
|
|
81
84
|
) -> Credentials:
|
|
82
85
|
"""Generate a down-scoped credential.
|
|
83
86
|
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
:param scopes
|
|
90
|
-
List of Google scopes to include in down-scoped credentials object.
|
|
87
|
+
Args:
|
|
88
|
+
credentials (google.auth.credentials.Credentials):
|
|
89
|
+
Credentials object used to generate down-scoped credentials.
|
|
90
|
+
scopes (list[str]): List of Google scopes to
|
|
91
|
+
include in down-scoped credentials object.
|
|
91
92
|
|
|
92
|
-
:
|
|
93
|
-
|
|
93
|
+
Returns:
|
|
94
|
+
google.auth.credentials.Credentials: Down-scoped credentials object.
|
|
94
95
|
"""
|
|
95
96
|
# credentials sourced from a service account or metadata are children of
|
|
96
97
|
# Scoped class and are capable of being re-scoped
|