cloud-sql-python-connector 1.14.0__py2.py3-none-any.whl → 1.15.0__py2.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.14.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/METADATA +65 -1
- cloud_sql_python_connector-1.15.0.dist-info/RECORD +24 -0
- google/cloud/sql/connector/__init__.py +4 -0
- google/cloud/sql/connector/client.py +36 -16
- google/cloud/sql/connector/connection_info.py +2 -0
- google/cloud/sql/connector/connection_name.py +11 -1
- google/cloud/sql/connector/connector.py +17 -12
- google/cloud/sql/connector/exceptions.py +7 -0
- google/cloud/sql/connector/instance.py +7 -27
- google/cloud/sql/connector/lazy.py +5 -15
- google/cloud/sql/connector/resolver.py +70 -0
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.14.0.dist-info/RECORD +0 -23
- {cloud_sql_python_connector-1.14.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/LICENSE +0 -0
- {cloud_sql_python_connector-1.14.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/WHEEL +0 -0
- {cloud_sql_python_connector-1.14.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/top_level.txt +0 -0
{cloud_sql_python_connector-1.14.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.0
|
|
4
4
|
Summary: The Cloud SQL Python Connector is a library that can be used alongside a database driver to allow users with sufficient permissions to connect to a Cloud SQL database without having to manually allowlist IPs or manage SSL certificates.
|
|
5
5
|
Home-page: https://github.com/GoogleCloudPlatform/cloud-sql-python-connector
|
|
6
6
|
Author: Google LLC
|
|
@@ -22,6 +22,7 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: aiofiles
|
|
23
23
|
Requires-Dist: aiohttp
|
|
24
24
|
Requires-Dist: cryptography>=42.0.0
|
|
25
|
+
Requires-Dist: dnspython>=2.0.0
|
|
25
26
|
Requires-Dist: Requests
|
|
26
27
|
Requires-Dist: google-auth>=2.28.0
|
|
27
28
|
Provides-Extra: asyncpg
|
|
@@ -400,6 +401,69 @@ conn = connector.connect(
|
|
|
400
401
|
)
|
|
401
402
|
```
|
|
402
403
|
|
|
404
|
+
### Using DNS domain names to identify instances
|
|
405
|
+
|
|
406
|
+
The connector can be configured to use DNS to look up an instance. This would
|
|
407
|
+
allow you to configure your application to connect to a database instance, and
|
|
408
|
+
centrally configure which instance in your DNS zone.
|
|
409
|
+
|
|
410
|
+
#### Configure your DNS Records
|
|
411
|
+
|
|
412
|
+
Add a DNS TXT record for the Cloud SQL instance to a **private** DNS server
|
|
413
|
+
or a private Google Cloud DNS Zone used by your application.
|
|
414
|
+
|
|
415
|
+
> [!NOTE]
|
|
416
|
+
>
|
|
417
|
+
> You are strongly discouraged from adding DNS records for your
|
|
418
|
+
> Cloud SQL instances to a public DNS server. This would allow anyone on the
|
|
419
|
+
> internet to discover the Cloud SQL instance name.
|
|
420
|
+
|
|
421
|
+
For example: suppose you wanted to use the domain name
|
|
422
|
+
`prod-db.mycompany.example.com` to connect to your database instance
|
|
423
|
+
`my-project:region:my-instance`. You would create the following DNS record:
|
|
424
|
+
|
|
425
|
+
* Record type: `TXT`
|
|
426
|
+
* Name: `prod-db.mycompany.example.com` – This is the domain name used by the application
|
|
427
|
+
* Value: `my-project:my-region:my-instance` – This is the Cloud SQL instance connection name
|
|
428
|
+
|
|
429
|
+
#### Configure the connector
|
|
430
|
+
|
|
431
|
+
Configure the connector to resolve DNS names by initializing it with
|
|
432
|
+
`resolver=DnsResolver` and replacing the instance connection name with the DNS
|
|
433
|
+
name in `connector.connect`:
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from google.cloud.sql.connector import Connector, DnsResolver
|
|
437
|
+
import pymysql
|
|
438
|
+
import sqlalchemy
|
|
439
|
+
|
|
440
|
+
# helper function to return SQLAlchemy connection pool
|
|
441
|
+
def init_connection_pool(connector: Connector) -> sqlalchemy.engine.Engine:
|
|
442
|
+
# function used to generate database connection
|
|
443
|
+
def getconn() -> pymysql.connections.Connection:
|
|
444
|
+
conn = connector.connect(
|
|
445
|
+
"prod-db.mycompany.example.com", # using DNS name
|
|
446
|
+
"pymysql",
|
|
447
|
+
user="my-user",
|
|
448
|
+
password="my-password",
|
|
449
|
+
db="my-db-name"
|
|
450
|
+
)
|
|
451
|
+
return conn
|
|
452
|
+
|
|
453
|
+
# create connection pool
|
|
454
|
+
pool = sqlalchemy.create_engine(
|
|
455
|
+
"mysql+pymysql://",
|
|
456
|
+
creator=getconn,
|
|
457
|
+
)
|
|
458
|
+
return pool
|
|
459
|
+
|
|
460
|
+
# initialize Cloud SQL Python Connector with `resolver=DnsResolver`
|
|
461
|
+
with Connector(resolver=DnsResolver) as connector:
|
|
462
|
+
# initialize connection pool
|
|
463
|
+
pool = init_connection_pool(connector)
|
|
464
|
+
# ... use SQLAlchemy engine normally
|
|
465
|
+
```
|
|
466
|
+
|
|
403
467
|
### Using the Python Connector with Python Web Frameworks
|
|
404
468
|
|
|
405
469
|
The Python Connector can be used alongside popular Python web frameworks such
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
google/cloud/sql/connector/__init__.py,sha256=kSGB2q6hQwkFLD1eI7RjY4BvPLvg2FD3wfPBmbnsjDE,1149
|
|
2
|
+
google/cloud/sql/connector/asyncpg.py,sha256=HUOB0rRhr2-zMbygDtdWTDNlYqEPkN0ADqCIpM3waDc,1839
|
|
3
|
+
google/cloud/sql/connector/client.py,sha256=ocUN1nrgZyJRnxgh6Ym3w0c_qKb8hqKCmR1OiJ2KUpM,12622
|
|
4
|
+
google/cloud/sql/connector/connection_info.py,sha256=cFNOeHJ0lQODf7q5Hc25LXsNWzWgzYcFMrBuNdZ1y5k,4430
|
|
5
|
+
google/cloud/sql/connector/connection_name.py,sha256=4F5XhgmSWGEeF-3TJk2E61YPbxMAecPGw56nPTZmQ2o,2067
|
|
6
|
+
google/cloud/sql/connector/connector.py,sha256=9t2iz_Tx1NluGgaM8JlZUfb7HWHgsWjmLnAL8WvKp4E,21157
|
|
7
|
+
google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
|
|
8
|
+
google/cloud/sql/connector/exceptions.py,sha256=eyflYggkKZx3t944FTPqQrX-R75lZgtpOwC4uVLPhG0,1823
|
|
9
|
+
google/cloud/sql/connector/instance.py,sha256=nRzhE7t_M6AhjiYvEZHOjkBHx3d9wGRCC6RekgDApFI,8379
|
|
10
|
+
google/cloud/sql/connector/lazy.py,sha256=p1wLte49xE_aHiY5shjMbxwytgW-ycS0Hd67C34gaDY,4631
|
|
11
|
+
google/cloud/sql/connector/pg8000.py,sha256=gZisjfXP4rNIjyxOQS75fvr0Xe6eLrFo4mIhVjoMLTw,1815
|
|
12
|
+
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
google/cloud/sql/connector/pymysql.py,sha256=82DwSJUhZHH2AW-vO7qhkljyShwj6Kmd17WCbOGZH74,2106
|
|
14
|
+
google/cloud/sql/connector/pytds.py,sha256=h-1pDhqLDkhviKBBOJ29gnIeYHKWw7Dtj91VLu1iNUI,2453
|
|
15
|
+
google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
|
|
16
|
+
google/cloud/sql/connector/refresh_utils.py,sha256=HfuuLdlqg2IxusB4T3BMj-BJlGsXtHsrCX_Hp-eJ_j0,5063
|
|
17
|
+
google/cloud/sql/connector/resolver.py,sha256=cNDJMhmS_0CIAmz8kCABKHJNLNm1wiwIVoPEN-gWzzY,2850
|
|
18
|
+
google/cloud/sql/connector/utils.py,sha256=7g1fX-JsZc7xOgTuxrAiroFHQIQk_BEJZNYaKOinwQY,3508
|
|
19
|
+
google/cloud/sql/connector/version.py,sha256=l3R3usCjpr4Wwp3w2gpWFcrGmsLi8Y1gAAvZC15RLv0,598
|
|
20
|
+
cloud_sql_python_connector-1.15.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
21
|
+
cloud_sql_python_connector-1.15.0.dist-info/METADATA,sha256=Qszn2k_QqcVlX92c_1A-7MurYUoojTIMbx5M13aPZnI,30314
|
|
22
|
+
cloud_sql_python_connector-1.15.0.dist-info/WHEEL,sha256=OpXWERl2xLPRHTvd2ZXo_iluPEQd8uSbYkJ53NAER_Y,109
|
|
23
|
+
cloud_sql_python_connector-1.15.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
24
|
+
cloud_sql_python_connector-1.15.0.dist-info/RECORD,,
|
|
@@ -18,12 +18,16 @@ from google.cloud.sql.connector.connector import Connector
|
|
|
18
18
|
from google.cloud.sql.connector.connector import create_async_connector
|
|
19
19
|
from google.cloud.sql.connector.enums import IPTypes
|
|
20
20
|
from google.cloud.sql.connector.enums import RefreshStrategy
|
|
21
|
+
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
22
|
+
from google.cloud.sql.connector.resolver import DnsResolver
|
|
21
23
|
from google.cloud.sql.connector.version import __version__
|
|
22
24
|
|
|
23
25
|
__all__ = [
|
|
24
26
|
"__version__",
|
|
25
27
|
"create_async_connector",
|
|
26
28
|
"Connector",
|
|
29
|
+
"DefaultResolver",
|
|
30
|
+
"DnsResolver",
|
|
27
31
|
"IPTypes",
|
|
28
32
|
"RefreshStrategy",
|
|
29
33
|
]
|
|
@@ -26,6 +26,7 @@ from google.auth.credentials import TokenState
|
|
|
26
26
|
from google.auth.transport import requests
|
|
27
27
|
|
|
28
28
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
29
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
29
30
|
from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
|
|
30
31
|
from google.cloud.sql.connector.refresh_utils import _downscope_credentials
|
|
31
32
|
from google.cloud.sql.connector.refresh_utils import retry_50x
|
|
@@ -128,8 +129,19 @@ class CloudSQLClient:
|
|
|
128
129
|
resp = await self._client.get(url, headers=headers)
|
|
129
130
|
if resp.status >= 500:
|
|
130
131
|
resp = await retry_50x(self._client.get, url, headers=headers)
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
# try to get response json for better error message
|
|
133
|
+
try:
|
|
134
|
+
ret_dict = await resp.json()
|
|
135
|
+
if resp.status >= 400:
|
|
136
|
+
# if detailed error message is in json response, use as error message
|
|
137
|
+
message = ret_dict.get("error", {}).get("message")
|
|
138
|
+
if message:
|
|
139
|
+
resp.reason = message
|
|
140
|
+
# skip, raise_for_status will catch all errors in finally block
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
finally:
|
|
144
|
+
resp.raise_for_status()
|
|
133
145
|
|
|
134
146
|
if ret_dict["region"] != region:
|
|
135
147
|
raise ValueError(
|
|
@@ -198,8 +210,19 @@ class CloudSQLClient:
|
|
|
198
210
|
resp = await self._client.post(url, headers=headers, json=data)
|
|
199
211
|
if resp.status >= 500:
|
|
200
212
|
resp = await retry_50x(self._client.post, url, headers=headers, json=data)
|
|
201
|
-
|
|
202
|
-
|
|
213
|
+
# try to get response json for better error message
|
|
214
|
+
try:
|
|
215
|
+
ret_dict = await resp.json()
|
|
216
|
+
if resp.status >= 400:
|
|
217
|
+
# if detailed error message is in json response, use as error message
|
|
218
|
+
message = ret_dict.get("error", {}).get("message")
|
|
219
|
+
if message:
|
|
220
|
+
resp.reason = message
|
|
221
|
+
# skip, raise_for_status will catch all errors in finally block
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
finally:
|
|
225
|
+
resp.raise_for_status()
|
|
203
226
|
|
|
204
227
|
ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
|
|
205
228
|
|
|
@@ -223,9 +246,7 @@ class CloudSQLClient:
|
|
|
223
246
|
|
|
224
247
|
async def get_connection_info(
|
|
225
248
|
self,
|
|
226
|
-
|
|
227
|
-
region: str,
|
|
228
|
-
instance: str,
|
|
249
|
+
conn_name: ConnectionName,
|
|
229
250
|
keys: asyncio.Future,
|
|
230
251
|
enable_iam_auth: bool,
|
|
231
252
|
) -> ConnectionInfo:
|
|
@@ -233,10 +254,8 @@ class CloudSQLClient:
|
|
|
233
254
|
Admin API.
|
|
234
255
|
|
|
235
256
|
Args:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
region (str): The region the Cloud SQL instance is located in.
|
|
239
|
-
instance (str): Name of the Cloud SQL instance.
|
|
257
|
+
conn_name (ConnectionName): The Cloud SQL instance's
|
|
258
|
+
connection name.
|
|
240
259
|
keys (asyncio.Future): A future to the client's public-private key
|
|
241
260
|
pair.
|
|
242
261
|
enable_iam_auth (bool): Whether an automatic IAM database
|
|
@@ -256,16 +275,16 @@ class CloudSQLClient:
|
|
|
256
275
|
|
|
257
276
|
metadata_task = asyncio.create_task(
|
|
258
277
|
self._get_metadata(
|
|
259
|
-
project,
|
|
260
|
-
region,
|
|
261
|
-
|
|
278
|
+
conn_name.project,
|
|
279
|
+
conn_name.region,
|
|
280
|
+
conn_name.instance_name,
|
|
262
281
|
)
|
|
263
282
|
)
|
|
264
283
|
|
|
265
284
|
ephemeral_task = asyncio.create_task(
|
|
266
285
|
self._get_ephemeral(
|
|
267
|
-
project,
|
|
268
|
-
|
|
286
|
+
conn_name.project,
|
|
287
|
+
conn_name.instance_name,
|
|
269
288
|
pub_key,
|
|
270
289
|
enable_iam_auth,
|
|
271
290
|
)
|
|
@@ -289,6 +308,7 @@ class CloudSQLClient:
|
|
|
289
308
|
ephemeral_cert, expiration = await ephemeral_task
|
|
290
309
|
|
|
291
310
|
return ConnectionInfo(
|
|
311
|
+
conn_name,
|
|
292
312
|
ephemeral_cert,
|
|
293
313
|
metadata["server_ca_cert"],
|
|
294
314
|
priv_key,
|
|
@@ -21,6 +21,7 @@ from typing import Any, Optional, TYPE_CHECKING
|
|
|
21
21
|
|
|
22
22
|
from aiofiles.tempfile import TemporaryDirectory
|
|
23
23
|
|
|
24
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
24
25
|
from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
|
|
25
26
|
from google.cloud.sql.connector.exceptions import TLSVersionError
|
|
26
27
|
from google.cloud.sql.connector.utils import write_to_file
|
|
@@ -38,6 +39,7 @@ class ConnectionInfo:
|
|
|
38
39
|
"""Contains all necessary information to connect securely to the
|
|
39
40
|
server-side Proxy running on a Cloud SQL instance."""
|
|
40
41
|
|
|
42
|
+
conn_name: ConnectionName
|
|
41
43
|
client_cert: str
|
|
42
44
|
server_ca_cert: str
|
|
43
45
|
private_key: bytes
|
|
@@ -31,12 +31,21 @@ class ConnectionName:
|
|
|
31
31
|
project: str
|
|
32
32
|
region: str
|
|
33
33
|
instance_name: str
|
|
34
|
+
domain_name: str = ""
|
|
34
35
|
|
|
35
36
|
def __str__(self) -> str:
|
|
37
|
+
if self.domain_name:
|
|
38
|
+
return f"{self.domain_name} -> {self.project}:{self.region}:{self.instance_name}"
|
|
36
39
|
return f"{self.project}:{self.region}:{self.instance_name}"
|
|
37
40
|
|
|
38
41
|
|
|
39
|
-
def
|
|
42
|
+
def _parse_connection_name(connection_name: str) -> ConnectionName:
|
|
43
|
+
return _parse_connection_name_with_domain_name(connection_name, "")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_connection_name_with_domain_name(
|
|
47
|
+
connection_name: str, domain_name: str
|
|
48
|
+
) -> ConnectionName:
|
|
40
49
|
if CONN_NAME_REGEX.fullmatch(connection_name) is None:
|
|
41
50
|
raise ValueError(
|
|
42
51
|
"Arg `instance_connection_string` must have "
|
|
@@ -48,4 +57,5 @@ def _parse_instance_connection_name(connection_name: str) -> ConnectionName:
|
|
|
48
57
|
connection_name_split[1],
|
|
49
58
|
connection_name_split[3],
|
|
50
59
|
connection_name_split[4],
|
|
60
|
+
domain_name,
|
|
51
61
|
)
|
|
@@ -37,6 +37,8 @@ from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
|
37
37
|
import google.cloud.sql.connector.pg8000 as pg8000
|
|
38
38
|
import google.cloud.sql.connector.pymysql as pymysql
|
|
39
39
|
import google.cloud.sql.connector.pytds as pytds
|
|
40
|
+
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
41
|
+
from google.cloud.sql.connector.resolver import DnsResolver
|
|
40
42
|
from google.cloud.sql.connector.utils import format_database_user
|
|
41
43
|
from google.cloud.sql.connector.utils import generate_keys
|
|
42
44
|
|
|
@@ -63,6 +65,7 @@ class Connector:
|
|
|
63
65
|
user_agent: Optional[str] = None,
|
|
64
66
|
universe_domain: Optional[str] = None,
|
|
65
67
|
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
68
|
+
resolver: Type[DefaultResolver] | Type[DnsResolver] = DefaultResolver,
|
|
66
69
|
) -> None:
|
|
67
70
|
"""Initializes a Connector instance.
|
|
68
71
|
|
|
@@ -104,6 +107,12 @@ class Connector:
|
|
|
104
107
|
of the following: RefreshStrategy.LAZY ("LAZY") or
|
|
105
108
|
RefreshStrategy.BACKGROUND ("BACKGROUND").
|
|
106
109
|
Default: RefreshStrategy.BACKGROUND
|
|
110
|
+
|
|
111
|
+
resolver (DefaultResolver | DnsResolver): The class name of the
|
|
112
|
+
resolver to use for resolving the Cloud SQL instance connection
|
|
113
|
+
name. To resolve a DNS record to an instance connection name, use
|
|
114
|
+
DnsResolver.
|
|
115
|
+
Default: DefaultResolver
|
|
107
116
|
"""
|
|
108
117
|
# if refresh_strategy is str, convert to RefreshStrategy enum
|
|
109
118
|
if isinstance(refresh_strategy, str):
|
|
@@ -157,6 +166,7 @@ class Connector:
|
|
|
157
166
|
self._enable_iam_auth = enable_iam_auth
|
|
158
167
|
self._quota_project = quota_project
|
|
159
168
|
self._user_agent = user_agent
|
|
169
|
+
self._resolver = resolver()
|
|
160
170
|
# if ip_type is str, convert to IPTypes enum
|
|
161
171
|
if isinstance(ip_type, str):
|
|
162
172
|
ip_type = IPTypes._from_str(ip_type)
|
|
@@ -269,31 +279,28 @@ class Connector:
|
|
|
269
279
|
if (instance_connection_string, enable_iam_auth) in self._cache:
|
|
270
280
|
cache = self._cache[(instance_connection_string, enable_iam_auth)]
|
|
271
281
|
else:
|
|
282
|
+
conn_name = await self._resolver.resolve(instance_connection_string)
|
|
272
283
|
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
273
284
|
logger.debug(
|
|
274
|
-
f"['{
|
|
275
|
-
" to lazy refresh"
|
|
285
|
+
f"['{conn_name}']: Refresh strategy is set to lazy refresh"
|
|
276
286
|
)
|
|
277
287
|
cache = LazyRefreshCache(
|
|
278
|
-
|
|
288
|
+
conn_name,
|
|
279
289
|
self._client,
|
|
280
290
|
self._keys,
|
|
281
291
|
enable_iam_auth,
|
|
282
292
|
)
|
|
283
293
|
else:
|
|
284
294
|
logger.debug(
|
|
285
|
-
f"['{
|
|
286
|
-
" to backgound refresh"
|
|
295
|
+
f"['{conn_name}']: Refresh strategy is set to backgound refresh"
|
|
287
296
|
)
|
|
288
297
|
cache = RefreshAheadCache(
|
|
289
|
-
|
|
298
|
+
conn_name,
|
|
290
299
|
self._client,
|
|
291
300
|
self._keys,
|
|
292
301
|
enable_iam_auth,
|
|
293
302
|
)
|
|
294
|
-
logger.debug(
|
|
295
|
-
f"['{instance_connection_string}']: Connection info added to cache"
|
|
296
|
-
)
|
|
303
|
+
logger.debug(f"['{conn_name}']: Connection info added to cache")
|
|
297
304
|
self._cache[(instance_connection_string, enable_iam_auth)] = cache
|
|
298
305
|
|
|
299
306
|
connect_func = {
|
|
@@ -332,9 +339,7 @@ class Connector:
|
|
|
332
339
|
# the cache and re-raise the error
|
|
333
340
|
await self._remove_cached(instance_connection_string, enable_iam_auth)
|
|
334
341
|
raise
|
|
335
|
-
logger.debug(
|
|
336
|
-
f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
|
|
337
|
-
)
|
|
342
|
+
logger.debug(f"['{conn_info.conn_name}']: Connecting to {ip_address}:3307")
|
|
338
343
|
# format `user` param for automatic IAM database authn
|
|
339
344
|
if enable_iam_auth:
|
|
340
345
|
formatted_user = format_database_user(
|
|
@@ -70,3 +70,10 @@ class IncompatibleDriverError(Exception):
|
|
|
70
70
|
Exception to be raised when the database driver given is for the wrong
|
|
71
71
|
database engine. (i.e. asyncpg for a MySQL database)
|
|
72
72
|
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DnsResolutionError(Exception):
|
|
76
|
+
"""
|
|
77
|
+
Exception to be raised when an instance connection name can not be resolved
|
|
78
|
+
from a DNS record.
|
|
79
|
+
"""
|
|
@@ -22,11 +22,9 @@ from datetime import timedelta
|
|
|
22
22
|
from datetime import timezone
|
|
23
23
|
import logging
|
|
24
24
|
|
|
25
|
-
import aiohttp
|
|
26
|
-
|
|
27
25
|
from google.cloud.sql.connector.client import CloudSQLClient
|
|
28
26
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
29
|
-
from google.cloud.sql.connector.connection_name import
|
|
27
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
30
28
|
from google.cloud.sql.connector.exceptions import RefreshNotValidError
|
|
31
29
|
from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
|
|
32
30
|
from google.cloud.sql.connector.refresh_utils import _is_valid
|
|
@@ -47,7 +45,7 @@ class RefreshAheadCache:
|
|
|
47
45
|
|
|
48
46
|
def __init__(
|
|
49
47
|
self,
|
|
50
|
-
|
|
48
|
+
conn_name: ConnectionName,
|
|
51
49
|
client: CloudSQLClient,
|
|
52
50
|
keys: asyncio.Future,
|
|
53
51
|
enable_iam_auth: bool = False,
|
|
@@ -55,8 +53,8 @@ class RefreshAheadCache:
|
|
|
55
53
|
"""Initializes a RefreshAheadCache instance.
|
|
56
54
|
|
|
57
55
|
Args:
|
|
58
|
-
|
|
59
|
-
connection
|
|
56
|
+
conn_name (ConnectionName): The Cloud SQL instance's
|
|
57
|
+
connection name.
|
|
60
58
|
client (CloudSQLClient): The Cloud SQL Client instance.
|
|
61
59
|
keys (asyncio.Future): A future to the client's public-private key
|
|
62
60
|
pair.
|
|
@@ -64,13 +62,6 @@ class RefreshAheadCache:
|
|
|
64
62
|
(Postgres and MySQL) as the default authentication method for all
|
|
65
63
|
connections.
|
|
66
64
|
"""
|
|
67
|
-
# validate and parse instance connection name
|
|
68
|
-
conn_name = _parse_instance_connection_name(instance_connection_string)
|
|
69
|
-
self._project, self._region, self._instance = (
|
|
70
|
-
conn_name.project,
|
|
71
|
-
conn_name.region,
|
|
72
|
-
conn_name.instance_name,
|
|
73
|
-
)
|
|
74
65
|
self._conn_name = conn_name
|
|
75
66
|
|
|
76
67
|
self._enable_iam_auth = enable_iam_auth
|
|
@@ -108,35 +99,24 @@ class RefreshAheadCache:
|
|
|
108
99
|
"""
|
|
109
100
|
self._refresh_in_progress.set()
|
|
110
101
|
logger.debug(
|
|
111
|
-
f"['{self._conn_name}']: Connection info refresh
|
|
102
|
+
f"['{self._conn_name}']: Connection info refresh operation started"
|
|
112
103
|
)
|
|
113
104
|
|
|
114
105
|
try:
|
|
115
106
|
await self._refresh_rate_limiter.acquire()
|
|
116
107
|
connection_info = await self._client.get_connection_info(
|
|
117
|
-
self.
|
|
118
|
-
self._region,
|
|
119
|
-
self._instance,
|
|
108
|
+
self._conn_name,
|
|
120
109
|
self._keys,
|
|
121
110
|
self._enable_iam_auth,
|
|
122
111
|
)
|
|
123
112
|
logger.debug(
|
|
124
|
-
f"['{self._conn_name}']: Connection info
|
|
113
|
+
f"['{self._conn_name}']: Connection info refresh operation complete"
|
|
125
114
|
)
|
|
126
115
|
logger.debug(
|
|
127
116
|
f"['{self._conn_name}']: Current certificate "
|
|
128
117
|
f"expiration = {connection_info.expiration.isoformat()}"
|
|
129
118
|
)
|
|
130
119
|
|
|
131
|
-
except aiohttp.ClientResponseError as e:
|
|
132
|
-
logger.debug(
|
|
133
|
-
f"['{self._conn_name}']: Connection info "
|
|
134
|
-
f"refresh operation failed: {str(e)}"
|
|
135
|
-
)
|
|
136
|
-
if e.status == 403:
|
|
137
|
-
e.message = "Forbidden: Authenticated IAM principal does not seem authorized to make API request. Verify 'Cloud SQL Admin API' is enabled within your GCP project and 'Cloud SQL Client' role has been granted to IAM principal."
|
|
138
|
-
raise
|
|
139
|
-
|
|
140
120
|
except Exception as e:
|
|
141
121
|
logger.debug(
|
|
142
122
|
f"['{self._conn_name}']: Connection info "
|
|
@@ -21,7 +21,7 @@ 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_name import
|
|
24
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
25
25
|
from google.cloud.sql.connector.refresh_utils import _refresh_buffer
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(name=__name__)
|
|
@@ -38,7 +38,7 @@ class LazyRefreshCache:
|
|
|
38
38
|
|
|
39
39
|
def __init__(
|
|
40
40
|
self,
|
|
41
|
-
|
|
41
|
+
conn_name: ConnectionName,
|
|
42
42
|
client: CloudSQLClient,
|
|
43
43
|
keys: asyncio.Future,
|
|
44
44
|
enable_iam_auth: bool = False,
|
|
@@ -46,8 +46,8 @@ class LazyRefreshCache:
|
|
|
46
46
|
"""Initializes a LazyRefreshCache instance.
|
|
47
47
|
|
|
48
48
|
Args:
|
|
49
|
-
|
|
50
|
-
connection
|
|
49
|
+
conn_name (ConnectionName): The Cloud SQL instance's
|
|
50
|
+
connection name.
|
|
51
51
|
client (CloudSQLClient): The Cloud SQL Client instance.
|
|
52
52
|
keys (asyncio.Future): A future to the client's public-private key
|
|
53
53
|
pair.
|
|
@@ -55,15 +55,7 @@ class LazyRefreshCache:
|
|
|
55
55
|
(Postgres and MySQL) as the default authentication method for all
|
|
56
56
|
connections.
|
|
57
57
|
"""
|
|
58
|
-
# validate and parse instance connection name
|
|
59
|
-
conn_name = _parse_instance_connection_name(instance_connection_string)
|
|
60
|
-
self._project, self._region, self._instance = (
|
|
61
|
-
conn_name.project,
|
|
62
|
-
conn_name.region,
|
|
63
|
-
conn_name.instance_name,
|
|
64
|
-
)
|
|
65
58
|
self._conn_name = conn_name
|
|
66
|
-
|
|
67
59
|
self._enable_iam_auth = enable_iam_auth
|
|
68
60
|
self._keys = keys
|
|
69
61
|
self._client = client
|
|
@@ -103,9 +95,7 @@ class LazyRefreshCache:
|
|
|
103
95
|
)
|
|
104
96
|
try:
|
|
105
97
|
conn_info = await self._client.get_connection_info(
|
|
106
|
-
self.
|
|
107
|
-
self._region,
|
|
108
|
-
self._instance,
|
|
98
|
+
self._conn_name,
|
|
109
99
|
self._keys,
|
|
110
100
|
self._enable_iam_auth,
|
|
111
101
|
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright 2024 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 dns.asyncresolver
|
|
16
|
+
|
|
17
|
+
from google.cloud.sql.connector.connection_name import (
|
|
18
|
+
_parse_connection_name_with_domain_name,
|
|
19
|
+
)
|
|
20
|
+
from google.cloud.sql.connector.connection_name import _parse_connection_name
|
|
21
|
+
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
22
|
+
from google.cloud.sql.connector.exceptions import DnsResolutionError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DefaultResolver:
|
|
26
|
+
"""DefaultResolver simply validates and parses instance connection name."""
|
|
27
|
+
|
|
28
|
+
async def resolve(self, connection_name: str) -> ConnectionName:
|
|
29
|
+
return _parse_connection_name(connection_name)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DnsResolver(dns.asyncresolver.Resolver):
|
|
33
|
+
"""
|
|
34
|
+
DnsResolver resolves domain names into instance connection names using
|
|
35
|
+
TXT records in DNS.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
async def resolve(self, dns: str) -> ConnectionName: # type: ignore
|
|
39
|
+
try:
|
|
40
|
+
conn_name = _parse_connection_name(dns)
|
|
41
|
+
except ValueError:
|
|
42
|
+
# The connection name was not project:region:instance format.
|
|
43
|
+
# Attempt to query a TXT record to get connection name.
|
|
44
|
+
conn_name = await self.query_dns(dns)
|
|
45
|
+
return conn_name
|
|
46
|
+
|
|
47
|
+
async def query_dns(self, dns: str) -> ConnectionName:
|
|
48
|
+
try:
|
|
49
|
+
# Attempt to query the TXT records.
|
|
50
|
+
records = await super().resolve(dns, "TXT", raise_on_no_answer=True)
|
|
51
|
+
# Sort the TXT record values alphabetically, strip quotes as record
|
|
52
|
+
# values can be returned as raw strings
|
|
53
|
+
rdata = [record.to_text().strip('"') for record in records]
|
|
54
|
+
rdata.sort()
|
|
55
|
+
# Attempt to parse records, returning the first valid record.
|
|
56
|
+
for record in rdata:
|
|
57
|
+
try:
|
|
58
|
+
conn_name = _parse_connection_name_with_domain_name(record, dns)
|
|
59
|
+
return conn_name
|
|
60
|
+
except Exception:
|
|
61
|
+
continue
|
|
62
|
+
# If all records failed to parse, throw error
|
|
63
|
+
raise DnsResolutionError(
|
|
64
|
+
f"Unable to parse TXT record for `{dns}` -> `{rdata[0]}`"
|
|
65
|
+
)
|
|
66
|
+
# Don't override above DnsResolutionError
|
|
67
|
+
except DnsResolutionError:
|
|
68
|
+
raise
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise DnsResolutionError(f"Unable to resolve TXT record for `{dns}`") from e
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
google/cloud/sql/connector/__init__.py,sha256=x8ui7u3gMlMI-LLJkKTtUU9yvzZvQkTJ-yzr2FH4VTU,983
|
|
2
|
-
google/cloud/sql/connector/asyncpg.py,sha256=HUOB0rRhr2-zMbygDtdWTDNlYqEPkN0ADqCIpM3waDc,1839
|
|
3
|
-
google/cloud/sql/connector/client.py,sha256=l6hsHXv9BOXUQ1qopf2MNyZRwbnuiLn1_-zPgmOHnAw,11700
|
|
4
|
-
google/cloud/sql/connector/connection_info.py,sha256=E8SzJQFmMyNXQcp-CeZxpgxkOth04W_7Sb-4j5kvhkQ,4330
|
|
5
|
-
google/cloud/sql/connector/connection_name.py,sha256=WI1R7hYX3GwpvCdhiK6dZb247uE-IIrYnLxP__OLCPY,1723
|
|
6
|
-
google/cloud/sql/connector/connector.py,sha256=p4iAOXWN1OiIVydnIFxeD4us_6FKvcY_096ZLIoEAno,20718
|
|
7
|
-
google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
|
|
8
|
-
google/cloud/sql/connector/exceptions.py,sha256=OVenqBkkn98xFeytHuOXFTWa9R5bCmemBtbSIm8caoM,1665
|
|
9
|
-
google/cloud/sql/connector/instance.py,sha256=Bcx3IJ_JPJ6Nv90X68duJufk0RrFWWZmvyJZ42csjP8,9329
|
|
10
|
-
google/cloud/sql/connector/lazy.py,sha256=_Hlkph2H3lvXBCFfQzQ6dUtmkRv6gT4NVSOQrl6MOxY,5073
|
|
11
|
-
google/cloud/sql/connector/pg8000.py,sha256=gZisjfXP4rNIjyxOQS75fvr0Xe6eLrFo4mIhVjoMLTw,1815
|
|
12
|
-
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
google/cloud/sql/connector/pymysql.py,sha256=82DwSJUhZHH2AW-vO7qhkljyShwj6Kmd17WCbOGZH74,2106
|
|
14
|
-
google/cloud/sql/connector/pytds.py,sha256=h-1pDhqLDkhviKBBOJ29gnIeYHKWw7Dtj91VLu1iNUI,2453
|
|
15
|
-
google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
|
|
16
|
-
google/cloud/sql/connector/refresh_utils.py,sha256=HfuuLdlqg2IxusB4T3BMj-BJlGsXtHsrCX_Hp-eJ_j0,5063
|
|
17
|
-
google/cloud/sql/connector/utils.py,sha256=7g1fX-JsZc7xOgTuxrAiroFHQIQk_BEJZNYaKOinwQY,3508
|
|
18
|
-
google/cloud/sql/connector/version.py,sha256=BCiCczuvVPOFNsp7M8KCiYFZGKdIdqdbubonBmpx3VY,598
|
|
19
|
-
cloud_sql_python_connector-1.14.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
20
|
-
cloud_sql_python_connector-1.14.0.dist-info/METADATA,sha256=SNj_QOZV8taf8OJluweyW6ADuCNf7tyDJ4rR4tSLgCA,28058
|
|
21
|
-
cloud_sql_python_connector-1.14.0.dist-info/WHEEL,sha256=OpXWERl2xLPRHTvd2ZXo_iluPEQd8uSbYkJ53NAER_Y,109
|
|
22
|
-
cloud_sql_python_connector-1.14.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
23
|
-
cloud_sql_python_connector-1.14.0.dist-info/RECORD,,
|
{cloud_sql_python_connector-1.14.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
{cloud_sql_python_connector-1.14.0.dist-info → cloud_sql_python_connector-1.15.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|