data-connect-hub 0.1.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.
- data_connect_hub/__init__.py +75 -0
- data_connect_hub/_auth.py +72 -0
- data_connect_hub/_flight.py +342 -0
- data_connect_hub/_rest.py +331 -0
- data_connect_hub/client.py +326 -0
- data_connect_hub/exceptions.py +88 -0
- data_connect_hub/models.py +146 -0
- data_connect_hub/py.typed +0 -0
- data_connect_hub-0.1.0.dist-info/METADATA +316 -0
- data_connect_hub-0.1.0.dist-info/RECORD +13 -0
- data_connect_hub-0.1.0.dist-info/WHEEL +5 -0
- data_connect_hub-0.1.0.dist-info/licenses/LICENSE +201 -0
- data_connect_hub-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Data Connect Hub Python SDK."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from .client import DataConnectClient
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
DCHAuthenticationError,
|
|
8
|
+
DCHConfigError,
|
|
9
|
+
DCHConnectionError,
|
|
10
|
+
DCHError,
|
|
11
|
+
DCHForbiddenError,
|
|
12
|
+
DCHHTTPError,
|
|
13
|
+
DCHNoDataError,
|
|
14
|
+
DCHNotFoundError,
|
|
15
|
+
DCHQueryError,
|
|
16
|
+
DCHResponseError,
|
|
17
|
+
DCHServerError,
|
|
18
|
+
DCHTimeoutError,
|
|
19
|
+
DCHValidationError,
|
|
20
|
+
)
|
|
21
|
+
from .models import (
|
|
22
|
+
Capabilities,
|
|
23
|
+
ConnectionType,
|
|
24
|
+
ConnectionTypeStatus,
|
|
25
|
+
CreateConnectionRequest,
|
|
26
|
+
CreateConnectionTypeRequest,
|
|
27
|
+
CredentialField,
|
|
28
|
+
CredentialsRef,
|
|
29
|
+
DataConnection,
|
|
30
|
+
DataConnectionState,
|
|
31
|
+
DataConnectionStatus,
|
|
32
|
+
DataFormat,
|
|
33
|
+
EnumValue,
|
|
34
|
+
UpdateConnectionRequest,
|
|
35
|
+
UpdateConnectionTypeRequest,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
__version__ = version("data-connect-hub")
|
|
40
|
+
except PackageNotFoundError:
|
|
41
|
+
# Imported from a source tree without an installed distribution. Keep this
|
|
42
|
+
# PEP 440 parseable so callers can compare it without special-casing.
|
|
43
|
+
__version__ = "0.0.0+unknown"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Capabilities",
|
|
47
|
+
"ConnectionType",
|
|
48
|
+
"ConnectionTypeStatus",
|
|
49
|
+
"CreateConnectionRequest",
|
|
50
|
+
"CreateConnectionTypeRequest",
|
|
51
|
+
"CredentialField",
|
|
52
|
+
"CredentialsRef",
|
|
53
|
+
"DCHAuthenticationError",
|
|
54
|
+
"DCHConfigError",
|
|
55
|
+
"DCHConnectionError",
|
|
56
|
+
"DCHError",
|
|
57
|
+
"DCHForbiddenError",
|
|
58
|
+
"DCHHTTPError",
|
|
59
|
+
"DCHNoDataError",
|
|
60
|
+
"DCHNotFoundError",
|
|
61
|
+
"DCHQueryError",
|
|
62
|
+
"DCHResponseError",
|
|
63
|
+
"DCHServerError",
|
|
64
|
+
"DCHTimeoutError",
|
|
65
|
+
"DCHValidationError",
|
|
66
|
+
"DataConnectClient",
|
|
67
|
+
"DataConnection",
|
|
68
|
+
"DataConnectionState",
|
|
69
|
+
"DataConnectionStatus",
|
|
70
|
+
"DataFormat",
|
|
71
|
+
"EnumValue",
|
|
72
|
+
"UpdateConnectionRequest",
|
|
73
|
+
"UpdateConnectionTypeRequest",
|
|
74
|
+
"__version__",
|
|
75
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Header construction and token caching for REST and Flight SQL authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from .exceptions import DCHConfigError
|
|
9
|
+
|
|
10
|
+
_BEARER_PREFIX = "Bearer "
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _normalize_token(token: str) -> str:
|
|
14
|
+
"""Ensure *token* carries the ``Bearer`` prefix exactly once.
|
|
15
|
+
|
|
16
|
+
Raises ``DCHConfigError`` if the token appears to use a non-Bearer
|
|
17
|
+
auth scheme (e.g. ``Basic``).
|
|
18
|
+
"""
|
|
19
|
+
if not token:
|
|
20
|
+
return token
|
|
21
|
+
|
|
22
|
+
while token.upper().startswith(_BEARER_PREFIX.upper()):
|
|
23
|
+
token = token[len(_BEARER_PREFIX) :]
|
|
24
|
+
|
|
25
|
+
detected_scheme = token.split(None, 1)[0] if token else ""
|
|
26
|
+
if detected_scheme.lower() in ("basic", "digest"):
|
|
27
|
+
raise DCHConfigError(f"Unsupported auth scheme {detected_scheme!r} — pass only the raw Bearer token value")
|
|
28
|
+
|
|
29
|
+
return f"{_BEARER_PREFIX}{token}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_headers(
|
|
33
|
+
*,
|
|
34
|
+
token: str,
|
|
35
|
+
tenant_id: str,
|
|
36
|
+
) -> dict[str, str]:
|
|
37
|
+
"""Build standard HTTP headers for authentication."""
|
|
38
|
+
headers: dict[str, str] = {}
|
|
39
|
+
if token:
|
|
40
|
+
headers["Authorization"] = _normalize_token(token)
|
|
41
|
+
if tenant_id:
|
|
42
|
+
headers["x-tenant-id"] = tenant_id
|
|
43
|
+
return headers
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TokenCache:
|
|
47
|
+
"""Thread-safe cache that calls a provider once and reuses the token until refreshed."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, provider: Callable[[], str]) -> None:
|
|
50
|
+
self._provider = provider
|
|
51
|
+
self._token: str | None = None
|
|
52
|
+
self._lock = threading.Lock()
|
|
53
|
+
|
|
54
|
+
def _call_provider(self) -> str:
|
|
55
|
+
try:
|
|
56
|
+
return self._provider()
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
raise DCHConfigError(f"Token provider failed: {exc}") from exc
|
|
59
|
+
|
|
60
|
+
def get(self) -> str:
|
|
61
|
+
with self._lock:
|
|
62
|
+
if self._token is None:
|
|
63
|
+
self._token = self._call_provider()
|
|
64
|
+
return self._token
|
|
65
|
+
|
|
66
|
+
def refresh(self) -> str:
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._token = self._call_provider()
|
|
69
|
+
return self._token
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
ADBC_HEADER_PREFIX = "adbc.flight.sql.rpc.call_header."
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Arrow Flight SQL client for tabular data queries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Callable, Generator, Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
import adbc_driver_flightsql.dbapi as flight_dbapi
|
|
15
|
+
import pyarrow as pa
|
|
16
|
+
import pyarrow.flight as flight
|
|
17
|
+
from adbc_driver_flightsql import DatabaseOptions
|
|
18
|
+
|
|
19
|
+
from ._auth import ADBC_HEADER_PREFIX, TokenCache, build_headers
|
|
20
|
+
from .exceptions import DCHConfigError, DCHConnectionError, DCHQueryError
|
|
21
|
+
|
|
22
|
+
_GRPC_UNAUTHENTICATED = "unauthenticated"
|
|
23
|
+
_CMD_GET_TABLES_TYPE_URL = "type.googleapis.com/arrow.flight.protocol.sql.CommandGetTables"
|
|
24
|
+
|
|
25
|
+
_QUERY_TIMEOUT = DatabaseOptions.TIMEOUT_QUERY.value
|
|
26
|
+
_FETCH_TIMEOUT = DatabaseOptions.TIMEOUT_FETCH.value
|
|
27
|
+
_TLS_ROOT_CERTS = DatabaseOptions.TLS_ROOT_CERTS.value
|
|
28
|
+
_TLS_SKIP_VERIFY = DatabaseOptions.TLS_SKIP_VERIFY.value
|
|
29
|
+
|
|
30
|
+
_FLIGHT_DISABLE_SERVER_VERIFICATION = "disable_server_verification"
|
|
31
|
+
_FLIGHT_TLS_ROOT_CERTS = "tls_root_certs"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_auth_error(exc: Exception) -> bool:
|
|
35
|
+
return _GRPC_UNAUTHENTICATED in str(exc).lower()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FlightClient:
|
|
39
|
+
"""Thin wrapper around ADBC Flight SQL for Data Connect Hub queries.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
url : str
|
|
44
|
+
gRPC endpoint, e.g. ``grpc://host:50051`` or ``grpc+tls://host:50051``.
|
|
45
|
+
token : str
|
|
46
|
+
Static Bearer token value.
|
|
47
|
+
tenant_id : str
|
|
48
|
+
Tenant identifier.
|
|
49
|
+
token_provider : Callable[[], str], optional
|
|
50
|
+
A callable that returns a valid Bearer token string. The SDK calls
|
|
51
|
+
this once and caches the result. On authentication failure, the
|
|
52
|
+
token is refreshed automatically. Mutually exclusive with *token*.
|
|
53
|
+
timeout : float, optional
|
|
54
|
+
Timeout in seconds for Flight SQL RPC calls (applies to both query and fetch).
|
|
55
|
+
ca_cert : str, optional
|
|
56
|
+
Path to a PEM-encoded CA certificate file for TLS verification.
|
|
57
|
+
insecure : bool
|
|
58
|
+
Skip TLS certificate verification (default False).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
url: str,
|
|
64
|
+
token: str = "",
|
|
65
|
+
tenant_id: str = "",
|
|
66
|
+
*,
|
|
67
|
+
token_provider: Callable[[], str] | None = None,
|
|
68
|
+
timeout: float | None = None,
|
|
69
|
+
ca_cert: str | None = None,
|
|
70
|
+
insecure: bool = False,
|
|
71
|
+
) -> None:
|
|
72
|
+
if token and token_provider:
|
|
73
|
+
raise DCHConfigError(
|
|
74
|
+
"Cannot specify both 'token' and 'token_provider'."
|
|
75
|
+
" Please provide either a static token or a token_provider callable, not both."
|
|
76
|
+
)
|
|
77
|
+
self._url = url
|
|
78
|
+
self._tenant_id = tenant_id
|
|
79
|
+
self._token_cache: TokenCache | None = TokenCache(token_provider) if token_provider else None
|
|
80
|
+
self._insecure = insecure
|
|
81
|
+
self._tls_root_certs: str | None = None
|
|
82
|
+
self._adbc_opts: dict[str, str] = {}
|
|
83
|
+
if timeout is not None:
|
|
84
|
+
self._adbc_opts[_QUERY_TIMEOUT] = str(timeout)
|
|
85
|
+
self._adbc_opts[_FETCH_TIMEOUT] = str(timeout)
|
|
86
|
+
if insecure:
|
|
87
|
+
self._adbc_opts[_TLS_SKIP_VERIFY] = "true"
|
|
88
|
+
elif ca_cert:
|
|
89
|
+
cert_path = Path(ca_cert)
|
|
90
|
+
if not cert_path.is_file():
|
|
91
|
+
raise DCHConfigError(f"CA certificate file not found: {ca_cert}")
|
|
92
|
+
self._tls_root_certs = cert_path.read_text()
|
|
93
|
+
self._adbc_opts[_TLS_ROOT_CERTS] = self._tls_root_certs
|
|
94
|
+
self._static_headers: dict[str, str] = {}
|
|
95
|
+
if not token_provider:
|
|
96
|
+
self._static_headers = build_headers(token=token, tenant_id=tenant_id)
|
|
97
|
+
|
|
98
|
+
def _headers(self) -> dict[str, str]:
|
|
99
|
+
if self._token_cache:
|
|
100
|
+
return build_headers(token=self._token_cache.get(), tenant_id=self._tenant_id)
|
|
101
|
+
return dict(self._static_headers)
|
|
102
|
+
|
|
103
|
+
def _base_kwargs(self) -> dict[str, str]:
|
|
104
|
+
prefixed = {f"{ADBC_HEADER_PREFIX}{k.lower()}": v for k, v in self._headers().items()}
|
|
105
|
+
return {**self._adbc_opts, **prefixed}
|
|
106
|
+
|
|
107
|
+
def _call_options(self) -> flight.FlightCallOptions:
|
|
108
|
+
return flight.FlightCallOptions(headers=[(k.lower().encode(), v.encode()) for k, v in self._headers().items()])
|
|
109
|
+
|
|
110
|
+
def _flight_connect(self) -> flight.FlightClient:
|
|
111
|
+
kwargs: dict[str, Any] = {}
|
|
112
|
+
if self._insecure:
|
|
113
|
+
kwargs[_FLIGHT_DISABLE_SERVER_VERIFICATION] = True
|
|
114
|
+
if self._tls_root_certs:
|
|
115
|
+
kwargs[_FLIGHT_TLS_ROOT_CERTS] = self._tls_root_certs.encode()
|
|
116
|
+
try:
|
|
117
|
+
return flight.connect(self._url, **kwargs)
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
raise DCHConnectionError(str(exc)) from exc
|
|
120
|
+
|
|
121
|
+
def _connect(self, connection_id: str) -> flight_dbapi.Connection:
|
|
122
|
+
db_kwargs = {
|
|
123
|
+
**self._base_kwargs(),
|
|
124
|
+
f"{ADBC_HEADER_PREFIX}x-data-connection-id": connection_id,
|
|
125
|
+
}
|
|
126
|
+
try:
|
|
127
|
+
return flight_dbapi.connect(self._url, db_kwargs=db_kwargs)
|
|
128
|
+
except flight_dbapi.Error as exc:
|
|
129
|
+
raise DCHConnectionError(str(exc)) from exc
|
|
130
|
+
|
|
131
|
+
def read(self, sql: str, connection_id: str, *, parameters: Sequence[Any] | None = None) -> pa.Table:
|
|
132
|
+
"""Execute *sql* and return the full result as a PyArrow Table."""
|
|
133
|
+
try:
|
|
134
|
+
return self._do_read(sql, connection_id, parameters=parameters)
|
|
135
|
+
except DCHConnectionError as exc:
|
|
136
|
+
if self._token_cache is not None and _is_auth_error(exc):
|
|
137
|
+
self._token_cache.refresh()
|
|
138
|
+
return self._do_read(sql, connection_id, parameters=parameters)
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
def _do_read(self, sql: str, connection_id: str, *, parameters: Sequence[Any] | None = None) -> pa.Table:
|
|
142
|
+
conn = self._connect(connection_id)
|
|
143
|
+
try:
|
|
144
|
+
cursor = conn.cursor()
|
|
145
|
+
try:
|
|
146
|
+
cursor.execute(sql, parameters)
|
|
147
|
+
return cursor.fetch_arrow_table()
|
|
148
|
+
except flight_dbapi.Error as exc:
|
|
149
|
+
raise DCHQueryError(str(exc)) from exc
|
|
150
|
+
finally:
|
|
151
|
+
with contextlib.suppress(Exception):
|
|
152
|
+
cursor.close()
|
|
153
|
+
finally:
|
|
154
|
+
conn.close()
|
|
155
|
+
|
|
156
|
+
def read_batches(
|
|
157
|
+
self, sql: str, connection_id: str, *, parameters: Sequence[Any] | None = None
|
|
158
|
+
) -> Generator[pa.RecordBatch, None, None]:
|
|
159
|
+
"""Execute *sql* and return a streaming iterator of RecordBatches.
|
|
160
|
+
|
|
161
|
+
Yields one :class:`pyarrow.RecordBatch` per iteration. The
|
|
162
|
+
connection is opened and the cursor closed automatically when the
|
|
163
|
+
generator is first iterated and when it is exhausted or closed::
|
|
164
|
+
|
|
165
|
+
for batch in client.read_batches("SELECT ...", "conn-1"):
|
|
166
|
+
process(batch)
|
|
167
|
+
"""
|
|
168
|
+
return self._iter_batches(sql, connection_id, parameters)
|
|
169
|
+
|
|
170
|
+
def _iter_batches(
|
|
171
|
+
self, sql: str, connection_id: str, parameters: Sequence[Any] | None
|
|
172
|
+
) -> Generator[pa.RecordBatch, None, None]:
|
|
173
|
+
try:
|
|
174
|
+
conn = self._connect(connection_id)
|
|
175
|
+
except DCHConnectionError as exc:
|
|
176
|
+
if self._token_cache is not None and _is_auth_error(exc):
|
|
177
|
+
self._token_cache.refresh()
|
|
178
|
+
conn = self._connect(connection_id)
|
|
179
|
+
else:
|
|
180
|
+
raise
|
|
181
|
+
try:
|
|
182
|
+
cursor = conn.cursor()
|
|
183
|
+
try:
|
|
184
|
+
cursor.execute(sql, parameters)
|
|
185
|
+
reader = cursor.fetch_record_batch()
|
|
186
|
+
yield from reader
|
|
187
|
+
except flight_dbapi.Error as exc:
|
|
188
|
+
raise DCHQueryError(str(exc)) from exc
|
|
189
|
+
finally:
|
|
190
|
+
with contextlib.suppress(Exception):
|
|
191
|
+
cursor.close()
|
|
192
|
+
finally:
|
|
193
|
+
with contextlib.suppress(Exception):
|
|
194
|
+
conn.close()
|
|
195
|
+
|
|
196
|
+
def read_pandas(self, sql: str, connection_id: str, *, parameters: Sequence[Any] | None = None) -> pd.DataFrame:
|
|
197
|
+
"""Execute *sql* and return the result as a pandas DataFrame."""
|
|
198
|
+
return self.read(sql, connection_id, parameters=parameters).to_pandas()
|
|
199
|
+
|
|
200
|
+
def server_info(self) -> dict[str, Any]:
|
|
201
|
+
"""Return Flight SQL server metadata."""
|
|
202
|
+
try:
|
|
203
|
+
return self._do_server_info()
|
|
204
|
+
except DCHConnectionError as exc:
|
|
205
|
+
if self._token_cache is not None and _is_auth_error(exc):
|
|
206
|
+
self._token_cache.refresh()
|
|
207
|
+
return self._do_server_info()
|
|
208
|
+
raise
|
|
209
|
+
|
|
210
|
+
def _do_server_info(self) -> dict[str, Any]:
|
|
211
|
+
try:
|
|
212
|
+
conn = flight_dbapi.connect(self._url, db_kwargs=self._base_kwargs())
|
|
213
|
+
except flight_dbapi.Error as exc:
|
|
214
|
+
raise DCHConnectionError(str(exc)) from exc
|
|
215
|
+
try:
|
|
216
|
+
info: dict[str, Any] = {str(k): v for k, v in conn.adbc_get_info().items()}
|
|
217
|
+
info["supported_connectors"] = self._do_supported_connectors()
|
|
218
|
+
return info
|
|
219
|
+
except flight_dbapi.Error as exc:
|
|
220
|
+
raise DCHConnectionError(str(exc)) from exc
|
|
221
|
+
finally:
|
|
222
|
+
conn.close()
|
|
223
|
+
|
|
224
|
+
def _do_supported_connectors(self) -> list[str]:
|
|
225
|
+
client = self._flight_connect()
|
|
226
|
+
try:
|
|
227
|
+
action = flight.Action("GetSupportedConnectors", b"")
|
|
228
|
+
results = list(client.do_action(action, self._call_options()))
|
|
229
|
+
if not results:
|
|
230
|
+
return []
|
|
231
|
+
body = results[0].body.to_pybytes()
|
|
232
|
+
try:
|
|
233
|
+
reader = pa.ipc.open_stream(body)
|
|
234
|
+
table = reader.read_all()
|
|
235
|
+
connectors: list[str] = table.column("name").to_pylist()
|
|
236
|
+
except pa.ArrowInvalid:
|
|
237
|
+
connectors = [str(entry["name"]) for entry in json.loads(body)]
|
|
238
|
+
return connectors
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
raise DCHConnectionError(str(exc)) from exc
|
|
241
|
+
finally:
|
|
242
|
+
client.close()
|
|
243
|
+
|
|
244
|
+
def get_tables(
|
|
245
|
+
self,
|
|
246
|
+
connection_id: str,
|
|
247
|
+
*,
|
|
248
|
+
table_name_filter: str | None = None,
|
|
249
|
+
include_schema: bool = False,
|
|
250
|
+
) -> pa.Table:
|
|
251
|
+
"""Retrieve table metadata via Flight SQL ``CommandGetTables``.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
connection_id : str
|
|
256
|
+
Data-connection ID to query.
|
|
257
|
+
table_name_filter : str, optional
|
|
258
|
+
SQL ``LIKE`` pattern to filter table names (e.g. ``"cities"``).
|
|
259
|
+
include_schema : bool
|
|
260
|
+
If True, the response includes each table's Arrow schema
|
|
261
|
+
serialized as IPC bytes in a ``table_schema`` column.
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
return self._do_get_tables(
|
|
265
|
+
connection_id, table_name_filter=table_name_filter, include_schema=include_schema
|
|
266
|
+
)
|
|
267
|
+
except DCHConnectionError as exc:
|
|
268
|
+
if self._token_cache is not None and _is_auth_error(exc):
|
|
269
|
+
self._token_cache.refresh()
|
|
270
|
+
return self._do_get_tables(
|
|
271
|
+
connection_id, table_name_filter=table_name_filter, include_schema=include_schema
|
|
272
|
+
)
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
def _do_get_tables(
|
|
276
|
+
self,
|
|
277
|
+
connection_id: str,
|
|
278
|
+
*,
|
|
279
|
+
table_name_filter: str | None = None,
|
|
280
|
+
include_schema: bool = False,
|
|
281
|
+
) -> pa.Table:
|
|
282
|
+
cmd = _build_command_get_tables(
|
|
283
|
+
table_name_filter_pattern=table_name_filter,
|
|
284
|
+
include_schema=include_schema,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
headers = [
|
|
288
|
+
*self._call_options().headers,
|
|
289
|
+
(b"x-data-connection-id", connection_id.encode()),
|
|
290
|
+
]
|
|
291
|
+
opts = flight.FlightCallOptions(headers=headers)
|
|
292
|
+
|
|
293
|
+
client = self._flight_connect()
|
|
294
|
+
try:
|
|
295
|
+
descriptor = flight.FlightDescriptor.for_command(cmd)
|
|
296
|
+
info = client.get_flight_info(descriptor, opts)
|
|
297
|
+
if not info.endpoints:
|
|
298
|
+
raise DCHConnectionError("server returned no Flight endpoints for CommandGetTables")
|
|
299
|
+
reader = client.do_get(info.endpoints[0].ticket, opts)
|
|
300
|
+
return reader.read_all()
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
raise DCHConnectionError(str(exc)) from exc
|
|
303
|
+
finally:
|
|
304
|
+
client.close()
|
|
305
|
+
|
|
306
|
+
def close(self) -> None:
|
|
307
|
+
"""No-op — connections are opened and closed per call."""
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _encode_varint(value: int) -> bytes:
|
|
311
|
+
result = bytearray()
|
|
312
|
+
while value > 0x7F:
|
|
313
|
+
result.append((value & 0x7F) | 0x80)
|
|
314
|
+
value >>= 7
|
|
315
|
+
result.append(value & 0x7F)
|
|
316
|
+
return bytes(result)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# CommandGetTables protobuf field numbers (from FlightSql.proto)
|
|
320
|
+
_CGT_FIELD_TABLE_NAME_FILTER = 3
|
|
321
|
+
_CGT_FIELD_INCLUDE_SCHEMA = 5
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _build_command_get_tables(
|
|
325
|
+
*,
|
|
326
|
+
table_name_filter_pattern: str | None = None,
|
|
327
|
+
include_schema: bool = False,
|
|
328
|
+
) -> bytes:
|
|
329
|
+
"""Serialize a Flight SQL ``CommandGetTables`` wrapped in ``google.protobuf.Any``."""
|
|
330
|
+
from google.protobuf import any_pb2 # type: ignore[import-untyped]
|
|
331
|
+
|
|
332
|
+
msg = b""
|
|
333
|
+
if table_name_filter_pattern is not None:
|
|
334
|
+
encoded = table_name_filter_pattern.encode("utf-8")
|
|
335
|
+
msg += _encode_varint((_CGT_FIELD_TABLE_NAME_FILTER << 3) | 2) + _encode_varint(len(encoded)) + encoded
|
|
336
|
+
if include_schema:
|
|
337
|
+
msg += _encode_varint((_CGT_FIELD_INCLUDE_SCHEMA << 3) | 0) + _encode_varint(1)
|
|
338
|
+
|
|
339
|
+
any_msg = any_pb2.Any()
|
|
340
|
+
any_msg.type_url = _CMD_GET_TABLES_TYPE_URL
|
|
341
|
+
any_msg.value = msg
|
|
342
|
+
return bytes(any_msg.SerializeToString())
|