cloud-sql-python-connector 1.9.2__py2.py3-none-any.whl → 1.11.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.9.2.dist-info → cloud_sql_python_connector-1.11.0.dist-info}/METADATA +75 -13
- cloud_sql_python_connector-1.11.0.dist-info/RECORD +22 -0
- google/cloud/sql/connector/__init__.py +9 -2
- google/cloud/sql/connector/client.py +96 -20
- google/cloud/sql/connector/connection_info.py +105 -0
- google/cloud/sql/connector/connector.py +98 -47
- google/cloud/sql/connector/enums.py +85 -0
- google/cloud/sql/connector/exceptions.py +11 -3
- google/cloud/sql/connector/instance.py +49 -179
- google/cloud/sql/connector/lazy.py +132 -0
- google/cloud/sql/connector/refresh_utils.py +48 -1
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.9.2.dist-info/RECORD +0 -19
- {cloud_sql_python_connector-1.9.2.dist-info → cloud_sql_python_connector-1.11.0.dist-info}/LICENSE +0 -0
- {cloud_sql_python_connector-1.9.2.dist-info → cloud_sql_python_connector-1.11.0.dist-info}/WHEEL +0 -0
- {cloud_sql_python_connector-1.9.2.dist-info → cloud_sql_python_connector-1.11.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
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 asyncio
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from datetime import timedelta
|
|
18
|
+
from datetime import timezone
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
from google.cloud.sql.connector.client import CloudSQLClient
|
|
23
|
+
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
24
|
+
from google.cloud.sql.connector.instance import _parse_instance_connection_name
|
|
25
|
+
from google.cloud.sql.connector.refresh_utils import _refresh_buffer
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(name=__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LazyRefreshCache:
|
|
31
|
+
"""Cache that refreshes connection info when a caller requests a connection.
|
|
32
|
+
|
|
33
|
+
Only refreshes the cache when a new connection is requested and the current
|
|
34
|
+
certificate is close to or already expired.
|
|
35
|
+
|
|
36
|
+
This is the recommended option for serverless environments.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
instance_connection_string: str,
|
|
42
|
+
client: CloudSQLClient,
|
|
43
|
+
keys: asyncio.Future,
|
|
44
|
+
enable_iam_auth: bool = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Initializes a LazyRefreshCache instance.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
instance_connection_string (str): The Cloud SQL Instance's
|
|
50
|
+
connection string (also known as an instance connection name).
|
|
51
|
+
client (CloudSQLClient): The Cloud SQL Client instance.
|
|
52
|
+
keys (asyncio.Future): A future to the client's public-private key
|
|
53
|
+
pair.
|
|
54
|
+
enable_iam_auth (bool): Enables automatic IAM database authentication
|
|
55
|
+
(Postgres and MySQL) as the default authentication method for all
|
|
56
|
+
connections.
|
|
57
|
+
"""
|
|
58
|
+
# validate and parse instance connection name
|
|
59
|
+
self._project, self._region, self._instance = _parse_instance_connection_name(
|
|
60
|
+
instance_connection_string
|
|
61
|
+
)
|
|
62
|
+
self._instance_connection_string = instance_connection_string
|
|
63
|
+
|
|
64
|
+
self._enable_iam_auth = enable_iam_auth
|
|
65
|
+
self._keys = keys
|
|
66
|
+
self._client = client
|
|
67
|
+
self._lock = asyncio.Lock()
|
|
68
|
+
self._cached: Optional[ConnectionInfo] = None
|
|
69
|
+
self._needs_refresh = False
|
|
70
|
+
|
|
71
|
+
async def force_refresh(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Invalidates the cache and configures the next call to
|
|
74
|
+
connect_info() to retrieve a fresh ConnectionInfo instance.
|
|
75
|
+
"""
|
|
76
|
+
async with self._lock:
|
|
77
|
+
self._needs_refresh = True
|
|
78
|
+
|
|
79
|
+
async def connect_info(self) -> ConnectionInfo:
|
|
80
|
+
"""Retrieves ConnectionInfo instance for establishing a secure
|
|
81
|
+
connection to the Cloud SQL instance.
|
|
82
|
+
"""
|
|
83
|
+
async with self._lock:
|
|
84
|
+
# If connection info is cached, check expiration.
|
|
85
|
+
# Pad expiration with a buffer to give the client plenty of time to
|
|
86
|
+
# establish a connection to the server with the certificate.
|
|
87
|
+
if (
|
|
88
|
+
self._cached
|
|
89
|
+
and not self._needs_refresh
|
|
90
|
+
and datetime.now(timezone.utc)
|
|
91
|
+
< (self._cached.expiration - timedelta(seconds=_refresh_buffer))
|
|
92
|
+
):
|
|
93
|
+
logger.debug(
|
|
94
|
+
f"['{self._instance_connection_string}']: Connection info "
|
|
95
|
+
"is still valid, using cached info"
|
|
96
|
+
)
|
|
97
|
+
return self._cached
|
|
98
|
+
logger.debug(
|
|
99
|
+
f"['{self._instance_connection_string}']: Connection info "
|
|
100
|
+
"refresh operation started"
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
conn_info = await self._client.get_connection_info(
|
|
104
|
+
self._project,
|
|
105
|
+
self._region,
|
|
106
|
+
self._instance,
|
|
107
|
+
self._keys,
|
|
108
|
+
self._enable_iam_auth,
|
|
109
|
+
)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.debug(
|
|
112
|
+
f"['{self._instance_connection_string}']: Connection info "
|
|
113
|
+
f"refresh operation failed: {str(e)}"
|
|
114
|
+
)
|
|
115
|
+
raise
|
|
116
|
+
logger.debug(
|
|
117
|
+
f"['{self._instance_connection_string}']: Connection info "
|
|
118
|
+
"refresh operation completed successfully"
|
|
119
|
+
)
|
|
120
|
+
logger.debug(
|
|
121
|
+
f"['{self._instance_connection_string}']: Current certificate "
|
|
122
|
+
f"expiration = {str(conn_info.expiration)}"
|
|
123
|
+
)
|
|
124
|
+
self._cached = conn_info
|
|
125
|
+
self._needs_refresh = False
|
|
126
|
+
return conn_info
|
|
127
|
+
|
|
128
|
+
async def close(self) -> None:
|
|
129
|
+
"""Close is a no-op and provided purely for a consistent interface with
|
|
130
|
+
other cache types.
|
|
131
|
+
"""
|
|
132
|
+
pass
|
|
@@ -20,8 +20,10 @@ import asyncio
|
|
|
20
20
|
import copy
|
|
21
21
|
import datetime
|
|
22
22
|
import logging
|
|
23
|
-
|
|
23
|
+
import random
|
|
24
|
+
from typing import Any, Callable, List
|
|
24
25
|
|
|
26
|
+
import aiohttp
|
|
25
27
|
from google.auth.credentials import Credentials
|
|
26
28
|
from google.auth.credentials import Scoped
|
|
27
29
|
import google.auth.transport.requests
|
|
@@ -105,3 +107,48 @@ def _downscope_credentials(
|
|
|
105
107
|
request = google.auth.transport.requests.Request()
|
|
106
108
|
scoped_creds.refresh(request)
|
|
107
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
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
google/cloud/sql/connector/__init__.py,sha256=Z3AkPOQwjNF1ogo2DR18GN-CnYvI4MTmhsCe8hURKUo,883
|
|
2
|
-
google/cloud/sql/connector/asyncpg.py,sha256=c4lAlvEgGf_eBq0G8MVNPRAJQQvo6DcwybiXDW36qkU,1838
|
|
3
|
-
google/cloud/sql/connector/client.py,sha256=NZZnPqE_f13S04tpRCs0UGF-RxR4EL-_vH7ebQBXTaA,8494
|
|
4
|
-
google/cloud/sql/connector/connector.py,sha256=q1P0lqDjU5O5U8FQjNa9punrdExwcjvVz7SodbaVmSw,19147
|
|
5
|
-
google/cloud/sql/connector/exceptions.py,sha256=rTBtbCoiZ3U5IFVsyXPw5baHbCODixx2hPDQeXUzgzs,1524
|
|
6
|
-
google/cloud/sql/connector/instance.py,sha256=Oed1kFTaVolce74DzsSRbooj6mUthwZtt75OamTfOQs,14966
|
|
7
|
-
google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
|
|
8
|
-
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
|
|
10
|
-
google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
|
|
11
|
-
google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
|
|
12
|
-
google/cloud/sql/connector/refresh_utils.py,sha256=P2ye3qDymdK1iOlGCXby4VIaqwvHdc9GEmuUsMcufxY,3680
|
|
13
|
-
google/cloud/sql/connector/utils.py,sha256=FQs7RgoViJLIFgJIynVT-_DwAVWnpyXIKu5WZ4m10Ts,3448
|
|
14
|
-
google/cloud/sql/connector/version.py,sha256=x6crtIS26FuBXZ_fZ636i8UD16NlwWdlYJf5SEIqLIc,597
|
|
15
|
-
cloud_sql_python_connector-1.9.2.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
16
|
-
cloud_sql_python_connector-1.9.2.dist-info/METADATA,sha256=5ufJWE8PP34QHa8_1oh1vPJmzaOY-Byqfso7o_BbCnw,24113
|
|
17
|
-
cloud_sql_python_connector-1.9.2.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
|
|
18
|
-
cloud_sql_python_connector-1.9.2.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
19
|
-
cloud_sql_python_connector-1.9.2.dist-info/RECORD,,
|
{cloud_sql_python_connector-1.9.2.dist-info → cloud_sql_python_connector-1.11.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
{cloud_sql_python_connector-1.9.2.dist-info → cloud_sql_python_connector-1.11.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|