cloud-sql-python-connector 1.10.0__py2.py3-none-any.whl → 1.12.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.12.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
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.12
19
19
  Requires-Python: >=3.8
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
+ Requires-Dist: aiofiles
22
23
  Requires-Dist: aiohttp
23
24
  Requires-Dist: cryptography (>=42.0.0)
24
25
  Requires-Dist: Requests
@@ -0,0 +1,22 @@
1
+ google/cloud/sql/connector/__init__.py,sha256=x8ui7u3gMlMI-LLJkKTtUU9yvzZvQkTJ-yzr2FH4VTU,983
2
+ google/cloud/sql/connector/asyncpg.py,sha256=c4lAlvEgGf_eBq0G8MVNPRAJQQvo6DcwybiXDW36qkU,1838
3
+ google/cloud/sql/connector/client.py,sha256=wGO6ESxWWYT5qZ-Utg_qh-Rtl_4k5tIjoD-ILcP4REQ,11481
4
+ google/cloud/sql/connector/connection_info.py,sha256=exxhtb0-rk8J3OIotf8drEPMx8pke17hAX_Mbt7gOts,4336
5
+ google/cloud/sql/connector/connector.py,sha256=4BeJAZNBg_2vbF7XtJvsESKXW7-hM1pyyBu5NIWGbHk,21429
6
+ google/cloud/sql/connector/enums.py,sha256=44tfMm8D_t0ls6GfKwiQ6YkslWM21PZR40RhJPWntkc,2882
7
+ google/cloud/sql/connector/exceptions.py,sha256=OVenqBkkn98xFeytHuOXFTWa9R5bCmemBtbSIm8caoM,1665
8
+ google/cloud/sql/connector/instance.py,sha256=EySszNNHMFjAFskf6-G6Upikvb-tYjEwXx9A_OJBl14,10136
9
+ google/cloud/sql/connector/lazy.py,sha256=og4UPiJxeuLgN7tyqrFAnQF3895Yqdf6kkd4HZSzVJA,5093
10
+ google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
11
+ google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
13
+ google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
14
+ google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
15
+ google/cloud/sql/connector/refresh_utils.py,sha256=CajL-MOcQBBRUkywowdHB3GNOr0WpQn37DXG7dRpkMk,5069
16
+ google/cloud/sql/connector/utils.py,sha256=nQVZVfEijbO3XEj1oU68cR-u8Nx99rRM2a8aXaygOg0,3534
17
+ google/cloud/sql/connector/version.py,sha256=QmuVMbUhLPcMOkkWA-fPpOpI__yRbL45K8wgkam4960,598
18
+ cloud_sql_python_connector-1.12.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
19
+ cloud_sql_python_connector-1.12.0.dist-info/METADATA,sha256=LZkFfR0wlQVzFYCJgoQ_AlB8EDT3soKmVdPdF66mDTU,25949
20
+ cloud_sql_python_connector-1.12.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
21
+ cloud_sql_python_connector-1.12.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
22
+ cloud_sql_python_connector-1.12.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"]
@@ -17,9 +17,10 @@ from __future__ import annotations
17
17
  from dataclasses import dataclass
18
18
  import logging
19
19
  import ssl
20
- from tempfile import TemporaryDirectory
21
20
  from typing import Any, Dict, Optional, TYPE_CHECKING
22
21
 
22
+ from aiofiles.tempfile import TemporaryDirectory
23
+
23
24
  from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
24
25
  from google.cloud.sql.connector.exceptions import TLSVersionError
25
26
  from google.cloud.sql.connector.utils import write_to_file
@@ -45,7 +46,7 @@ class ConnectionInfo:
45
46
  expiration: datetime.datetime
46
47
  context: Optional[ssl.SSLContext] = None
47
48
 
48
- def create_ssl_context(self, enable_iam_auth: bool = False) -> ssl.SSLContext:
49
+ async def create_ssl_context(self, enable_iam_auth: bool = False) -> ssl.SSLContext:
49
50
  """Constructs a SSL/TLS context for the given connection info.
50
51
 
51
52
  Cache the SSL context to ensure we don't read from disk repeatedly when
@@ -83,8 +84,8 @@ class ConnectionInfo:
83
84
  # tmpdir and its contents are automatically deleted after the CA cert
84
85
  # and ephemeral cert are loaded into the SSLcontext. The values
85
86
  # need to be written to files in order to be loaded by the SSLContext
86
- with TemporaryDirectory() as tmpdir:
87
- ca_filename, cert_filename, key_filename = write_to_file(
87
+ async with TemporaryDirectory() as tmpdir:
88
+ ca_filename, cert_filename, key_filename = await write_to_file(
88
89
  tmpdir, self.server_ca_cert, self.client_cert, self.private_key
89
90
  )
90
91
  context.load_cert_chain(cert_filename, keyfile=key_filename)
@@ -101,5 +102,5 @@ class ConnectionInfo:
101
102
  return self.ip_addrs[ip_type.value]
102
103
  raise CloudSQLIPTypeError(
103
104
  "Cloud SQL instance does not have any IP addresses matching "
104
- f"preference: {ip_type.value})"
105
+ f"preference: {ip_type.value}"
105
106
  )
@@ -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,53 +336,43 @@ 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(
381
367
  ip_address,
382
- conn_info.create_ssl_context(enable_iam_auth),
368
+ await conn_info.create_ssl_context(enable_iam_auth),
383
369
  **kwargs,
384
370
  )
385
371
  # synchronous drivers are blocking and run using executor
386
372
  connect_partial = partial(
387
373
  connector,
388
374
  ip_address,
389
- conn_info.create_ssl_context(enable_iam_auth),
375
+ await conn_info.create_ssl_context(enable_iam_auth),
390
376
  **kwargs,
391
377
  )
392
378
  return await self._loop.run_in_executor(None, connect_partial)
@@ -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
@@ -13,8 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  """
16
+
16
17
  from typing import Tuple
17
18
 
19
+ import aiofiles
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,7 +59,7 @@ async def generate_keys() -> Tuple[bytes, str]:
57
59
  return priv_key, pub_key
58
60
 
59
61
 
60
- def write_to_file(
62
+ async def write_to_file(
61
63
  dir_path: str, serverCaCert: str, ephemeralCert: str, priv_key: bytes
62
64
  ) -> Tuple[str, str, str]:
63
65
  """
@@ -68,12 +70,12 @@ def write_to_file(
68
70
  cert_filename = f"{dir_path}/cert.pem"
69
71
  key_filename = f"{dir_path}/priv.pem"
70
72
 
71
- with open(ca_filename, "w+") as ca_out:
72
- ca_out.write(serverCaCert)
73
- with open(cert_filename, "w+") as ephemeral_out:
74
- ephemeral_out.write(ephemeralCert)
75
- with open(key_filename, "wb") as priv_out:
76
- priv_out.write(priv_key)
73
+ async with aiofiles.open(ca_filename, "w+") as ca_out:
74
+ await ca_out.write(serverCaCert)
75
+ async with aiofiles.open(cert_filename, "w+") as ephemeral_out:
76
+ await ephemeral_out.write(ephemeralCert)
77
+ async with aiofiles.open(key_filename, "wb") as priv_out:
78
+ await priv_out.write(priv_key)
77
79
 
78
80
  return (ca_filename, cert_filename, key_filename)
79
81
 
@@ -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.12.0"
@@ -1,22 +0,0 @@
1
- google/cloud/sql/connector/__init__.py,sha256=x8ui7u3gMlMI-LLJkKTtUU9yvzZvQkTJ-yzr2FH4VTU,983
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
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
9
- google/cloud/sql/connector/lazy.py,sha256=og4UPiJxeuLgN7tyqrFAnQF3895Yqdf6kkd4HZSzVJA,5093
10
- google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
11
- google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
13
- google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
14
- google/cloud/sql/connector/rate_limiter.py,sha256=bPTupfM4f5QRSCmCAWMqjQylEB7bUM-50plVBU3W2CA,2993
15
- google/cloud/sql/connector/refresh_utils.py,sha256=P2ye3qDymdK1iOlGCXby4VIaqwvHdc9GEmuUsMcufxY,3680
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,,