cloud-sql-python-connector 1.17.0.dev0__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.
@@ -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
+ """