cloud-sql-python-connector 1.20.4__py3-none-any.whl → 1.22.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.20.4.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/METADATA +25 -4
- cloud_sql_python_connector-1.22.0.dist-info/RECORD +26 -0
- {cloud_sql_python_connector-1.20.4.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/WHEEL +1 -1
- google/cloud/sql/connector/__init__.py +2 -2
- google/cloud/sql/connector/asyncpg.py +3 -1
- google/cloud/sql/connector/client.py +61 -19
- google/cloud/sql/connector/connection_info.py +15 -5
- google/cloud/sql/connector/connection_name.py +2 -4
- google/cloud/sql/connector/connector.py +95 -62
- google/cloud/sql/connector/enums.py +2 -1
- google/cloud/sql/connector/exceptions.py +6 -6
- google/cloud/sql/connector/instance.py +1 -1
- google/cloud/sql/connector/lazy.py +5 -5
- google/cloud/sql/connector/monitored_cache.py +42 -11
- google/cloud/sql/connector/pg8000.py +3 -1
- google/cloud/sql/connector/psycopg.py +235 -0
- google/cloud/sql/connector/pymysql.py +8 -1
- google/cloud/sql/connector/pytds.py +1 -1
- google/cloud/sql/connector/rate_limiter.py +3 -3
- google/cloud/sql/connector/refresh_utils.py +4 -2
- google/cloud/sql/connector/resolver.py +118 -36
- google/cloud/sql/connector/utils.py +40 -8
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.20.4.dist-info/RECORD +0 -25
- {cloud_sql_python_connector-1.20.4.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/licenses/LICENSE +0 -0
- {cloud_sql_python_connector-1.20.4.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/top_level.txt +0 -0
|
@@ -61,7 +61,8 @@ class DriverMapping(Enum):
|
|
|
61
61
|
"""Maps a given database driver to it's corresponding database engine."""
|
|
62
62
|
|
|
63
63
|
ASYNCPG = "POSTGRES"
|
|
64
|
-
PG8000 = "POSTGRES"
|
|
64
|
+
PG8000 = "POSTGRES" # noqa: PIE796
|
|
65
|
+
PSYCOPG = "POSTGRES" # noqa: PIE796
|
|
65
66
|
PYMYSQL = "MYSQL"
|
|
66
67
|
PYTDS = "SQLSERVER"
|
|
67
68
|
|
|
@@ -21,7 +21,6 @@ class ConnectorLoopError(Exception):
|
|
|
21
21
|
in an invalid state (event loop in current thread).
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
pass
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class TLSVersionError(Exception):
|
|
@@ -29,7 +28,6 @@ class TLSVersionError(Exception):
|
|
|
29
28
|
Raised when the required TLS protocol version is not supported.
|
|
30
29
|
"""
|
|
31
30
|
|
|
32
|
-
pass
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
class CloudSQLIPTypeError(Exception):
|
|
@@ -37,7 +35,6 @@ class CloudSQLIPTypeError(Exception):
|
|
|
37
35
|
Raised when IP address for the preferred IP type is not found.
|
|
38
36
|
"""
|
|
39
37
|
|
|
40
|
-
pass
|
|
41
38
|
|
|
42
39
|
|
|
43
40
|
class PlatformNotSupportedError(Exception):
|
|
@@ -45,7 +42,6 @@ class PlatformNotSupportedError(Exception):
|
|
|
45
42
|
Raised when a feature is not supported on the current platform.
|
|
46
43
|
"""
|
|
47
44
|
|
|
48
|
-
pass
|
|
49
45
|
|
|
50
46
|
|
|
51
47
|
class AutoIAMAuthNotSupported(Exception):
|
|
@@ -54,7 +50,6 @@ class AutoIAMAuthNotSupported(Exception):
|
|
|
54
50
|
supported with database engine version.
|
|
55
51
|
"""
|
|
56
52
|
|
|
57
|
-
pass
|
|
58
53
|
|
|
59
54
|
|
|
60
55
|
class RefreshNotValidError(Exception):
|
|
@@ -62,7 +57,6 @@ class RefreshNotValidError(Exception):
|
|
|
62
57
|
Exception to be raised when the task returned from refresh is not valid.
|
|
63
58
|
"""
|
|
64
59
|
|
|
65
|
-
pass
|
|
66
60
|
|
|
67
61
|
|
|
68
62
|
class IncompatibleDriverError(Exception):
|
|
@@ -79,6 +73,12 @@ class DnsResolutionError(Exception):
|
|
|
79
73
|
"""
|
|
80
74
|
|
|
81
75
|
|
|
76
|
+
class DnsLoopError(DnsResolutionError):
|
|
77
|
+
"""
|
|
78
|
+
Exception to be raised when a CNAME loop is detected during DNS resolution.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
82
|
class CacheClosedError(Exception):
|
|
83
83
|
"""
|
|
84
84
|
Exception to be raised when a ConnectionInfoCache can not be accessed after
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
15
17
|
import asyncio
|
|
16
18
|
from datetime import datetime
|
|
17
19
|
from datetime import timedelta
|
|
18
20
|
from datetime import timezone
|
|
19
21
|
import logging
|
|
20
|
-
from typing import Optional
|
|
21
22
|
|
|
22
23
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
23
24
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
@@ -61,7 +62,7 @@ class LazyRefreshCache(ConnectionInfoCache):
|
|
|
61
62
|
self._keys = keys
|
|
62
63
|
self._client = client
|
|
63
64
|
self._lock = asyncio.Lock()
|
|
64
|
-
self._cached:
|
|
65
|
+
self._cached: ConnectionInfo | None = None
|
|
65
66
|
self._needs_refresh = False
|
|
66
67
|
self._closed = False
|
|
67
68
|
|
|
@@ -112,7 +113,7 @@ class LazyRefreshCache(ConnectionInfoCache):
|
|
|
112
113
|
except Exception as e:
|
|
113
114
|
logger.debug(
|
|
114
115
|
f"['{self._conn_name}']: Connection info "
|
|
115
|
-
f"refresh operation failed: {
|
|
116
|
+
f"refresh operation failed: {e!s}"
|
|
116
117
|
)
|
|
117
118
|
raise
|
|
118
119
|
logger.debug(
|
|
@@ -121,7 +122,7 @@ class LazyRefreshCache(ConnectionInfoCache):
|
|
|
121
122
|
)
|
|
122
123
|
logger.debug(
|
|
123
124
|
f"['{self._conn_name}']: Current certificate "
|
|
124
|
-
f"expiration = {
|
|
125
|
+
f"expiration = {conn_info.expiration!s}"
|
|
125
126
|
)
|
|
126
127
|
self._cached = conn_info
|
|
127
128
|
self._needs_refresh = False
|
|
@@ -132,4 +133,3 @@ class LazyRefreshCache(ConnectionInfoCache):
|
|
|
132
133
|
other cache types.
|
|
133
134
|
"""
|
|
134
135
|
self._closed = True
|
|
135
|
-
return
|
|
@@ -12,18 +12,24 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
15
17
|
import asyncio
|
|
16
18
|
import logging
|
|
17
19
|
import ssl
|
|
18
|
-
from typing import Any, Callable
|
|
20
|
+
from typing import Any, Callable
|
|
21
|
+
|
|
22
|
+
import aiohttp
|
|
19
23
|
|
|
20
24
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
21
25
|
from google.cloud.sql.connector.connection_info import ConnectionInfoCache
|
|
22
26
|
from google.cloud.sql.connector.exceptions import CacheClosedError
|
|
27
|
+
from google.cloud.sql.connector.exceptions import DnsLoopError
|
|
23
28
|
from google.cloud.sql.connector.instance import RefreshAheadCache
|
|
24
29
|
from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
25
30
|
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
26
31
|
from google.cloud.sql.connector.resolver import DnsResolver
|
|
32
|
+
from google.cloud.sql.connector.resolver import is_instance_dns_name
|
|
27
33
|
|
|
28
34
|
logger = logging.getLogger(name=__name__)
|
|
29
35
|
|
|
@@ -31,18 +37,27 @@ logger = logging.getLogger(name=__name__)
|
|
|
31
37
|
class MonitoredCache(ConnectionInfoCache):
|
|
32
38
|
def __init__(
|
|
33
39
|
self,
|
|
34
|
-
cache:
|
|
40
|
+
cache: RefreshAheadCache | LazyRefreshCache,
|
|
35
41
|
failover_period: int,
|
|
36
|
-
resolver:
|
|
42
|
+
resolver: DefaultResolver | DnsResolver,
|
|
37
43
|
) -> None:
|
|
38
44
|
self.resolver = resolver
|
|
39
45
|
self.cache = cache
|
|
40
|
-
self.domain_name_ticker:
|
|
46
|
+
self.domain_name_ticker: asyncio.Task | None = None
|
|
41
47
|
self.sockets: list[ssl.SSLSocket] = []
|
|
42
48
|
|
|
43
49
|
# If domain name is configured for instance and failover period is set,
|
|
44
50
|
# poll for DNS record changes.
|
|
45
|
-
|
|
51
|
+
is_instance_dns = False
|
|
52
|
+
if self.cache.conn_name.domain_name:
|
|
53
|
+
is_instance_dns = is_instance_dns_name(
|
|
54
|
+
self.cache.conn_name.domain_name or ""
|
|
55
|
+
)
|
|
56
|
+
if (
|
|
57
|
+
self.cache.conn_name.domain_name
|
|
58
|
+
and failover_period > 0
|
|
59
|
+
and not is_instance_dns
|
|
60
|
+
):
|
|
46
61
|
self.domain_name_ticker = asyncio.create_task(
|
|
47
62
|
ticker(failover_period, self._check_domain_name)
|
|
48
63
|
)
|
|
@@ -87,13 +102,29 @@ class MonitoredCache(ConnectionInfoCache):
|
|
|
87
102
|
)
|
|
88
103
|
await self.close()
|
|
89
104
|
|
|
90
|
-
except Exception as e:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
except Exception as e: # noqa: BLE001
|
|
106
|
+
if self._is_non_transient(e):
|
|
107
|
+
logger.debug(
|
|
108
|
+
f"['{self.cache.conn_name}']: Domain name check failed with "
|
|
109
|
+
f"non-transient error, closing all connections: {e}"
|
|
110
|
+
)
|
|
111
|
+
await self.close()
|
|
112
|
+
else:
|
|
113
|
+
# Domain name checks should not be fatal for transient errors, log error and continue.
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"['{self.cache.conn_name}']: Unable to check domain name, "
|
|
116
|
+
f"domain name {self.cache.conn_name.domain_name} did not "
|
|
117
|
+
f"resolve (transient error): {e}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _is_non_transient(self, e: Exception) -> bool:
|
|
121
|
+
return (
|
|
122
|
+
isinstance(e, (ValueError, DnsLoopError))
|
|
123
|
+
or (
|
|
124
|
+
isinstance(e, aiohttp.ClientResponseError)
|
|
125
|
+
and e.status in (403, 404)
|
|
96
126
|
)
|
|
127
|
+
)
|
|
97
128
|
|
|
98
129
|
async def connect_info(self) -> ConnectionInfo:
|
|
99
130
|
if self.closed:
|
|
@@ -48,7 +48,9 @@ def connect(
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
user = kwargs.pop("user")
|
|
51
|
-
db = kwargs.pop("db")
|
|
51
|
+
db = kwargs.pop("database", kwargs.pop("db", None))
|
|
52
|
+
if db is None:
|
|
53
|
+
raise KeyError("database")
|
|
52
54
|
passwd = kwargs.pop("password", None)
|
|
53
55
|
return pg8000.dbapi.connect(
|
|
54
56
|
user,
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Copyright 2026 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 logging
|
|
16
|
+
import os
|
|
17
|
+
import selectors
|
|
18
|
+
import socket
|
|
19
|
+
import ssl
|
|
20
|
+
import tempfile
|
|
21
|
+
import threading
|
|
22
|
+
from typing import Any, TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
import psycopg
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(name=__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _proxy(local: socket.socket, remote: "ssl.SSLSocket") -> None:
|
|
31
|
+
"""Single-threaded selectors-based proxy to avoid SSLSocket thread-safety issues."""
|
|
32
|
+
sel = selectors.DefaultSelector()
|
|
33
|
+
sel.register(local, selectors.EVENT_READ, data="local")
|
|
34
|
+
sel.register(remote, selectors.EVENT_READ, data="remote")
|
|
35
|
+
|
|
36
|
+
def forward_pending() -> bool:
|
|
37
|
+
"""Read any pending decrypted data from SSL buffer and forward it.
|
|
38
|
+
Returns True if EOF was reached or error occurred (should exit).
|
|
39
|
+
"""
|
|
40
|
+
if not hasattr(remote, "pending"):
|
|
41
|
+
return False
|
|
42
|
+
pending_bytes = remote.pending()
|
|
43
|
+
if not isinstance(pending_bytes, int):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
while pending_bytes > 0:
|
|
47
|
+
try:
|
|
48
|
+
data = remote.recv(8192)
|
|
49
|
+
except OSError as e:
|
|
50
|
+
logger.debug("psycopg proxy: remote recv pending error: %s", e)
|
|
51
|
+
return True
|
|
52
|
+
if not data:
|
|
53
|
+
logger.debug("psycopg proxy: remote pending EOF")
|
|
54
|
+
return True
|
|
55
|
+
try:
|
|
56
|
+
local.sendall(data)
|
|
57
|
+
except OSError as e:
|
|
58
|
+
logger.debug("psycopg proxy: local send pending error: %s", e)
|
|
59
|
+
return True
|
|
60
|
+
try:
|
|
61
|
+
pending_bytes = remote.pending()
|
|
62
|
+
except OSError:
|
|
63
|
+
break
|
|
64
|
+
if not isinstance(pending_bytes, int):
|
|
65
|
+
break
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
while True:
|
|
70
|
+
# First check if there is any pending data in SSL buffer
|
|
71
|
+
if forward_pending():
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
events = sel.select()
|
|
75
|
+
|
|
76
|
+
for key, mask in events:
|
|
77
|
+
if key.data == "local":
|
|
78
|
+
try:
|
|
79
|
+
data = local.recv(8192)
|
|
80
|
+
except OSError as e:
|
|
81
|
+
logger.debug("psycopg proxy: local recv error: %s", e)
|
|
82
|
+
return
|
|
83
|
+
if not data:
|
|
84
|
+
logger.debug("psycopg proxy: local EOF")
|
|
85
|
+
return
|
|
86
|
+
try:
|
|
87
|
+
remote.sendall(data)
|
|
88
|
+
except OSError as e:
|
|
89
|
+
logger.debug("psycopg proxy: remote send error: %s", e)
|
|
90
|
+
return
|
|
91
|
+
elif key.data == "remote":
|
|
92
|
+
try:
|
|
93
|
+
data = remote.recv(8192)
|
|
94
|
+
except OSError as e:
|
|
95
|
+
logger.debug("psycopg proxy: remote recv error: %s", e)
|
|
96
|
+
return
|
|
97
|
+
if not data:
|
|
98
|
+
logger.debug("psycopg proxy: remote EOF")
|
|
99
|
+
return
|
|
100
|
+
try:
|
|
101
|
+
local.sendall(data)
|
|
102
|
+
except OSError as e:
|
|
103
|
+
logger.debug("psycopg proxy: local send error: %s", e)
|
|
104
|
+
return
|
|
105
|
+
except OSError as e:
|
|
106
|
+
logger.debug("psycopg proxy: OSError in loop: %s", e)
|
|
107
|
+
finally:
|
|
108
|
+
sel.close()
|
|
109
|
+
for s in (local, remote):
|
|
110
|
+
try:
|
|
111
|
+
s.shutdown(socket.SHUT_RDWR)
|
|
112
|
+
except OSError:
|
|
113
|
+
pass
|
|
114
|
+
try:
|
|
115
|
+
s.close()
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def connect(
|
|
121
|
+
ip_address: str, remote_sock: "ssl.SSLSocket", **kwargs: Any
|
|
122
|
+
) -> "psycopg.Connection":
|
|
123
|
+
"""Create a psycopg DBAPI connection object.
|
|
124
|
+
|
|
125
|
+
Because psycopg does not accept a pre-connected socket, this function
|
|
126
|
+
creates a temporary Unix domain socket, tells psycopg to connect there,
|
|
127
|
+
and runs a background proxy that forwards bytes between that socket and
|
|
128
|
+
the already-established Cloud SQL TLS connection.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
ip_address (str): IP address of the Cloud SQL instance.
|
|
132
|
+
remote_sock (ssl.SSLSocket): SSL/TLS secure socket stream connected to the
|
|
133
|
+
Cloud SQL proxy server.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
psycopg.Connection: A psycopg Connection object for the Cloud SQL instance.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
import psycopg
|
|
140
|
+
except ImportError:
|
|
141
|
+
raise ImportError(
|
|
142
|
+
'Unable to import module "psycopg." Please install and try again.'
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if not hasattr(socket, "AF_UNIX"):
|
|
146
|
+
raise NotImplementedError(
|
|
147
|
+
"Unix domain sockets (AF_UNIX) are not supported on this platform"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
tmpdir = tempfile.mkdtemp()
|
|
151
|
+
socket_path = os.path.join(tmpdir, ".s.PGSQL.5432")
|
|
152
|
+
logger.debug("psycopg: created Unix socket at %s", socket_path)
|
|
153
|
+
|
|
154
|
+
local_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
155
|
+
local_sock.bind(socket_path)
|
|
156
|
+
local_sock.listen(1)
|
|
157
|
+
|
|
158
|
+
def _accept_and_proxy() -> None:
|
|
159
|
+
"""Accept one connection then proxy bytes until the connection closes."""
|
|
160
|
+
unix_conn = None
|
|
161
|
+
try:
|
|
162
|
+
unix_conn, _ = local_sock.accept()
|
|
163
|
+
local_sock.close()
|
|
164
|
+
logger.debug("psycopg proxy: accepted connection, starting proxy")
|
|
165
|
+
_proxy(unix_conn, remote_sock)
|
|
166
|
+
except Exception as e: # noqa: BLE001
|
|
167
|
+
logger.debug("psycopg proxy: error in accept/proxy thread: %s", e)
|
|
168
|
+
# Ensure cleanup on any exception
|
|
169
|
+
if unix_conn:
|
|
170
|
+
try:
|
|
171
|
+
unix_conn.shutdown(socket.SHUT_RDWR)
|
|
172
|
+
except OSError:
|
|
173
|
+
pass
|
|
174
|
+
try:
|
|
175
|
+
unix_conn.close()
|
|
176
|
+
except OSError:
|
|
177
|
+
pass
|
|
178
|
+
try:
|
|
179
|
+
remote_sock.shutdown(socket.SHUT_RDWR)
|
|
180
|
+
except OSError:
|
|
181
|
+
pass
|
|
182
|
+
try:
|
|
183
|
+
remote_sock.close()
|
|
184
|
+
except OSError:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
threading.Thread(target=_accept_and_proxy, daemon=True).start()
|
|
188
|
+
|
|
189
|
+
user = kwargs.pop("user")
|
|
190
|
+
db = kwargs.pop("db")
|
|
191
|
+
passwd = kwargs.pop("password", None)
|
|
192
|
+
# SSL is already handled by the underlying SSLSocket; disable it on the
|
|
193
|
+
# Unix socket so psycopg does not attempt a second TLS handshake.
|
|
194
|
+
kwargs.pop("sslmode", None)
|
|
195
|
+
timeout = kwargs.pop("timeout", None)
|
|
196
|
+
if timeout is not None:
|
|
197
|
+
kwargs["connect_timeout"] = int(timeout)
|
|
198
|
+
|
|
199
|
+
logger.debug("psycopg: connecting as user=%s dbname=%s", user, db)
|
|
200
|
+
try:
|
|
201
|
+
conn = psycopg.connect(
|
|
202
|
+
user=user,
|
|
203
|
+
dbname=db,
|
|
204
|
+
password=passwd,
|
|
205
|
+
host=tmpdir,
|
|
206
|
+
port=5432,
|
|
207
|
+
sslmode="disable",
|
|
208
|
+
**kwargs,
|
|
209
|
+
)
|
|
210
|
+
logger.debug("psycopg: connection established")
|
|
211
|
+
return conn
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.debug("psycopg: connection failed: %s", e)
|
|
214
|
+
# psycopg never connected (or failed mid-handshake); close the server
|
|
215
|
+
# socket so the proxy thread unblocks and exits cleanly.
|
|
216
|
+
try:
|
|
217
|
+
local_sock.close()
|
|
218
|
+
except OSError:
|
|
219
|
+
pass
|
|
220
|
+
try:
|
|
221
|
+
remote_sock.close()
|
|
222
|
+
except OSError:
|
|
223
|
+
pass
|
|
224
|
+
raise
|
|
225
|
+
finally:
|
|
226
|
+
# The socket file and its parent directory are only needed during the
|
|
227
|
+
# initial connect() call; remove them now regardless of outcome.
|
|
228
|
+
try:
|
|
229
|
+
os.remove(socket_path)
|
|
230
|
+
except OSError:
|
|
231
|
+
pass
|
|
232
|
+
try:
|
|
233
|
+
os.rmdir(tmpdir)
|
|
234
|
+
except OSError:
|
|
235
|
+
pass
|
|
@@ -47,11 +47,18 @@ def connect(
|
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
# allow automatic IAM database authentication to not require password
|
|
50
|
-
kwargs["password"] = kwargs
|
|
50
|
+
kwargs["password"] = kwargs.get("password", None)
|
|
51
51
|
|
|
52
52
|
# pop timeout as timeout arg is called 'connect_timeout' for pymysql
|
|
53
53
|
timeout = kwargs.pop("timeout")
|
|
54
54
|
kwargs["connect_timeout"] = kwargs.get("connect_timeout", timeout)
|
|
55
|
+
|
|
56
|
+
# map 'db' to 'database' to avoid deprecation warning in pymysql,
|
|
57
|
+
# giving precedence to 'database' if both are provided
|
|
58
|
+
db = kwargs.pop("database", kwargs.pop("db", None))
|
|
59
|
+
if db is not None:
|
|
60
|
+
kwargs["database"] = db
|
|
61
|
+
|
|
55
62
|
# Create pymysql connection object and hand in pre-made connection
|
|
56
63
|
conn = pymysql.Connection(host=ip_address, defer_connect=True, **kwargs)
|
|
57
64
|
conn.connect(sock)
|
|
@@ -47,7 +47,7 @@ def connect(ip_address: str, sock: ssl.SSLSocket, **kwargs: Any) -> "pytds.Conne
|
|
|
47
47
|
'Unable to import module "pytds." Please install and try again.'
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
db = kwargs.pop("db", None)
|
|
50
|
+
db = kwargs.pop("database", kwargs.pop("db", None))
|
|
51
51
|
|
|
52
52
|
if kwargs.pop("active_directory_auth", False):
|
|
53
53
|
if platform.system() == "Windows":
|
|
@@ -13,12 +13,12 @@ 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
|
+
from __future__ import annotations
|
|
16
17
|
|
|
17
18
|
import asyncio
|
|
18
|
-
from typing import Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class AsyncRateLimiter
|
|
21
|
+
class AsyncRateLimiter:
|
|
22
22
|
"""An asyncio-compatible rate limiter which uses the Token Bucket algorithm
|
|
23
23
|
(https://en.wikipedia.org/wiki/Token_bucket) to limit the number
|
|
24
24
|
of function calls over a time interval using an event queue.
|
|
@@ -35,7 +35,7 @@ class AsyncRateLimiter(object):
|
|
|
35
35
|
self,
|
|
36
36
|
max_capacity: int = 1,
|
|
37
37
|
rate: float = 1 / 60,
|
|
38
|
-
loop:
|
|
38
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
39
39
|
) -> None:
|
|
40
40
|
self.rate = rate
|
|
41
41
|
self.max_capacity = max_capacity
|
|
@@ -72,7 +72,7 @@ async def _is_valid(task: asyncio.Task) -> bool:
|
|
|
72
72
|
# only valid if now is before the cert expires
|
|
73
73
|
if datetime.datetime.now(datetime.timezone.utc) < metadata.expiration:
|
|
74
74
|
return True
|
|
75
|
-
except Exception:
|
|
75
|
+
except Exception: # noqa: BLE001
|
|
76
76
|
# supress any errors from task
|
|
77
77
|
logger.debug("Current instance metadata is invalid.")
|
|
78
78
|
return False
|
|
@@ -80,7 +80,7 @@ async def _is_valid(task: asyncio.Task) -> bool:
|
|
|
80
80
|
|
|
81
81
|
def _downscope_credentials(
|
|
82
82
|
credentials: Credentials,
|
|
83
|
-
scopes: list[str] =
|
|
83
|
+
scopes: list[str] | None = None,
|
|
84
84
|
) -> Credentials:
|
|
85
85
|
"""Generate a down-scoped credential.
|
|
86
86
|
|
|
@@ -95,6 +95,8 @@ def _downscope_credentials(
|
|
|
95
95
|
"""
|
|
96
96
|
# credentials sourced from a service account or metadata are children of
|
|
97
97
|
# Scoped class and are capable of being re-scoped
|
|
98
|
+
if scopes is None:
|
|
99
|
+
scopes = ["https://www.googleapis.com/auth/sqlservice.login"]
|
|
98
100
|
if isinstance(credentials, Scoped):
|
|
99
101
|
scoped_creds = credentials.with_scopes(scopes=scopes)
|
|
100
102
|
# authenticated user credentials can not be re-scoped
|