cloud-sql-python-connector 1.22.0__py3-none-any.whl → 1.23.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.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/METADATA +10 -9
- cloud_sql_python_connector-1.23.0.dist-info/RECORD +27 -0
- google/cloud/sql/connector/client.py +15 -3
- google/cloud/sql/connector/connection_info.py +8 -1
- google/cloud/sql/connector/connector.py +349 -134
- google/cloud/sql/connector/enums.py +3 -0
- google/cloud/sql/connector/exceptions.py +33 -0
- google/cloud/sql/connector/instance.py +2 -2
- google/cloud/sql/connector/lazy.py +2 -2
- google/cloud/sql/connector/monitored_cache.py +10 -9
- google/cloud/sql/connector/psycopg.py +241 -153
- google/cloud/sql/connector/refresh_utils.py +4 -3
- google/cloud/sql/connector/sqldata_client.py +654 -0
- google/cloud/sql/connector/version.py +1 -1
- cloud_sql_python_connector-1.22.0.dist-info/RECORD +0 -26
- {cloud_sql_python_connector-1.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/WHEEL +0 -0
- {cloud_sql_python_connector-1.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/licenses/LICENSE +0 -0
- {cloud_sql_python_connector-1.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
import errno
|
|
20
|
+
import io
|
|
21
|
+
import logging
|
|
22
|
+
import queue
|
|
23
|
+
import socket
|
|
24
|
+
import threading
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from google.api_core.client_options import ClientOptions
|
|
28
|
+
from google.api_core.exceptions import FailedPrecondition
|
|
29
|
+
from google.api_core.exceptions import ResourceExhausted
|
|
30
|
+
from google.auth.credentials import Credentials
|
|
31
|
+
import grpc
|
|
32
|
+
|
|
33
|
+
from google.cloud import sqladmin_v1beta4
|
|
34
|
+
from google.cloud.sql.connector.enums import IPTypes
|
|
35
|
+
from google.cloud.sql.connector.exceptions import CloudSQLIPTypeError
|
|
36
|
+
|
|
37
|
+
SERVER_PROXY_PORT = 3307
|
|
38
|
+
_EOF_SENTINEL = object()
|
|
39
|
+
_STREAM_EOF = object()
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_grpc_status_error(
|
|
45
|
+
err: Exception,
|
|
46
|
+
exc_cls: type[Exception],
|
|
47
|
+
status_code: grpc.StatusCode,
|
|
48
|
+
) -> bool:
|
|
49
|
+
"""Checks whether an exception or its cause matches the given gRPC status."""
|
|
50
|
+
if isinstance(err, exc_cls):
|
|
51
|
+
return True
|
|
52
|
+
if isinstance(err, grpc.RpcError):
|
|
53
|
+
try:
|
|
54
|
+
return err.code() == status_code
|
|
55
|
+
except Exception: # noqa: BLE001, S110
|
|
56
|
+
pass
|
|
57
|
+
if hasattr(err, "code") and callable(err.code):
|
|
58
|
+
try:
|
|
59
|
+
return err.code() == status_code
|
|
60
|
+
except Exception: # noqa: BLE001, S110
|
|
61
|
+
pass
|
|
62
|
+
cause = getattr(err, "__cause__", None) or getattr(err, "__context__", None)
|
|
63
|
+
if isinstance(cause, Exception) and cause is not err:
|
|
64
|
+
return _is_grpc_status_error(cause, exc_cls, status_code)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_resource_exhausted_error(err: Exception) -> bool:
|
|
69
|
+
"""Checks whether an exception represents a RESOURCE_EXHAUSTED error."""
|
|
70
|
+
return _is_grpc_status_error(
|
|
71
|
+
err, ResourceExhausted, grpc.StatusCode.RESOURCE_EXHAUSTED
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_failed_precondition_error(err: Exception) -> bool:
|
|
76
|
+
"""Checks whether an exception represents a FAILED_PRECONDITION error."""
|
|
77
|
+
return _is_grpc_status_error(
|
|
78
|
+
err, FailedPrecondition, grpc.StatusCode.FAILED_PRECONDITION
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _RequestQueue:
|
|
84
|
+
"""Thread-safe queue iterator feeding requests to synchronous gRPC stream."""
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._queue: queue.Queue = queue.Queue(maxsize=1024)
|
|
88
|
+
self._closed = False
|
|
89
|
+
self._lock = threading.Lock()
|
|
90
|
+
|
|
91
|
+
def put(self, item: Any) -> None:
|
|
92
|
+
with self._lock:
|
|
93
|
+
if self._closed:
|
|
94
|
+
raise BrokenPipeError(errno.EPIPE, "Stream request queue is closed")
|
|
95
|
+
self._queue.put(item)
|
|
96
|
+
|
|
97
|
+
def close(self) -> None:
|
|
98
|
+
with self._lock:
|
|
99
|
+
if self._closed:
|
|
100
|
+
return
|
|
101
|
+
self._closed = True
|
|
102
|
+
self._queue.put(_STREAM_EOF)
|
|
103
|
+
|
|
104
|
+
def __iter__(self) -> _RequestQueue:
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __next__(self) -> Any:
|
|
108
|
+
item = self._queue.get()
|
|
109
|
+
if item is _STREAM_EOF:
|
|
110
|
+
raise StopIteration
|
|
111
|
+
return item
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SqlDataRawIO(io.RawIOBase):
|
|
115
|
+
"""RawIO wrapper around SqlDataSocket to support makefile()."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, sock: SqlDataSocket) -> None:
|
|
118
|
+
self._sock = sock
|
|
119
|
+
|
|
120
|
+
def readable(self) -> bool:
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
def writable(self) -> bool:
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def seekable(self) -> bool:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def readinto(self, b: Any) -> int:
|
|
130
|
+
return self._sock.recv_into(b)
|
|
131
|
+
|
|
132
|
+
def write(self, b: Any) -> int:
|
|
133
|
+
self._sock.sendall(b)
|
|
134
|
+
return len(b)
|
|
135
|
+
|
|
136
|
+
def close(self) -> None:
|
|
137
|
+
if not self.closed:
|
|
138
|
+
super().close()
|
|
139
|
+
self._sock.close()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SqlDataSocket(socket.socket):
|
|
143
|
+
"""Direct in-process socket adapter connected to a synchronous gRPC SqlData stream.
|
|
144
|
+
|
|
145
|
+
Provides full socket interface compatibility for synchronous database drivers
|
|
146
|
+
(pg8000, pymysql, pytds) while avoiding local TCP loopback and asyncio scheduling.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
request_queue: _RequestQueue,
|
|
152
|
+
response_stream: Any,
|
|
153
|
+
grpc_client: sqladmin_v1beta4.SqlDataServiceClient | None = None,
|
|
154
|
+
timeout: float | None = None,
|
|
155
|
+
on_close: Callable[[], None] | None = None,
|
|
156
|
+
on_success: Callable[[], None] | None = None,
|
|
157
|
+
on_resource_exhausted: Callable[[Exception], None] | None = None,
|
|
158
|
+
fallback_fn: Callable[[], socket.socket] | None = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
super().__init__(socket.AF_INET, socket.SOCK_STREAM)
|
|
161
|
+
self._request_queue = request_queue
|
|
162
|
+
self._response_stream = response_stream
|
|
163
|
+
self._grpc_client = grpc_client
|
|
164
|
+
self._timeout = timeout
|
|
165
|
+
self._on_close = on_close
|
|
166
|
+
self._on_success = on_success
|
|
167
|
+
self._on_resource_exhausted = on_resource_exhausted
|
|
168
|
+
self._fallback_fn = fallback_fn
|
|
169
|
+
|
|
170
|
+
self._read_queue: queue.Queue = queue.Queue(maxsize=1024)
|
|
171
|
+
self._read_buf = b""
|
|
172
|
+
self._read_offset = 0
|
|
173
|
+
self._closed = False
|
|
174
|
+
self._first_read_done = False
|
|
175
|
+
self._write_buffer = bytearray()
|
|
176
|
+
self._direct_sock: socket.socket | None = None
|
|
177
|
+
self._error: Exception | None = None
|
|
178
|
+
self._close_lock = threading.Lock()
|
|
179
|
+
|
|
180
|
+
# Start background stream consumer thread
|
|
181
|
+
self._reader_thread = threading.Thread(
|
|
182
|
+
target=self._reader_loop, daemon=True, name="sqldata-reader"
|
|
183
|
+
)
|
|
184
|
+
self._reader_thread.start()
|
|
185
|
+
|
|
186
|
+
def _reader_loop(self) -> None:
|
|
187
|
+
try:
|
|
188
|
+
for resp in self._response_stream:
|
|
189
|
+
if self._closed:
|
|
190
|
+
break
|
|
191
|
+
raw_pb = (
|
|
192
|
+
sqladmin_v1beta4.StreamSqlDataResponse.pb(resp)
|
|
193
|
+
if hasattr(sqladmin_v1beta4.StreamSqlDataResponse, "pb")
|
|
194
|
+
and isinstance(resp, sqladmin_v1beta4.StreamSqlDataResponse)
|
|
195
|
+
else resp
|
|
196
|
+
)
|
|
197
|
+
which = (
|
|
198
|
+
raw_pb.WhichOneof("message")
|
|
199
|
+
if hasattr(raw_pb, "WhichOneof")
|
|
200
|
+
else None
|
|
201
|
+
)
|
|
202
|
+
if which == "terminate_session" or (
|
|
203
|
+
which is None
|
|
204
|
+
and hasattr(resp, "terminate_session")
|
|
205
|
+
and resp.terminate_session
|
|
206
|
+
):
|
|
207
|
+
logger.debug("Received TerminateSession from server")
|
|
208
|
+
self._closed = True
|
|
209
|
+
break
|
|
210
|
+
if not self._first_read_done:
|
|
211
|
+
self._first_read_done = True
|
|
212
|
+
if self._on_success:
|
|
213
|
+
self._on_success()
|
|
214
|
+
if which == "data" or (which is None and hasattr(resp, "data") and resp.data):
|
|
215
|
+
data = resp.data.data
|
|
216
|
+
if data:
|
|
217
|
+
self._read_queue.put(data)
|
|
218
|
+
elif which == "session_metadata" or (
|
|
219
|
+
which is None and hasattr(resp, "session_metadata") and resp.session_metadata
|
|
220
|
+
):
|
|
221
|
+
logger.debug("Received SessionMetadata")
|
|
222
|
+
except Exception as e: # noqa: BLE001
|
|
223
|
+
if not self._closed:
|
|
224
|
+
logger.debug(f"gRPC sync stream reader encountered: {e}")
|
|
225
|
+
self._error = e
|
|
226
|
+
if is_resource_exhausted_error(e) and self._on_resource_exhausted:
|
|
227
|
+
self._on_resource_exhausted(e)
|
|
228
|
+
finally:
|
|
229
|
+
self._read_queue.put(_EOF_SENTINEL)
|
|
230
|
+
|
|
231
|
+
def sendall( # type: ignore[override]
|
|
232
|
+
self, data: Any, flags: int = 0
|
|
233
|
+
) -> None:
|
|
234
|
+
if self._direct_sock is not None:
|
|
235
|
+
self._direct_sock.sendall(data, flags)
|
|
236
|
+
return
|
|
237
|
+
if self._closed:
|
|
238
|
+
raise BrokenPipeError(errno.EPIPE, "Socket is closed")
|
|
239
|
+
if not data:
|
|
240
|
+
return
|
|
241
|
+
data_bytes = bytes(data) if not isinstance(data, bytes) else data
|
|
242
|
+
if not self._first_read_done:
|
|
243
|
+
self._write_buffer.extend(data_bytes)
|
|
244
|
+
packet = sqladmin_v1beta4.DataPacket(data=data_bytes)
|
|
245
|
+
req = sqladmin_v1beta4.StreamSqlDataRequest(data=packet)
|
|
246
|
+
self._request_queue.put(req)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def send( # type: ignore[override]
|
|
250
|
+
self, data: Any, flags: int = 0
|
|
251
|
+
) -> int:
|
|
252
|
+
if self._direct_sock is not None:
|
|
253
|
+
return self._direct_sock.send(data, flags)
|
|
254
|
+
self.sendall(data, flags)
|
|
255
|
+
return len(data)
|
|
256
|
+
|
|
257
|
+
def recv(self, bufsize: int, flags: int = 0) -> bytes:
|
|
258
|
+
if self._direct_sock is not None:
|
|
259
|
+
return self._direct_sock.recv(bufsize, flags)
|
|
260
|
+
|
|
261
|
+
if bufsize <= 0:
|
|
262
|
+
return b""
|
|
263
|
+
|
|
264
|
+
# Return from buffered chunk if available
|
|
265
|
+
if self._read_offset < len(self._read_buf):
|
|
266
|
+
remaining = len(self._read_buf) - self._read_offset
|
|
267
|
+
to_copy = min(bufsize, remaining)
|
|
268
|
+
chunk = self._read_buf[self._read_offset : self._read_offset + to_copy]
|
|
269
|
+
self._read_offset += to_copy
|
|
270
|
+
if self._read_offset >= len(self._read_buf):
|
|
271
|
+
self._read_buf = b""
|
|
272
|
+
self._read_offset = 0
|
|
273
|
+
return chunk
|
|
274
|
+
|
|
275
|
+
# Pull from incoming queue
|
|
276
|
+
try:
|
|
277
|
+
item = self._read_queue.get(block=True, timeout=self._timeout)
|
|
278
|
+
except queue.Empty:
|
|
279
|
+
if self._closed:
|
|
280
|
+
return b""
|
|
281
|
+
raise TimeoutError("timed out")
|
|
282
|
+
|
|
283
|
+
if item is _EOF_SENTINEL:
|
|
284
|
+
if self._error is not None:
|
|
285
|
+
if (
|
|
286
|
+
not self._first_read_done
|
|
287
|
+
and self._fallback_fn is not None
|
|
288
|
+
and is_failed_precondition_error(self._error)
|
|
289
|
+
):
|
|
290
|
+
logger.info(
|
|
291
|
+
f"SQL Data Service returned error before first read: {self._error}. "
|
|
292
|
+
"Triggering transparent fallback to direct TLS."
|
|
293
|
+
)
|
|
294
|
+
try:
|
|
295
|
+
self._direct_sock = self._fallback_fn()
|
|
296
|
+
self._direct_sock.settimeout(self._timeout)
|
|
297
|
+
if self._write_buffer:
|
|
298
|
+
self._direct_sock.sendall(bytes(self._write_buffer))
|
|
299
|
+
self._first_read_done = True
|
|
300
|
+
self._write_buffer.clear()
|
|
301
|
+
return self._direct_sock.recv(bufsize, flags)
|
|
302
|
+
except Exception as fallback_err:
|
|
303
|
+
raise fallback_err from self._error
|
|
304
|
+
|
|
305
|
+
raise OSError(
|
|
306
|
+
errno.ECONNRESET, f"Connection error: {self._error}"
|
|
307
|
+
) from self._error
|
|
308
|
+
return b""
|
|
309
|
+
|
|
310
|
+
self._first_read_done = True
|
|
311
|
+
self._write_buffer.clear()
|
|
312
|
+
if len(item) <= bufsize:
|
|
313
|
+
return item
|
|
314
|
+
|
|
315
|
+
self._read_buf = item
|
|
316
|
+
self._read_offset = bufsize
|
|
317
|
+
return item[:bufsize]
|
|
318
|
+
|
|
319
|
+
def recv_into(self, buffer: Any, nbytes: int = 0, flags: int = 0) -> int:
|
|
320
|
+
if self._direct_sock is not None:
|
|
321
|
+
return self._direct_sock.recv_into(buffer, nbytes, flags)
|
|
322
|
+
target_len = len(buffer) if nbytes == 0 else min(nbytes, len(buffer))
|
|
323
|
+
if target_len <= 0:
|
|
324
|
+
return 0
|
|
325
|
+
data = self.recv(target_len, flags)
|
|
326
|
+
n = len(data)
|
|
327
|
+
buffer[:n] = data
|
|
328
|
+
return n
|
|
329
|
+
|
|
330
|
+
def makefile( # type: ignore[override]
|
|
331
|
+
self,
|
|
332
|
+
mode: str = "r",
|
|
333
|
+
buffering: int = -1,
|
|
334
|
+
encoding: str | None = None,
|
|
335
|
+
errors: str | None = None,
|
|
336
|
+
newline: str | None = None,
|
|
337
|
+
) -> Any:
|
|
338
|
+
if self._direct_sock is not None:
|
|
339
|
+
return self._direct_sock.makefile( # type: ignore[call-overload]
|
|
340
|
+
mode=mode,
|
|
341
|
+
buffering=buffering,
|
|
342
|
+
encoding=encoding,
|
|
343
|
+
errors=errors,
|
|
344
|
+
newline=newline,
|
|
345
|
+
)
|
|
346
|
+
raw = SqlDataRawIO(self)
|
|
347
|
+
if buffering == 0:
|
|
348
|
+
return raw
|
|
349
|
+
|
|
350
|
+
reading = "r" in mode or "+" in mode
|
|
351
|
+
writing = "w" in mode or "a" in mode or "+" in mode
|
|
352
|
+
binary = "b" in mode
|
|
353
|
+
|
|
354
|
+
buf: Any
|
|
355
|
+
if reading and writing:
|
|
356
|
+
buf = io.BufferedRWPair(raw, raw)
|
|
357
|
+
elif reading:
|
|
358
|
+
buffer_size = io.DEFAULT_BUFFER_SIZE if buffering <= 0 else buffering
|
|
359
|
+
buf = io.BufferedReader(raw, buffer_size=buffer_size)
|
|
360
|
+
elif writing:
|
|
361
|
+
buffer_size = io.DEFAULT_BUFFER_SIZE if buffering <= 0 else buffering
|
|
362
|
+
buf = io.BufferedWriter(raw, buffer_size=buffer_size)
|
|
363
|
+
else:
|
|
364
|
+
buffer_size = io.DEFAULT_BUFFER_SIZE if buffering <= 0 else buffering
|
|
365
|
+
buf = io.BufferedReader(raw, buffer_size=buffer_size)
|
|
366
|
+
|
|
367
|
+
if binary:
|
|
368
|
+
return buf
|
|
369
|
+
return io.TextIOWrapper(
|
|
370
|
+
buf, encoding=encoding, errors=errors, newline=newline
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def settimeout(self, value: float | None) -> None:
|
|
374
|
+
if value is not None and value < 0:
|
|
375
|
+
raise ValueError("Timeout value must be non-negative")
|
|
376
|
+
self._timeout = value
|
|
377
|
+
if self._direct_sock is not None:
|
|
378
|
+
self._direct_sock.settimeout(value)
|
|
379
|
+
|
|
380
|
+
def gettimeout(self) -> float | None:
|
|
381
|
+
if self._direct_sock is not None:
|
|
382
|
+
return self._direct_sock.gettimeout()
|
|
383
|
+
return self._timeout
|
|
384
|
+
|
|
385
|
+
def setblocking(self, flag: bool) -> None:
|
|
386
|
+
self._timeout = None if flag else 0.0
|
|
387
|
+
if self._direct_sock is not None:
|
|
388
|
+
self._direct_sock.setblocking(flag)
|
|
389
|
+
|
|
390
|
+
def connect(self, *args: Any, **kwargs: Any) -> None:
|
|
391
|
+
# Already connected, no-op for driver compatibility (e.g. pymysql)
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
def connect_ex(self, *args: Any, **kwargs: Any) -> int:
|
|
395
|
+
return 0
|
|
396
|
+
|
|
397
|
+
def setsockopt(self, *args: Any, **kwargs: Any) -> None:
|
|
398
|
+
if self._direct_sock is not None:
|
|
399
|
+
self._direct_sock.setsockopt(*args, **kwargs)
|
|
400
|
+
|
|
401
|
+
def getsockopt(self, *args: Any, **kwargs: Any) -> Any: # type: ignore[override]
|
|
402
|
+
if self._direct_sock is not None:
|
|
403
|
+
return self._direct_sock.getsockopt(*args, **kwargs)
|
|
404
|
+
return 0
|
|
405
|
+
|
|
406
|
+
def getsockname(self) -> tuple[str, int]:
|
|
407
|
+
if self._direct_sock is not None:
|
|
408
|
+
return self._direct_sock.getsockname()
|
|
409
|
+
return ("127.0.0.1", SERVER_PROXY_PORT)
|
|
410
|
+
|
|
411
|
+
def getpeername(self) -> tuple[str, int]:
|
|
412
|
+
if self._direct_sock is not None:
|
|
413
|
+
return self._direct_sock.getpeername()
|
|
414
|
+
return ("127.0.0.1", SERVER_PROXY_PORT)
|
|
415
|
+
|
|
416
|
+
def shutdown(self, how: int = socket.SHUT_RDWR) -> None:
|
|
417
|
+
self.close()
|
|
418
|
+
|
|
419
|
+
def close(self) -> None:
|
|
420
|
+
with self._close_lock:
|
|
421
|
+
if self._closed:
|
|
422
|
+
return
|
|
423
|
+
self._closed = True
|
|
424
|
+
|
|
425
|
+
if self._direct_sock is not None:
|
|
426
|
+
try:
|
|
427
|
+
self._direct_sock.close()
|
|
428
|
+
except Exception: # noqa: BLE001, S110
|
|
429
|
+
pass
|
|
430
|
+
|
|
431
|
+
self._request_queue.close()
|
|
432
|
+
self._read_queue.put(_EOF_SENTINEL)
|
|
433
|
+
try:
|
|
434
|
+
if hasattr(self._response_stream, "cancel"):
|
|
435
|
+
self._response_stream.cancel()
|
|
436
|
+
except Exception: # noqa: BLE001, S110
|
|
437
|
+
pass
|
|
438
|
+
try:
|
|
439
|
+
if (
|
|
440
|
+
self._grpc_client is not None
|
|
441
|
+
and hasattr(self._grpc_client, "transport")
|
|
442
|
+
and hasattr(self._grpc_client.transport, "close")
|
|
443
|
+
):
|
|
444
|
+
self._grpc_client.transport.close()
|
|
445
|
+
except Exception: # noqa: BLE001, S110
|
|
446
|
+
pass
|
|
447
|
+
try:
|
|
448
|
+
super().close()
|
|
449
|
+
except Exception: # noqa: BLE001, S110
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
if self._on_close:
|
|
453
|
+
try:
|
|
454
|
+
self._on_close()
|
|
455
|
+
except Exception: # noqa: BLE001, S110
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class SqlDataClient:
|
|
460
|
+
"""Client that establishes direct synchronous gRPC SqlDataService connections."""
|
|
461
|
+
|
|
462
|
+
def __init__(
|
|
463
|
+
self,
|
|
464
|
+
endpoint: str,
|
|
465
|
+
credentials: Credentials,
|
|
466
|
+
quota_project: str | None = None,
|
|
467
|
+
timeout: float | None = None,
|
|
468
|
+
) -> None:
|
|
469
|
+
self._endpoint = endpoint
|
|
470
|
+
self._credentials = credentials
|
|
471
|
+
self._quota_project = quota_project
|
|
472
|
+
self._timeout = timeout
|
|
473
|
+
self._grpc_client: sqladmin_v1beta4.SqlDataServiceClient | None = None
|
|
474
|
+
self._active_sockets: set[SqlDataSocket] = set()
|
|
475
|
+
self._on_close_callbacks: list[Callable[[], None]] = []
|
|
476
|
+
|
|
477
|
+
async def connect(
|
|
478
|
+
self,
|
|
479
|
+
instance_connection_name: str,
|
|
480
|
+
region: str,
|
|
481
|
+
project: str,
|
|
482
|
+
get_conn_info: Callable[[], Any],
|
|
483
|
+
enable_iam_auth: bool,
|
|
484
|
+
on_fallback: Callable[[str], None],
|
|
485
|
+
is_fallback_cached: Callable[[str], bool],
|
|
486
|
+
on_resource_exhausted: Callable[[Exception], None] | None = None,
|
|
487
|
+
on_success: Callable[[], None] | None = None,
|
|
488
|
+
connect_timeout: float = 30.0,
|
|
489
|
+
) -> socket.socket:
|
|
490
|
+
"""Connects via synchronous gRPC and returns a SqlDataSocket or direct TLS fallback socket."""
|
|
491
|
+
use_fallback = is_fallback_cached(instance_connection_name)
|
|
492
|
+
|
|
493
|
+
async def get_direct_socket_sync_factory() -> Callable[[], socket.socket]:
|
|
494
|
+
conn_info = await get_conn_info()
|
|
495
|
+
targets: list[str] = []
|
|
496
|
+
for t in [IPTypes.PRIVATE, IPTypes.PSC, IPTypes.PUBLIC]:
|
|
497
|
+
try:
|
|
498
|
+
targets.extend(conn_info.get_preferred_ips(t))
|
|
499
|
+
except CloudSQLIPTypeError as e:
|
|
500
|
+
logger.debug(f"IP type {t} not available: {e}")
|
|
501
|
+
continue
|
|
502
|
+
if not targets:
|
|
503
|
+
raise ValueError(
|
|
504
|
+
"Cannot fallback to direct connection: no IP address available."
|
|
505
|
+
)
|
|
506
|
+
ssl_context = await conn_info.create_ssl_context(enable_iam_auth)
|
|
507
|
+
|
|
508
|
+
def create_direct_sock() -> socket.socket:
|
|
509
|
+
last_ex: Exception | None = None
|
|
510
|
+
for target_ip in targets:
|
|
511
|
+
logger.debug(f"Direct TLS connecting to {target_ip}:{SERVER_PROXY_PORT}")
|
|
512
|
+
try:
|
|
513
|
+
raw_sock = socket.create_connection(
|
|
514
|
+
(target_ip, SERVER_PROXY_PORT), timeout=connect_timeout
|
|
515
|
+
)
|
|
516
|
+
ssl_sock = ssl_context.wrap_socket(
|
|
517
|
+
raw_sock, server_hostname=target_ip
|
|
518
|
+
)
|
|
519
|
+
ssl_sock.settimeout(None)
|
|
520
|
+
return ssl_sock
|
|
521
|
+
except Exception as e: # noqa: BLE001
|
|
522
|
+
logger.debug(f"Direct TLS connection to {target_ip} failed: {e}")
|
|
523
|
+
last_ex = e
|
|
524
|
+
if last_ex:
|
|
525
|
+
raise last_ex
|
|
526
|
+
raise ValueError(
|
|
527
|
+
"Cannot fallback to direct connection: no IP address available."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
return create_direct_sock
|
|
531
|
+
|
|
532
|
+
if use_fallback:
|
|
533
|
+
logger.debug("Using cached fallback direct TLS connection")
|
|
534
|
+
create_direct = await get_direct_socket_sync_factory()
|
|
535
|
+
return create_direct()
|
|
536
|
+
|
|
537
|
+
# Connect synchronous gRPC stream via shared GAPIC client
|
|
538
|
+
if self._grpc_client is None:
|
|
539
|
+
endpoint = self._endpoint.removeprefix("https://").removeprefix("http://")
|
|
540
|
+
client_options = ClientOptions(
|
|
541
|
+
api_endpoint=endpoint,
|
|
542
|
+
quota_project_id=self._quota_project,
|
|
543
|
+
)
|
|
544
|
+
self._grpc_client = sqladmin_v1beta4.SqlDataServiceClient(
|
|
545
|
+
credentials=self._credentials,
|
|
546
|
+
client_options=client_options,
|
|
547
|
+
)
|
|
548
|
+
grpc_client = self._grpc_client
|
|
549
|
+
|
|
550
|
+
instance_id = (
|
|
551
|
+
f"projects/{project}/instances/{instance_connection_name.split(':')[-1]}"
|
|
552
|
+
)
|
|
553
|
+
location_id = f"locations/{region}"
|
|
554
|
+
|
|
555
|
+
metadata = [
|
|
556
|
+
(
|
|
557
|
+
"x-goog-request-params",
|
|
558
|
+
f"instance_id={instance_id}&location_id={location_id}",
|
|
559
|
+
)
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
loop = asyncio.get_running_loop()
|
|
563
|
+
|
|
564
|
+
def fallback_fn() -> socket.socket:
|
|
565
|
+
on_fallback(instance_connection_name)
|
|
566
|
+
try:
|
|
567
|
+
create_direct_sock_fn = asyncio.run_coroutine_threadsafe(
|
|
568
|
+
get_direct_socket_sync_factory(), loop
|
|
569
|
+
).result(timeout=connect_timeout)
|
|
570
|
+
except Exception as e:
|
|
571
|
+
logger.debug(f"Could not prepare direct fallback factory: {e}")
|
|
572
|
+
raise ValueError(
|
|
573
|
+
"No direct fallback connection factory available."
|
|
574
|
+
) from e
|
|
575
|
+
return create_direct_sock_fn()
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
request_queue = _RequestQueue()
|
|
579
|
+
start_session = sqladmin_v1beta4.StartSession(
|
|
580
|
+
instance_id=instance_id, location_id=location_id
|
|
581
|
+
)
|
|
582
|
+
req = sqladmin_v1beta4.StreamSqlDataRequest(
|
|
583
|
+
start_session=start_session
|
|
584
|
+
)
|
|
585
|
+
request_queue.put(req)
|
|
586
|
+
|
|
587
|
+
response_stream: Any
|
|
588
|
+
if hasattr(grpc_client, "transport") and hasattr(
|
|
589
|
+
grpc_client.transport, "stream_sql_data"
|
|
590
|
+
):
|
|
591
|
+
response_stream = grpc_client.transport.stream_sql_data( # type: ignore[call-arg]
|
|
592
|
+
request_queue, # type: ignore[arg-type]
|
|
593
|
+
metadata=metadata,
|
|
594
|
+
timeout=self._timeout,
|
|
595
|
+
)
|
|
596
|
+
else:
|
|
597
|
+
response_stream = grpc_client.stream_sql_data( # type: ignore[arg-type]
|
|
598
|
+
requests=request_queue, metadata=metadata, timeout=self._timeout
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
sock = SqlDataSocket(
|
|
602
|
+
request_queue=request_queue,
|
|
603
|
+
response_stream=response_stream,
|
|
604
|
+
grpc_client=None,
|
|
605
|
+
timeout=connect_timeout,
|
|
606
|
+
on_close=lambda: self._active_sockets.discard(sock),
|
|
607
|
+
on_success=on_success,
|
|
608
|
+
on_resource_exhausted=on_resource_exhausted,
|
|
609
|
+
fallback_fn=fallback_fn,
|
|
610
|
+
)
|
|
611
|
+
self._active_sockets.add(sock)
|
|
612
|
+
return sock
|
|
613
|
+
|
|
614
|
+
except Exception as e:
|
|
615
|
+
logger.debug(f"Sync gRPC connection attempt failed: {e}")
|
|
616
|
+
if is_resource_exhausted_error(e):
|
|
617
|
+
if on_resource_exhausted:
|
|
618
|
+
on_resource_exhausted(e)
|
|
619
|
+
raise
|
|
620
|
+
|
|
621
|
+
if not is_failed_precondition_error(e):
|
|
622
|
+
raise
|
|
623
|
+
|
|
624
|
+
# Fallback to direct TLS on FAILED_PRECONDITION
|
|
625
|
+
logger.info(
|
|
626
|
+
f"SQL Data Service connection failed for {instance_connection_name}. "
|
|
627
|
+
"Falling back to direct TLS connection."
|
|
628
|
+
)
|
|
629
|
+
on_fallback(instance_connection_name)
|
|
630
|
+
create_direct = await get_direct_socket_sync_factory()
|
|
631
|
+
return create_direct()
|
|
632
|
+
|
|
633
|
+
async def close(self) -> None:
|
|
634
|
+
"""Closes all active sockets created by this client."""
|
|
635
|
+
for sock in list(self._active_sockets):
|
|
636
|
+
try:
|
|
637
|
+
sock.close()
|
|
638
|
+
except Exception: # noqa: BLE001, S110
|
|
639
|
+
pass
|
|
640
|
+
self._active_sockets.clear()
|
|
641
|
+
if self._grpc_client is not None:
|
|
642
|
+
try:
|
|
643
|
+
if hasattr(self._grpc_client, "transport") and hasattr(
|
|
644
|
+
self._grpc_client.transport, "close"
|
|
645
|
+
):
|
|
646
|
+
self._grpc_client.transport.close()
|
|
647
|
+
except Exception: # noqa: BLE001, S110
|
|
648
|
+
pass
|
|
649
|
+
self._grpc_client = None
|
|
650
|
+
for cb in self._on_close_callbacks:
|
|
651
|
+
try:
|
|
652
|
+
cb()
|
|
653
|
+
except Exception: # noqa: BLE001, S110
|
|
654
|
+
pass
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
cloud_sql_python_connector-1.22.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
2
|
-
google/cloud/sql/connector/__init__.py,sha256=pfa-FD-VRpYI49OUul9p6UFDp7G_yT2f6WS9rZMn8CM,1148
|
|
3
|
-
google/cloud/sql/connector/asyncpg.py,sha256=MpZhwFke0WXvYD3OcEF1Ok0qHl-CU7ZIPy85PCog5QY,2002
|
|
4
|
-
google/cloud/sql/connector/client.py,sha256=he2qm0cUQD4FGzMfT6eBye-KLEQtFdQsedsawNUhm7c,14598
|
|
5
|
-
google/cloud/sql/connector/connection_info.py,sha256=INQMBXGxXSth2MsAnqe9e9pCOiqMj55V3fzwxCmoAZc,5399
|
|
6
|
-
google/cloud/sql/connector/connection_name.py,sha256=SCCVeWb8YN0f9My09_l9FGTr6iRbi1b_52TJQaS6xzM,2574
|
|
7
|
-
google/cloud/sql/connector/connector.py,sha256=dEn9rN5fzRx4BE891f-lBeEpRNi7MhnkTvkuq5TLoMY,28040
|
|
8
|
-
google/cloud/sql/connector/enums.py,sha256=wESKVNRFNwKk-Bs093zM6YSCGEgaDHu3yLBscWuKpn8,2994
|
|
9
|
-
google/cloud/sql/connector/exceptions.py,sha256=hFdDZRR48qLYSFI_CRxbftrKkt77Jl5ci6SEHcAGMok,2209
|
|
10
|
-
google/cloud/sql/connector/instance.py,sha256=xz-TLSaYOyUQdbKsv2m-MDvA2kjJCkcaj3GXfsYIw2g,8691
|
|
11
|
-
google/cloud/sql/connector/lazy.py,sha256=N3Ud8KobhASnsH0pACG8oEHjmS7Z7MVzkHJ9skDLxMo,4932
|
|
12
|
-
google/cloud/sql/connector/monitored_cache.py,sha256=n-nG19VZ7hfzz0Dbg245mrkhRga1f-bP56aRku212mk,6656
|
|
13
|
-
google/cloud/sql/connector/pg8000.py,sha256=tbqFSIoSguMX1hTZ4kWeBOKLhLHKX1Vq5SespfA0Y0w,1801
|
|
14
|
-
google/cloud/sql/connector/psycopg.py,sha256=LcGencGQlQPDKWTeQrk1SiLMtVB09uZw6RdPQAosC8k,8038
|
|
15
|
-
google/cloud/sql/connector/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
google/cloud/sql/connector/pymysql.py,sha256=MOau37nTOTesUR5Hor7lLtEckvvGThzjECEWQYHcM8U,2166
|
|
17
|
-
google/cloud/sql/connector/pytds.py,sha256=e-KBCqv0m3a7cRi9YmFHNuYqClhlJbf-KoF611w-1Ck,2304
|
|
18
|
-
google/cloud/sql/connector/rate_limiter.py,sha256=60k-r9h-nUBqovVIHnIGbW0NMgUGD4exH_rZ02ukNa0,2924
|
|
19
|
-
google/cloud/sql/connector/refresh_utils.py,sha256=kouF2OJc-NEu7MQS94GVJ7jN04-7a_wLKL-46kAVQuM,5219
|
|
20
|
-
google/cloud/sql/connector/resolver.py,sha256=4U8G0Pxwmy64qoRmP5shLnmIykxcGcGCoFSH_7tr120,6353
|
|
21
|
-
google/cloud/sql/connector/utils.py,sha256=ApLG4z4H0S4lWXQ9PyclXbrEoY5VrshqN9QB6BG8lyg,4132
|
|
22
|
-
google/cloud/sql/connector/version.py,sha256=0JV131RUM4deaBtRNiyn9A-CSQMDjxlhfH0wdHi3fGQ,598
|
|
23
|
-
cloud_sql_python_connector-1.22.0.dist-info/METADATA,sha256=Sn1H3KwvLEFOSGWPJY5APuFykTx73RLrk5DjCCP_t50,31731
|
|
24
|
-
cloud_sql_python_connector-1.22.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
25
|
-
cloud_sql_python_connector-1.22.0.dist-info/top_level.txt,sha256=_1QvSJIhFAGfxb79D6DhB7SUw2X6T4rwnz_LLrbcD3c,7
|
|
26
|
-
cloud_sql_python_connector-1.22.0.dist-info/RECORD,,
|
{cloud_sql_python_connector-1.22.0.dist-info → cloud_sql_python_connector-1.23.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|