cloud-sql-python-connector 1.10.0__tar.gz → 1.11.0__tar.gz
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.10.0 → cloud-sql-python-connector-1.11.0}/PKG-INFO +1 -1
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/cloud_sql_python_connector.egg-info/PKG-INFO +1 -1
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/client.py +13 -7
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/connection_info.py +1 -1
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/connector.py +29 -32
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/exceptions.py +0 -7
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/instance.py +1 -1
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/refresh_utils.py +48 -1
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/version.py +1 -1
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/LICENSE +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/README.md +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/cloud_sql_python_connector.egg-info/SOURCES.txt +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/cloud_sql_python_connector.egg-info/dependency_links.txt +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/cloud_sql_python_connector.egg-info/not-zip-safe +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/cloud_sql_python_connector.egg-info/requires.txt +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/cloud_sql_python_connector.egg-info/top_level.txt +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/__init__.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/asyncpg.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/enums.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/lazy.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/pg8000.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/py.typed +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/pymysql.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/pytds.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/rate_limiter.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/google/cloud/sql/connector/utils.py +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/setup.cfg +0 -0
- {cloud-sql-python-connector-1.10.0 → cloud-sql-python-connector-1.11.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.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
|
|
@@ -28,6 +28,7 @@ from google.auth.transport import requests
|
|
|
28
28
|
from google.cloud.sql.connector.connection_info import ConnectionInfo
|
|
29
29
|
from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
|
|
30
30
|
from google.cloud.sql.connector.refresh_utils import _downscope_credentials
|
|
31
|
+
from google.cloud.sql.connector.refresh_utils import retry_50x
|
|
31
32
|
from google.cloud.sql.connector.version import __version__ as version
|
|
32
33
|
|
|
33
34
|
if TYPE_CHECKING:
|
|
@@ -124,7 +125,10 @@ class CloudSQLClient:
|
|
|
124
125
|
|
|
125
126
|
url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/projects/{project}/instances/{instance}/connectSettings"
|
|
126
127
|
|
|
127
|
-
resp = await self._client.get(url, headers=headers
|
|
128
|
+
resp = await self._client.get(url, headers=headers)
|
|
129
|
+
if resp.status >= 500:
|
|
130
|
+
resp = await retry_50x(self._client.get, url, headers=headers)
|
|
131
|
+
resp.raise_for_status()
|
|
128
132
|
ret_dict = await resp.json()
|
|
129
133
|
|
|
130
134
|
if ret_dict["region"] != region:
|
|
@@ -137,8 +141,10 @@ class CloudSQLClient:
|
|
|
137
141
|
if "ipAddresses" in ret_dict
|
|
138
142
|
else {}
|
|
139
143
|
)
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
# Remove trailing period from PSC DNS name.
|
|
145
|
+
psc_dns = ret_dict.get("dnsName")
|
|
146
|
+
if psc_dns:
|
|
147
|
+
ip_addresses["PSC"] = psc_dns.rstrip(".")
|
|
142
148
|
|
|
143
149
|
return {
|
|
144
150
|
"ip_addresses": ip_addresses,
|
|
@@ -186,10 +192,10 @@ class CloudSQLClient:
|
|
|
186
192
|
login_creds = _downscope_credentials(self._credentials)
|
|
187
193
|
data["access_token"] = login_creds.token
|
|
188
194
|
|
|
189
|
-
resp = await self._client.post(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
resp = await self._client.post(url, headers=headers, json=data)
|
|
196
|
+
if resp.status >= 500:
|
|
197
|
+
resp = await retry_50x(self._client.post, url, headers=headers, json=data)
|
|
198
|
+
resp.raise_for_status()
|
|
193
199
|
ret_dict = await resp.json()
|
|
194
200
|
|
|
195
201
|
ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
|
|
@@ -19,7 +19,6 @@ from __future__ import annotations
|
|
|
19
19
|
import asyncio
|
|
20
20
|
from functools import partial
|
|
21
21
|
import logging
|
|
22
|
-
import socket
|
|
23
22
|
from threading import Thread
|
|
24
23
|
from types import TracebackType
|
|
25
24
|
from typing import Any, Dict, Optional, Type, Union
|
|
@@ -34,7 +33,6 @@ from google.cloud.sql.connector.enums import DriverMapping
|
|
|
34
33
|
from google.cloud.sql.connector.enums import IPTypes
|
|
35
34
|
from google.cloud.sql.connector.enums import RefreshStrategy
|
|
36
35
|
from google.cloud.sql.connector.exceptions import ConnectorLoopError
|
|
37
|
-
from google.cloud.sql.connector.exceptions import DnsNameResolutionError
|
|
38
36
|
from google.cloud.sql.connector.instance import RefreshAheadCache
|
|
39
37
|
from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
40
38
|
import google.cloud.sql.connector.pg8000 as pg8000
|
|
@@ -263,8 +261,6 @@ class Connector:
|
|
|
263
261
|
and then subsequent attempt with IAM database authentication.
|
|
264
262
|
KeyError: Unsupported database driver Must be one of pymysql, asyncpg,
|
|
265
263
|
pg8000, and pytds.
|
|
266
|
-
DnsNameResolutionError: Could not resolve PSC IP address from DNS
|
|
267
|
-
host name.
|
|
268
264
|
"""
|
|
269
265
|
if self._keys is None:
|
|
270
266
|
self._keys = asyncio.create_task(generate_keys())
|
|
@@ -340,41 +336,31 @@ class Connector:
|
|
|
340
336
|
kwargs.pop("ssl", None)
|
|
341
337
|
kwargs.pop("port", None)
|
|
342
338
|
|
|
343
|
-
# attempt to
|
|
339
|
+
# attempt to get connection info for Cloud SQL instance
|
|
344
340
|
try:
|
|
345
341
|
conn_info = await cache.connect_info()
|
|
346
342
|
# validate driver matches intended database engine
|
|
347
343
|
DriverMapping.validate_engine(driver, conn_info.database_version)
|
|
348
344
|
ip_address = conn_info.get_preferred_ip(ip_type)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
raise DnsNameResolutionError(
|
|
362
|
-
f"['{instance_connection_string}']: DNS name could not be resolved into IP address"
|
|
363
|
-
) from e
|
|
364
|
-
logger.debug(
|
|
365
|
-
f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
|
|
345
|
+
except Exception:
|
|
346
|
+
# with an error from Cloud SQL Admin API call or IP type, invalidate
|
|
347
|
+
# the cache and re-raise the error
|
|
348
|
+
await self._remove_cached(instance_connection_string)
|
|
349
|
+
raise
|
|
350
|
+
logger.debug(
|
|
351
|
+
f"['{instance_connection_string}']: Connecting to {ip_address}:3307"
|
|
352
|
+
)
|
|
353
|
+
# format `user` param for automatic IAM database authn
|
|
354
|
+
if enable_iam_auth:
|
|
355
|
+
formatted_user = format_database_user(
|
|
356
|
+
conn_info.database_version, kwargs["user"]
|
|
366
357
|
)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
conn_info.database_version, kwargs["user"]
|
|
358
|
+
if formatted_user != kwargs["user"]:
|
|
359
|
+
logger.debug(
|
|
360
|
+
f"['{instance_connection_string}']: Truncated IAM database username from {kwargs['user']} to {formatted_user}"
|
|
371
361
|
)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
f"['{instance_connection_string}']: Truncated IAM database username from {kwargs['user']} to {formatted_user}"
|
|
375
|
-
)
|
|
376
|
-
kwargs["user"] = formatted_user
|
|
377
|
-
|
|
362
|
+
kwargs["user"] = formatted_user
|
|
363
|
+
try:
|
|
378
364
|
# async drivers are unblocking and can be awaited directly
|
|
379
365
|
if driver in ASYNC_DRIVERS:
|
|
380
366
|
return await connector(
|
|
@@ -396,6 +382,17 @@ class Connector:
|
|
|
396
382
|
await cache.force_refresh()
|
|
397
383
|
raise
|
|
398
384
|
|
|
385
|
+
async def _remove_cached(self, instance_connection_string: str) -> None:
|
|
386
|
+
"""Stops all background refreshes and deletes the connection
|
|
387
|
+
info cache from the map of caches.
|
|
388
|
+
"""
|
|
389
|
+
logger.debug(
|
|
390
|
+
f"['{instance_connection_string}']: Removing connection info from cache"
|
|
391
|
+
)
|
|
392
|
+
# remove cache from stored caches and close it
|
|
393
|
+
cache = self._cache.pop(instance_connection_string)
|
|
394
|
+
await cache.close()
|
|
395
|
+
|
|
399
396
|
def __enter__(self) -> Any:
|
|
400
397
|
"""Enter context manager by returning Connector object"""
|
|
401
398
|
return self
|
|
@@ -57,13 +57,6 @@ class AutoIAMAuthNotSupported(Exception):
|
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
59
|
|
|
60
|
-
class DnsNameResolutionError(Exception):
|
|
61
|
-
"""
|
|
62
|
-
Exception to be raised when the DnsName of a PSC connection to a
|
|
63
|
-
Cloud SQL instance can not be resolved to a proper IP address.
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
60
|
class RefreshNotValidError(Exception):
|
|
68
61
|
"""
|
|
69
62
|
Exception to be raised when the task returned from refresh is not valid.
|
|
@@ -150,7 +150,7 @@ class RefreshAheadCache:
|
|
|
150
150
|
f"refresh operation failed: {str(e)}"
|
|
151
151
|
)
|
|
152
152
|
if e.status == 403:
|
|
153
|
-
e.message = "Forbidden: Authenticated IAM principal does not
|
|
153
|
+
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."
|
|
154
154
|
raise
|
|
155
155
|
|
|
156
156
|
except Exception as e:
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|