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.
- {cloud_sql_python_connector-1.6.0.dist-info → cloud_sql_python_connector-1.8.0.dist-info}/METADATA +16 -17
- cloud_sql_python_connector-1.8.0.dist-info/RECORD +19 -0
- google/cloud/sql/connector/client.py +221 -0
- google/cloud/sql/connector/connector.py +110 -52
- google/cloud/sql/connector/exceptions.py +0 -8
- google/cloud/sql/connector/instance.py +30 -91
- google/cloud/sql/connector/rate_limiter.py +3 -1
- google/cloud/sql/connector/refresh_utils.py +6 -192
- google/cloud/sql/connector/utils.py +1 -24
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.6.0.dist-info/RECORD +0 -18
- {cloud_sql_python_connector-1.6.0.dist-info → cloud_sql_python_connector-1.8.0.dist-info}/LICENSE +0 -0
- {cloud_sql_python_connector-1.6.0.dist-info → cloud_sql_python_connector-1.8.0.dist-info}/WHEEL +0 -0
- {cloud_sql_python_connector-1.6.0.dist-info → cloud_sql_python_connector-1.8.0.dist-info}/top_level.txt +0 -0
{cloud_sql_python_connector-1.6.0.dist-info → cloud_sql_python_connector-1.8.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cloud-sql-python-connector
|
|
3
|
-
Version: 1.
|
|
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 (>=
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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=
|
|
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 `
|
|
298
|
-
`
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
51
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
@@ -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,,
|
{cloud_sql_python_connector-1.6.0.dist-info → cloud_sql_python_connector-1.8.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
{cloud_sql_python_connector-1.6.0.dist-info → cloud_sql_python_connector-1.8.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|