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
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
from
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any
|
|
16
19
|
|
|
17
20
|
import dns.asyncresolver
|
|
18
21
|
|
|
@@ -22,12 +25,32 @@ from google.cloud.sql.connector.connection_name import (
|
|
|
22
25
|
_parse_connection_name_with_domain_name,
|
|
23
26
|
)
|
|
24
27
|
from google.cloud.sql.connector.connection_name import ConnectionName
|
|
28
|
+
from google.cloud.sql.connector.exceptions import DnsLoopError
|
|
25
29
|
from google.cloud.sql.connector.exceptions import DnsResolutionError
|
|
26
30
|
|
|
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\.?$"
|
|
35
|
+
)
|
|
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
|
+
|
|
27
47
|
|
|
28
48
|
class DefaultResolver:
|
|
29
49
|
"""DefaultResolver simply validates and parses instance connection name."""
|
|
30
50
|
|
|
51
|
+
def __init__(self, *args: Any, client: Any | None = None, **kwargs: Any) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
31
54
|
async def resolve(self, connection_name: str) -> ConnectionName:
|
|
32
55
|
return _parse_connection_name(connection_name)
|
|
33
56
|
|
|
@@ -38,54 +61,113 @@ class DnsResolver(dns.asyncresolver.Resolver):
|
|
|
38
61
|
TXT records in DNS.
|
|
39
62
|
"""
|
|
40
63
|
|
|
64
|
+
def __init__(self, *args: Any, client: Any | None = None, **kwargs: Any) -> None:
|
|
65
|
+
super().__init__(*args, **kwargs)
|
|
66
|
+
self._client = client
|
|
67
|
+
|
|
41
68
|
async def resolve(self, dns: str) -> ConnectionName: # type: ignore
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
conn_name =
|
|
50
|
-
|
|
69
|
+
current = dns
|
|
70
|
+
visited = {current}
|
|
71
|
+
|
|
72
|
+
# Max 10 iterations to prevent infinite CNAME loops
|
|
73
|
+
for _ in range(10):
|
|
74
|
+
try:
|
|
75
|
+
domain_val = dns if current != dns else ""
|
|
76
|
+
conn_name = _parse_connection_name_with_domain_name(
|
|
77
|
+
current, domain_val
|
|
78
|
+
)
|
|
79
|
+
return conn_name
|
|
80
|
+
except ValueError:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
dns_normalized = current.rstrip(".")
|
|
84
|
+
match = INSTANCE_DNS_NAME_PATTERN.match(dns_normalized.lower())
|
|
85
|
+
if match:
|
|
86
|
+
region = match.group(3)
|
|
87
|
+
if region != "global":
|
|
88
|
+
if self._client is None:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"SQLAdmin client is not configured in the resolver."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
dns_name_with_dot = dns_normalized + "."
|
|
94
|
+
resp = await self._client.resolve_connect_settings(
|
|
95
|
+
dns_name_with_dot, region
|
|
96
|
+
)
|
|
97
|
+
resolved_conn_name = resp["connectionName"]
|
|
98
|
+
return _parse_connection_name_with_domain_name(
|
|
99
|
+
resolved_conn_name, dns
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not _is_valid_domain(current):
|
|
51
103
|
raise ValueError(
|
|
52
104
|
"Arg `instance_connection_string` must have "
|
|
53
105
|
"format: PROJECT:REGION:INSTANCE or be a valid DNS domain "
|
|
54
106
|
f"name, got {dns}."
|
|
55
107
|
)
|
|
56
|
-
return conn_name
|
|
57
108
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
109
|
+
cname_found = False
|
|
110
|
+
try:
|
|
111
|
+
cname = await self.resolve_cname(current)
|
|
112
|
+
cname_found = True
|
|
113
|
+
except DnsResolutionError:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
if cname_found:
|
|
117
|
+
if cname in visited:
|
|
118
|
+
raise DnsLoopError(f"CNAME loop detected for `{dns}`")
|
|
119
|
+
visited.add(cname)
|
|
120
|
+
current = cname
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
rdata = await self.resolve_txt(current)
|
|
125
|
+
except DnsResolutionError as e:
|
|
126
|
+
raise DnsResolutionError(
|
|
127
|
+
f"Unable to resolve TXT record for `{dns}`"
|
|
128
|
+
) from e
|
|
67
129
|
|
|
68
|
-
async def query_dns(self, dns: str) -> ConnectionName:
|
|
69
|
-
try:
|
|
70
|
-
# Attempt to query the TXT records.
|
|
71
|
-
records = await super().resolve(dns, "TXT", raise_on_no_answer=True)
|
|
72
|
-
# Sort the TXT record values alphabetically, strip quotes as record
|
|
73
|
-
# values can be returned as raw strings
|
|
74
|
-
rdata = [record.to_text().strip('"') for record in records]
|
|
75
130
|
rdata.sort()
|
|
76
|
-
# Attempt to parse records, returning the first valid record.
|
|
77
131
|
for record in rdata:
|
|
78
132
|
try:
|
|
79
|
-
conn_name = _parse_connection_name_with_domain_name(
|
|
133
|
+
conn_name = _parse_connection_name_with_domain_name(
|
|
134
|
+
record, dns
|
|
135
|
+
)
|
|
80
136
|
return conn_name
|
|
81
|
-
except Exception:
|
|
137
|
+
except Exception: # noqa: BLE001, S112
|
|
82
138
|
continue
|
|
83
|
-
|
|
139
|
+
|
|
84
140
|
raise DnsResolutionError(
|
|
85
|
-
f"Unable to parse TXT record for `{
|
|
141
|
+
f"Unable to parse TXT record for `{current}` -> `{rdata[0]}`"
|
|
142
|
+
if rdata
|
|
143
|
+
else f"Unable to resolve TXT record for `{current}`"
|
|
86
144
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
145
|
+
|
|
146
|
+
raise DnsLoopError(
|
|
147
|
+
f"CNAME loop detected or max resolution depth reached for `{dns}`"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def resolve_cname(self, dns: str) -> str:
|
|
151
|
+
try:
|
|
152
|
+
answers = await super().resolve(dns, "CNAME", raise_on_no_answer=True)
|
|
153
|
+
return str(answers[0].target).rstrip(".")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise DnsResolutionError(
|
|
156
|
+
f"Unable to resolve CNAME record for `{dns}`"
|
|
157
|
+
) from e
|
|
158
|
+
|
|
159
|
+
async def resolve_txt(self, dns: str) -> list[str]:
|
|
160
|
+
try:
|
|
161
|
+
answers = await super().resolve(dns, "TXT", raise_on_no_answer=True)
|
|
162
|
+
return [record.to_text().strip('"') for record in answers]
|
|
90
163
|
except Exception as e:
|
|
91
|
-
raise DnsResolutionError(
|
|
164
|
+
raise DnsResolutionError(
|
|
165
|
+
f"Unable to resolve TXT record for `{dns}`"
|
|
166
|
+
) from e
|
|
167
|
+
|
|
168
|
+
async def resolve_a_record(self, dns: str) -> list[str]:
|
|
169
|
+
try:
|
|
170
|
+
records = await super().resolve(dns, "A", raise_on_no_answer=True)
|
|
171
|
+
return [record.to_text() for record in records]
|
|
172
|
+
except Exception: # noqa: BLE001
|
|
173
|
+
return []
|
|
@@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import asyncio
|
|
18
|
+
import tempfile
|
|
19
|
+
|
|
18
20
|
from cryptography.hazmat.backends import default_backend
|
|
19
21
|
from cryptography.hazmat.primitives import serialization
|
|
20
22
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
@@ -57,6 +59,33 @@ async def generate_keys() -> tuple[bytes, str]:
|
|
|
57
59
|
return priv_key, pub_key
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
class AsyncTemporaryDirectory:
|
|
63
|
+
"""Async context manager wrapper around tempfile.TemporaryDirectory."""
|
|
64
|
+
|
|
65
|
+
async def __aenter__(self) -> str:
|
|
66
|
+
self.temp_dir = await asyncio.to_thread(tempfile.TemporaryDirectory)
|
|
67
|
+
return self.temp_dir.name
|
|
68
|
+
|
|
69
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
70
|
+
await asyncio.to_thread(self.temp_dir.cleanup)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _write_files(
|
|
74
|
+
ca_filename: str,
|
|
75
|
+
serverCaCert: str,
|
|
76
|
+
cert_filename: str,
|
|
77
|
+
ephemeralCert: str,
|
|
78
|
+
key_filename: str,
|
|
79
|
+
priv_key: bytes,
|
|
80
|
+
) -> None:
|
|
81
|
+
with open(ca_filename, "w+") as ca_out:
|
|
82
|
+
ca_out.write(serverCaCert)
|
|
83
|
+
with open(cert_filename, "w+") as ephemeral_out:
|
|
84
|
+
ephemeral_out.write(ephemeralCert)
|
|
85
|
+
with open(key_filename, "wb") as priv_out:
|
|
86
|
+
priv_out.write(priv_key)
|
|
87
|
+
|
|
88
|
+
|
|
60
89
|
async def write_to_file(
|
|
61
90
|
dir_path: str, serverCaCert: str, ephemeralCert: str, priv_key: bytes
|
|
62
91
|
) -> tuple[str, str, str]:
|
|
@@ -68,12 +97,15 @@ async def write_to_file(
|
|
|
68
97
|
cert_filename = f"{dir_path}/cert.pem"
|
|
69
98
|
key_filename = f"{dir_path}/priv.pem"
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
await asyncio.to_thread(
|
|
101
|
+
_write_files,
|
|
102
|
+
ca_filename,
|
|
103
|
+
serverCaCert,
|
|
104
|
+
cert_filename,
|
|
105
|
+
ephemeralCert,
|
|
106
|
+
key_filename,
|
|
107
|
+
priv_key,
|
|
108
|
+
)
|
|
77
109
|
|
|
78
110
|
return (ca_filename, cert_filename, key_filename)
|
|
79
111
|
|
|
@@ -91,7 +123,7 @@ def format_database_user(database_version: str, user: str) -> str:
|
|
|
91
123
|
# remove suffix for Postgres service accounts
|
|
92
124
|
if database_version.startswith("POSTGRES"):
|
|
93
125
|
suffix = ".gserviceaccount.com"
|
|
94
|
-
user = user
|
|
126
|
+
user = user.removesuffix(suffix)
|
|
95
127
|
return user
|
|
96
128
|
|
|
97
129
|
# remove everything after and including the @ for MySQL
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
cloud_sql_python_connector-1.20.4.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
2
|
-
google/cloud/sql/connector/__init__.py,sha256=w6w35V-m2QlpcL-ShKd2BvNF-xXOwJ2FoidE7npPE7c,1148
|
|
3
|
-
google/cloud/sql/connector/asyncpg.py,sha256=v4ohOnSYjHTOvJPhTE761xP1KKo4FUJQW_WAnNW_2g4,1918
|
|
4
|
-
google/cloud/sql/connector/client.py,sha256=uVOMnRe1AxiSh6BnK4-SuSYrRID_jnm7SgMMN6s5jp4,13076
|
|
5
|
-
google/cloud/sql/connector/connection_info.py,sha256=BSuW-ovtEUYTIvDzQkaE3GofCs3Xoi0TtztFaAq4Kvo,4868
|
|
6
|
-
google/cloud/sql/connector/connection_name.py,sha256=bI4M3Qa2mtBHyD08oF41FF5W9FBOXWu4dGGg50Na_A4,2606
|
|
7
|
-
google/cloud/sql/connector/connector.py,sha256=MqL4iX9xK0Vys59L0VQpxf4VY39UUyYeL7I3rnFYSdY,26792
|
|
8
|
-
google/cloud/sql/connector/enums.py,sha256=dVBS9eEWNrHtzze0AJlmtbdUogG3EocNWOQcI_8qPd0,2937
|
|
9
|
-
google/cloud/sql/connector/exceptions.py,sha256=BtuE5bkMLxNPYZo2SrFlRBHp3ueWKZ0otnPbfnvr2TM,2125
|
|
10
|
-
google/cloud/sql/connector/instance.py,sha256=jDEQf-OXFzqKO9a4bnzRQ0qUyO3_xKuiEIRWc_7wpyE,8694
|
|
11
|
-
google/cloud/sql/connector/lazy.py,sha256=oB6F-O2fk8ZguY4J7Vrts91bqbVb6P5wj72Lf6UJIQA,4948
|
|
12
|
-
google/cloud/sql/connector/monitored_cache.py,sha256=LFYjX8hsdN_Dt0cMwLSWgAhWWQpZ35qA0CLlpc6xhsE,5587
|
|
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=PrTQ1lB5EWAcN4BPX_5hb-WerncS4X76bBk4y0es0ks,1948
|
|
16
|
-
google/cloud/sql/connector/pytds.py,sha256=KZxmJQQG9XfmsmUoEGX7RpMirWdoreJ2-_FuHO6Td-s,2280
|
|
17
|
-
google/cloud/sql/connector/rate_limiter.py,sha256=zqZ9pVXHan4LWoGT8Bq6U3HpZcrdzKhlXVYwsX3ftA0,2928
|
|
18
|
-
google/cloud/sql/connector/refresh_utils.py,sha256=jaYLHrKlJtqeZBM8gOrZtI-6mbh6-1PTi8o0aFGH5Yo,5151
|
|
19
|
-
google/cloud/sql/connector/resolver.py,sha256=pKcdPCr0iwFwKf2cv0F3HtCyOWNas1JsrnRMOudnEr8,3714
|
|
20
|
-
google/cloud/sql/connector/utils.py,sha256=AA_oBBxRuULb7-hHHK8HQJU5fttzBpzdIZwMNZ5kNCM,3473
|
|
21
|
-
google/cloud/sql/connector/version.py,sha256=cYVlxi8BSHlb8lkhQjFjx99XUVh4oz0nUaEXXDcL7Yo,598
|
|
22
|
-
cloud_sql_python_connector-1.20.4.dist-info/METADATA,sha256=-C2J-fOMiODP2lhLrt6fW0AM9WL8_sL5Ui91WXwoa80,31181
|
|
23
|
-
cloud_sql_python_connector-1.20.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
24
|
-
cloud_sql_python_connector-1.20.4.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
25
|
-
cloud_sql_python_connector-1.20.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|