cloud-sql-python-connector 1.6.0__py2.py3-none-any.whl → 1.8.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.6.0
3
+ Version: 1.8.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
@@ -15,21 +15,22 @@ Classifier: Programming Language :: Python :: 3.8
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
18
19
  Requires-Python: >=3.8
19
20
  Description-Content-Type: text/markdown
20
21
  License-File: LICENSE
21
22
  Requires-Dist: aiohttp
22
- Requires-Dist: cryptography (>=38.0.3)
23
+ Requires-Dist: cryptography (>=42.0.0)
23
24
  Requires-Dist: Requests
24
25
  Requires-Dist: google-auth
25
26
  Provides-Extra: asyncpg
26
27
  Requires-Dist: asyncpg (>=0.29.0) ; extra == 'asyncpg'
27
28
  Provides-Extra: pg8000
28
- Requires-Dist: pg8000 (>=1.30.4) ; extra == 'pg8000'
29
+ Requires-Dist: pg8000 (>=1.30.5) ; extra == 'pg8000'
29
30
  Provides-Extra: pymysql
30
31
  Requires-Dist: PyMySQL (>=1.1.0) ; extra == 'pymysql'
31
32
  Provides-Extra: pytds
32
- Requires-Dist: python-tds (>=1.13.0) ; extra == 'pytds'
33
+ Requires-Dist: python-tds (>=1.15.0) ; extra == 'pytds'
33
34
 
34
35
  <p align="center">
35
36
  <a href="https://cloud.google.com/blog/topics/developers-practitioners/how-connect-cloud-sql-using-python-easy-way">
@@ -144,7 +145,7 @@ format "project:region:instance".
144
145
  ### How to use this Connector
145
146
 
146
147
  To connect to Cloud SQL using the connector, inititalize a `Connector`
147
- object and call it's `connect` method with the proper input parameters.
148
+ object and call its `connect` method with the proper input parameters.
148
149
 
149
150
  The `Connector` itself creates connection objects by calling its `connect` method but does not manage database connection pooling. For this reason, it is recommended to use the connector alongside a library that can create connection pools, such as [SQLAlchemy](https://www.sqlalchemy.org/). This will allow for connections to remain open and be reused, reducing connection overhead and the number of connections needed.
150
151
 
@@ -200,7 +201,7 @@ with pool.connect() as db_conn:
200
201
  print(row)
201
202
  ```
202
203
 
203
- To close the `Connector` object's background resources, call it's `close()` method as follows:
204
+ To close the `Connector` object's background resources, call its `close()` method as follows:
204
205
 
205
206
  ```python
206
207
  connector.close()
@@ -221,7 +222,7 @@ from google.cloud.sql.connector import Connector, IPTypes
221
222
 
222
223
  # Note: all parameters below are optional
223
224
  connector = Connector(
224
- ip_type=IPTypes.PUBLIC,
225
+ ip_type="public", # can also be "private" or "psc"
225
226
  enable_iam_auth=False,
226
227
  timeout=30,
227
228
  credentials=custom_creds # google.auth.credentials.Credentials
@@ -294,18 +295,16 @@ using both public and private IP addresses, as well as
294
295
  with, set the `ip_type` keyword argument when initializing a `Connector()` or when
295
296
  calling `connector.connect()`.
296
297
 
297
- Possible values for `ip_type` are `IPTypes.PUBLIC` (default value),
298
- `IPTypes.PRIVATE`, and `IPTypes.PSC`.
298
+ Possible values for `ip_type` are `"public"` (default value),
299
+ `"private"`, and `"psc"`.
299
300
 
300
301
  Example:
301
302
 
302
303
  ```python
303
- from google.cloud.sql.connector import IPTypes
304
-
305
304
  conn = connector.connect(
306
305
  "project:region:instance",
307
306
  "pymysql",
308
- ip_type=IPTypes.PRIVATE # use private IP
307
+ ip_type="private" # use private IP
309
308
  ... insert other kwargs ...
310
309
  )
311
310
  ```
@@ -366,7 +365,7 @@ conn = connector.connect(
366
365
  db="my-db-name",
367
366
  active_directory_auth=True,
368
367
  server_name="private.[instance].[location].[project].cloudsql.[domain]",
369
- ip_type=IPTypes.PRIVATE
368
+ ip_type="private"
370
369
  )
371
370
  ```
372
371
 
@@ -391,7 +390,7 @@ your web application through the following:
391
390
  ```python
392
391
  from flask import Flask
393
392
  from flask_sqlalchemy import SQLAlchemy
394
- from google.cloud.sql.connector import Connector, IPTypes
393
+ from google.cloud.sql.connector import Connector
395
394
 
396
395
 
397
396
  # initialize Python Connector object
@@ -405,7 +404,7 @@ def getconn():
405
404
  user="my-user",
406
405
  password="my-password",
407
406
  db="my-database",
408
- ip_type= IPTypes.PUBLIC # IPTypes.PRIVATE for private IP
407
+ ip_type="public" # "private" for private IP
409
408
  )
410
409
  return conn
411
410
 
@@ -440,7 +439,7 @@ from sqlalchemy import create_engine
440
439
  from sqlalchemy.engine import Engine
441
440
  from sqlalchemy.ext.declarative import declarative_base
442
441
  from sqlalchemy.orm import sessionmaker
443
- from google.cloud.sql.connector import Connector, IPTypes
442
+ from google.cloud.sql.connector import Connector
444
443
 
445
444
  # helper function to return SQLAlchemy connection pool
446
445
  def init_connection_pool(connector: Connector) -> Engine:
@@ -452,7 +451,7 @@ def init_connection_pool(connector: Connector) -> Engine:
452
451
  user="my-user",
453
452
  password="my-password",
454
453
  db="my-database",
455
- ip_type= IPTypes.PUBLIC # IPTypes.PRIVATE for private IP
454
+ ip_type="public" # "private" for private IP
456
455
  )
457
456
  return conn
458
457
 
@@ -0,0 +1,19 @@
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=n1v6xxtfuvNZYCUfNUWY6FdNw9uvgOnmFxpSE0NmN5M,8263
4
+ google/cloud/sql/connector/connector.py,sha256=nx5SVEJUF6pJt3VAmjw3-XBpWKFdWHJDEABM_pPzsvc,16867
5
+ google/cloud/sql/connector/exceptions.py,sha256=rTBtbCoiZ3U5IFVsyXPw5baHbCODixx2hPDQeXUzgzs,1524
6
+ google/cloud/sql/connector/instance.py,sha256=G7-SCqsLjZiOYZ-Ds2RIErs6oaT0ewnAN683fs7EebI,14920
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=uFu1vr_bHDmu9LzGZmYfAmD9Cb7yvaTRwh3jChsfq0g,597
15
+ cloud_sql_python_connector-1.8.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
16
+ cloud_sql_python_connector-1.8.0.dist-info/METADATA,sha256=FUIl4XRA6VgxpVcTI0sNtdIxch7MtBumAsGZ1TwdvFA,24102
17
+ cloud_sql_python_connector-1.8.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
18
+ cloud_sql_python_connector-1.8.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
19
+ cloud_sql_python_connector-1.8.0.dist-info/RECORD,,
@@ -0,0 +1,221 @@
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
+ from __future__ import annotations
16
+
17
+ import datetime
18
+ import logging
19
+ from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
20
+
21
+ import aiohttp
22
+ from cryptography.hazmat.backends import default_backend
23
+ from cryptography.x509 import load_pem_x509_certificate
24
+ import google.auth.transport.requests
25
+
26
+ from google.cloud.sql.connector.refresh_utils import _downscope_credentials
27
+ from google.cloud.sql.connector.version import __version__ as version
28
+
29
+ if TYPE_CHECKING:
30
+ from google.auth.credentials import Credentials
31
+
32
+ USER_AGENT: str = f"cloud-sql-python-connector/{version}"
33
+ API_VERSION: str = "v1beta4"
34
+
35
+ logger = logging.getLogger(name=__name__)
36
+
37
+
38
+ def _format_user_agent(driver: Optional[str], custom: Optional[str]) -> str:
39
+ agent = f"{USER_AGENT}+{driver}" if driver else USER_AGENT
40
+ if custom:
41
+ agent = f"{agent} {custom}"
42
+ return agent
43
+
44
+
45
+ class CloudSQLClient:
46
+ def __init__(
47
+ self,
48
+ sqladmin_api_endpoint: str,
49
+ quota_project: Optional[str],
50
+ credentials: Credentials,
51
+ client: Optional[aiohttp.ClientSession] = None,
52
+ driver: Optional[str] = None,
53
+ user_agent: Optional[str] = None,
54
+ ) -> None:
55
+ """
56
+ Establish the client to be used for Cloud SQL Admin API requests.
57
+
58
+ Args:
59
+ sqladmin_api_endpoint (str): Base URL to use when calling
60
+ the Cloud SQL Admin API endpoints.
61
+ quota_project (str): The Project ID for an existing Google Cloud
62
+ project. The project specified is used for quota and
63
+ billing purposes.
64
+ credentials (google.auth.credentials.Credentials):
65
+ A credentials object created from the google-auth Python library.
66
+ Must have the Cloud SQL Admin scopes. For more info check out
67
+ https://google-auth.readthedocs.io/en/latest/.
68
+ client (aiohttp.ClientSession): Async client used to make requests to
69
+ Cloud SQL Admin APIs.
70
+ Optional, defaults to None and creates new client.
71
+ driver (str): Database driver to be used by the client.
72
+ """
73
+ user_agent = _format_user_agent(driver, user_agent)
74
+ headers = {
75
+ "x-goog-api-client": user_agent,
76
+ "User-Agent": user_agent,
77
+ "Content-Type": "application/json",
78
+ }
79
+ if quota_project:
80
+ headers["x-goog-user-project"] = quota_project
81
+
82
+ self._client = client if client else aiohttp.ClientSession(headers=headers)
83
+ self._credentials = credentials
84
+ self._sqladmin_api_endpoint = sqladmin_api_endpoint
85
+ self._user_agent = user_agent
86
+
87
+ async def _get_metadata(
88
+ self,
89
+ project: str,
90
+ region: str,
91
+ instance: str,
92
+ ) -> Dict[str, Any]:
93
+ """Requests metadata from the Cloud SQL Instance
94
+ and returns a dictionary containing the IP addresses and certificate
95
+ authority of the Cloud SQL Instance.
96
+
97
+ :type project: str
98
+ :param project:
99
+ A string representing the name of the project.
100
+
101
+ :type region: str
102
+ :param region : A string representing the name of the region.
103
+
104
+ :type instance: str
105
+ :param instance: A string representing the name of the instance.
106
+
107
+ :rtype: Dict[str: Union[Dict, str]]
108
+ :returns: Returns a dictionary containing a dictionary of all IP
109
+ addresses and their type and a string representing the
110
+ certificate authority.
111
+ """
112
+ if not self._credentials.valid:
113
+ request = google.auth.transport.requests.Request()
114
+ self._credentials.refresh(request)
115
+
116
+ headers = {
117
+ "Authorization": f"Bearer {self._credentials.token}",
118
+ }
119
+
120
+ url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/projects/{project}/instances/{instance}/connectSettings"
121
+
122
+ logger.debug(f"['{instance}']: Requesting metadata")
123
+
124
+ resp = await self._client.get(url, headers=headers, raise_for_status=True)
125
+ ret_dict = await resp.json()
126
+
127
+ if ret_dict["region"] != region:
128
+ raise ValueError(
129
+ f'[{project}:{region}:{instance}]: Provided region was mismatched - got region {region}, expected {ret_dict["region"]}.'
130
+ )
131
+
132
+ ip_addresses = (
133
+ {ip["type"]: ip["ipAddress"] for ip in ret_dict["ipAddresses"]}
134
+ if "ipAddresses" in ret_dict
135
+ else {}
136
+ )
137
+ if "dnsName" in ret_dict:
138
+ ip_addresses["PSC"] = ret_dict["dnsName"]
139
+
140
+ return {
141
+ "ip_addresses": ip_addresses,
142
+ "server_ca_cert": ret_dict["serverCaCert"]["cert"],
143
+ "database_version": ret_dict["databaseVersion"],
144
+ }
145
+
146
+ async def _get_ephemeral(
147
+ self,
148
+ project: str,
149
+ instance: str,
150
+ pub_key: str,
151
+ enable_iam_auth: bool = False,
152
+ ) -> Tuple[str, datetime.datetime]:
153
+ """Asynchronously requests an ephemeral certificate from the Cloud SQL Instance.
154
+
155
+ :type project: str
156
+ :param project : A string representing the name of the project.
157
+
158
+ :type instance: str
159
+ :param instance: A string representing the name of the instance.
160
+
161
+ :type pub_key:
162
+ :param str: A string representing PEM-encoded RSA public key.
163
+
164
+ :type enable_iam_auth: bool
165
+ :param enable_iam_auth
166
+ Enables automatic IAM database authentication for Postgres or MySQL
167
+ instances.
168
+
169
+ :rtype: str
170
+ :returns: An ephemeral certificate from the Cloud SQL instance that allows
171
+ authorized connections to the instance.
172
+ """
173
+
174
+ logger.debug(f"['{instance}']: Requesting ephemeral certificate")
175
+
176
+ if not self._credentials.valid:
177
+ request = google.auth.transport.requests.Request()
178
+ self._credentials.refresh(request)
179
+
180
+ headers = {
181
+ "Authorization": f"Bearer {self._credentials.token}",
182
+ }
183
+
184
+ url = f"{self._sqladmin_api_endpoint}/sql/{API_VERSION}/projects/{project}/instances/{instance}:generateEphemeralCert"
185
+
186
+ data = {"public_key": pub_key}
187
+
188
+ if enable_iam_auth:
189
+ # down-scope credentials with only IAM login scope (refreshes them too)
190
+ login_creds = _downscope_credentials(self._credentials)
191
+ data["access_token"] = login_creds.token
192
+
193
+ resp = await self._client.post(
194
+ url, headers=headers, json=data, raise_for_status=True
195
+ )
196
+
197
+ ret_dict = await resp.json()
198
+
199
+ ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
200
+
201
+ # decode cert to read expiration
202
+ x509 = load_pem_x509_certificate(
203
+ ephemeral_cert.encode("UTF-8"), default_backend()
204
+ )
205
+ expiration = x509.not_valid_after_utc
206
+ # for IAM authentication OAuth2 token is embedded in cert so it
207
+ # must still be valid for successful connection
208
+ if enable_iam_auth:
209
+ token_expiration: datetime.datetime = login_creds.expiry
210
+ # google.auth library strips timezone info for backwards compatibality
211
+ # reasons with Python 2. Add it back to allow timezone aware datetimes.
212
+ # Ref: https://github.com/googleapis/google-auth-library-python/blob/49a5ff7411a2ae4d32a7d11700f9f961c55406a9/google/auth/_helpers.py#L93-L99
213
+ token_expiration = token_expiration.replace(tzinfo=datetime.timezone.utc)
214
+
215
+ if expiration > token_expiration:
216
+ expiration = token_expiration
217
+ return ephemeral_cert, expiration
218
+
219
+ async def close(self) -> None:
220
+ """Close CloudSQLClient gracefully."""
221
+ await self._client.close()
@@ -13,6 +13,7 @@ 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 __future__ import annotations
17
18
 
18
19
  import asyncio
@@ -21,9 +22,14 @@ import logging
21
22
  import socket
22
23
  from threading import Thread
23
24
  from types import TracebackType
24
- from typing import Any, Dict, Optional, Type, TYPE_CHECKING
25
+ from typing import Any, Dict, Optional, Type
26
+
27
+ import google.auth
28
+ from google.auth.credentials import Credentials
29
+ from google.auth.credentials import with_scopes_if_required
25
30
 
26
31
  import google.cloud.sql.connector.asyncpg as asyncpg
32
+ from google.cloud.sql.connector.client import CloudSQLClient
27
33
  from google.cloud.sql.connector.exceptions import ConnectorLoopError
28
34
  from google.cloud.sql.connector.exceptions import DnsNameResolutionError
29
35
  from google.cloud.sql.connector.instance import Instance
@@ -34,9 +40,6 @@ import google.cloud.sql.connector.pytds as pytds
34
40
  from google.cloud.sql.connector.utils import format_database_user
35
41
  from google.cloud.sql.connector.utils import generate_keys
36
42
 
37
- if TYPE_CHECKING:
38
- from google.auth.credentials import Credentials
39
-
40
43
  logger = logging.getLogger(name=__name__)
41
44
 
42
45
  ASYNC_DRIVERS = ["asyncpg"]
@@ -45,10 +48,10 @@ ASYNC_DRIVERS = ["asyncpg"]
45
48
  class Connector:
46
49
  """A class to configure and create connections to Cloud SQL instances.
47
50
 
48
- :type ip_type: IPTypes
51
+ :type ip_type: str | IPTypes
49
52
  :param ip_type
50
- The IP type (public or private) used to connect. IP types
51
- can be either IPTypes.PUBLIC or IPTypes.PRIVATE.
53
+ The IP type used to connect. IP types can be either IPTypes.PUBLIC
54
+ ("PUBLIC"), IPTypes.PRIVATE ("PRIVATE"), or IPTypes.PSC ("PSC").
52
55
 
53
56
  :type enable_iam_auth: bool
54
57
  :param enable_iam_auth
@@ -84,13 +87,14 @@ class Connector:
84
87
 
85
88
  def __init__(
86
89
  self,
87
- ip_type: IPTypes = IPTypes.PUBLIC,
90
+ ip_type: str | IPTypes = IPTypes.PUBLIC,
88
91
  enable_iam_auth: bool = False,
89
92
  timeout: int = 30,
90
93
  credentials: Optional[Credentials] = None,
91
- loop: asyncio.AbstractEventLoop = None,
94
+ loop: Optional[asyncio.AbstractEventLoop] = None,
92
95
  quota_project: Optional[str] = None,
93
96
  sqladmin_api_endpoint: str = "https://sqladmin.googleapis.com",
97
+ user_agent: Optional[str] = None,
94
98
  ) -> None:
95
99
  # if event loop is given, use for background tasks
96
100
  if loop:
@@ -107,14 +111,32 @@ class Connector:
107
111
  loop=self._loop,
108
112
  )
109
113
  self._instances: Dict[str, Instance] = {}
110
-
114
+ self._client: Optional[CloudSQLClient] = None
115
+
116
+ # initialize credentials
117
+ scopes = ["https://www.googleapis.com/auth/sqlservice.admin"]
118
+ if credentials:
119
+ # verify custom credentials are proper type
120
+ # and atleast base class of google.auth.credentials
121
+ if not isinstance(credentials, Credentials):
122
+ raise TypeError(
123
+ "credentials must be of type google.auth.credentials.Credentials,"
124
+ f" got {type(credentials)}"
125
+ )
126
+ self._credentials = with_scopes_if_required(credentials, scopes=scopes)
127
+ # otherwise use application default credentials
128
+ else:
129
+ self._credentials, _ = google.auth.default(scopes=scopes)
111
130
  # set default params for connections
112
131
  self._timeout = timeout
113
132
  self._enable_iam_auth = enable_iam_auth
114
- self._ip_type = ip_type
115
133
  self._quota_project = quota_project
116
134
  self._sqladmin_api_endpoint = sqladmin_api_endpoint
117
- self._credentials = credentials
135
+ self._user_agent = user_agent
136
+ # if ip_type is str, convert to IPTypes enum
137
+ if isinstance(ip_type, str):
138
+ ip_type = IPTypes._from_str(ip_type)
139
+ self._ip_type = ip_type
118
140
 
119
141
  def connect(
120
142
  self, instance_connection_string: str, driver: str, **kwargs: Any
@@ -191,26 +213,31 @@ class Connector:
191
213
  # Use the Instance to establish an SSL Connection.
192
214
  #
193
215
  # Return a DBAPI connection
216
+ if self._client is None:
217
+ # lazy init client as it has to be initialized in async context
218
+ self._client = CloudSQLClient(
219
+ self._sqladmin_api_endpoint,
220
+ self._quota_project,
221
+ self._credentials,
222
+ user_agent=self._user_agent,
223
+ driver=driver,
224
+ )
194
225
  enable_iam_auth = kwargs.pop("enable_iam_auth", self._enable_iam_auth)
195
226
  if instance_connection_string in self._instances:
196
227
  instance = self._instances[instance_connection_string]
197
228
  if enable_iam_auth != instance._enable_iam_auth:
198
229
  raise ValueError(
199
- f"connect() called with `enable_iam_auth={enable_iam_auth}`, "
200
- f"but previously used enable_iam_auth={instance._enable_iam_auth}`. "
230
+ f"connect() called with 'enable_iam_auth={enable_iam_auth}', "
231
+ f"but previously used 'enable_iam_auth={instance._enable_iam_auth}'. "
201
232
  "If you require both for your use case, please use a new "
202
233
  "connector.Connector object."
203
234
  )
204
235
  else:
205
236
  instance = Instance(
206
237
  instance_connection_string,
207
- driver,
238
+ self._client,
208
239
  self._keys,
209
- self._loop,
210
- self._credentials,
211
240
  enable_iam_auth,
212
- self._quota_project,
213
- self._sqladmin_api_endpoint,
214
241
  )
215
242
  self._instances[instance_connection_string] = instance
216
243
 
@@ -228,6 +255,9 @@ class Connector:
228
255
  raise KeyError(f"Driver '{driver}' is not supported.")
229
256
 
230
257
  ip_type = kwargs.pop("ip_type", self._ip_type)
258
+ # if ip_type is str, convert to IPTypes enum
259
+ if isinstance(ip_type, str):
260
+ ip_type = IPTypes._from_str(ip_type)
231
261
  kwargs["timeout"] = kwargs.get("timeout", self._timeout)
232
262
 
233
263
  # Host and ssl options come from the certificates and metadata, so we don't
@@ -308,15 +338,17 @@ class Connector:
308
338
 
309
339
  def close(self) -> None:
310
340
  """Close Connector by stopping tasks and releasing resources."""
311
- close_future = asyncio.run_coroutine_threadsafe(
312
- self.close_async(), loop=self._loop
313
- )
314
- # Will attempt to safely shut down tasks for 5s
315
- close_future.result(timeout=5)
341
+ if self._loop.is_running():
342
+ close_future = asyncio.run_coroutine_threadsafe(
343
+ self.close_async(), loop=self._loop
344
+ )
345
+ # Will attempt to safely shut down tasks for 3s
346
+ close_future.result(timeout=3)
316
347
  # if background thread exists for Connector, clean it up
317
348
  if self._thread:
318
- # stop event loop running in background thread
319
- self._loop.call_soon_threadsafe(self._loop.stop)
349
+ if self._loop.is_running():
350
+ # stop event loop running in background thread
351
+ self._loop.call_soon_threadsafe(self._loop.stop)
320
352
  # wait for thread to finish closing (i.e. loop to stop)
321
353
  self._thread.join()
322
354
 
@@ -326,6 +358,8 @@ class Connector:
326
358
  await asyncio.gather(
327
359
  *[instance.close() for instance in self._instances.values()]
328
360
  )
361
+ if self._client:
362
+ await self._client.close()
329
363
 
330
364
 
331
365
  async def create_async_connector(
@@ -333,37 +367,61 @@ async def create_async_connector(
333
367
  enable_iam_auth: bool = False,
334
368
  timeout: int = 30,
335
369
  credentials: Optional[Credentials] = None,
336
- loop: asyncio.AbstractEventLoop = None,
370
+ loop: Optional[asyncio.AbstractEventLoop] = None,
371
+ quota_project: Optional[str] = None,
372
+ sqladmin_api_endpoint: str = "https://sqladmin.googleapis.com",
373
+ user_agent: Optional[str] = None,
337
374
  ) -> Connector:
338
375
  """
339
- Create Connector object for asyncio connections that can auto-detect
340
- and use current thread's running event loop.
376
+ Create Connector object for asyncio connections that can auto-detect
377
+ and use current thread's running event loop.
341
378
 
342
379
  :type ip_type: IPTypes
343
- :param ip_type
344
- The IP type (public or private) used to connect. IP types
345
- can be either IPTypes.PUBLIC or IPTypes.PRIVATE.
346
-
347
- :type enable_iam_auth: bool
348
- :param enable_iam_auth
349
- Enables automatic IAM database authentication for Postgres or MySQL
350
- instances.
351
-
352
- :type timeout: int
353
- :param timeout
354
- The time limit for a connection before raising a TimeoutError.
355
-
356
- :type credentials: google.auth.credentials.Credentials
357
- :param credentials
358
- Credentials object used to authenticate connections to Cloud SQL server.
359
- If not specified, Application Default Credentials are used.
360
-
361
- :type loop: asyncio.AbstractEventLoop
362
- :param loop
363
- Event loop to run asyncio tasks, if not specified, defaults
364
- to current thread's running event loop.
380
+ :param ip_type
381
+ The IP type (public or private) used to connect. IP types
382
+ can be either IPTypes.PUBLIC or IPTypes.PRIVATE.
383
+
384
+ :type enable_iam_auth: bool
385
+ :param enable_iam_auth
386
+ Enables automatic IAM database authentication for Postgres or MySQL
387
+ instances.
388
+
389
+ :type timeout: int
390
+ :param timeout
391
+ The time limit for a connection before raising a TimeoutError.
392
+
393
+ :type credentials: google.auth.credentials.Credentials
394
+ :param credentials
395
+ Credentials object used to authenticate connections to Cloud SQL server.
396
+ If not specified, Application Default Credentials are used.
397
+
398
+ :type quota_project: str
399
+ :param quota_project
400
+ The Project ID for an existing Google Cloud project. The project specified
401
+ is used for quota and billing purposes. If not specified, defaults to
402
+ project sourced from environment.
403
+
404
+ :type loop: asyncio.AbstractEventLoop
405
+ :param loop
406
+ Event loop to run asyncio tasks, if not specified, defaults to
407
+ creating new event loop on background thread.
408
+
409
+ :type sqladmin_api_endpoint: str
410
+ :param sqladmin_api_endpoint:
411
+ Base URL to use when calling the Cloud SQL Admin API endpoint.
412
+ Defaults to "https://sqladmin.googleapis.com", this argument should
413
+ only be used in development.
365
414
  """
366
415
  # if no loop given, automatically detect running event loop
367
416
  if loop is None:
368
417
  loop = asyncio.get_running_loop()
369
- return Connector(ip_type, enable_iam_auth, timeout, credentials, loop)
418
+ return Connector(
419
+ ip_type=ip_type,
420
+ enable_iam_auth=enable_iam_auth,
421
+ timeout=timeout,
422
+ credentials=credentials,
423
+ loop=loop,
424
+ quota_project=quota_project,
425
+ sqladmin_api_endpoint=sqladmin_api_endpoint,
426
+ user_agent=user_agent,
427
+ )
@@ -48,14 +48,6 @@ class PlatformNotSupportedError(Exception):
48
48
  pass
49
49
 
50
50
 
51
- class CredentialsTypeError(Exception):
52
- """
53
- Raised when credentials parameter is not proper type.
54
- """
55
-
56
- pass
57
-
58
-
59
51
  class AutoIAMAuthNotSupported(Exception):
60
52
  """
61
53
  Exception to be raised when Automatic IAM Authentication is not
@@ -13,6 +13,7 @@ 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 __future__ import annotations
17
18
 
18
19
  import asyncio
@@ -21,23 +22,18 @@ import logging
21
22
  import re
22
23
  import ssl
23
24
  from tempfile import TemporaryDirectory
24
- from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
25
+ from typing import Any, Dict, Tuple, TYPE_CHECKING
25
26
 
26
27
  import aiohttp
27
- from google.auth.credentials import Credentials
28
28
 
29
+ from google.cloud.sql.connector.client import CloudSQLClient
29
30
  from google.cloud.sql.connector.exceptions import AutoIAMAuthNotSupported
30
31
  from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
31
- from google.cloud.sql.connector.exceptions import CredentialsTypeError
32
32
  from google.cloud.sql.connector.exceptions import TLSVersionError
33
33
  from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
34
- from google.cloud.sql.connector.refresh_utils import _get_ephemeral
35
- from google.cloud.sql.connector.refresh_utils import _get_metadata
36
34
  from google.cloud.sql.connector.refresh_utils import _is_valid
37
35
  from google.cloud.sql.connector.refresh_utils import _seconds_until_refresh
38
- from google.cloud.sql.connector.utils import _auth_init
39
36
  from google.cloud.sql.connector.utils import write_to_file
40
- from google.cloud.sql.connector.version import __version__ as version
41
37
 
42
38
  if TYPE_CHECKING:
43
39
  import datetime
@@ -68,6 +64,20 @@ class IPTypes(Enum):
68
64
  PRIVATE: str = "PRIVATE"
69
65
  PSC: str = "PSC"
70
66
 
67
+ @classmethod
68
+ def _missing_(cls, value: object) -> None:
69
+ raise ValueError(
70
+ f"Incorrect value for ip_type, got '{value}'. Want one of: "
71
+ f"{', '.join([repr(m.value) for m in cls])}, 'PUBLIC'."
72
+ )
73
+
74
+ @classmethod
75
+ def _from_str(cls, ip_type_str: str) -> IPTypes:
76
+ """Convert IP type from a str into IPTypes."""
77
+ if ip_type_str.upper() == "PUBLIC":
78
+ ip_type_str = "PRIMARY"
79
+ return cls(ip_type_str.upper())
80
+
71
81
 
72
82
  class ConnectionInfo:
73
83
  ip_addrs: Dict[str, Any]
@@ -143,68 +153,15 @@ class Instance:
143
153
  string.
144
154
  :type instance_connection_string: str
145
155
 
146
- :param user_agent_string:
147
- The user agent string to append to SQLAdmin API requests
148
- :type user_agent_string: str
149
-
150
- :type credentials: google.auth.credentials.Credentials
151
- :param credentials
152
- Credentials object used to authenticate connections to Cloud SQL server.
153
- If not specified, Application Default Credentials are used.
154
-
155
156
  :param enable_iam_auth
156
157
  Enables automatic IAM database authentication for Postgres or MySQL
157
158
  instances.
158
159
  :type enable_iam_auth: bool
159
-
160
- :param loop:
161
- A new event loop for the refresh function to run in.
162
- :type loop: asyncio.AbstractEventLoop
163
-
164
- :type quota_project: str
165
- :param quota_project
166
- The Project ID for an existing Google Cloud project. The project specified
167
- is used for quota and billing purposes. If not specified, defaults to
168
- project sourced from environment.
169
-
170
- :type sqladmin_api_endpoint: str
171
- :param sqladmin_api_endpoint:
172
- Base URL to use when calling the Cloud SQL Admin API endpoint.
173
- Defaults to "https://sqladmin.googleapis.com", this argument should
174
- only be used in development.
175
160
  """
176
161
 
177
- # asyncio.AbstractEventLoop is used because the default loop,
178
- # SelectorEventLoop, is usable on both Unix and Windows but has limited
179
- # functionality on Windows. It is recommended to use ProactorEventLoop
180
- # while developing on Windows.
181
- # Link to Github issue:
182
- # https://github.com/GoogleCloudPlatform/cloud-sql-python-connector/issues/22
183
- _loop: asyncio.AbstractEventLoop
184
-
185
162
  _enable_iam_auth: bool
186
-
187
- __client_session: Optional[aiohttp.ClientSession] = None
188
-
189
- @property
190
- def _client_session(self) -> aiohttp.ClientSession:
191
- if self.__client_session is None:
192
- headers = {
193
- "x-goog-api-client": self._user_agent_string,
194
- "User-Agent": self._user_agent_string,
195
- "Content-Type": "application/json",
196
- }
197
- if self._quota_project:
198
- headers["x-goog-user-project"] = self._quota_project
199
- self.__client_session = aiohttp.ClientSession(headers=headers)
200
- return self.__client_session
201
-
202
- _credentials: Optional[Credentials] = None
203
163
  _keys: asyncio.Future
204
-
205
164
  _instance_connection_string: str
206
- _user_agent_string: str
207
- _sqladmin_api_endpoint: str
208
165
  _instance: str
209
166
  _project: str
210
167
  _region: str
@@ -217,13 +174,9 @@ class Instance:
217
174
  def __init__(
218
175
  self,
219
176
  instance_connection_string: str,
220
- driver_name: str,
177
+ client: CloudSQLClient,
221
178
  keys: asyncio.Future,
222
- loop: asyncio.AbstractEventLoop,
223
- credentials: Optional[Credentials] = None,
224
179
  enable_iam_auth: bool = False,
225
- quota_project: str = None,
226
- sqladmin_api_endpoint: str = "https://sqladmin.googleapis.com",
227
180
  ) -> None:
228
181
  # validate and parse instance connection name
229
182
  self._project, self._region, self._instance = _parse_instance_connection_name(
@@ -232,21 +185,11 @@ class Instance:
232
185
  self._instance_connection_string = instance_connection_string
233
186
 
234
187
  self._enable_iam_auth = enable_iam_auth
235
-
236
- self._user_agent_string = f"{APPLICATION_NAME}/{version}+{driver_name}"
237
- self._quota_project = quota_project
238
- self._sqladmin_api_endpoint = sqladmin_api_endpoint
239
- self._loop = loop
240
188
  self._keys = keys
241
- # validate credentials type
242
- if not isinstance(credentials, Credentials) and credentials is not None:
243
- raise CredentialsTypeError(
244
- "Arg credentials must be type 'google.auth.credentials.Credentials' "
245
- "or None (to use Application Default Credentials)"
246
- )
247
- self._credentials = _auth_init(credentials)
189
+ self._client = client
248
190
  self._refresh_rate_limiter = AsyncRateLimiter(
249
- max_capacity=2, rate=1 / 30, loop=self._loop
191
+ max_capacity=2,
192
+ rate=1 / 30,
250
193
  )
251
194
  self._refresh_in_progress = asyncio.locks.Event()
252
195
  self._current = self._schedule_refresh(0)
@@ -284,22 +227,16 @@ class Instance:
284
227
 
285
228
  logger.debug(f"['{self._instance_connection_string}']: Creating context")
286
229
 
287
- metadata_task = self._loop.create_task(
288
- _get_metadata(
289
- self._client_session,
290
- self._sqladmin_api_endpoint,
291
- self._credentials,
230
+ metadata_task = asyncio.create_task(
231
+ self._client._get_metadata(
292
232
  self._project,
293
233
  self._region,
294
234
  self._instance,
295
235
  )
296
236
  )
297
237
 
298
- ephemeral_task = self._loop.create_task(
299
- _get_ephemeral(
300
- self._client_session,
301
- self._sqladmin_api_endpoint,
302
- self._credentials,
238
+ ephemeral_task = asyncio.create_task(
239
+ self._client._get_ephemeral(
303
240
  self._project,
304
241
  self._instance,
305
242
  pub_key,
@@ -371,7 +308,7 @@ class Instance:
371
308
  logger.debug(f"['{self._instance_connection_string}']: Entering sleep")
372
309
  if delay > 0:
373
310
  await asyncio.sleep(delay)
374
- refresh_task = self._loop.create_task(self._perform_refresh())
311
+ refresh_task = asyncio.create_task(self._perform_refresh())
375
312
  refresh_data = await refresh_task
376
313
  except asyncio.CancelledError:
377
314
  logger.debug(
@@ -402,7 +339,7 @@ class Instance:
402
339
  return refresh_data
403
340
 
404
341
  # schedule refresh task and return it
405
- scheduled_task = self._loop.create_task(_refresh_task(self, delay))
342
+ scheduled_task = asyncio.create_task(_refresh_task(self, delay))
406
343
  return scheduled_task
407
344
 
408
345
  async def connect_info(
@@ -448,4 +385,6 @@ class Instance:
448
385
  logger.debug(
449
386
  f"['{self._instance_connection_string}']: Waiting for _client_session to close"
450
387
  )
451
- await self._client_session.close()
388
+ # gracefully wait for tasks to cancel
389
+ tasks = asyncio.gather(self._current, self._next, return_exceptions=True)
390
+ await asyncio.wait_for(tasks, timeout=2.0)
@@ -13,7 +13,9 @@ 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
  import asyncio
18
+ from typing import Optional
17
19
 
18
20
 
19
21
  class AsyncRateLimiter(object):
@@ -41,7 +43,7 @@ class AsyncRateLimiter(object):
41
43
  self,
42
44
  max_capacity: int = 1,
43
45
  rate: float = 1 / 60,
44
- loop: asyncio.AbstractEventLoop = None,
46
+ loop: Optional[asyncio.AbstractEventLoop] = None,
45
47
  ) -> None:
46
48
  self.rate = rate
47
49
  self.max_capacity = max_capacity
@@ -13,214 +13,26 @@ 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 __future__ import annotations
17
18
 
18
19
  import asyncio
19
20
  import copy
20
21
  import datetime
21
22
  import logging
22
- from typing import Any, Dict, List, Tuple, TYPE_CHECKING
23
+ from typing import List
23
24
 
24
- from cryptography.hazmat.backends import default_backend
25
- from cryptography.x509 import load_pem_x509_certificate
26
25
  from google.auth.credentials import Credentials
27
26
  from google.auth.credentials import Scoped
28
27
  import google.auth.transport.requests
29
28
 
30
- if TYPE_CHECKING:
31
- import aiohttp
32
-
33
29
  logger = logging.getLogger(name=__name__)
34
30
 
35
- _sql_api_version: str = "v1beta4"
36
-
37
31
  # _refresh_buffer is the amount of time before a refresh's result expires
38
32
  # that a new refresh operation begins.
39
33
  _refresh_buffer: int = 4 * 60 # 4 minutes
40
34
 
41
35
 
42
- async def _get_metadata(
43
- client_session: aiohttp.ClientSession,
44
- sqladmin_api_endpoint: str,
45
- credentials: Credentials,
46
- project: str,
47
- region: str,
48
- instance: str,
49
- ) -> Dict[str, Any]:
50
- """Requests metadata from the Cloud SQL Instance
51
- and returns a dictionary containing the IP addresses and certificate
52
- authority of the Cloud SQL Instance.
53
-
54
- :type sqladmin_api_endpoint: str
55
- :param sqladmin_api_endpoint:
56
- Base URL to use when calling the Cloud SQL Admin API endpoint.
57
-
58
- :type credentials: google.auth.credentials.Credentials
59
- :param credentials:
60
- A credentials object created from the google-auth Python library.
61
- Must have the SQL Admin API scopes. For more info check out
62
- https://google-auth.readthedocs.io/en/latest/.
63
-
64
- :type project: str
65
- :param project:
66
- A string representing the name of the project.
67
-
68
- :type region: str
69
- :param region : A string representing the name of the region.
70
-
71
- :type instance: str
72
- :param instance: A string representing the name of the instance.
73
-
74
- :rtype: Dict[str: Union[Dict, str]]
75
- :returns: Returns a dictionary containing a dictionary of all IP
76
- addresses and their type and a string representing the
77
- certificate authority.
78
-
79
- :raises TypeError: If any of the arguments are not the specified type.
80
- """
81
-
82
- if not isinstance(credentials, Credentials):
83
- raise TypeError(
84
- "credentials must be of type google.auth.credentials.Credentials,"
85
- f" got {type(credentials)}"
86
- )
87
- elif not isinstance(project, str):
88
- raise TypeError(f"project must be of type str, got {type(project)}")
89
- elif not isinstance(region, str):
90
- raise TypeError(f"region must be of type str, got {type(region)}")
91
- elif not isinstance(instance, str):
92
- raise TypeError(f"instance must be of type str, got {type(instance)}")
93
-
94
- if not credentials.valid:
95
- request = google.auth.transport.requests.Request()
96
- credentials.refresh(request)
97
-
98
- headers = {
99
- "Authorization": f"Bearer {credentials.token}",
100
- }
101
-
102
- url = f"{sqladmin_api_endpoint}/sql/{_sql_api_version}/projects/{project}/instances/{instance}/connectSettings"
103
-
104
- logger.debug(f"['{instance}']: Requesting metadata")
105
-
106
- resp = await client_session.get(url, headers=headers, raise_for_status=True)
107
- ret_dict = await resp.json()
108
-
109
- if ret_dict["region"] != region:
110
- raise ValueError(
111
- f'[{project}:{region}:{instance}]: Provided region was mismatched - got region {region}, expected {ret_dict["region"]}.'
112
- )
113
-
114
- ip_addresses = (
115
- {ip["type"]: ip["ipAddress"] for ip in ret_dict["ipAddresses"]}
116
- if "ipAddresses" in ret_dict
117
- else {}
118
- )
119
- if "dnsName" in ret_dict:
120
- ip_addresses["PSC"] = ret_dict["dnsName"]
121
-
122
- metadata = {
123
- "ip_addresses": ip_addresses,
124
- "server_ca_cert": ret_dict["serverCaCert"]["cert"],
125
- "database_version": ret_dict["databaseVersion"],
126
- }
127
-
128
- return metadata
129
-
130
-
131
- async def _get_ephemeral(
132
- client_session: aiohttp.ClientSession,
133
- sqladmin_api_endpoint: str,
134
- credentials: Credentials,
135
- project: str,
136
- instance: str,
137
- pub_key: str,
138
- enable_iam_auth: bool = False,
139
- ) -> Tuple[str, datetime.datetime]:
140
- """Asynchronously requests an ephemeral certificate from the Cloud SQL Instance.
141
-
142
- :type sqladmin_api_endpoint: str
143
- :param sqladmin_api_endpoint:
144
- Base URL to use when calling the Cloud SQL Admin API endpoint.
145
-
146
- :type credentials: google.auth.credentials.Credentials
147
- :param credentials: A credentials object
148
- created from the google-auth library. Must be
149
- using the SQL Admin API scopes. For more info, check out
150
- https://google-auth.readthedocs.io/en/latest/.
151
-
152
- :type project: str
153
- :param project : A string representing the name of the project.
154
-
155
- :type instance: str
156
- :param instance: A string representing the name of the instance.
157
-
158
- :type pub_key:
159
- :param str: A string representing PEM-encoded RSA public key.
160
-
161
- :type enable_iam_auth: bool
162
- :param enable_iam_auth
163
- Enables automatic IAM database authentication for Postgres or MySQL
164
- instances.
165
-
166
- :rtype: str
167
- :returns: An ephemeral certificate from the Cloud SQL instance that allows
168
- authorized connections to the instance.
169
-
170
- :raises TypeError: If one of the arguments passed in is None.
171
- """
172
-
173
- logger.debug(f"['{instance}']: Requesting ephemeral certificate")
174
-
175
- if not isinstance(credentials, Credentials):
176
- raise TypeError(
177
- "credentials must be of type google.auth.credentials.Credentials,"
178
- f" got {type(credentials)}"
179
- )
180
- elif not isinstance(project, str):
181
- raise TypeError(f"project must be of type str, got {type(project)}")
182
- elif not isinstance(instance, str):
183
- raise TypeError(f"instance must be of type str, got {type(instance)}")
184
- elif not isinstance(pub_key, str):
185
- raise TypeError(f"pub_key must be of type str, got {type(pub_key)}")
186
-
187
- if not credentials.valid:
188
- request = google.auth.transport.requests.Request()
189
- credentials.refresh(request)
190
-
191
- headers = {
192
- "Authorization": f"Bearer {credentials.token}",
193
- }
194
-
195
- url = f"{sqladmin_api_endpoint}/sql/{_sql_api_version}/projects/{project}/instances/{instance}:generateEphemeralCert"
196
-
197
- data = {"public_key": pub_key}
198
-
199
- if enable_iam_auth:
200
- # down-scope credentials with only IAM login scope (refreshes them too)
201
- login_creds = _downscope_credentials(credentials)
202
- data["access_token"] = login_creds.token
203
-
204
- resp = await client_session.post(
205
- url, headers=headers, json=data, raise_for_status=True
206
- )
207
-
208
- ret_dict = await resp.json()
209
-
210
- ephemeral_cert: str = ret_dict["ephemeralCert"]["cert"]
211
-
212
- # decode cert to read expiration
213
- x509 = load_pem_x509_certificate(ephemeral_cert.encode("UTF-8"), default_backend())
214
- expiration = x509.not_valid_after
215
- # for IAM authentication OAuth2 token is embedded in cert so it
216
- # must still be valid for successful connection
217
- if enable_iam_auth:
218
- token_expiration: datetime.datetime = login_creds.expiry
219
- if expiration > token_expiration:
220
- expiration = token_expiration
221
- return ephemeral_cert, expiration
222
-
223
-
224
36
  def _seconds_until_refresh(
225
37
  expiration: datetime.datetime,
226
38
  ) -> int:
@@ -234,7 +46,9 @@ def _seconds_until_refresh(
234
46
  :returns: Time in seconds to wait before performing next refresh.
235
47
  """
236
48
 
237
- duration = int((expiration - datetime.datetime.utcnow()).total_seconds())
49
+ duration = int(
50
+ (expiration - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
51
+ )
238
52
 
239
53
  # if certificate duration is less than 1 hour
240
54
  if duration < 3600:
@@ -251,7 +65,7 @@ async def _is_valid(task: asyncio.Task) -> bool:
251
65
  try:
252
66
  metadata = await task
253
67
  # only valid if now is before the cert expires
254
- if datetime.datetime.utcnow() < metadata.expiration:
68
+ if datetime.datetime.now(datetime.timezone.utc) < metadata.expiration:
255
69
  return True
256
70
  except Exception:
257
71
  # supress any errors from task
@@ -13,14 +13,11 @@ 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
- from typing import Optional, Tuple
16
+ from typing import Tuple
17
17
 
18
18
  from cryptography.hazmat.backends import default_backend
19
19
  from cryptography.hazmat.primitives import serialization
20
20
  from cryptography.hazmat.primitives.asymmetric import rsa
21
- from google.auth import default
22
- from google.auth.credentials import Credentials
23
- from google.auth.credentials import with_scopes_if_required
24
21
 
25
22
 
26
23
  async def generate_keys() -> Tuple[bytes, str]:
@@ -104,23 +101,3 @@ def format_database_user(database_version: str, user: str) -> str:
104
101
  return user.split("@")[0]
105
102
 
106
103
  return user
107
-
108
-
109
- def _auth_init(credentials: Optional[Credentials]) -> Credentials:
110
- """Creates google.auth credentials object with scopes required to make
111
- calls to the Cloud SQL Admin APIs.
112
-
113
- :type credentials: google.auth.credentials.Credentials
114
- :param credentials
115
- Credentials object used to authenticate connections to Cloud SQL server.
116
- If not specified, Application Default Credentials are used.
117
- """
118
- scopes = ["https://www.googleapis.com/auth/sqlservice.admin"]
119
- # if Credentials object is passed in, use for authentication
120
- if isinstance(credentials, Credentials):
121
- credentials = with_scopes_if_required(credentials, scopes=scopes)
122
- # otherwise use application default credentials
123
- else:
124
- credentials, _ = default(scopes=scopes)
125
-
126
- return credentials
@@ -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.6.0"
15
+ __version__ = "1.8.0"
@@ -1,18 +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/connector.py,sha256=KwrB9Jx_qNwbLoEO7Bf7eFp76cm3nL4ZCYNduH1AUbk,14315
4
- google/cloud/sql/connector/exceptions.py,sha256=99sY_6UVuSeSg1jFusXOpPWEsbzDwk-LLo4WZkcsL2A,1649
5
- google/cloud/sql/connector/instance.py,sha256=zFwdDSWjiKsY8szgY0fn9zLtiRWoAiWYpUW-CK4d-1Q,17728
6
- google/cloud/sql/connector/pg8000.py,sha256=EZuO7XgCkG_Y79NQlh3Vv8Ff2w85BFJyFVO237hx1GM,1814
7
- google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- google/cloud/sql/connector/pymysql.py,sha256=wldUgwDPlPa8Nj5F3JaipuP8hJyvzlgOAJbRnOoaB1U,2105
9
- google/cloud/sql/connector/pytds.py,sha256=vKJoE7E2P0GrhZojg8bGZkqCMLc8dgKAcXE4ee7vEVc,2452
10
- google/cloud/sql/connector/rate_limiter.py,sha256=Ci3f-94090bj0cWJUu-vGZFc32IWRCyi8TIgChsQvxY,2954
11
- google/cloud/sql/connector/refresh_utils.py,sha256=04lDyHu8kG98y4J6OLCt_fePYnSXlCZNyN8q98wE68Q,10223
12
- google/cloud/sql/connector/utils.py,sha256=QOn2lKRB6lGd9CEbI8XCOYzXpxs7Oz4pknunSYdeHzk,4405
13
- google/cloud/sql/connector/version.py,sha256=aZ3zQ5-k8PViHHPlQGcYGqk9pS4YC_tfVYHDVYM9YuU,597
14
- cloud_sql_python_connector-1.6.0.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
15
- cloud_sql_python_connector-1.6.0.dist-info/METADATA,sha256=4bQn_w88lf1oCknl3iH0Fuli4p-VgTzwGO0-doez9SM,24146
16
- cloud_sql_python_connector-1.6.0.dist-info/WHEEL,sha256=bb2Ot9scclHKMOLDEHY6B2sicWOgugjFKaJsT7vwMQo,110
17
- cloud_sql_python_connector-1.6.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
18
- cloud_sql_python_connector-1.6.0.dist-info/RECORD,,