cloud-sql-python-connector 1.17.0.dev0__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.17.0.dev0.dist-info/LICENSE +202 -0
- cloud_sql_python_connector-1.17.0.dev0.dist-info/METADATA +838 -0
- cloud_sql_python_connector-1.17.0.dev0.dist-info/RECORD +24 -0
- cloud_sql_python_connector-1.17.0.dev0.dist-info/WHEEL +5 -0
- cloud_sql_python_connector-1.17.0.dev0.dist-info/top_level.txt +1 -0
- google/cloud/sql/connector/__init__.py +33 -0
- google/cloud/sql/connector/asyncpg.py +65 -0
- google/cloud/sql/connector/client.py +324 -0
- google/cloud/sql/connector/connection_info.py +108 -0
- google/cloud/sql/connector/connection_name.py +61 -0
- google/cloud/sql/connector/connector.py +528 -0
- google/cloud/sql/connector/enums.py +86 -0
- google/cloud/sql/connector/exceptions.py +79 -0
- google/cloud/sql/connector/instance.py +214 -0
- google/cloud/sql/connector/lazy.py +124 -0
- google/cloud/sql/connector/pg8000.py +66 -0
- google/cloud/sql/connector/py.typed +0 -0
- google/cloud/sql/connector/pymysql.py +64 -0
- google/cloud/sql/connector/pytds.py +78 -0
- google/cloud/sql/connector/rate_limiter.py +87 -0
- google/cloud/sql/connector/refresh_utils.py +154 -0
- google/cloud/sql/connector/resolver.py +70 -0
- google/cloud/sql/connector/utils.py +103 -0
- google/cloud/sql/connector/version.py +15 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2021 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import copy
|
|
21
|
+
import datetime
|
|
22
|
+
import logging
|
|
23
|
+
import random
|
|
24
|
+
from typing import Any, Callable
|
|
25
|
+
|
|
26
|
+
import aiohttp
|
|
27
|
+
from google.auth.credentials import Credentials
|
|
28
|
+
from google.auth.credentials import Scoped
|
|
29
|
+
import google.auth.transport.requests
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(name=__name__)
|
|
32
|
+
|
|
33
|
+
# _refresh_buffer is the amount of time before a refresh's result expires
|
|
34
|
+
# that a new refresh operation begins.
|
|
35
|
+
_refresh_buffer: int = 4 * 60 # 4 minutes
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _seconds_until_refresh(
|
|
39
|
+
expiration: datetime.datetime,
|
|
40
|
+
) -> int:
|
|
41
|
+
"""
|
|
42
|
+
Calculates the duration to wait before starting the next refresh.
|
|
43
|
+
|
|
44
|
+
Usually the duration will be half of the time until certificate
|
|
45
|
+
expiration.
|
|
46
|
+
|
|
47
|
+
:rtype: int
|
|
48
|
+
:returns: Time in seconds to wait before performing next refresh.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
duration = int(
|
|
52
|
+
(expiration - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# if certificate duration is less than 1 hour
|
|
56
|
+
if duration < 3600:
|
|
57
|
+
# something is wrong with certificate, refresh now
|
|
58
|
+
if duration < _refresh_buffer:
|
|
59
|
+
return 0
|
|
60
|
+
# otherwise wait until 4 minutes before expiration for next refresh
|
|
61
|
+
return duration - _refresh_buffer
|
|
62
|
+
|
|
63
|
+
return duration // 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _is_valid(task: asyncio.Task) -> bool:
|
|
67
|
+
try:
|
|
68
|
+
metadata = await task
|
|
69
|
+
# only valid if now is before the cert expires
|
|
70
|
+
if datetime.datetime.now(datetime.timezone.utc) < metadata.expiration:
|
|
71
|
+
return True
|
|
72
|
+
except Exception:
|
|
73
|
+
# supress any errors from task
|
|
74
|
+
logger.debug("Current instance metadata is invalid.")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _downscope_credentials(
|
|
79
|
+
credentials: Credentials,
|
|
80
|
+
scopes: list[str] = ["https://www.googleapis.com/auth/sqlservice.login"],
|
|
81
|
+
) -> Credentials:
|
|
82
|
+
"""Generate a down-scoped credential.
|
|
83
|
+
|
|
84
|
+
:type credentials: google.auth.credentials.Credentials
|
|
85
|
+
:param credentials
|
|
86
|
+
Credentials object used to generate down-scoped credentials.
|
|
87
|
+
|
|
88
|
+
:type scopes: list[str]
|
|
89
|
+
:param scopes
|
|
90
|
+
List of Google scopes to include in down-scoped credentials object.
|
|
91
|
+
|
|
92
|
+
:rtype: google.auth.credentials.Credentials
|
|
93
|
+
:returns: Down-scoped credentials object.
|
|
94
|
+
"""
|
|
95
|
+
# credentials sourced from a service account or metadata are children of
|
|
96
|
+
# Scoped class and are capable of being re-scoped
|
|
97
|
+
if isinstance(credentials, Scoped):
|
|
98
|
+
scoped_creds = credentials.with_scopes(scopes=scopes)
|
|
99
|
+
# authenticated user credentials can not be re-scoped
|
|
100
|
+
else:
|
|
101
|
+
# create shallow copy to not overwrite scopes on default credentials
|
|
102
|
+
scoped_creds = copy.copy(credentials)
|
|
103
|
+
# overwrite '_scopes' to down-scope user credentials
|
|
104
|
+
# Cloud SDK reference: https://github.com/google-cloud-sdk-unofficial/google-cloud-sdk/blob/93920ccb6d2cce0fe6d1ce841e9e33410551d66b/lib/googlecloudsdk/command_lib/sql/generate_login_token_util.py#L116
|
|
105
|
+
scoped_creds._scopes = scopes
|
|
106
|
+
# down-scoped credentials require refresh, are invalid after being re-scoped
|
|
107
|
+
request = google.auth.transport.requests.Request()
|
|
108
|
+
scoped_creds.refresh(request)
|
|
109
|
+
return scoped_creds
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _exponential_backoff(attempt: int) -> float:
|
|
113
|
+
"""Calculates a duration to backoff in milliseconds based on the attempt i.
|
|
114
|
+
|
|
115
|
+
The formula is:
|
|
116
|
+
|
|
117
|
+
base * multi^(attempt + 1 + random)
|
|
118
|
+
|
|
119
|
+
With base = 200ms and multi = 1.618, and random = [0.0, 1.0),
|
|
120
|
+
the backoff values would fall between the following low and high ends:
|
|
121
|
+
|
|
122
|
+
Attempt Low (ms) High (ms)
|
|
123
|
+
|
|
124
|
+
0 324 524
|
|
125
|
+
1 524 847
|
|
126
|
+
2 847 1371
|
|
127
|
+
3 1371 2218
|
|
128
|
+
4 2218 3588
|
|
129
|
+
|
|
130
|
+
The theoretical worst case scenario would have a client wait 8.5s in total
|
|
131
|
+
for an API request to complete (with the first four attempts failing, and
|
|
132
|
+
the fifth succeeding).
|
|
133
|
+
"""
|
|
134
|
+
base = 200
|
|
135
|
+
multi = 1.618
|
|
136
|
+
exp = attempt + 1 + random.random()
|
|
137
|
+
return base * pow(multi, exp)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def retry_50x(
|
|
141
|
+
request_coro: Callable, *args: Any, **kwargs: Any
|
|
142
|
+
) -> aiohttp.ClientResponse:
|
|
143
|
+
"""Retry any 50x HTTP response up to X number of times."""
|
|
144
|
+
max_retries = 5
|
|
145
|
+
for i in range(max_retries):
|
|
146
|
+
resp = await request_coro(*args, **kwargs)
|
|
147
|
+
# backoff for any 50X errors
|
|
148
|
+
if resp.status >= 500 and i < max_retries:
|
|
149
|
+
# calculate backoff time
|
|
150
|
+
backoff = _exponential_backoff(i)
|
|
151
|
+
await asyncio.sleep(backoff / 1000)
|
|
152
|
+
else:
|
|
153
|
+
break
|
|
154
|
+
return resp
|
|
@@ -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
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2019 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import aiofiles
|
|
18
|
+
from cryptography.hazmat.backends import default_backend
|
|
19
|
+
from cryptography.hazmat.primitives import serialization
|
|
20
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def generate_keys() -> tuple[bytes, str]:
|
|
24
|
+
"""A helper function to generate the private and public keys.
|
|
25
|
+
|
|
26
|
+
backend - The value specified is default_backend(). This is because the
|
|
27
|
+
cryptography library used to support different backends, but now only uses
|
|
28
|
+
the default_backend().
|
|
29
|
+
|
|
30
|
+
public_exponent - The public exponent is one of the variables used in the
|
|
31
|
+
generation of the keys. 65537 is recommended due to being a good balance
|
|
32
|
+
between speed and security.
|
|
33
|
+
|
|
34
|
+
key_size - The cryptography documentation recommended a key_size
|
|
35
|
+
of at least 2048.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
private_key_obj = rsa.generate_private_key(
|
|
39
|
+
backend=default_backend(), public_exponent=65537, key_size=2048
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
pub_key = (
|
|
43
|
+
private_key_obj.public_key()
|
|
44
|
+
.public_bytes(
|
|
45
|
+
encoding=serialization.Encoding.PEM,
|
|
46
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
47
|
+
)
|
|
48
|
+
.decode("UTF-8")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
priv_key = private_key_obj.private_bytes(
|
|
52
|
+
encoding=serialization.Encoding.PEM,
|
|
53
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
54
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return priv_key, pub_key
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def write_to_file(
|
|
61
|
+
dir_path: str, serverCaCert: str, ephemeralCert: str, priv_key: bytes
|
|
62
|
+
) -> tuple[str, str, str]:
|
|
63
|
+
"""
|
|
64
|
+
Helper function to write the serverCaCert, ephemeral certificate and
|
|
65
|
+
private key to .pem files in a given directory
|
|
66
|
+
"""
|
|
67
|
+
ca_filename = f"{dir_path}/ca.pem"
|
|
68
|
+
cert_filename = f"{dir_path}/cert.pem"
|
|
69
|
+
key_filename = f"{dir_path}/priv.pem"
|
|
70
|
+
|
|
71
|
+
async with aiofiles.open(ca_filename, "w+") as ca_out:
|
|
72
|
+
await ca_out.write(serverCaCert)
|
|
73
|
+
async with aiofiles.open(cert_filename, "w+") as ephemeral_out:
|
|
74
|
+
await ephemeral_out.write(ephemeralCert)
|
|
75
|
+
async with aiofiles.open(key_filename, "wb") as priv_out:
|
|
76
|
+
await priv_out.write(priv_key)
|
|
77
|
+
|
|
78
|
+
return (ca_filename, cert_filename, key_filename)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def format_database_user(database_version: str, user: str) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Format database `user` param for Cloud SQL automatic IAM authentication.
|
|
84
|
+
|
|
85
|
+
:type database_version: str
|
|
86
|
+
:param database_version
|
|
87
|
+
Cloud SQL database version. (i.e. POSTGRES_14, MYSQL8_0, etc.)
|
|
88
|
+
|
|
89
|
+
:type user: str
|
|
90
|
+
:param user
|
|
91
|
+
Database username to connect to Cloud SQL database with.
|
|
92
|
+
"""
|
|
93
|
+
# remove suffix for Postgres service accounts
|
|
94
|
+
if database_version.startswith("POSTGRES"):
|
|
95
|
+
suffix = ".gserviceaccount.com"
|
|
96
|
+
user = user[: -len(suffix)] if user.endswith(suffix) else user
|
|
97
|
+
return user
|
|
98
|
+
|
|
99
|
+
# remove everything after and including the @ for MySQL
|
|
100
|
+
if database_version.startswith("MYSQL") and "@" in user:
|
|
101
|
+
return user.split("@")[0]
|
|
102
|
+
|
|
103
|
+
return user
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2021 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
|
+
__version__ = "1.17.0.dev0"
|