cloud-sql-python-connector 1.10.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-sql-python-connector
3
- Version: 1.10.0
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,22 +1,22 @@
1
1
  google/cloud/sql/connector/__init__.py,sha256=x8ui7u3gMlMI-LLJkKTtUU9yvzZvQkTJ-yzr2FH4VTU,983
2
2
  google/cloud/sql/connector/asyncpg.py,sha256=c4lAlvEgGf_eBq0G8MVNPRAJQQvo6DcwybiXDW36qkU,1838
3
- google/cloud/sql/connector/client.py,sha256=VvfDgJVZ1V2X54m1P6X3GCL7HHUoSvvAzEl1PKDDQ1I,11119
4
- google/cloud/sql/connector/connection_info.py,sha256=esasEjExqkHt3V9tgx7jqeGgSPJMCuTZQmSMEFqAvWY,4309
5
- google/cloud/sql/connector/connector.py,sha256=PtRRQ0n-OAM4d3bDmLZErPRikSudDFmd9xY9SbCVfcc,21761
3
+ google/cloud/sql/connector/client.py,sha256=wGO6ESxWWYT5qZ-Utg_qh-Rtl_4k5tIjoD-ILcP4REQ,11481
4
+ google/cloud/sql/connector/connection_info.py,sha256=3nFuPtGPLUEZRy3Zx9Z2DvTdh6TJqmhOOoE6yKmSGBQ,4308
5
+ google/cloud/sql/connector/connector.py,sha256=NTubTmsT8rPpGYuhxXgKDW3MX7KUi4WSalKCHJkHFY8,21417
6
6
  google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
7
- google/cloud/sql/connector/exceptions.py,sha256=wRK3gKXomzi9FUs9qju3WN1FDUTuwozzZEkucTe4wKI,1860
8
- google/cloud/sql/connector/instance.py,sha256=v3x936OddIaqcGD8aIRi-bzcpOncTExwZTKu_4MpzBo,10137
7
+ google/cloud/sql/connector/exceptions.py,sha256=OVenqBkkn98xFeytHuOXFTWa9R5bCmemBtbSIm8caoM,1665
8
+ google/cloud/sql/connector/instance.py,sha256=EySszNNHMFjAFskf6-G6Upikvb-tYjEwXx9A_OJBl14,10136
9
9
  google/cloud/sql/connector/lazy.py,sha256=og4UPiJxeuLgN7tyqrFAnQF3895Yqdf6kkd4HZSzVJA,5093
10
10
  google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
11
11
  google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
13
13
  google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
14
14
  google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
15
- google/cloud/sql/connector/refresh_utils.py,sha256=P2ye3qDymdK1iOlGCXby4VIaqwvHdc9GEmuUsMcufxY,3680
15
+ google/cloud/sql/connector/refresh_utils.py,sha256=CajL-MOcQBBRUkywowdHB3GNOr0WpQn37DXG7dRpkMk,5069
16
16
  google/cloud/sql/connector/utils.py,sha256=FQs7RgoViJLIFgJIynVT-_DwAVWnpyXIKu5WZ4m10Ts,3448
17
- google/cloud/sql/connector/version.py,sha256=4BE16mU-VOqsOw52ua4QjeLiTwW8fwyo0zXrk3YHwF0,598
18
- cloud_sql_python_connector-1.10.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
19
- cloud_sql_python_connector-1.10.0.dist-info/METADATA,sha256=M8VvudvgqHr2ETZrjHR_G7qsOOw-yK8FlzmT4VwZ93w,25925
20
- cloud_sql_python_connector-1.10.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
21
- cloud_sql_python_connector-1.10.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
22
- cloud_sql_python_connector-1.10.0.dist-info/RECORD,,
17
+ google/cloud/sql/connector/version.py,sha256=l5iWT7w_8JLh0wHTJOCrcxkaD1xxBwPGJDzeSMQLXTI,598
18
+ cloud_sql_python_connector-1.11.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
19
+ cloud_sql_python_connector-1.11.0.dist-info/METADATA,sha256=4NukfHGYVymtt0NbBrH6bFW3BM544K072H-_f8gx6KM,25925
20
+ cloud_sql_python_connector-1.11.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
21
+ cloud_sql_python_connector-1.11.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
22
+ cloud_sql_python_connector-1.11.0.dist-info/RECORD,,
@@ -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, raise_for_status=True)
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
- if "dnsName" in ret_dict:
141
- ip_addresses["PSC"] = ret_dict["dnsName"]
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
- url, headers=headers, json=data, raise_for_status=True
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"]
@@ -101,5 +101,5 @@ class ConnectionInfo:
101
101
  return self.ip_addrs[ip_type.value]
102
102
  raise CloudSQLIPTypeError(
103
103
  "Cloud SQL instance does not have any IP addresses matching "
104
- f"preference: {ip_type.value})"
104
+ f"preference: {ip_type.value}"
105
105
  )
@@ -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 make connection to Cloud SQL instance
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
- # resolve DNS name into IP address for PSC
350
- if ip_type.value == "PSC":
351
- addr_info = await self._loop.getaddrinfo(
352
- ip_address, None, family=socket.AF_INET, type=socket.SOCK_STREAM
353
- )
354
- # getaddrinfo returns a list of 5-tuples that contain socket
355
- # connection info in the form
356
- # (family, type, proto, canonname, sockaddr), where sockaddr is a
357
- # 2-tuple in the form (ip_address, port)
358
- try:
359
- ip_address = addr_info[0][4][0]
360
- except IndexError as e:
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
- # format `user` param for automatic IAM database authn
368
- if enable_iam_auth:
369
- formatted_user = format_database_user(
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
- if formatted_user != kwargs["user"]:
373
- logger.debug(
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 seeem 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."
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
- from typing import List
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
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "1.10.0"
15
+ __version__ = "1.11.0"