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,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
|