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
{cloud_sql_python_connector-1.20.4.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
|
|
@@ -21,12 +21,14 @@ Classifier: Operating System :: OS Independent
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: aiofiles
|
|
25
24
|
Requires-Dist: aiohttp
|
|
26
25
|
Requires-Dist: cryptography>=42.0.0
|
|
27
26
|
Requires-Dist: dnspython>=2.0.0
|
|
28
27
|
Requires-Dist: Requests
|
|
29
28
|
Requires-Dist: google-auth>=2.28.0
|
|
29
|
+
Requires-Dist: grpcio
|
|
30
|
+
Requires-Dist: protobuf
|
|
31
|
+
Requires-Dist: googleapis-common-protos
|
|
30
32
|
Provides-Extra: pymysql
|
|
31
33
|
Requires-Dist: PyMySQL>=1.1.0; extra == "pymysql"
|
|
32
34
|
Provides-Extra: pg8000
|
|
@@ -35,6 +37,8 @@ Provides-Extra: pytds
|
|
|
35
37
|
Requires-Dist: python-tds>=1.15.0; extra == "pytds"
|
|
36
38
|
Provides-Extra: asyncpg
|
|
37
39
|
Requires-Dist: asyncpg>=0.30.0; extra == "asyncpg"
|
|
40
|
+
Provides-Extra: psycopg
|
|
41
|
+
Requires-Dist: psycopg>=3.1.0; extra == "psycopg"
|
|
38
42
|
Dynamic: license-file
|
|
39
43
|
|
|
40
44
|
<p align="center">
|
|
@@ -80,7 +84,8 @@ providing the following benefits:
|
|
|
80
84
|
The Cloud SQL Python Connector is a package to be used alongside a database driver.
|
|
81
85
|
Currently supported drivers are:
|
|
82
86
|
- [`pymysql`](https://github.com/PyMySQL/PyMySQL) (MySQL)
|
|
83
|
-
- [`pg8000`](https://
|
|
87
|
+
- [`pg8000`](https://codeberg.org/tlocke/pg8000) (PostgreSQL)
|
|
88
|
+
- [`psycopg`](https://github.com/psycopg/psycopg) (PostgreSQL)
|
|
84
89
|
- [`asyncpg`](https://github.com/MagicStack/asyncpg) (PostgreSQL)
|
|
85
90
|
- [`pytds`](https://github.com/denisenkom/pytds) (SQL Server)
|
|
86
91
|
|
|
@@ -95,12 +100,16 @@ based on your database dialect.
|
|
|
95
100
|
pip install "cloud-sql-python-connector[pymysql]"
|
|
96
101
|
```
|
|
97
102
|
### Postgres
|
|
98
|
-
There are
|
|
103
|
+
There are three different database drivers that are supported for the Postgres dialect:
|
|
99
104
|
|
|
100
105
|
#### pg8000
|
|
101
106
|
```
|
|
102
107
|
pip install "cloud-sql-python-connector[pg8000]"
|
|
103
108
|
```
|
|
109
|
+
#### psycopg
|
|
110
|
+
```
|
|
111
|
+
pip install "cloud-sql-python-connector[psycopg]"
|
|
112
|
+
```
|
|
104
113
|
#### asyncpg
|
|
105
114
|
```
|
|
106
115
|
pip install "cloud-sql-python-connector[asyncpg]"
|
|
@@ -176,6 +185,18 @@ pool = sqlalchemy.create_engine(
|
|
|
176
185
|
db="my-db-name"
|
|
177
186
|
),
|
|
178
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
|
+
)
|
|
179
200
|
```
|
|
180
201
|
|
|
181
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,,
|
|
@@ -23,11 +23,11 @@ from google.cloud.sql.connector.resolver import DnsResolver
|
|
|
23
23
|
from google.cloud.sql.connector.version import __version__
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
|
-
"__version__",
|
|
27
|
-
"create_async_connector",
|
|
28
26
|
"Connector",
|
|
29
27
|
"DefaultResolver",
|
|
30
28
|
"DnsResolver",
|
|
31
29
|
"IPTypes",
|
|
32
30
|
"RefreshStrategy",
|
|
31
|
+
"__version__",
|
|
32
|
+
"create_async_connector",
|
|
33
33
|
]
|
|
@@ -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(
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import datetime
|
|
19
19
|
import logging
|
|
20
|
-
from typing import Any,
|
|
20
|
+
from typing import Any, TYPE_CHECKING
|
|
21
21
|
|
|
22
22
|
import aiohttp
|
|
23
23
|
from cryptography.hazmat.backends import default_backend
|
|
@@ -42,7 +42,7 @@ DEFAULT_SERVICE_ENDPOINT: str = "https://sqladmin.googleapis.com"
|
|
|
42
42
|
logger = logging.getLogger(name=__name__)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def _format_user_agent(driver:
|
|
45
|
+
def _format_user_agent(driver: str | None, custom: str | None) -> str:
|
|
46
46
|
agent = f"{USER_AGENT}+{driver}" if driver else USER_AGENT
|
|
47
47
|
if custom and isinstance(custom, str):
|
|
48
48
|
agent = f"{agent} {custom}"
|
|
@@ -52,12 +52,12 @@ def _format_user_agent(driver: Optional[str], custom: Optional[str]) -> str:
|
|
|
52
52
|
class CloudSQLClient:
|
|
53
53
|
def __init__(
|
|
54
54
|
self,
|
|
55
|
-
sqladmin_api_endpoint:
|
|
56
|
-
quota_project:
|
|
55
|
+
sqladmin_api_endpoint: str | None,
|
|
56
|
+
quota_project: str | None,
|
|
57
57
|
credentials: Credentials,
|
|
58
|
-
client:
|
|
59
|
-
driver:
|
|
60
|
-
user_agent:
|
|
58
|
+
client: aiohttp.ClientSession | None = None,
|
|
59
|
+
driver: str | None = None,
|
|
60
|
+
user_agent: str | None = None,
|
|
61
61
|
) -> None:
|
|
62
62
|
"""Establishes the client to be used for Cloud SQL Admin API requests.
|
|
63
63
|
|
|
@@ -135,7 +135,7 @@ class CloudSQLClient:
|
|
|
135
135
|
if message:
|
|
136
136
|
resp.reason = message
|
|
137
137
|
# skip, raise_for_status will catch all errors in finally block
|
|
138
|
-
except Exception:
|
|
138
|
+
except Exception: # noqa: BLE001, S110
|
|
139
139
|
pass
|
|
140
140
|
finally:
|
|
141
141
|
resp.raise_for_status()
|
|
@@ -146,7 +146,7 @@ class CloudSQLClient:
|
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
ip_addresses = (
|
|
149
|
-
{ip["type"]: ip["ipAddress"] for ip in ret_dict["ipAddresses"]}
|
|
149
|
+
{ip["type"]: [ip["ipAddress"]] for ip in ret_dict["ipAddresses"]}
|
|
150
150
|
if "ipAddresses" in ret_dict
|
|
151
151
|
else {}
|
|
152
152
|
)
|
|
@@ -156,20 +156,22 @@ class CloudSQLClient:
|
|
|
156
156
|
if ret_dict.get("pscEnabled"):
|
|
157
157
|
# Find PSC instance DNS name in the dns_names field
|
|
158
158
|
psc_dns_names = [
|
|
159
|
-
d["name"]
|
|
159
|
+
d["name"].rstrip(".")
|
|
160
160
|
for d in ret_dict.get("dnsNames", [])
|
|
161
161
|
if d["connectionType"] == "PRIVATE_SERVICE_CONNECT"
|
|
162
162
|
and d["dnsScope"] == "INSTANCE"
|
|
163
163
|
]
|
|
164
|
-
|
|
164
|
+
# Sort: .sql-psc.goog first
|
|
165
|
+
psc_dns_names.sort(key=lambda x: x.endswith(".sql-psc.goog"), reverse=True)
|
|
165
166
|
|
|
166
167
|
# Fall back do dns_name field if dns_names is not set
|
|
167
|
-
if
|
|
168
|
+
if not psc_dns_names:
|
|
168
169
|
dns_name = ret_dict.get("dnsName", None)
|
|
170
|
+
if dns_name:
|
|
171
|
+
psc_dns_names = [dns_name.rstrip(".")]
|
|
169
172
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
ip_addresses["PSC"] = dns_name.rstrip(".")
|
|
173
|
+
if psc_dns_names:
|
|
174
|
+
ip_addresses["PSC"] = psc_dns_names
|
|
173
175
|
|
|
174
176
|
return {
|
|
175
177
|
"ip_addresses": ip_addresses,
|
|
@@ -177,6 +179,47 @@ class CloudSQLClient:
|
|
|
177
179
|
"database_version": ret_dict["databaseVersion"],
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
async def resolve_connect_settings(
|
|
183
|
+
self,
|
|
184
|
+
dns_name: str,
|
|
185
|
+
location: str,
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
"""Asynchronously calls the resolveConnectSettings endpoint to resolve a
|
|
188
|
+
PSC DNS name to a connection name.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
dns_name (str): The DNS name of the Cloud SQL instance.
|
|
192
|
+
location (str): The region/location of the instance.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
A dictionary containing the resolve response (e.g. connectionName).
|
|
196
|
+
"""
|
|
197
|
+
# before making Cloud SQL Admin API calls, refresh creds if required
|
|
198
|
+
if self._credentials.token_state != TokenState.FRESH:
|
|
199
|
+
self._credentials.refresh(requests.Request())
|
|
200
|
+
|
|
201
|
+
headers = {
|
|
202
|
+
"Authorization": f"Bearer {self._credentials.token}",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/locations/{location}/dns/{dns_name}:resolveConnectSettings"
|
|
206
|
+
|
|
207
|
+
resp = await self._client.get(url, headers=headers)
|
|
208
|
+
if resp.status >= 500:
|
|
209
|
+
resp = await retry_50x(self._client.get, url, headers=headers)
|
|
210
|
+
try:
|
|
211
|
+
ret_dict = await resp.json()
|
|
212
|
+
if resp.status >= 400:
|
|
213
|
+
message = ret_dict.get("error", {}).get("message")
|
|
214
|
+
if message:
|
|
215
|
+
resp.reason = message
|
|
216
|
+
except Exception: # noqa: BLE001, S110
|
|
217
|
+
pass
|
|
218
|
+
finally:
|
|
219
|
+
resp.raise_for_status()
|
|
220
|
+
|
|
221
|
+
return ret_dict
|
|
222
|
+
|
|
180
223
|
async def _get_ephemeral(
|
|
181
224
|
self,
|
|
182
225
|
project: str,
|
|
@@ -223,7 +266,7 @@ class CloudSQLClient:
|
|
|
223
266
|
if message:
|
|
224
267
|
resp.reason = message
|
|
225
268
|
# skip, raise_for_status will catch all errors in finally block
|
|
226
|
-
except Exception:
|
|
269
|
+
except Exception: # noqa: BLE001, S110
|
|
227
270
|
pass
|
|
228
271
|
finally:
|
|
229
272
|
resp.raise_for_status()
|
|
@@ -244,8 +287,7 @@ class CloudSQLClient:
|
|
|
244
287
|
# Ref: https://github.com/googleapis/google-auth-library-python/blob/49a5ff7411a2ae4d32a7d11700f9f961c55406a9/google/auth/_helpers.py#L93-L99
|
|
245
288
|
token_expiration = token_expiration.replace(tzinfo=datetime.timezone.utc)
|
|
246
289
|
|
|
247
|
-
|
|
248
|
-
expiration = token_expiration
|
|
290
|
+
expiration = min(expiration, token_expiration)
|
|
249
291
|
return ephemeral_cert, expiration
|
|
250
292
|
|
|
251
293
|
async def get_connection_info(
|
|
@@ -274,7 +316,7 @@ class CloudSQLClient:
|
|
|
274
316
|
"""
|
|
275
317
|
priv_key, pub_key = await keys
|
|
276
318
|
# before making Cloud SQL Admin API calls, refresh creds if required
|
|
277
|
-
if
|
|
319
|
+
if self._credentials.token_state != TokenState.FRESH:
|
|
278
320
|
self._credentials.refresh(requests.Request())
|
|
279
321
|
|
|
280
322
|
metadata_task = asyncio.create_task(
|
|
@@ -18,13 +18,12 @@ import abc
|
|
|
18
18
|
from dataclasses import dataclass
|
|
19
19
|
import logging
|
|
20
20
|
import ssl
|
|
21
|
-
from typing import Any,
|
|
22
|
-
|
|
23
|
-
from aiofiles.tempfile import TemporaryDirectory
|
|
21
|
+
from typing import Any, TYPE_CHECKING
|
|
24
22
|
|
|
25
23
|
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
26
24
|
from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
|
|
27
25
|
from google.cloud.sql.connector.exceptions import TLSVersionError
|
|
26
|
+
from google.cloud.sql.connector.utils import AsyncTemporaryDirectory
|
|
28
27
|
from google.cloud.sql.connector.utils import write_to_file
|
|
29
28
|
|
|
30
29
|
if TYPE_CHECKING:
|
|
@@ -68,7 +67,7 @@ class ConnectionInfo:
|
|
|
68
67
|
ip_addrs: dict[str, Any]
|
|
69
68
|
database_version: str
|
|
70
69
|
expiration: datetime.datetime
|
|
71
|
-
context:
|
|
70
|
+
context: ssl.SSLContext | None = None
|
|
72
71
|
|
|
73
72
|
async def create_ssl_context(self, enable_iam_auth: bool = False) -> ssl.SSLContext:
|
|
74
73
|
"""Constructs a SSL/TLS context for the given connection info.
|
|
@@ -108,7 +107,7 @@ class ConnectionInfo:
|
|
|
108
107
|
# tmpdir and its contents are automatically deleted after the CA cert
|
|
109
108
|
# and ephemeral cert are loaded into the SSLcontext. The values
|
|
110
109
|
# need to be written to files in order to be loaded by the SSLContext
|
|
111
|
-
async with
|
|
110
|
+
async with AsyncTemporaryDirectory() as tmpdir:
|
|
112
111
|
ca_filename, cert_filename, key_filename = await write_to_file(
|
|
113
112
|
tmpdir, self.server_ca_cert, self.client_cert, self.private_key
|
|
114
113
|
)
|
|
@@ -122,6 +121,17 @@ class ConnectionInfo:
|
|
|
122
121
|
"""Returns the first IP address for the instance, according to the preference
|
|
123
122
|
supplied by ip_type. If no IP addressess with the given preference are found,
|
|
124
123
|
an error is raised."""
|
|
124
|
+
if ip_type.value in self.ip_addrs:
|
|
125
|
+
return self.ip_addrs[ip_type.value][0]
|
|
126
|
+
raise CloudSQLIPTypeError(
|
|
127
|
+
"Cloud SQL instance does not have any IP addresses matching "
|
|
128
|
+
f"preference: {ip_type.value}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def get_preferred_ips(self, ip_type: IPTypes) -> list[str]:
|
|
132
|
+
"""Returns all IP addresses for the instance, according to the preference
|
|
133
|
+
supplied by ip_type. If no IP addressess with the given preference are found,
|
|
134
|
+
an error is raised."""
|
|
125
135
|
if ip_type.value in self.ip_addrs:
|
|
126
136
|
return self.ip_addrs[ip_type.value]
|
|
127
137
|
raise CloudSQLIPTypeError(
|
|
@@ -18,7 +18,7 @@ import re
|
|
|
18
18
|
# Instance connection name is the format <PROJECT>:<REGION>:<INSTANCE_NAME>
|
|
19
19
|
# Additionally, we have to support legacy "domain-scoped" projects
|
|
20
20
|
# (e.g. "google.com:PROJECT")
|
|
21
|
-
CONN_NAME_REGEX = re.compile(
|
|
21
|
+
CONN_NAME_REGEX = re.compile("([^:]+(:[^:]+)?):([^:]+):([^:]+)")
|
|
22
22
|
# The domain name pattern in accordance with RFC 1035, RFC 1123 and RFC 2181.
|
|
23
23
|
DOMAIN_NAME_REGEX = re.compile(
|
|
24
24
|
r"^(?:[_a-z0-9](?:[_a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?)?$"
|
|
@@ -48,9 +48,7 @@ class ConnectionName:
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
def _is_valid_domain(domain_name: str) -> bool:
|
|
51
|
-
|
|
52
|
-
return False
|
|
53
|
-
return True
|
|
51
|
+
return DOMAIN_NAME_REGEX.fullmatch(domain_name) is not None
|
|
54
52
|
|
|
55
53
|
|
|
56
54
|
def _parse_connection_name(connection_name: str) -> ConnectionName:
|
|
@@ -23,13 +23,17 @@ import os
|
|
|
23
23
|
import socket
|
|
24
24
|
from threading import Thread
|
|
25
25
|
from types import TracebackType
|
|
26
|
-
from typing import Any, Callable
|
|
26
|
+
from typing import Any, Callable
|
|
27
27
|
|
|
28
28
|
import google.auth
|
|
29
29
|
from google.auth.credentials import Credentials
|
|
30
30
|
from google.auth.credentials import with_scopes_if_required
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
from google.cloud.sql.connector import asyncpg
|
|
33
|
+
from google.cloud.sql.connector import pg8000
|
|
34
|
+
from google.cloud.sql.connector import psycopg
|
|
35
|
+
from google.cloud.sql.connector import pymysql
|
|
36
|
+
from google.cloud.sql.connector import pytds
|
|
33
37
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
34
38
|
from google.cloud.sql.connector.enums import DriverMapping
|
|
35
39
|
from google.cloud.sql.connector.enums import IPTypes
|
|
@@ -39,9 +43,6 @@ from google.cloud.sql.connector.exceptions import ConnectorLoopError
|
|
|
39
43
|
from google.cloud.sql.connector.instance import RefreshAheadCache
|
|
40
44
|
from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
41
45
|
from google.cloud.sql.connector.monitored_cache import MonitoredCache
|
|
42
|
-
import google.cloud.sql.connector.pg8000 as pg8000
|
|
43
|
-
import google.cloud.sql.connector.pymysql as pymysql
|
|
44
|
-
import google.cloud.sql.connector.pytds as pytds
|
|
45
46
|
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
46
47
|
from google.cloud.sql.connector.resolver import DnsResolver
|
|
47
48
|
from google.cloud.sql.connector.utils import format_database_user
|
|
@@ -64,14 +65,14 @@ class Connector:
|
|
|
64
65
|
ip_type: str | IPTypes = IPTypes.PUBLIC,
|
|
65
66
|
enable_iam_auth: bool = False,
|
|
66
67
|
timeout: int = 30,
|
|
67
|
-
credentials:
|
|
68
|
-
loop:
|
|
69
|
-
quota_project:
|
|
70
|
-
sqladmin_api_endpoint:
|
|
71
|
-
user_agent:
|
|
72
|
-
universe_domain:
|
|
68
|
+
credentials: Credentials | None = None,
|
|
69
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
70
|
+
quota_project: str | None = None,
|
|
71
|
+
sqladmin_api_endpoint: str | None = None,
|
|
72
|
+
user_agent: str | None = None,
|
|
73
|
+
universe_domain: str | None = None,
|
|
73
74
|
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
74
|
-
resolver: type[DefaultResolver
|
|
75
|
+
resolver: type[DefaultResolver | DnsResolver] = DefaultResolver,
|
|
75
76
|
failover_period: int = 30,
|
|
76
77
|
) -> None:
|
|
77
78
|
"""Initializes a Connector instance.
|
|
@@ -133,10 +134,10 @@ class Connector:
|
|
|
133
134
|
# if event loop is given, use for background tasks
|
|
134
135
|
if loop:
|
|
135
136
|
self._loop: asyncio.AbstractEventLoop = loop
|
|
136
|
-
self._thread:
|
|
137
|
+
self._thread: Thread | None = None
|
|
137
138
|
# if lazy refresh is specified we should lazy init keys
|
|
138
139
|
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
139
|
-
self._keys:
|
|
140
|
+
self._keys: asyncio.Future | None = None
|
|
140
141
|
else:
|
|
141
142
|
self._keys = loop.create_task(generate_keys())
|
|
142
143
|
# if no event loop is given, spin up new loop in background thread
|
|
@@ -155,7 +156,7 @@ class Connector:
|
|
|
155
156
|
# initialize dict to store caches, key is a tuple consisting of instance
|
|
156
157
|
# connection name string and enable_iam_auth boolean flag
|
|
157
158
|
self._cache: dict[tuple[str, bool], MonitoredCache] = {}
|
|
158
|
-
self._client:
|
|
159
|
+
self._client: CloudSQLClient | None = None
|
|
159
160
|
self._closed: bool = False
|
|
160
161
|
|
|
161
162
|
# initialize credentials
|
|
@@ -176,7 +177,8 @@ class Connector:
|
|
|
176
177
|
self._timeout = timeout
|
|
177
178
|
self._enable_iam_auth = enable_iam_auth
|
|
178
179
|
self._user_agent = user_agent
|
|
179
|
-
self.
|
|
180
|
+
self._resolver_cls = resolver
|
|
181
|
+
self._resolver: DefaultResolver | DnsResolver | None = None
|
|
180
182
|
self._failover_period = failover_period
|
|
181
183
|
# if ip_type is str, convert to IPTypes enum
|
|
182
184
|
if isinstance(ip_type, str):
|
|
@@ -316,6 +318,8 @@ class Connector:
|
|
|
316
318
|
user_agent=self._user_agent,
|
|
317
319
|
driver=driver,
|
|
318
320
|
)
|
|
321
|
+
if self._resolver is None:
|
|
322
|
+
self._resolver = self._resolver_cls(client=self._client)
|
|
319
323
|
enable_iam_auth = kwargs.pop("enable_iam_auth", self._enable_iam_auth)
|
|
320
324
|
|
|
321
325
|
conn_name = await self._resolver.resolve(instance_connection_string)
|
|
@@ -329,7 +333,7 @@ class Connector:
|
|
|
329
333
|
logger.debug(
|
|
330
334
|
f"['{conn_name}']: Refresh strategy is set to lazy refresh"
|
|
331
335
|
)
|
|
332
|
-
cache:
|
|
336
|
+
cache: LazyRefreshCache | RefreshAheadCache = LazyRefreshCache(
|
|
333
337
|
conn_name,
|
|
334
338
|
self._client,
|
|
335
339
|
self._keys,
|
|
@@ -359,6 +363,7 @@ class Connector:
|
|
|
359
363
|
"pg8000": pg8000.connect,
|
|
360
364
|
"asyncpg": asyncpg.connect,
|
|
361
365
|
"pytds": pytds.connect,
|
|
366
|
+
"psycopg": psycopg.connect,
|
|
362
367
|
}
|
|
363
368
|
|
|
364
369
|
# only accept supported database drivers
|
|
@@ -384,13 +389,14 @@ class Connector:
|
|
|
384
389
|
conn_info = await monitored_cache.connect_info()
|
|
385
390
|
# validate driver matches intended database engine
|
|
386
391
|
DriverMapping.validate_engine(driver, conn_info.database_version)
|
|
387
|
-
|
|
392
|
+
preferred_ips = conn_info.get_preferred_ips(ip_type)
|
|
388
393
|
except Exception:
|
|
389
394
|
# with an error from Cloud SQL Admin API call or IP type, invalidate
|
|
390
395
|
# the cache and re-raise the error
|
|
391
396
|
await self._remove_cached(str(conn_name), enable_iam_auth)
|
|
392
397
|
raise
|
|
393
398
|
|
|
399
|
+
targets = []
|
|
394
400
|
# If the connector is configured with a custom DNS name, attempt to use
|
|
395
401
|
# that DNS name to connect to the instance. Fall back to the metadata IP
|
|
396
402
|
# address if the DNS name does not resolve to an IP address.
|
|
@@ -398,26 +404,29 @@ class Connector:
|
|
|
398
404
|
try:
|
|
399
405
|
ips = await self._resolver.resolve_a_record(conn_info.conn_name.domain_name)
|
|
400
406
|
if ips:
|
|
401
|
-
|
|
407
|
+
targets.extend(ips)
|
|
402
408
|
logger.debug(
|
|
403
409
|
f"['{instance_connection_string}']: Custom DNS name "
|
|
404
|
-
f"'{conn_info.conn_name.domain_name}' resolved to '{
|
|
410
|
+
f"'{conn_info.conn_name.domain_name}' resolved to '{ips}', "
|
|
405
411
|
"using it to connect"
|
|
406
412
|
)
|
|
407
413
|
else:
|
|
408
414
|
logger.debug(
|
|
409
415
|
f"['{instance_connection_string}']: Custom DNS name "
|
|
410
416
|
f"'{conn_info.conn_name.domain_name}' resolved but returned no "
|
|
411
|
-
f"entries, using '{
|
|
417
|
+
f"entries, using '{preferred_ips}' from instance metadata"
|
|
412
418
|
)
|
|
413
|
-
|
|
419
|
+
targets.extend(preferred_ips)
|
|
420
|
+
except Exception as e: # noqa: BLE001
|
|
414
421
|
logger.debug(
|
|
415
422
|
f"['{instance_connection_string}']: Custom DNS name "
|
|
416
423
|
f"'{conn_info.conn_name.domain_name}' did not resolve to an IP "
|
|
417
|
-
f"address: {e}, using '{
|
|
424
|
+
f"address: {e}, using '{preferred_ips}' from instance metadata"
|
|
418
425
|
)
|
|
426
|
+
targets.extend(preferred_ips)
|
|
427
|
+
else:
|
|
428
|
+
targets.extend(preferred_ips)
|
|
419
429
|
|
|
420
|
-
logger.debug(f"['{conn_info.conn_name}']: Connecting to {ip_address}:3307")
|
|
421
430
|
# format `user` param for automatic IAM database authn
|
|
422
431
|
if enable_iam_auth:
|
|
423
432
|
formatted_user = format_database_user(
|
|
@@ -428,32 +437,56 @@ class Connector:
|
|
|
428
437
|
f"['{instance_connection_string}']: Truncated IAM database username from {kwargs['user']} to {formatted_user}"
|
|
429
438
|
)
|
|
430
439
|
kwargs["user"] = formatted_user
|
|
440
|
+
|
|
431
441
|
try:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
442
|
+
last_ex = None
|
|
443
|
+
for target_ip in targets:
|
|
444
|
+
logger.debug(f"['{conn_info.conn_name}']: Connecting to {target_ip}:3307")
|
|
445
|
+
try:
|
|
446
|
+
# async drivers are unblocking and can be awaited directly
|
|
447
|
+
if driver in ASYNC_DRIVERS:
|
|
448
|
+
conn = await connector(
|
|
449
|
+
target_ip,
|
|
450
|
+
await conn_info.create_ssl_context(enable_iam_auth),
|
|
451
|
+
**kwargs,
|
|
452
|
+
)
|
|
453
|
+
last_ex = None
|
|
454
|
+
return conn
|
|
455
|
+
|
|
456
|
+
# Create socket with SSLContext for sync drivers
|
|
457
|
+
ctx = await conn_info.create_ssl_context(enable_iam_auth)
|
|
458
|
+
raw_sock = socket.create_connection((target_ip, SERVER_PROXY_PORT))
|
|
459
|
+
try:
|
|
460
|
+
sock = ctx.wrap_socket(
|
|
461
|
+
raw_sock,
|
|
462
|
+
server_hostname=target_ip,
|
|
463
|
+
)
|
|
464
|
+
except Exception:
|
|
465
|
+
raw_sock.close()
|
|
466
|
+
raise
|
|
467
|
+
|
|
468
|
+
# If this connection was opened using a domain name, then store it
|
|
469
|
+
# for later in case we need to forcibly close it on failover.
|
|
470
|
+
if conn_info.conn_name.domain_name:
|
|
471
|
+
monitored_cache.sockets.append(sock)
|
|
472
|
+
# Synchronous drivers are blocking and run using executor
|
|
473
|
+
connect_partial = partial(
|
|
474
|
+
connector,
|
|
475
|
+
target_ip,
|
|
476
|
+
sock,
|
|
477
|
+
**kwargs,
|
|
478
|
+
)
|
|
479
|
+
conn = await self._loop.run_in_executor(None, connect_partial)
|
|
480
|
+
last_ex = None
|
|
481
|
+
return conn
|
|
482
|
+
except Exception as e: # noqa: BLE001
|
|
483
|
+
logger.debug(
|
|
484
|
+
f"['{conn_info.conn_name}']: Connection to {target_ip} failed: {e}"
|
|
485
|
+
)
|
|
486
|
+
last_ex = e
|
|
487
|
+
|
|
488
|
+
if last_ex:
|
|
489
|
+
raise last_ex
|
|
457
490
|
|
|
458
491
|
except Exception:
|
|
459
492
|
# with any exception, we attempt a force refresh, then throw the error
|
|
@@ -479,9 +512,9 @@ class Connector:
|
|
|
479
512
|
|
|
480
513
|
def __exit__(
|
|
481
514
|
self,
|
|
482
|
-
exc_type:
|
|
483
|
-
exc_val:
|
|
484
|
-
exc_tb:
|
|
515
|
+
exc_type: type[BaseException] | None,
|
|
516
|
+
exc_val: BaseException | None,
|
|
517
|
+
exc_tb: TracebackType | None,
|
|
485
518
|
) -> None:
|
|
486
519
|
"""Exit context manager by closing Connector"""
|
|
487
520
|
self.close()
|
|
@@ -492,9 +525,9 @@ class Connector:
|
|
|
492
525
|
|
|
493
526
|
async def __aexit__(
|
|
494
527
|
self,
|
|
495
|
-
exc_type:
|
|
496
|
-
exc_val:
|
|
497
|
-
exc_tb:
|
|
528
|
+
exc_type: type[BaseException] | None,
|
|
529
|
+
exc_val: BaseException | None,
|
|
530
|
+
exc_tb: TracebackType | None,
|
|
498
531
|
) -> None:
|
|
499
532
|
"""Exit async context manager by closing Connector"""
|
|
500
533
|
await self.close_async()
|
|
@@ -528,14 +561,14 @@ async def create_async_connector(
|
|
|
528
561
|
ip_type: str | IPTypes = IPTypes.PUBLIC,
|
|
529
562
|
enable_iam_auth: bool = False,
|
|
530
563
|
timeout: int = 30,
|
|
531
|
-
credentials:
|
|
532
|
-
loop:
|
|
533
|
-
quota_project:
|
|
534
|
-
sqladmin_api_endpoint:
|
|
535
|
-
user_agent:
|
|
536
|
-
universe_domain:
|
|
564
|
+
credentials: Credentials | None = None,
|
|
565
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
566
|
+
quota_project: str | None = None,
|
|
567
|
+
sqladmin_api_endpoint: str | None = None,
|
|
568
|
+
user_agent: str | None = None,
|
|
569
|
+
universe_domain: str | None = None,
|
|
537
570
|
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
538
|
-
resolver: type[DefaultResolver
|
|
571
|
+
resolver: type[DefaultResolver | DnsResolver] = DefaultResolver,
|
|
539
572
|
failover_period: int = 30,
|
|
540
573
|
) -> Connector:
|
|
541
574
|
"""Helper function to create Connector object for asyncio connections.
|