cloud-sql-python-connector 1.21.0__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.21.0.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/METADATA +22 -3
- cloud_sql_python_connector-1.22.0.dist-info/RECORD +26 -0
- {cloud_sql_python_connector-1.21.0.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/WHEEL +1 -1
- google/cloud/sql/connector/asyncpg.py +3 -1
- google/cloud/sql/connector/connector.py +2 -0
- google/cloud/sql/connector/enums.py +1 -0
- google/cloud/sql/connector/exceptions.py +6 -0
- google/cloud/sql/connector/monitored_cache.py +35 -6
- google/cloud/sql/connector/pg8000.py +3 -1
- google/cloud/sql/connector/psycopg.py +235 -0
- google/cloud/sql/connector/pymysql.py +7 -0
- google/cloud/sql/connector/pytds.py +1 -1
- google/cloud/sql/connector/resolver.py +18 -5
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.21.0.dist-info/RECORD +0 -25
- {cloud_sql_python_connector-1.21.0.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/licenses/LICENSE +0 -0
- {cloud_sql_python_connector-1.21.0.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/top_level.txt +0 -0
{cloud_sql_python_connector-1.21.0.dist-info → cloud_sql_python_connector-1.22.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.22.0
|
|
4
4
|
Summary: Google Cloud SQL Python Connector library
|
|
5
5
|
Author-email: Google LLC <googleapis-packages@google.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -37,6 +37,8 @@ Provides-Extra: pytds
|
|
|
37
37
|
Requires-Dist: python-tds>=1.15.0; extra == "pytds"
|
|
38
38
|
Provides-Extra: asyncpg
|
|
39
39
|
Requires-Dist: asyncpg>=0.30.0; extra == "asyncpg"
|
|
40
|
+
Provides-Extra: psycopg
|
|
41
|
+
Requires-Dist: psycopg>=3.1.0; extra == "psycopg"
|
|
40
42
|
Dynamic: license-file
|
|
41
43
|
|
|
42
44
|
<p align="center">
|
|
@@ -82,7 +84,8 @@ providing the following benefits:
|
|
|
82
84
|
The Cloud SQL Python Connector is a package to be used alongside a database driver.
|
|
83
85
|
Currently supported drivers are:
|
|
84
86
|
- [`pymysql`](https://github.com/PyMySQL/PyMySQL) (MySQL)
|
|
85
|
-
- [`pg8000`](https://
|
|
87
|
+
- [`pg8000`](https://codeberg.org/tlocke/pg8000) (PostgreSQL)
|
|
88
|
+
- [`psycopg`](https://github.com/psycopg/psycopg) (PostgreSQL)
|
|
86
89
|
- [`asyncpg`](https://github.com/MagicStack/asyncpg) (PostgreSQL)
|
|
87
90
|
- [`pytds`](https://github.com/denisenkom/pytds) (SQL Server)
|
|
88
91
|
|
|
@@ -97,12 +100,16 @@ based on your database dialect.
|
|
|
97
100
|
pip install "cloud-sql-python-connector[pymysql]"
|
|
98
101
|
```
|
|
99
102
|
### Postgres
|
|
100
|
-
There are
|
|
103
|
+
There are three different database drivers that are supported for the Postgres dialect:
|
|
101
104
|
|
|
102
105
|
#### pg8000
|
|
103
106
|
```
|
|
104
107
|
pip install "cloud-sql-python-connector[pg8000]"
|
|
105
108
|
```
|
|
109
|
+
#### psycopg
|
|
110
|
+
```
|
|
111
|
+
pip install "cloud-sql-python-connector[psycopg]"
|
|
112
|
+
```
|
|
106
113
|
#### asyncpg
|
|
107
114
|
```
|
|
108
115
|
pip install "cloud-sql-python-connector[asyncpg]"
|
|
@@ -178,6 +185,18 @@ pool = sqlalchemy.create_engine(
|
|
|
178
185
|
db="my-db-name"
|
|
179
186
|
),
|
|
180
187
|
)
|
|
188
|
+
|
|
189
|
+
# Or with Postgres (psycopg):
|
|
190
|
+
pool = sqlalchemy.create_engine(
|
|
191
|
+
"postgresql+psycopg://",
|
|
192
|
+
creator=lambda: connector.connect(
|
|
193
|
+
"project:region:instance",
|
|
194
|
+
"psycopg",
|
|
195
|
+
user="my-user",
|
|
196
|
+
password="my-password",
|
|
197
|
+
db="my-db-name"
|
|
198
|
+
),
|
|
199
|
+
)
|
|
181
200
|
```
|
|
182
201
|
|
|
183
202
|
The returned connection pool engine can then be used to query and modify the database.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
cloud_sql_python_connector-1.22.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
2
|
+
google/cloud/sql/connector/__init__.py,sha256=pfa-FD-VRpYI49OUul9p6UFDp7G_yT2f6WS9rZMn8CM,1148
|
|
3
|
+
google/cloud/sql/connector/asyncpg.py,sha256=MpZhwFke0WXvYD3OcEF1Ok0qHl-CU7ZIPy85PCog5QY,2002
|
|
4
|
+
google/cloud/sql/connector/client.py,sha256=he2qm0cUQD4FGzMfT6eBye-KLEQtFdQsedsawNUhm7c,14598
|
|
5
|
+
google/cloud/sql/connector/connection_info.py,sha256=INQMBXGxXSth2MsAnqe9e9pCOiqMj55V3fzwxCmoAZc,5399
|
|
6
|
+
google/cloud/sql/connector/connection_name.py,sha256=SCCVeWb8YN0f9My09_l9FGTr6iRbi1b_52TJQaS6xzM,2574
|
|
7
|
+
google/cloud/sql/connector/connector.py,sha256=dEn9rN5fzRx4BE891f-lBeEpRNi7MhnkTvkuq5TLoMY,28040
|
|
8
|
+
google/cloud/sql/connector/enums.py,sha256=wESKVNRFNwKk-Bs093zM6YSCGEgaDHu3yLBscWuKpn8,2994
|
|
9
|
+
google/cloud/sql/connector/exceptions.py,sha256=hFdDZRR48qLYSFI_CRxbftrKkt77Jl5ci6SEHcAGMok,2209
|
|
10
|
+
google/cloud/sql/connector/instance.py,sha256=xz-TLSaYOyUQdbKsv2m-MDvA2kjJCkcaj3GXfsYIw2g,8691
|
|
11
|
+
google/cloud/sql/connector/lazy.py,sha256=N3Ud8KobhASnsH0pACG8oEHjmS7Z7MVzkHJ9skDLxMo,4932
|
|
12
|
+
google/cloud/sql/connector/monitored_cache.py,sha256=n-nG19VZ7hfzz0Dbg245mrkhRga1f-bP56aRku212mk,6656
|
|
13
|
+
google/cloud/sql/connector/pg8000.py,sha256=tbqFSIoSguMX1hTZ4kWeBOKLhLHKX1Vq5SespfA0Y0w,1801
|
|
14
|
+
google/cloud/sql/connector/psycopg.py,sha256=LcGencGQlQPDKWTeQrk1SiLMtVB09uZw6RdPQAosC8k,8038
|
|
15
|
+
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
google/cloud/sql/connector/pymysql.py,sha256=MOau37nTOTesUR5Hor7lLtEckvvGThzjECEWQYHcM8U,2166
|
|
17
|
+
google/cloud/sql/connector/pytds.py,sha256=e-KBCqv0m3a7cRi9YmFHNuYqClhlJbf-KoF611w-1Ck,2304
|
|
18
|
+
google/cloud/sql/connector/rate_limiter.py,sha256=60k-r9h-nUBqovVIHnIGbW0NMgUGD4exH_rZ02ukNa0,2924
|
|
19
|
+
google/cloud/sql/connector/refresh_utils.py,sha256=kouF2OJc-NEu7MQS94GVJ7jN04-7a_wLKL-46kAVQuM,5219
|
|
20
|
+
google/cloud/sql/connector/resolver.py,sha256=4U8G0Pxwmy64qoRmP5shLnmIykxcGcGCoFSH_7tr120,6353
|
|
21
|
+
google/cloud/sql/connector/utils.py,sha256=ApLG4z4H0S4lWXQ9PyclXbrEoY5VrshqN9QB6BG8lyg,4132
|
|
22
|
+
google/cloud/sql/connector/version.py,sha256=0JV131RUM4deaBtRNiyn9A-CSQMDjxlhfH0wdHi3fGQ,598
|
|
23
|
+
cloud_sql_python_connector-1.22.0.dist-info/METADATA,sha256=Sn1H3KwvLEFOSGWPJY5APuFykTx73RLrk5DjCCP_t50,31731
|
|
24
|
+
cloud_sql_python_connector-1.22.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
25
|
+
cloud_sql_python_connector-1.22.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
26
|
+
cloud_sql_python_connector-1.22.0.dist-info/RECORD,,
|
|
@@ -51,7 +51,9 @@ async def connect(
|
|
|
51
51
|
'Unable to import module "asyncpg." Please install and try again.'
|
|
52
52
|
)
|
|
53
53
|
user = kwargs.pop("user")
|
|
54
|
-
db = kwargs.pop("db")
|
|
54
|
+
db = kwargs.pop("database", kwargs.pop("db", None))
|
|
55
|
+
if db is None:
|
|
56
|
+
raise KeyError("database")
|
|
55
57
|
passwd = kwargs.pop("password", None)
|
|
56
58
|
|
|
57
59
|
return await asyncpg.connect(
|
|
@@ -31,6 +31,7 @@ from google.auth.credentials import with_scopes_if_required
|
|
|
31
31
|
|
|
32
32
|
from google.cloud.sql.connector import asyncpg
|
|
33
33
|
from google.cloud.sql.connector import pg8000
|
|
34
|
+
from google.cloud.sql.connector import psycopg
|
|
34
35
|
from google.cloud.sql.connector import pymysql
|
|
35
36
|
from google.cloud.sql.connector import pytds
|
|
36
37
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
@@ -362,6 +363,7 @@ class Connector:
|
|
|
362
363
|
"pg8000": pg8000.connect,
|
|
363
364
|
"asyncpg": asyncpg.connect,
|
|
364
365
|
"pytds": pytds.connect,
|
|
366
|
+
"psycopg": psycopg.connect,
|
|
365
367
|
}
|
|
366
368
|
|
|
367
369
|
# only accept supported database drivers
|
|
@@ -73,6 +73,12 @@ class DnsResolutionError(Exception):
|
|
|
73
73
|
"""
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
class DnsLoopError(DnsResolutionError):
|
|
77
|
+
"""
|
|
78
|
+
Exception to be raised when a CNAME loop is detected during DNS resolution.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
76
82
|
class CacheClosedError(Exception):
|
|
77
83
|
"""
|
|
78
84
|
Exception to be raised when a ConnectionInfoCache can not be accessed after
|
|
@@ -19,13 +19,17 @@ import logging
|
|
|
19
19
|
import ssl
|
|
20
20
|
from typing import Any, Callable
|
|
21
21
|
|
|
22
|
+
import aiohttp
|
|
23
|
+
|
|
22
24
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
23
25
|
from google.cloud.sql.connector.connection_info import ConnectionInfoCache
|
|
24
26
|
from google.cloud.sql.connector.exceptions import CacheClosedError
|
|
27
|
+
from google.cloud.sql.connector.exceptions import DnsLoopError
|
|
25
28
|
from google.cloud.sql.connector.instance import RefreshAheadCache
|
|
26
29
|
from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
27
30
|
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
28
31
|
from google.cloud.sql.connector.resolver import DnsResolver
|
|
32
|
+
from google.cloud.sql.connector.resolver import is_instance_dns_name
|
|
29
33
|
|
|
30
34
|
logger = logging.getLogger(name=__name__)
|
|
31
35
|
|
|
@@ -44,7 +48,16 @@ class MonitoredCache(ConnectionInfoCache):
|
|
|
44
48
|
|
|
45
49
|
# If domain name is configured for instance and failover period is set,
|
|
46
50
|
# poll for DNS record changes.
|
|
47
|
-
|
|
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
|
+
):
|
|
48
61
|
self.domain_name_ticker = asyncio.create_task(
|
|
49
62
|
ticker(failover_period, self._check_domain_name)
|
|
50
63
|
)
|
|
@@ -90,12 +103,28 @@ class MonitoredCache(ConnectionInfoCache):
|
|
|
90
103
|
await self.close()
|
|
91
104
|
|
|
92
105
|
except Exception as e: # noqa: BLE001
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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)
|
|
98
126
|
)
|
|
127
|
+
)
|
|
99
128
|
|
|
100
129
|
async def connect_info(self) -> ConnectionInfo:
|
|
101
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
|
|
@@ -52,6 +52,13 @@ def connect(
|
|
|
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":
|
|
@@ -25,12 +25,25 @@ from google.cloud.sql.connector.connection_name import (
|
|
|
25
25
|
_parse_connection_name_with_domain_name,
|
|
26
26
|
)
|
|
27
27
|
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
28
|
+
from google.cloud.sql.connector.exceptions import DnsLoopError
|
|
28
29
|
from google.cloud.sql.connector.exceptions import DnsResolutionError
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
# ^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])
|
|
32
|
+
|
|
33
|
+
INSTANCE_DNS_NAME_PATTERN = re.compile(
|
|
34
|
+
r"^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.(sql|sql-psa|sql-psc)\.goog\.?$"
|
|
32
35
|
)
|
|
33
36
|
|
|
37
|
+
GLOBAL_INSTANCE_DNS_PATTERN = re.compile(r"\.global\.sql(-\w+)?\.goog\.?$")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_instance_dns_name(name: str) -> bool:
|
|
41
|
+
lower = name.lower()
|
|
42
|
+
return bool(
|
|
43
|
+
INSTANCE_DNS_NAME_PATTERN.search(lower)
|
|
44
|
+
and not GLOBAL_INSTANCE_DNS_PATTERN.search(lower)
|
|
45
|
+
)
|
|
46
|
+
|
|
34
47
|
|
|
35
48
|
class DefaultResolver:
|
|
36
49
|
"""DefaultResolver simply validates and parses instance connection name."""
|
|
@@ -68,7 +81,7 @@ class DnsResolver(dns.asyncresolver.Resolver):
|
|
|
68
81
|
pass
|
|
69
82
|
|
|
70
83
|
dns_normalized = current.rstrip(".")
|
|
71
|
-
match =
|
|
84
|
+
match = INSTANCE_DNS_NAME_PATTERN.match(dns_normalized.lower())
|
|
72
85
|
if match:
|
|
73
86
|
region = match.group(3)
|
|
74
87
|
if region != "global":
|
|
@@ -102,7 +115,7 @@ class DnsResolver(dns.asyncresolver.Resolver):
|
|
|
102
115
|
|
|
103
116
|
if cname_found:
|
|
104
117
|
if cname in visited:
|
|
105
|
-
raise
|
|
118
|
+
raise DnsLoopError(f"CNAME loop detected for `{dns}`")
|
|
106
119
|
visited.add(cname)
|
|
107
120
|
current = cname
|
|
108
121
|
continue
|
|
@@ -130,7 +143,7 @@ class DnsResolver(dns.asyncresolver.Resolver):
|
|
|
130
143
|
else f"Unable to resolve TXT record for `{current}`"
|
|
131
144
|
)
|
|
132
145
|
|
|
133
|
-
raise
|
|
146
|
+
raise DnsLoopError(
|
|
134
147
|
f"CNAME loop detected or max resolution depth reached for `{dns}`"
|
|
135
148
|
)
|
|
136
149
|
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
cloud_sql_python_connector-1.21.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
2
|
-
google/cloud/sql/connector/__init__.py,sha256=pfa-FD-VRpYI49OUul9p6UFDp7G_yT2f6WS9rZMn8CM,1148
|
|
3
|
-
google/cloud/sql/connector/asyncpg.py,sha256=v4ohOnSYjHTOvJPhTE761xP1KKo4FUJQW_WAnNW_2g4,1918
|
|
4
|
-
google/cloud/sql/connector/client.py,sha256=he2qm0cUQD4FGzMfT6eBye-KLEQtFdQsedsawNUhm7c,14598
|
|
5
|
-
google/cloud/sql/connector/connection_info.py,sha256=INQMBXGxXSth2MsAnqe9e9pCOiqMj55V3fzwxCmoAZc,5399
|
|
6
|
-
google/cloud/sql/connector/connection_name.py,sha256=SCCVeWb8YN0f9My09_l9FGTr6iRbi1b_52TJQaS6xzM,2574
|
|
7
|
-
google/cloud/sql/connector/connector.py,sha256=9IDpZ8u1cQPoBvyrUoVuuoAcRAJ_RbCPsEUbsbIcXeY,27953
|
|
8
|
-
google/cloud/sql/connector/enums.py,sha256=o8QzPw3xLlr9GruxdMbydJ1N1Y2N-dfzbSKFDgMnxRU,2953
|
|
9
|
-
google/cloud/sql/connector/exceptions.py,sha256=WvgtVvbXWLAfEfTL8rAQsZOTjaJFVImMo1bookFBl3Y,2071
|
|
10
|
-
google/cloud/sql/connector/instance.py,sha256=xz-TLSaYOyUQdbKsv2m-MDvA2kjJCkcaj3GXfsYIw2g,8691
|
|
11
|
-
google/cloud/sql/connector/lazy.py,sha256=N3Ud8KobhASnsH0pACG8oEHjmS7Z7MVzkHJ9skDLxMo,4932
|
|
12
|
-
google/cloud/sql/connector/monitored_cache.py,sha256=MHsn8KVCrsnLr9qI6lEe9Nctvj1Cp2NpErglrg-63Y0,5607
|
|
13
|
-
google/cloud/sql/connector/pg8000.py,sha256=TnS_SNbgY19oNEawOx-qJyW-f_k42-SnR3VTBOSr5FI,1717
|
|
14
|
-
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
google/cloud/sql/connector/pymysql.py,sha256=6EQLUaLgQ9V5IGal4m43sFmRlNEEMFDDixkceRuu5HA,1924
|
|
16
|
-
google/cloud/sql/connector/pytds.py,sha256=KZxmJQQG9XfmsmUoEGX7RpMirWdoreJ2-_FuHO6Td-s,2280
|
|
17
|
-
google/cloud/sql/connector/rate_limiter.py,sha256=60k-r9h-nUBqovVIHnIGbW0NMgUGD4exH_rZ02ukNa0,2924
|
|
18
|
-
google/cloud/sql/connector/refresh_utils.py,sha256=kouF2OJc-NEu7MQS94GVJ7jN04-7a_wLKL-46kAVQuM,5219
|
|
19
|
-
google/cloud/sql/connector/resolver.py,sha256=ecgE0MQUZqXoVwwV4RPNp416JQ9XqCwG4oMaHTfIzX0,5821
|
|
20
|
-
google/cloud/sql/connector/utils.py,sha256=ApLG4z4H0S4lWXQ9PyclXbrEoY5VrshqN9QB6BG8lyg,4132
|
|
21
|
-
google/cloud/sql/connector/version.py,sha256=T8B-4zjneVFweA_cav_qt3c8Lxvxp12wayrS4RtS_zg,598
|
|
22
|
-
cloud_sql_python_connector-1.21.0.dist-info/METADATA,sha256=IbdOWUxj3-12_3LdcsuPru1UHjkGCMQCk2yxN-Rnaic,31243
|
|
23
|
-
cloud_sql_python_connector-1.21.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
24
|
-
cloud_sql_python_connector-1.21.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
25
|
-
cloud_sql_python_connector-1.21.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|