cloud-sql-python-connector 1.17.0__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.17.0.dist-info/LICENSE +202 -0
- cloud_sql_python_connector-1.17.0.dist-info/METADATA +838 -0
- cloud_sql_python_connector-1.17.0.dist-info/RECORD +24 -0
- cloud_sql_python_connector-1.17.0.dist-info/WHEEL +5 -0
- cloud_sql_python_connector-1.17.0.dist-info/top_level.txt +1 -0
- google/cloud/sql/connector/__init__.py +33 -0
- google/cloud/sql/connector/asyncpg.py +65 -0
- google/cloud/sql/connector/client.py +324 -0
- google/cloud/sql/connector/connection_info.py +108 -0
- google/cloud/sql/connector/connection_name.py +61 -0
- google/cloud/sql/connector/connector.py +528 -0
- google/cloud/sql/connector/enums.py +86 -0
- google/cloud/sql/connector/exceptions.py +79 -0
- google/cloud/sql/connector/instance.py +214 -0
- google/cloud/sql/connector/lazy.py +124 -0
- google/cloud/sql/connector/pg8000.py +66 -0
- google/cloud/sql/connector/py.typed +0 -0
- google/cloud/sql/connector/pymysql.py +64 -0
- google/cloud/sql/connector/pytds.py +78 -0
- google/cloud/sql/connector/rate_limiter.py +87 -0
- google/cloud/sql/connector/refresh_utils.py +154 -0
- google/cloud/sql/connector/resolver.py +70 -0
- google/cloud/sql/connector/utils.py +103 -0
- google/cloud/sql/connector/version.py +15 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2019 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
from functools import partial
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
from threading import Thread
|
|
24
|
+
from types import TracebackType
|
|
25
|
+
from typing import Any, Optional, Union
|
|
26
|
+
|
|
27
|
+
import google.auth
|
|
28
|
+
from google.auth.credentials import Credentials
|
|
29
|
+
from google.auth.credentials import with_scopes_if_required
|
|
30
|
+
|
|
31
|
+
import google.cloud.sql.connector.asyncpg as asyncpg
|
|
32
|
+
from google.cloud.sql.connector.client import CloudSQLClient
|
|
33
|
+
from google.cloud.sql.connector.enums import DriverMapping
|
|
34
|
+
from google.cloud.sql.connector.enums import IPTypes
|
|
35
|
+
from google.cloud.sql.connector.enums import RefreshStrategy
|
|
36
|
+
from google.cloud.sql.connector.instance import RefreshAheadCache
|
|
37
|
+
from google.cloud.sql.connector.lazy import LazyRefreshCache
|
|
38
|
+
import google.cloud.sql.connector.pg8000 as pg8000
|
|
39
|
+
import google.cloud.sql.connector.pymysql as pymysql
|
|
40
|
+
import google.cloud.sql.connector.pytds as pytds
|
|
41
|
+
from google.cloud.sql.connector.resolver import DefaultResolver
|
|
42
|
+
from google.cloud.sql.connector.resolver import DnsResolver
|
|
43
|
+
from google.cloud.sql.connector.utils import format_database_user
|
|
44
|
+
from google.cloud.sql.connector.utils import generate_keys
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(name=__name__)
|
|
47
|
+
|
|
48
|
+
ASYNC_DRIVERS = ["asyncpg"]
|
|
49
|
+
_DEFAULT_SCHEME = "https://"
|
|
50
|
+
_DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
|
|
51
|
+
_SQLADMIN_HOST_TEMPLATE = "sqladmin.{universe_domain}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Connector:
|
|
55
|
+
"""Configure and create secure connections to Cloud SQL."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
ip_type: str | IPTypes = IPTypes.PUBLIC,
|
|
60
|
+
enable_iam_auth: bool = False,
|
|
61
|
+
timeout: int = 30,
|
|
62
|
+
credentials: Optional[Credentials] = None,
|
|
63
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
64
|
+
quota_project: Optional[str] = None,
|
|
65
|
+
sqladmin_api_endpoint: Optional[str] = None,
|
|
66
|
+
user_agent: Optional[str] = None,
|
|
67
|
+
universe_domain: Optional[str] = None,
|
|
68
|
+
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
69
|
+
resolver: type[DefaultResolver] | type[DnsResolver] = DefaultResolver,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initializes a Connector instance.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
ip_type (str | IPTypes): The default IP address type used to connect to
|
|
75
|
+
Cloud SQL instances. Can be one of the following:
|
|
76
|
+
IPTypes.PUBLIC ("PUBLIC"), IPTypes.PRIVATE ("PRIVATE"), or
|
|
77
|
+
IPTypes.PSC ("PSC"). Default: IPTypes.PUBLIC
|
|
78
|
+
|
|
79
|
+
enable_iam_auth (bool): Enables automatic IAM database authentication
|
|
80
|
+
(Postgres and MySQL) as the default authentication method for all
|
|
81
|
+
connections.
|
|
82
|
+
|
|
83
|
+
timeout (int): The default time limit in seconds for a connection before
|
|
84
|
+
raising a TimeoutError.
|
|
85
|
+
|
|
86
|
+
credentials (google.auth.credentials.Credentials): A credentials object
|
|
87
|
+
created from the google-auth Python library to be used.
|
|
88
|
+
If not specified, Application Default Credentials (ADC) are used.
|
|
89
|
+
|
|
90
|
+
quota_project (str): The Project ID for an existing Google Cloud
|
|
91
|
+
project. The project specified is used for quota and billing
|
|
92
|
+
purposes. If not specified, defaults to project sourced from
|
|
93
|
+
environment.
|
|
94
|
+
|
|
95
|
+
loop (asyncio.AbstractEventLoop): Event loop to run asyncio tasks, if
|
|
96
|
+
not specified, defaults to creating new event loop on background
|
|
97
|
+
thread.
|
|
98
|
+
|
|
99
|
+
sqladmin_api_endpoint (str): Base URL to use when calling the Cloud SQL
|
|
100
|
+
Admin API endpoint. Defaults to "https://sqladmin.googleapis.com",
|
|
101
|
+
this argument should only be used in development.
|
|
102
|
+
|
|
103
|
+
universe_domain (str): The universe domain for Cloud SQL API calls.
|
|
104
|
+
Default: "googleapis.com".
|
|
105
|
+
|
|
106
|
+
refresh_strategy (str | RefreshStrategy): The default refresh strategy
|
|
107
|
+
used to refresh SSL/TLS cert and instance metadata. Can be one
|
|
108
|
+
of the following: RefreshStrategy.LAZY ("LAZY") or
|
|
109
|
+
RefreshStrategy.BACKGROUND ("BACKGROUND").
|
|
110
|
+
Default: RefreshStrategy.BACKGROUND
|
|
111
|
+
|
|
112
|
+
resolver (DefaultResolver | DnsResolver): The class name of the
|
|
113
|
+
resolver to use for resolving the Cloud SQL instance connection
|
|
114
|
+
name. To resolve a DNS record to an instance connection name, use
|
|
115
|
+
DnsResolver.
|
|
116
|
+
Default: DefaultResolver
|
|
117
|
+
"""
|
|
118
|
+
# if refresh_strategy is str, convert to RefreshStrategy enum
|
|
119
|
+
if isinstance(refresh_strategy, str):
|
|
120
|
+
refresh_strategy = RefreshStrategy._from_str(refresh_strategy)
|
|
121
|
+
self._refresh_strategy = refresh_strategy
|
|
122
|
+
# if event loop is given, use for background tasks
|
|
123
|
+
if loop:
|
|
124
|
+
self._loop: asyncio.AbstractEventLoop = loop
|
|
125
|
+
self._thread: Optional[Thread] = None
|
|
126
|
+
# if lazy refresh is specified we should lazy init keys
|
|
127
|
+
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
128
|
+
self._keys: Optional[asyncio.Future] = None
|
|
129
|
+
else:
|
|
130
|
+
self._keys = loop.create_task(generate_keys())
|
|
131
|
+
# if no event loop is given, spin up new loop in background thread
|
|
132
|
+
else:
|
|
133
|
+
self._loop = asyncio.new_event_loop()
|
|
134
|
+
self._thread = Thread(target=self._loop.run_forever, daemon=True)
|
|
135
|
+
self._thread.start()
|
|
136
|
+
# if lazy refresh is specified we should lazy init keys
|
|
137
|
+
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
138
|
+
self._keys = None
|
|
139
|
+
else:
|
|
140
|
+
self._keys = asyncio.wrap_future(
|
|
141
|
+
asyncio.run_coroutine_threadsafe(generate_keys(), self._loop),
|
|
142
|
+
loop=self._loop,
|
|
143
|
+
)
|
|
144
|
+
# initialize dict to store caches, key is a tuple consisting of instance
|
|
145
|
+
# connection name string and enable_iam_auth boolean flag
|
|
146
|
+
self._cache: dict[
|
|
147
|
+
tuple[str, bool], Union[RefreshAheadCache, LazyRefreshCache]
|
|
148
|
+
] = {}
|
|
149
|
+
self._client: Optional[CloudSQLClient] = None
|
|
150
|
+
|
|
151
|
+
# initialize credentials
|
|
152
|
+
scopes = ["https://www.googleapis.com/auth/sqlservice.admin"]
|
|
153
|
+
if credentials:
|
|
154
|
+
# verify custom credentials are proper type
|
|
155
|
+
# and atleast base class of google.auth.credentials
|
|
156
|
+
if not isinstance(credentials, Credentials):
|
|
157
|
+
raise TypeError(
|
|
158
|
+
"credentials must be of type google.auth.credentials.Credentials,"
|
|
159
|
+
f" got {type(credentials)}"
|
|
160
|
+
)
|
|
161
|
+
self._credentials = with_scopes_if_required(credentials, scopes=scopes)
|
|
162
|
+
# otherwise use application default credentials
|
|
163
|
+
else:
|
|
164
|
+
self._credentials, _ = google.auth.default(scopes=scopes)
|
|
165
|
+
# set default params for connections
|
|
166
|
+
self._timeout = timeout
|
|
167
|
+
self._enable_iam_auth = enable_iam_auth
|
|
168
|
+
self._user_agent = user_agent
|
|
169
|
+
self._resolver = resolver()
|
|
170
|
+
# if ip_type is str, convert to IPTypes enum
|
|
171
|
+
if isinstance(ip_type, str):
|
|
172
|
+
ip_type = IPTypes._from_str(ip_type)
|
|
173
|
+
self._ip_type = ip_type
|
|
174
|
+
# check for quota project arg and then env var
|
|
175
|
+
if quota_project:
|
|
176
|
+
self._quota_project = quota_project
|
|
177
|
+
else:
|
|
178
|
+
self._quota_project = os.environ.get("GOOGLE_CLOUD_QUOTA_PROJECT") # type: ignore
|
|
179
|
+
# check for universe domain arg and then env var
|
|
180
|
+
if universe_domain:
|
|
181
|
+
self._universe_domain = universe_domain
|
|
182
|
+
else:
|
|
183
|
+
self._universe_domain = os.environ.get("GOOGLE_CLOUD_UNIVERSE_DOMAIN") # type: ignore
|
|
184
|
+
# construct service endpoint for Cloud SQL Admin API calls
|
|
185
|
+
if not sqladmin_api_endpoint:
|
|
186
|
+
self._sqladmin_api_endpoint = (
|
|
187
|
+
_DEFAULT_SCHEME
|
|
188
|
+
+ _SQLADMIN_HOST_TEMPLATE.format(universe_domain=self.universe_domain)
|
|
189
|
+
)
|
|
190
|
+
# otherwise if endpoint override is passed in use it
|
|
191
|
+
else:
|
|
192
|
+
self._sqladmin_api_endpoint = sqladmin_api_endpoint
|
|
193
|
+
|
|
194
|
+
# validate that the universe domain of the credentials matches the
|
|
195
|
+
# universe domain of the service endpoint
|
|
196
|
+
if self._credentials.universe_domain != self.universe_domain:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"The configured universe domain ({self.universe_domain}) does "
|
|
199
|
+
"not match the universe domain found in the credentials "
|
|
200
|
+
f"({self._credentials.universe_domain}). If you haven't "
|
|
201
|
+
"configured the universe domain explicitly, `googleapis.com` "
|
|
202
|
+
"is the default."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def universe_domain(self) -> str:
|
|
207
|
+
return self._universe_domain or _DEFAULT_UNIVERSE_DOMAIN
|
|
208
|
+
|
|
209
|
+
def connect(
|
|
210
|
+
self, instance_connection_string: str, driver: str, **kwargs: Any
|
|
211
|
+
) -> Any:
|
|
212
|
+
"""Connect to a Cloud SQL instance.
|
|
213
|
+
|
|
214
|
+
Prepares and returns a database connection object connected to a Cloud
|
|
215
|
+
SQL instance using SSL/TLS. Starts a background refresh to periodically
|
|
216
|
+
retrieve up-to-date ephemeral certificate and instance metadata.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
instance_connection_string (str): The instance connection name of the
|
|
220
|
+
Cloud SQL instance to connect to. Takes the form of
|
|
221
|
+
"project-id:region:instance-name"
|
|
222
|
+
|
|
223
|
+
Example: "my-project:us-central1:my-instance"
|
|
224
|
+
|
|
225
|
+
driver (str): A string representing the database driver to connect
|
|
226
|
+
with. Supported drivers are pymysql, pg8000, and pytds.
|
|
227
|
+
|
|
228
|
+
**kwargs: Any driver-specific arguments to pass to the underlying
|
|
229
|
+
driver .connect call.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
A DB-API connection to the specified Cloud SQL instance.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
# connect runs sync database connections on background thread.
|
|
236
|
+
# Async database connections should call 'connect_async' directly to
|
|
237
|
+
# avoid hanging indefinitely.
|
|
238
|
+
connect_future = asyncio.run_coroutine_threadsafe(
|
|
239
|
+
self.connect_async(instance_connection_string, driver, **kwargs),
|
|
240
|
+
self._loop,
|
|
241
|
+
)
|
|
242
|
+
return connect_future.result()
|
|
243
|
+
|
|
244
|
+
async def connect_async(
|
|
245
|
+
self, instance_connection_string: str, driver: str, **kwargs: Any
|
|
246
|
+
) -> Any:
|
|
247
|
+
"""Connect asynchronously to a Cloud SQL instance.
|
|
248
|
+
|
|
249
|
+
Prepares and returns a database connection object connected to a Cloud
|
|
250
|
+
SQL instance using SSL/TLS. Schedules a refresh to periodically
|
|
251
|
+
retrieve up-to-date ephemeral certificate and instance metadata. Async
|
|
252
|
+
version of Connector.connect.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
instance_connection_string (str): The instance connection name of the
|
|
256
|
+
Cloud SQL instance to connect to. Takes the form of
|
|
257
|
+
"project-id:region:instance-name"
|
|
258
|
+
|
|
259
|
+
Example: "my-project:us-central1:my-instance"
|
|
260
|
+
|
|
261
|
+
driver (str): A string representing the database driver to connect
|
|
262
|
+
with. Supported drivers are pymysql, asyncpg, pg8000, and pytds.
|
|
263
|
+
|
|
264
|
+
**kwargs: Any driver-specific arguments to pass to the underlying
|
|
265
|
+
driver .connect call.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
A DB-API connection to the specified Cloud SQL instance.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
ValueError: Connection attempt with built-in database authentication
|
|
272
|
+
and then subsequent attempt with IAM database authentication.
|
|
273
|
+
KeyError: Unsupported database driver Must be one of pymysql, asyncpg,
|
|
274
|
+
pg8000, and pytds.
|
|
275
|
+
"""
|
|
276
|
+
if self._keys is None:
|
|
277
|
+
self._keys = asyncio.create_task(generate_keys())
|
|
278
|
+
if self._client is None:
|
|
279
|
+
# lazy init client as it has to be initialized in async context
|
|
280
|
+
self._client = CloudSQLClient(
|
|
281
|
+
self._sqladmin_api_endpoint,
|
|
282
|
+
self._quota_project,
|
|
283
|
+
self._credentials,
|
|
284
|
+
user_agent=self._user_agent,
|
|
285
|
+
driver=driver,
|
|
286
|
+
)
|
|
287
|
+
enable_iam_auth = kwargs.pop("enable_iam_auth", self._enable_iam_auth)
|
|
288
|
+
if (instance_connection_string, enable_iam_auth) in self._cache:
|
|
289
|
+
cache = self._cache[(instance_connection_string, enable_iam_auth)]
|
|
290
|
+
else:
|
|
291
|
+
conn_name = await self._resolver.resolve(instance_connection_string)
|
|
292
|
+
if self._refresh_strategy == RefreshStrategy.LAZY:
|
|
293
|
+
logger.debug(
|
|
294
|
+
f"['{conn_name}']: Refresh strategy is set to lazy refresh"
|
|
295
|
+
)
|
|
296
|
+
cache = LazyRefreshCache(
|
|
297
|
+
conn_name,
|
|
298
|
+
self._client,
|
|
299
|
+
self._keys,
|
|
300
|
+
enable_iam_auth,
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
logger.debug(
|
|
304
|
+
f"['{conn_name}']: Refresh strategy is set to backgound refresh"
|
|
305
|
+
)
|
|
306
|
+
cache = RefreshAheadCache(
|
|
307
|
+
conn_name,
|
|
308
|
+
self._client,
|
|
309
|
+
self._keys,
|
|
310
|
+
enable_iam_auth,
|
|
311
|
+
)
|
|
312
|
+
logger.debug(f"['{conn_name}']: Connection info added to cache")
|
|
313
|
+
self._cache[(instance_connection_string, enable_iam_auth)] = cache
|
|
314
|
+
|
|
315
|
+
connect_func = {
|
|
316
|
+
"pymysql": pymysql.connect,
|
|
317
|
+
"pg8000": pg8000.connect,
|
|
318
|
+
"asyncpg": asyncpg.connect,
|
|
319
|
+
"pytds": pytds.connect,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# only accept supported database drivers
|
|
323
|
+
try:
|
|
324
|
+
connector = connect_func[driver]
|
|
325
|
+
except KeyError:
|
|
326
|
+
raise KeyError(f"Driver '{driver}' is not supported.")
|
|
327
|
+
|
|
328
|
+
ip_type = kwargs.pop("ip_type", self._ip_type)
|
|
329
|
+
# if ip_type is str, convert to IPTypes enum
|
|
330
|
+
if isinstance(ip_type, str):
|
|
331
|
+
ip_type = IPTypes._from_str(ip_type)
|
|
332
|
+
kwargs["timeout"] = kwargs.get("timeout", self._timeout)
|
|
333
|
+
|
|
334
|
+
# Host and ssl options come from the certificates and metadata, so we don't
|
|
335
|
+
# want the user to specify them.
|
|
336
|
+
kwargs.pop("host", None)
|
|
337
|
+
kwargs.pop("ssl", None)
|
|
338
|
+
kwargs.pop("port", None)
|
|
339
|
+
|
|
340
|
+
# attempt to get connection info for Cloud SQL instance
|
|
341
|
+
try:
|
|
342
|
+
conn_info = await cache.connect_info()
|
|
343
|
+
# validate driver matches intended database engine
|
|
344
|
+
DriverMapping.validate_engine(driver, conn_info.database_version)
|
|
345
|
+
ip_address = conn_info.get_preferred_ip(ip_type)
|
|
346
|
+
except Exception:
|
|
347
|
+
# with an error from Cloud SQL Admin API call or IP type, invalidate
|
|
348
|
+
# the cache and re-raise the error
|
|
349
|
+
await self._remove_cached(instance_connection_string, enable_iam_auth)
|
|
350
|
+
raise
|
|
351
|
+
logger.debug(f"['{conn_info.conn_name}']: Connecting to {ip_address}:3307")
|
|
352
|
+
# format `user` param for automatic IAM database authn
|
|
353
|
+
if enable_iam_auth:
|
|
354
|
+
formatted_user = format_database_user(
|
|
355
|
+
conn_info.database_version, kwargs["user"]
|
|
356
|
+
)
|
|
357
|
+
if formatted_user != kwargs["user"]:
|
|
358
|
+
logger.debug(
|
|
359
|
+
f"['{instance_connection_string}']: Truncated IAM database username from {kwargs['user']} to {formatted_user}"
|
|
360
|
+
)
|
|
361
|
+
kwargs["user"] = formatted_user
|
|
362
|
+
try:
|
|
363
|
+
# async drivers are unblocking and can be awaited directly
|
|
364
|
+
if driver in ASYNC_DRIVERS:
|
|
365
|
+
return await connector(
|
|
366
|
+
ip_address,
|
|
367
|
+
await conn_info.create_ssl_context(enable_iam_auth),
|
|
368
|
+
**kwargs,
|
|
369
|
+
)
|
|
370
|
+
# synchronous drivers are blocking and run using executor
|
|
371
|
+
connect_partial = partial(
|
|
372
|
+
connector,
|
|
373
|
+
ip_address,
|
|
374
|
+
await conn_info.create_ssl_context(enable_iam_auth),
|
|
375
|
+
**kwargs,
|
|
376
|
+
)
|
|
377
|
+
return await self._loop.run_in_executor(None, connect_partial)
|
|
378
|
+
|
|
379
|
+
except Exception:
|
|
380
|
+
# with any exception, we attempt a force refresh, then throw the error
|
|
381
|
+
await cache.force_refresh()
|
|
382
|
+
raise
|
|
383
|
+
|
|
384
|
+
async def _remove_cached(
|
|
385
|
+
self, instance_connection_string: str, enable_iam_auth: bool
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Stops all background refreshes and deletes the connection
|
|
388
|
+
info cache from the map of caches.
|
|
389
|
+
"""
|
|
390
|
+
logger.debug(
|
|
391
|
+
f"['{instance_connection_string}']: Removing connection info from cache"
|
|
392
|
+
)
|
|
393
|
+
# remove cache from stored caches and close it
|
|
394
|
+
cache = self._cache.pop((instance_connection_string, enable_iam_auth))
|
|
395
|
+
await cache.close()
|
|
396
|
+
|
|
397
|
+
def __enter__(self) -> Any:
|
|
398
|
+
"""Enter context manager by returning Connector object"""
|
|
399
|
+
return self
|
|
400
|
+
|
|
401
|
+
def __exit__(
|
|
402
|
+
self,
|
|
403
|
+
exc_type: Optional[type[BaseException]],
|
|
404
|
+
exc_val: Optional[BaseException],
|
|
405
|
+
exc_tb: Optional[TracebackType],
|
|
406
|
+
) -> None:
|
|
407
|
+
"""Exit context manager by closing Connector"""
|
|
408
|
+
self.close()
|
|
409
|
+
|
|
410
|
+
async def __aenter__(self) -> Any:
|
|
411
|
+
"""Enter async context manager by returning Connector object"""
|
|
412
|
+
return self
|
|
413
|
+
|
|
414
|
+
async def __aexit__(
|
|
415
|
+
self,
|
|
416
|
+
exc_type: Optional[type[BaseException]],
|
|
417
|
+
exc_val: Optional[BaseException],
|
|
418
|
+
exc_tb: Optional[TracebackType],
|
|
419
|
+
) -> None:
|
|
420
|
+
"""Exit async context manager by closing Connector"""
|
|
421
|
+
await self.close_async()
|
|
422
|
+
|
|
423
|
+
def close(self) -> None:
|
|
424
|
+
"""Close Connector by stopping tasks and releasing resources."""
|
|
425
|
+
if self._loop.is_running():
|
|
426
|
+
close_future = asyncio.run_coroutine_threadsafe(
|
|
427
|
+
self.close_async(), loop=self._loop
|
|
428
|
+
)
|
|
429
|
+
# Will attempt to safely shut down tasks for 3s
|
|
430
|
+
close_future.result(timeout=3)
|
|
431
|
+
# if background thread exists for Connector, clean it up
|
|
432
|
+
if self._thread:
|
|
433
|
+
if self._loop.is_running():
|
|
434
|
+
# stop event loop running in background thread
|
|
435
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
436
|
+
# wait for thread to finish closing (i.e. loop to stop)
|
|
437
|
+
self._thread.join()
|
|
438
|
+
|
|
439
|
+
async def close_async(self) -> None:
|
|
440
|
+
"""Helper function to cancel the cache's tasks
|
|
441
|
+
and close aiohttp.ClientSession."""
|
|
442
|
+
await asyncio.gather(*[cache.close() for cache in self._cache.values()])
|
|
443
|
+
if self._client:
|
|
444
|
+
await self._client.close()
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
async def create_async_connector(
|
|
448
|
+
ip_type: str | IPTypes = IPTypes.PUBLIC,
|
|
449
|
+
enable_iam_auth: bool = False,
|
|
450
|
+
timeout: int = 30,
|
|
451
|
+
credentials: Optional[Credentials] = None,
|
|
452
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
|
453
|
+
quota_project: Optional[str] = None,
|
|
454
|
+
sqladmin_api_endpoint: Optional[str] = None,
|
|
455
|
+
user_agent: Optional[str] = None,
|
|
456
|
+
universe_domain: Optional[str] = None,
|
|
457
|
+
refresh_strategy: str | RefreshStrategy = RefreshStrategy.BACKGROUND,
|
|
458
|
+
resolver: type[DefaultResolver] | type[DnsResolver] = DefaultResolver,
|
|
459
|
+
) -> Connector:
|
|
460
|
+
"""Helper function to create Connector object for asyncio connections.
|
|
461
|
+
|
|
462
|
+
Force use of Connector in an asyncio context. Auto-detect and use current
|
|
463
|
+
thread's running event loop.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
ip_type (str | IPTypes): The default IP address type used to connect to
|
|
467
|
+
Cloud SQL instances. Can be one of the following:
|
|
468
|
+
IPTypes.PUBLIC ("PUBLIC"), IPTypes.PRIVATE ("PRIVATE"), or
|
|
469
|
+
IPTypes.PSC ("PSC"). Default: IPTypes.PUBLIC
|
|
470
|
+
|
|
471
|
+
enable_iam_auth (bool): Enables automatic IAM database authentication
|
|
472
|
+
(Postgres and MySQL) as the default authentication method for all
|
|
473
|
+
connections.
|
|
474
|
+
|
|
475
|
+
timeout (int): The default time limit in seconds for a connection before
|
|
476
|
+
raising a TimeoutError.
|
|
477
|
+
|
|
478
|
+
credentials (google.auth.credentials.Credentials): A credentials object
|
|
479
|
+
created from the google-auth Python library to be used.
|
|
480
|
+
If not specified, Application Default Credentials (ADC) are used.
|
|
481
|
+
|
|
482
|
+
quota_project (str): The Project ID for an existing Google Cloud
|
|
483
|
+
project. The project specified is used for quota and billing
|
|
484
|
+
purposes. If not specified, defaults to project sourced from
|
|
485
|
+
environment.
|
|
486
|
+
|
|
487
|
+
loop (asyncio.AbstractEventLoop): Event loop to run asyncio tasks, if
|
|
488
|
+
not specified, defaults to creating new event loop on background
|
|
489
|
+
thread.
|
|
490
|
+
|
|
491
|
+
sqladmin_api_endpoint (str): Base URL to use when calling the Cloud SQL
|
|
492
|
+
Admin API endpoint. Defaults to "https://sqladmin.googleapis.com",
|
|
493
|
+
this argument should only be used in development.
|
|
494
|
+
|
|
495
|
+
universe_domain (str): The universe domain for Cloud SQL API calls.
|
|
496
|
+
Default: "googleapis.com".
|
|
497
|
+
|
|
498
|
+
refresh_strategy (str | RefreshStrategy): The default refresh strategy
|
|
499
|
+
used to refresh SSL/TLS cert and instance metadata. Can be one
|
|
500
|
+
of the following: RefreshStrategy.LAZY ("LAZY") or
|
|
501
|
+
RefreshStrategy.BACKGROUND ("BACKGROUND").
|
|
502
|
+
Default: RefreshStrategy.BACKGROUND
|
|
503
|
+
|
|
504
|
+
resolver (DefaultResolver | DnsResolver): The class name of the
|
|
505
|
+
resolver to use for resolving the Cloud SQL instance connection
|
|
506
|
+
name. To resolve a DNS record to an instance connection name, use
|
|
507
|
+
DnsResolver.
|
|
508
|
+
Default: DefaultResolver
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
A Connector instance configured with running event loop.
|
|
512
|
+
"""
|
|
513
|
+
# if no loop given, automatically detect running event loop
|
|
514
|
+
if loop is None:
|
|
515
|
+
loop = asyncio.get_running_loop()
|
|
516
|
+
return Connector(
|
|
517
|
+
ip_type=ip_type,
|
|
518
|
+
enable_iam_auth=enable_iam_auth,
|
|
519
|
+
timeout=timeout,
|
|
520
|
+
credentials=credentials,
|
|
521
|
+
loop=loop,
|
|
522
|
+
quota_project=quota_project,
|
|
523
|
+
sqladmin_api_endpoint=sqladmin_api_endpoint,
|
|
524
|
+
user_agent=user_agent,
|
|
525
|
+
universe_domain=universe_domain,
|
|
526
|
+
refresh_strategy=refresh_strategy,
|
|
527
|
+
resolver=resolver,
|
|
528
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
from google.cloud.sql.connector.exceptions import IncompatibleDriverError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# TODO: Replace Enum with StrEnum when Python 3.11 is minimum supported version
|
|
23
|
+
class RefreshStrategy(Enum):
|
|
24
|
+
LAZY = "LAZY"
|
|
25
|
+
BACKGROUND = "BACKGROUND"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _missing_(cls, value: object) -> None:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Incorrect value for refresh_strategy, got '{value}'. Want one of: "
|
|
31
|
+
f"{', '.join([repr(m.value) for m in cls])}."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _from_str(cls, refresh_strategy: str) -> RefreshStrategy:
|
|
36
|
+
"""Convert refresh strategy from a str into RefreshStrategy."""
|
|
37
|
+
return cls(refresh_strategy.upper())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IPTypes(Enum):
|
|
41
|
+
PUBLIC = "PRIMARY"
|
|
42
|
+
PRIVATE = "PRIVATE"
|
|
43
|
+
PSC = "PSC"
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def _missing_(cls, value: object) -> None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Incorrect value for ip_type, got '{value}'. Want one of: "
|
|
49
|
+
f"{', '.join([repr(m.value) for m in cls])}, 'PUBLIC'."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def _from_str(cls, ip_type_str: str) -> IPTypes:
|
|
54
|
+
"""Convert IP type from a str into IPTypes."""
|
|
55
|
+
if ip_type_str.upper() == "PUBLIC":
|
|
56
|
+
ip_type_str = "PRIMARY"
|
|
57
|
+
return cls(ip_type_str.upper())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DriverMapping(Enum):
|
|
61
|
+
"""Maps a given database driver to it's corresponding database engine."""
|
|
62
|
+
|
|
63
|
+
ASYNCPG = "POSTGRES"
|
|
64
|
+
PG8000 = "POSTGRES"
|
|
65
|
+
PYMYSQL = "MYSQL"
|
|
66
|
+
PYTDS = "SQLSERVER"
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def validate_engine(driver: str, engine_version: str) -> None:
|
|
70
|
+
"""Validate that the given driver is compatible with the given engine.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
driver (str): Database driver being used. (i.e. "pg8000")
|
|
74
|
+
engine_version (str): Database engine version. (i.e. "POSTGRES_16")
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
IncompatibleDriverError: If the given driver is not compatible with
|
|
78
|
+
the given engine.
|
|
79
|
+
"""
|
|
80
|
+
mapping = DriverMapping[driver.upper()]
|
|
81
|
+
if not engine_version.startswith(mapping.value):
|
|
82
|
+
raise IncompatibleDriverError(
|
|
83
|
+
f"Database driver '{driver}' is incompatible with database "
|
|
84
|
+
f"version '{engine_version}'. Given driver can "
|
|
85
|
+
f"only be used with Cloud SQL {mapping.value} databases."
|
|
86
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2022 Google LLC
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConnectorLoopError(Exception):
|
|
19
|
+
"""
|
|
20
|
+
Raised when Connector.connect is called with Connector._loop
|
|
21
|
+
in an invalid state (event loop in current thread).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TLSVersionError(Exception):
|
|
28
|
+
"""
|
|
29
|
+
Raised when the required TLS protocol version is not supported.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CloudSQLIPTypeError(Exception):
|
|
36
|
+
"""
|
|
37
|
+
Raised when IP address for the preferred IP type is not found.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PlatformNotSupportedError(Exception):
|
|
44
|
+
"""
|
|
45
|
+
Raised when a feature is not supported on the current platform.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AutoIAMAuthNotSupported(Exception):
|
|
52
|
+
"""
|
|
53
|
+
Exception to be raised when Automatic IAM Authentication is not
|
|
54
|
+
supported with database engine version.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RefreshNotValidError(Exception):
|
|
61
|
+
"""
|
|
62
|
+
Exception to be raised when the task returned from refresh is not valid.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class IncompatibleDriverError(Exception):
|
|
69
|
+
"""
|
|
70
|
+
Exception to be raised when the database driver given is for the wrong
|
|
71
|
+
database engine. (i.e. asyncpg for a MySQL database)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DnsResolutionError(Exception):
|
|
76
|
+
"""
|
|
77
|
+
Exception to be raised when an instance connection name can not be resolved
|
|
78
|
+
from a DNS record.
|
|
79
|
+
"""
|