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,214 @@
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 datetime import datetime
21
+ from datetime import timedelta
22
+ from datetime import timezone
23
+ import logging
24
+
25
+ from google.cloud.sql.connector.client import CloudSQLClient
26
+ from google.cloud.sql.connector.connection_info import ConnectionInfo
27
+ from google.cloud.sql.connector.connection_name import ConnectionName
28
+ from google.cloud.sql.connector.exceptions import RefreshNotValidError
29
+ from google.cloud.sql.connector.rate_limiter import AsyncRateLimiter
30
+ from google.cloud.sql.connector.refresh_utils import _is_valid
31
+ from google.cloud.sql.connector.refresh_utils import _seconds_until_refresh
32
+
33
+ logger = logging.getLogger(name=__name__)
34
+
35
+ APPLICATION_NAME = "cloud-sql-python-connector"
36
+
37
+
38
+ class RefreshAheadCache:
39
+ """Cache that refreshes connection info in the background prior to expiration.
40
+
41
+ Background tasks are used to schedule refresh attempts to get a new
42
+ ephemeral certificate and Cloud SQL metadata (IP addresses, etc.) ahead of
43
+ expiration.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ conn_name: ConnectionName,
49
+ client: CloudSQLClient,
50
+ keys: asyncio.Future,
51
+ enable_iam_auth: bool = False,
52
+ ) -> None:
53
+ """Initializes a RefreshAheadCache instance.
54
+
55
+ Args:
56
+ conn_name (ConnectionName): The Cloud SQL instance's
57
+ connection name.
58
+ client (CloudSQLClient): The Cloud SQL Client instance.
59
+ keys (asyncio.Future): A future to the client's public-private key
60
+ pair.
61
+ enable_iam_auth (bool): Enables automatic IAM database authentication
62
+ (Postgres and MySQL) as the default authentication method for all
63
+ connections.
64
+ """
65
+ self._conn_name = conn_name
66
+
67
+ self._enable_iam_auth = enable_iam_auth
68
+ self._keys = keys
69
+ self._client = client
70
+ self._refresh_rate_limiter = AsyncRateLimiter(
71
+ max_capacity=2,
72
+ rate=1 / 30,
73
+ )
74
+ self._refresh_in_progress = asyncio.locks.Event()
75
+ self._current: asyncio.Task = self._schedule_refresh(0)
76
+ self._next: asyncio.Task = self._current
77
+
78
+ async def force_refresh(self) -> None:
79
+ """
80
+ Forces a new refresh attempt immediately to be used for future connection attempts.
81
+ """
82
+ # if next refresh is not already in progress, cancel it and schedule new one immediately
83
+ if not self._refresh_in_progress.is_set():
84
+ self._next.cancel()
85
+ self._next = self._schedule_refresh(0)
86
+ # block all sequential connection attempts on the next refresh result if current is invalid
87
+ if not await _is_valid(self._current):
88
+ self._current = self._next
89
+
90
+ async def _perform_refresh(self) -> ConnectionInfo:
91
+ """Retrieves instance metadata and ephemeral certificate from the
92
+ Cloud SQL Instance.
93
+
94
+ Returns:
95
+ A ConnectionInfo instance containing a string representing the
96
+ ephemeral certificate, a dict containing the instances IP adresses,
97
+ a string representing a PEM-encoded private key and a string
98
+ representing a PEM-encoded certificate authority.
99
+ """
100
+ self._refresh_in_progress.set()
101
+ logger.debug(
102
+ f"['{self._conn_name}']: Connection info refresh operation started"
103
+ )
104
+
105
+ try:
106
+ await self._refresh_rate_limiter.acquire()
107
+ connection_info = await self._client.get_connection_info(
108
+ self._conn_name,
109
+ self._keys,
110
+ self._enable_iam_auth,
111
+ )
112
+ logger.debug(
113
+ f"['{self._conn_name}']: Connection info refresh operation complete"
114
+ )
115
+ logger.debug(
116
+ f"['{self._conn_name}']: Current certificate "
117
+ f"expiration = {connection_info.expiration.isoformat()}"
118
+ )
119
+
120
+ except Exception as e:
121
+ logger.debug(
122
+ f"['{self._conn_name}']: Connection info "
123
+ f"refresh operation failed: {str(e)}"
124
+ )
125
+ raise
126
+
127
+ finally:
128
+ self._refresh_in_progress.clear()
129
+ return connection_info
130
+
131
+ def _schedule_refresh(self, delay: int) -> asyncio.Task:
132
+ """
133
+ Schedule task to sleep and then perform refresh to get ConnectionInfo.
134
+
135
+ Args:
136
+ delay (int): Time in seconds to sleep before performing a refresh.
137
+
138
+ Returns:
139
+ An asyncio.Task representing the scheduled refresh.
140
+ """
141
+
142
+ async def _refresh_task(self: RefreshAheadCache, delay: int) -> ConnectionInfo:
143
+ """
144
+ A coroutine that sleeps for the specified amount of time before
145
+ running _perform_refresh.
146
+ """
147
+ refresh_task: asyncio.Task
148
+ try:
149
+ if delay > 0:
150
+ await asyncio.sleep(delay)
151
+ refresh_task = asyncio.create_task(self._perform_refresh())
152
+ refresh_data = await refresh_task
153
+ # check that refresh is valid
154
+ if not await _is_valid(refresh_task):
155
+ raise RefreshNotValidError(
156
+ f"['{self._conn_name}']: Invalid refresh operation. Certficate appears to be expired."
157
+ )
158
+ except asyncio.CancelledError:
159
+ logger.debug(
160
+ f"['{self._conn_name}']: Scheduled refresh" " operation cancelled"
161
+ )
162
+ raise
163
+ # bad refresh attempt
164
+ except Exception as e:
165
+ logger.exception(
166
+ f"['{self._conn_name}']: "
167
+ "An error occurred while performing refresh. "
168
+ "Scheduling another refresh attempt immediately",
169
+ exc_info=e,
170
+ )
171
+ # check if current metadata is invalid (expired),
172
+ # don't want to replace valid metadata with invalid refresh
173
+ if not await _is_valid(self._current):
174
+ self._current = refresh_task
175
+ # schedule new refresh attempt immediately
176
+ self._next = self._schedule_refresh(0)
177
+ raise
178
+ # if valid refresh, replace current with valid metadata and schedule next refresh
179
+ self._current = refresh_task
180
+ # calculate refresh delay based on certificate expiration
181
+ delay = _seconds_until_refresh(refresh_data.expiration)
182
+ logger.debug(
183
+ f"['{self._conn_name}']: Connection info refresh"
184
+ " operation scheduled for "
185
+ f"{(datetime.now(timezone.utc) + timedelta(seconds=delay)).isoformat(timespec='seconds')} "
186
+ f"(now + {timedelta(seconds=delay)})"
187
+ )
188
+ self._next = self._schedule_refresh(delay)
189
+
190
+ return refresh_data
191
+
192
+ # schedule refresh task and return it
193
+ scheduled_task = asyncio.create_task(_refresh_task(self, delay))
194
+ return scheduled_task
195
+
196
+ async def connect_info(self) -> ConnectionInfo:
197
+ """Retrieves ConnectionInfo instance for establishing a secure
198
+ connection to the Cloud SQL instance.
199
+ """
200
+ return await self._current
201
+
202
+ async def close(self) -> None:
203
+ """Cleanup function to make sure tasks have finished to have a
204
+ graceful exit.
205
+ """
206
+ logger.debug(
207
+ f"['{self._conn_name}']: Canceling connection info "
208
+ "refresh operation tasks"
209
+ )
210
+ self._current.cancel()
211
+ self._next.cancel()
212
+ # gracefully wait for tasks to cancel
213
+ tasks = asyncio.gather(self._current, self._next, return_exceptions=True)
214
+ await asyncio.wait_for(tasks, timeout=2.0)
@@ -0,0 +1,124 @@
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
+ import asyncio
16
+ from datetime import datetime
17
+ from datetime import timedelta
18
+ from datetime import timezone
19
+ import logging
20
+ from typing import Optional
21
+
22
+ from google.cloud.sql.connector.client import CloudSQLClient
23
+ from google.cloud.sql.connector.connection_info import ConnectionInfo
24
+ from google.cloud.sql.connector.connection_name import ConnectionName
25
+ from google.cloud.sql.connector.refresh_utils import _refresh_buffer
26
+
27
+ logger = logging.getLogger(name=__name__)
28
+
29
+
30
+ class LazyRefreshCache:
31
+ """Cache that refreshes connection info when a caller requests a connection.
32
+
33
+ Only refreshes the cache when a new connection is requested and the current
34
+ certificate is close to or already expired.
35
+
36
+ This is the recommended option for serverless environments.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ conn_name: ConnectionName,
42
+ client: CloudSQLClient,
43
+ keys: asyncio.Future,
44
+ enable_iam_auth: bool = False,
45
+ ) -> None:
46
+ """Initializes a LazyRefreshCache instance.
47
+
48
+ Args:
49
+ conn_name (ConnectionName): The Cloud SQL instance's
50
+ connection name.
51
+ client (CloudSQLClient): The Cloud SQL Client instance.
52
+ keys (asyncio.Future): A future to the client's public-private key
53
+ pair.
54
+ enable_iam_auth (bool): Enables automatic IAM database authentication
55
+ (Postgres and MySQL) as the default authentication method for all
56
+ connections.
57
+ """
58
+ self._conn_name = conn_name
59
+ self._enable_iam_auth = enable_iam_auth
60
+ self._keys = keys
61
+ self._client = client
62
+ self._lock = asyncio.Lock()
63
+ self._cached: Optional[ConnectionInfo] = None
64
+ self._needs_refresh = False
65
+
66
+ async def force_refresh(self) -> None:
67
+ """
68
+ Invalidates the cache and configures the next call to
69
+ connect_info() to retrieve a fresh ConnectionInfo instance.
70
+ """
71
+ async with self._lock:
72
+ self._needs_refresh = True
73
+
74
+ async def connect_info(self) -> ConnectionInfo:
75
+ """Retrieves ConnectionInfo instance for establishing a secure
76
+ connection to the Cloud SQL instance.
77
+ """
78
+ async with self._lock:
79
+ # If connection info is cached, check expiration.
80
+ # Pad expiration with a buffer to give the client plenty of time to
81
+ # establish a connection to the server with the certificate.
82
+ if (
83
+ self._cached
84
+ and not self._needs_refresh
85
+ and datetime.now(timezone.utc)
86
+ < (self._cached.expiration - timedelta(seconds=_refresh_buffer))
87
+ ):
88
+ logger.debug(
89
+ f"['{self._conn_name}']: Connection info "
90
+ "is still valid, using cached info"
91
+ )
92
+ return self._cached
93
+ logger.debug(
94
+ f"['{self._conn_name}']: Connection info " "refresh operation started"
95
+ )
96
+ try:
97
+ conn_info = await self._client.get_connection_info(
98
+ self._conn_name,
99
+ self._keys,
100
+ self._enable_iam_auth,
101
+ )
102
+ except Exception as e:
103
+ logger.debug(
104
+ f"['{self._conn_name}']: Connection info "
105
+ f"refresh operation failed: {str(e)}"
106
+ )
107
+ raise
108
+ logger.debug(
109
+ f"['{self._conn_name}']: Connection info "
110
+ "refresh operation completed successfully"
111
+ )
112
+ logger.debug(
113
+ f"['{self._conn_name}']: Current certificate "
114
+ f"expiration = {str(conn_info.expiration)}"
115
+ )
116
+ self._cached = conn_info
117
+ self._needs_refresh = False
118
+ return conn_info
119
+
120
+ async def close(self) -> None:
121
+ """Close is a no-op and provided purely for a consistent interface with
122
+ other cache types.
123
+ """
124
+ pass
@@ -0,0 +1,66 @@
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
+ import socket
18
+ import ssl
19
+ from typing import Any, TYPE_CHECKING
20
+
21
+ SERVER_PROXY_PORT = 3307
22
+
23
+ if TYPE_CHECKING:
24
+ import pg8000
25
+
26
+
27
+ def connect(
28
+ ip_address: str, ctx: ssl.SSLContext, **kwargs: Any
29
+ ) -> "pg8000.dbapi.Connection":
30
+ """Helper function to create a pg8000 DB-API connection object.
31
+
32
+ :type ip_address: str
33
+ :param ip_address: A string containing an IP address for the Cloud SQL
34
+ instance.
35
+
36
+ :type ctx: ssl.SSLContext
37
+ :param ctx: An SSLContext object created from the Cloud SQL server CA
38
+ cert and ephemeral cert.
39
+
40
+
41
+ :rtype: pg8000.dbapi.Connection
42
+ :returns: A pg8000 Connection object for the Cloud SQL instance.
43
+ """
44
+ try:
45
+ import pg8000
46
+ except ImportError:
47
+ raise ImportError(
48
+ 'Unable to import module "pg8000." Please install and try again.'
49
+ )
50
+
51
+ # Create socket and wrap with context.
52
+ sock = ctx.wrap_socket(
53
+ socket.create_connection((ip_address, SERVER_PROXY_PORT)),
54
+ server_hostname=ip_address,
55
+ )
56
+
57
+ user = kwargs.pop("user")
58
+ db = kwargs.pop("db")
59
+ passwd = kwargs.pop("password", None)
60
+ return pg8000.dbapi.connect(
61
+ user,
62
+ database=db,
63
+ password=passwd,
64
+ sock=sock,
65
+ **kwargs,
66
+ )
File without changes
@@ -0,0 +1,64 @@
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
+ import socket
18
+ import ssl
19
+ from typing import Any, TYPE_CHECKING
20
+
21
+ SERVER_PROXY_PORT = 3307
22
+
23
+ if TYPE_CHECKING:
24
+ import pymysql
25
+
26
+
27
+ def connect(
28
+ ip_address: str, ctx: ssl.SSLContext, **kwargs: Any
29
+ ) -> "pymysql.connections.Connection":
30
+ """Helper function to create a pymysql DB-API connection object.
31
+
32
+ :type ip_address: str
33
+ :param ip_address: A string containing an IP address for the Cloud SQL
34
+ instance.
35
+
36
+ :type ctx: ssl.SSLContext
37
+ :param ctx: An SSLContext object created from the Cloud SQL server CA
38
+ cert and ephemeral cert.
39
+
40
+ :rtype: pymysql.Connection
41
+ :returns: A PyMySQL Connection object for the Cloud SQL instance.
42
+ """
43
+ try:
44
+ import pymysql
45
+ except ImportError:
46
+ raise ImportError(
47
+ 'Unable to import module "pymysql." Please install and try again.'
48
+ )
49
+
50
+ # allow automatic IAM database authentication to not require password
51
+ kwargs["password"] = kwargs["password"] if "password" in kwargs else None
52
+
53
+ # Create socket and wrap with context.
54
+ sock = ctx.wrap_socket(
55
+ socket.create_connection((ip_address, SERVER_PROXY_PORT)),
56
+ server_hostname=ip_address,
57
+ )
58
+ # pop timeout as timeout arg is called 'connect_timeout' for pymysql
59
+ timeout = kwargs.pop("timeout")
60
+ kwargs["connect_timeout"] = kwargs.get("connect_timeout", timeout)
61
+ # Create pymysql connection object and hand in pre-made connection
62
+ conn = pymysql.Connection(host=ip_address, defer_connect=True, **kwargs)
63
+ conn.connect(sock)
64
+ return conn
@@ -0,0 +1,78 @@
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
+ import platform
18
+ import socket
19
+ import ssl
20
+ from typing import Any, TYPE_CHECKING
21
+
22
+ from google.cloud.sql.connector.exceptions import PlatformNotSupportedError
23
+
24
+ SERVER_PROXY_PORT = 3307
25
+
26
+ if TYPE_CHECKING:
27
+ import pytds
28
+
29
+
30
+ def connect(ip_address: str, ctx: ssl.SSLContext, **kwargs: Any) -> "pytds.Connection":
31
+ """Helper function to create a pytds DB-API connection object.
32
+
33
+ :type ip_address: str
34
+ :param ip_address: A string containing an IP address for the Cloud SQL
35
+ instance.
36
+
37
+ :type ctx: ssl.SSLContext
38
+ :param ctx: An SSLContext object created from the Cloud SQL server CA
39
+ cert and ephemeral cert.
40
+
41
+
42
+ :rtype: pytds.Connection
43
+ :returns: A pytds Connection object for the Cloud SQL instance.
44
+ """
45
+ try:
46
+ import pytds
47
+ except ImportError:
48
+ raise ImportError(
49
+ 'Unable to import module "pytds." Please install and try again.'
50
+ )
51
+
52
+ db = kwargs.pop("db", None)
53
+
54
+ # Create socket and wrap with context.
55
+ sock = ctx.wrap_socket(
56
+ socket.create_connection((ip_address, SERVER_PROXY_PORT)),
57
+ server_hostname=ip_address,
58
+ )
59
+ if kwargs.pop("active_directory_auth", False):
60
+ if platform.system() == "Windows":
61
+ # Ignore username and password if using active directory auth
62
+ server_name = kwargs.pop("server_name")
63
+ return pytds.connect(
64
+ database=db,
65
+ auth=pytds.login.SspiAuth(port=1433, server_name=server_name),
66
+ sock=sock,
67
+ **kwargs,
68
+ )
69
+ else:
70
+ raise PlatformNotSupportedError(
71
+ "Active Directory authentication is currently only supported on Windows."
72
+ )
73
+
74
+ user = kwargs.pop("user")
75
+ passwd = kwargs.pop("password")
76
+ return pytds.connect(
77
+ ip_address, database=db, user=user, password=passwd, sock=sock, **kwargs
78
+ )
@@ -0,0 +1,87 @@
1
+ """
2
+ Copyright 2021 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
+ import asyncio
18
+ from typing import Optional
19
+
20
+
21
+ class AsyncRateLimiter(object):
22
+ """
23
+ An asyncio-compatible rate limiter which uses the Token Bucket algorithm
24
+ (https://en.wikipedia.org/wiki/Token_bucket) to limit the number of function calls over a time interval using an event queue.
25
+
26
+ :type max_capacity: int
27
+ :param: max_capacity:
28
+ The maximum capacity of tokens the bucket will store at any one time.
29
+ Default: 1
30
+
31
+ :type rate: float
32
+ :param: rate:
33
+ The number of tokens that should be added per second.
34
+
35
+ :type loop: asyncio.AbstractEventLoop
36
+ :param: loop:
37
+ The event loop to use. If not provided, the default event loop will be used.
38
+
39
+
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ max_capacity: int = 1,
45
+ rate: float = 1 / 60,
46
+ loop: Optional[asyncio.AbstractEventLoop] = None,
47
+ ) -> None:
48
+ self.rate = rate
49
+ self.max_capacity = max_capacity
50
+ self._loop = loop or asyncio.get_event_loop()
51
+ self._tokens: float = max_capacity
52
+ self._last_token_update = self._loop.time()
53
+ self._lock = asyncio.Lock()
54
+
55
+ def _update_token_count(self) -> None:
56
+ """
57
+ Calculates how much time has passed since the last leak and removes the
58
+ appropriate amount of events from the queue.
59
+ Leaking is done lazily, meaning that if there is a large time gap between
60
+ leaks, the next set of calls might be a burst if burst_size > 1
61
+ """
62
+ now = self._loop.time()
63
+ time_elapsed = now - self._last_token_update
64
+ new_tokens = time_elapsed * self.rate
65
+ self._tokens = min(new_tokens + self._tokens, self.max_capacity)
66
+ self._last_token_update = now
67
+
68
+ async def _wait_for_next_token(self) -> None:
69
+ """
70
+ Wait until enough time has elapsed to add another token.
71
+ """
72
+ token_deficit = 1 - self._tokens
73
+ if token_deficit > 0:
74
+ wait_time = token_deficit / self.rate
75
+ await asyncio.sleep(wait_time)
76
+
77
+ async def acquire(self) -> None:
78
+ """
79
+ Waits for a token to become available, if necessary, then subtracts token and allows
80
+ request to go through.
81
+ """
82
+ async with self._lock:
83
+ self._update_token_count()
84
+ if self._tokens < 1:
85
+ await self._wait_for_next_token()
86
+ self._update_token_count()
87
+ self._tokens -= 1