e6data-python-connector 3.0.0__py3-none-any.whl → 3.0.1__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.
- e6data_python_connector/async_cluster_manager.py +9 -4
- e6data_python_connector/async_connection.py +82 -9
- e6data_python_connector/async_connection_pool.py +4 -0
- e6data_python_connector/async_cursor.py +382 -21
- e6data_python_connector/async_dialect.py +2 -1
- e6data_python_connector/cluster_manager.py +61 -10
- e6data_python_connector/connection_pool.py +11 -9
- e6data_python_connector/dialect.py +7 -2
- e6data_python_connector/e6data_grpc.py +722 -110
- e6data_python_connector/result_batch.py +49 -0
- e6data_python_connector/result_decode.py +559 -0
- e6data_python_connector/result_decode_worker.py +50 -0
- e6data_python_connector/result_prefetch.py +421 -0
- e6data_python_connector/server/e6x_engine_pb2.py +149 -147
- e6data_python_connector/server/e6x_engine_pb2_grpc.py +43 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1.dist-info}/METADATA +147 -2
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1.dist-info}/RECORD +27 -20
- test/integration/benchmark_result_batch_v2.py +149 -0
- test/integration/result_batch_benchmark_support.py +196 -0
- test/integration/support.py +3 -0
- test/integration/test_result_batch_v2.py +120 -0
- test/test_connection_pool.py +3 -0
- test/test_oauth_auto_resume.py +7 -2
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1.dist-info}/WHEEL +0 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1.dist-info}/entry_points.txt +0 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {e6data_python_connector-3.0.0.dist-info → e6data_python_connector-3.0.1.dist-info}/top_level.txt +0 -0
|
@@ -123,10 +123,15 @@ class AsyncClusterManager:
|
|
|
123
123
|
options = dict(grpc_options or {})
|
|
124
124
|
if any(key.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for key in options):
|
|
125
125
|
raise ValueError('TLS authority overrides are unsupported.')
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
for name in ('max_receive_message_length', 'max_send_message_length'):
|
|
127
|
+
limits = [options.pop(key) for key in (name, 'grpc.' + name) if key in options]
|
|
128
|
+
for limit in limits:
|
|
129
|
+
if (isinstance(limit, bool) or not isinstance(limit, int)
|
|
130
|
+
or (limit != -1 and limit <= 0)):
|
|
131
|
+
raise ValueError(name + ' must be -1 (unlimited) or a positive integer.')
|
|
132
|
+
if len(limits) == 2 and limits[0] != limits[1]:
|
|
133
|
+
raise ValueError('Conflicting ' + name + ' options.')
|
|
134
|
+
options['grpc.' + name] = limits[0] if limits else -1
|
|
130
135
|
self._host, self._port = host, port
|
|
131
136
|
self._user, self._password = user, password
|
|
132
137
|
self._secure_channel, self._ssl_cert = secure_channel, ssl_cert
|
|
@@ -14,6 +14,9 @@ from .oauth_common import validate_positive_timeout, validate_token_endpoint
|
|
|
14
14
|
from .server import e6x_engine_pb2 as pb, e6x_engine_pb2_grpc as bindings
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
_UNSET_RECEIVE_MESSAGE_BYTES = object()
|
|
18
|
+
|
|
19
|
+
|
|
17
20
|
@dataclass(frozen=True)
|
|
18
21
|
class QueryRoute:
|
|
19
22
|
target: str
|
|
@@ -50,7 +53,11 @@ class AsyncConnection:
|
|
|
50
53
|
token_url=None, oauth_scope=None, access_token=None,
|
|
51
54
|
client_auth_method='basic', *, operation_timeout=600.0,
|
|
52
55
|
oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
|
|
53
|
-
max_receive_message_bytes=
|
|
56
|
+
max_receive_message_bytes=_UNSET_RECEIVE_MESSAGE_BYTES,
|
|
57
|
+
enable_result_batch_v2=False):
|
|
58
|
+
from .result_decode_worker import is_decode_worker
|
|
59
|
+
if is_decode_worker():
|
|
60
|
+
raise RuntimeError('A result decode worker cannot create a connection.')
|
|
54
61
|
if sys.version_info < (3, 11):
|
|
55
62
|
raise RuntimeError('The async API requires Python 3.11 or newer.')
|
|
56
63
|
try:
|
|
@@ -78,24 +85,47 @@ class AsyncConnection:
|
|
|
78
85
|
raise ValueError('Async OAuth requires verified TLS (secure=True).')
|
|
79
86
|
if ssl_cert is not None and not isinstance(ssl_cert, (str, bytes)):
|
|
80
87
|
raise ValueError('ssl_cert must be a certificate path or PEM bytes.')
|
|
88
|
+
if not isinstance(enable_result_batch_v2, bool):
|
|
89
|
+
raise ValueError('enable_result_batch_v2 must be a boolean.')
|
|
90
|
+
if enable_result_batch_v2:
|
|
91
|
+
from .result_decode import validate_decode_runtime
|
|
92
|
+
validate_decode_runtime()
|
|
81
93
|
settings = dict(host=host, port=port, username=username, password=password,
|
|
82
94
|
catalog=catalog, database=database, cluster_name=cluster_name,
|
|
83
95
|
secure=bool(secure), ssl_cert=ssl_cert, auto_resume=auto_resume,
|
|
84
96
|
scheme=scheme, debug=debug, require_fastbinary=require_fastbinary,
|
|
85
97
|
client_id=client_id, client_secret=client_secret, token_url=token_url,
|
|
86
98
|
oauth_scope=oauth_scope, access_token=access_token,
|
|
87
|
-
client_auth_method=client_auth_method
|
|
99
|
+
client_auth_method=client_auth_method,
|
|
100
|
+
enable_result_batch_v2=enable_result_batch_v2)
|
|
88
101
|
for name, value in [('operation_timeout', operation_timeout), ('oauth_timeout', oauth_timeout),
|
|
89
102
|
('cleanup_timeout', cleanup_timeout), ('auto_resume_timeout', auto_resume_timeout)]:
|
|
90
103
|
settings[name] = validate_positive_timeout(value, name)
|
|
91
|
-
if isinstance(max_receive_message_bytes, bool) or not isinstance(max_receive_message_bytes, int) or max_receive_message_bytes <= 0:
|
|
92
|
-
raise ValueError('max_receive_message_bytes must be a positive integer.')
|
|
93
104
|
options = dict(grpc_options or {})
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
receive_names = ('max_receive_message_length', 'grpc.max_receive_message_length')
|
|
106
|
+
receive_values = [options.pop(name) for name in receive_names if name in options]
|
|
107
|
+
for value in receive_values:
|
|
108
|
+
if (isinstance(value, bool) or not isinstance(value, int)
|
|
109
|
+
or (value != -1 and value <= 0)):
|
|
110
|
+
raise ValueError('max_receive_message_length must be -1 (unlimited) or a positive integer.')
|
|
111
|
+
if len(receive_values) == 2 and receive_values[0] != receive_values[1]:
|
|
112
|
+
raise ValueError('Conflicting max_receive_message_length options.')
|
|
113
|
+
if max_receive_message_bytes is _UNSET_RECEIVE_MESSAGE_BYTES:
|
|
114
|
+
max_receive_message_bytes = receive_values[0] if receive_values else -1
|
|
115
|
+
if (isinstance(max_receive_message_bytes, bool) or not isinstance(max_receive_message_bytes, int)
|
|
116
|
+
or (max_receive_message_bytes != -1 and max_receive_message_bytes <= 0)):
|
|
117
|
+
raise ValueError('max_receive_message_bytes must be -1 (unlimited) or a positive integer.')
|
|
118
|
+
if receive_values and receive_values[0] != max_receive_message_bytes:
|
|
119
|
+
raise ValueError('Set the receive limit using max_receive_message_bytes; supplied options must agree.')
|
|
120
|
+
send_names = ('max_send_message_length', 'grpc.max_send_message_length')
|
|
121
|
+
send_values = [options.pop(name) for name in send_names if name in options]
|
|
122
|
+
for value in send_values:
|
|
123
|
+
if (isinstance(value, bool) or not isinstance(value, int)
|
|
124
|
+
or (value != -1 and value <= 0)):
|
|
125
|
+
raise ValueError('max_send_message_length must be -1 (unlimited) or a positive integer.')
|
|
126
|
+
if len(send_values) == 2 and send_values[0] != send_values[1]:
|
|
127
|
+
raise ValueError('Conflicting max_send_message_length options.')
|
|
128
|
+
options['grpc.max_send_message_length'] = send_values[0] if send_values else -1
|
|
99
129
|
# Authority overrides can disable meaningful server identity validation.
|
|
100
130
|
if any(name.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for name in options):
|
|
101
131
|
raise ValueError('TLS authority overrides are unsupported.')
|
|
@@ -103,6 +133,9 @@ class AsyncConnection:
|
|
|
103
133
|
settings['grpc_options'] = MappingProxyType(options)
|
|
104
134
|
settings['max_receive_message_bytes'] = max_receive_message_bytes
|
|
105
135
|
self._config = MappingProxyType(settings)
|
|
136
|
+
if debug:
|
|
137
|
+
from .e6data_grpc import _configure_debug_logging
|
|
138
|
+
_configure_debug_logging()
|
|
106
139
|
self._oauth_enabled = not modes[0]
|
|
107
140
|
self._channel = self._client = self._token_provider = None
|
|
108
141
|
self._owns_token_provider = True
|
|
@@ -125,6 +158,11 @@ class AsyncConnection:
|
|
|
125
158
|
self._cleanup_error = None
|
|
126
159
|
self._cluster_manager = None
|
|
127
160
|
self._owns_cluster_manager = True
|
|
161
|
+
self._decoder_started = False
|
|
162
|
+
self._decoder_lease = None
|
|
163
|
+
if enable_result_batch_v2:
|
|
164
|
+
from .result_decode import DecoderLease
|
|
165
|
+
self._decoder_lease = DecoderLease()
|
|
128
166
|
|
|
129
167
|
def __getattr__(self, name):
|
|
130
168
|
config = self.__dict__.get('_config', {})
|
|
@@ -300,6 +338,24 @@ class AsyncConnection:
|
|
|
300
338
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
301
339
|
return metadata
|
|
302
340
|
|
|
341
|
+
async def _ensure_decoder(self, deadline):
|
|
342
|
+
self._check_owner()
|
|
343
|
+
if self._decoder_lease is not None and not self._decoder_started:
|
|
344
|
+
from .async_work import run_blocking
|
|
345
|
+
await run_blocking(self._decoder_lease.start, deadline, deadline=deadline)
|
|
346
|
+
self._check_owner()
|
|
347
|
+
self._decoder_started = True
|
|
348
|
+
|
|
349
|
+
async def _raw_result_rpc(self, method_name, request, metadata, deadline):
|
|
350
|
+
"""Transport only: the owning task already froze identity and metadata."""
|
|
351
|
+
call = getattr(self._client, method_name)(
|
|
352
|
+
request, metadata=metadata, timeout=self._remaining(deadline))
|
|
353
|
+
self._calls.add(call)
|
|
354
|
+
try:
|
|
355
|
+
return await call
|
|
356
|
+
finally:
|
|
357
|
+
self._calls.discard(call)
|
|
358
|
+
|
|
303
359
|
async def _rpc(self, method_name, request, *, deadline, route=None, safe_retry=False, _cleanup=False, _on_dispatch=None, _response_metadata=None):
|
|
304
360
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
305
361
|
refreshed = switched = False
|
|
@@ -349,6 +405,8 @@ class AsyncConnection:
|
|
|
349
405
|
|
|
350
406
|
self._cluster_manager = AsyncClusterManager(
|
|
351
407
|
self.host, self.port, user=self.username or '', password=self.password or '',
|
|
408
|
+
grpc_options={**self.grpc_options,
|
|
409
|
+
'grpc.max_receive_message_length': self.max_receive_message_bytes},
|
|
352
410
|
secure_channel=self.secure, cluster_uuid=self.cluster_name,
|
|
353
411
|
ssl_cert=self.ssl_cert, metadata_provider=metadata_provider if self._oauth_enabled else None,
|
|
354
412
|
initial_strategy=self.strategy, auto_resume_timeout=self.auto_resume_timeout,
|
|
@@ -500,6 +558,15 @@ class AsyncConnection:
|
|
|
500
558
|
# Native channel close with no grace cancels transports without waiting.
|
|
501
559
|
if self._channel is not None:
|
|
502
560
|
await self._channel.close()
|
|
561
|
+
if self._decoder_lease is not None:
|
|
562
|
+
# Retirement must survive loop shutdown and saturated work slots.
|
|
563
|
+
self._decoder_lease.retire(deadline)
|
|
564
|
+
try:
|
|
565
|
+
async with asyncio.timeout_at(deadline):
|
|
566
|
+
while self._decoder_lease.cleanup_pending:
|
|
567
|
+
await asyncio.sleep(min(0.005, self._remaining(deadline)))
|
|
568
|
+
except (Exception, asyncio.CancelledError):
|
|
569
|
+
self._cleanup_error = OperationalError('Result decoder cleanup remains pending.')
|
|
503
570
|
for resource, owned in ((self._cluster_manager, self._owns_cluster_manager),
|
|
504
571
|
(self._token_provider, self._owns_token_provider)):
|
|
505
572
|
if owned and resource is not None:
|
|
@@ -538,6 +605,12 @@ class AsyncConnection:
|
|
|
538
605
|
if self._owner is not None and self._owner != (os.getpid(), threading.get_ident(), asyncio.get_running_loop()):
|
|
539
606
|
raise ProgrammingError('Connection belongs to another owner.')
|
|
540
607
|
await self.close()
|
|
608
|
+
if self._decoder_lease is not None and self._decoder_lease.cleanup_pending:
|
|
609
|
+
raise ProgrammingError('Cannot reopen while result decoder cleanup remains pending.')
|
|
610
|
+
if self.enable_result_batch_v2:
|
|
611
|
+
from .result_decode import DecoderLease
|
|
612
|
+
self._decoder_lease = DecoderLease()
|
|
613
|
+
self._decoder_started = False
|
|
541
614
|
self._state = 'new'
|
|
542
615
|
self._session_id = self._session_task = self._close_task = self._open_task = None
|
|
543
616
|
if self._owns_token_provider:
|
|
@@ -189,6 +189,8 @@ class AsyncConnectionPool:
|
|
|
189
189
|
from .async_cluster_manager import AsyncClusterManager
|
|
190
190
|
self._cluster_manager = AsyncClusterManager(
|
|
191
191
|
config.host, config.port, user=config.username or '', password=config.password or '',
|
|
192
|
+
grpc_options={**config.grpc_options,
|
|
193
|
+
'grpc.max_receive_message_length': config.max_receive_message_bytes},
|
|
192
194
|
secure_channel=config.secure, ssl_cert=config.ssl_cert,
|
|
193
195
|
cluster_uuid=config.cluster_name, initial_strategy='blue',
|
|
194
196
|
metadata_provider=self._resume_metadata if config._oauth_enabled else None,
|
|
@@ -316,6 +318,8 @@ class AsyncConnectionPool:
|
|
|
316
318
|
healthy = healthy and cursor._state != 'SUBMISSION_UNKNOWN'
|
|
317
319
|
await cursor._close_owned(deadline)
|
|
318
320
|
healthy = healthy and cursor.cleanup_error is None
|
|
321
|
+
healthy = (healthy and cursor._pending_result is None
|
|
322
|
+
and not cursor._retired_results)
|
|
319
323
|
healthy = (healthy and not connection._routes and not connection._ambiguous_submissions
|
|
320
324
|
and connection._state == 'open')
|
|
321
325
|
except (Exception, asyncio.CancelledError):
|