e6data-python-connector 3.0.1rc2__tar.gz → 3.0.1rc4__tar.gz

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.
Files changed (125) hide show
  1. {e6data_python_connector-3.0.1rc2/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc4}/PKG-INFO +19 -6
  2. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/README.md +18 -5
  3. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cluster_manager.py +9 -4
  4. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection.py +30 -8
  5. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_connection_pool.py +2 -0
  6. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_cursor.py +3 -2
  7. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_manager.py +58 -9
  8. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6data_grpc.py +10 -6
  9. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode.py +155 -65
  10. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4/e6data_python_connector.egg-info}/PKG-INFO +19 -6
  11. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/setup.py +1 -1
  12. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_oauth_auto_resume.py +7 -2
  13. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/LICENSE +0 -0
  14. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/MANIFEST.in +0 -0
  15. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/__init__.py +0 -0
  16. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/aio.py +0 -0
  17. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_dialect.py +0 -0
  18. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_oauth.py +0 -0
  19. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/async_work.py +0 -0
  20. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/__init__.py +0 -0
  21. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  22. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  23. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/common.py +0 -0
  24. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/connection_pool.py +0 -0
  25. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/constants.py +0 -0
  26. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/datainputstream.py +0 -0
  27. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/date_time_utils.py +0 -0
  28. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/dialect.py +0 -0
  29. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  30. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/constants.py +0 -0
  31. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  32. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/exceptions.py +0 -0
  33. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth.py +0 -0
  34. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_cache.py +0 -0
  35. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/oauth_common.py +0 -0
  36. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_batch.py +0 -0
  37. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_decode_worker.py +0 -0
  38. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/result_prefetch.py +0 -0
  39. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/QueryEngineService.py +0 -0
  40. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/__init__.py +0 -0
  41. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/constants.py +0 -0
  42. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  43. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  44. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  45. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/server/ttypes.py +0 -0
  46. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/strategy.py +0 -0
  47. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector/typeId.py +0 -0
  48. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/SOURCES.txt +0 -0
  49. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  50. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  51. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/requires.txt +0 -0
  52. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/e6data_python_connector.egg-info/top_level.txt +0 -0
  53. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/pyproject.toml +0 -0
  54. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/setup.cfg +0 -0
  55. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/__init__.py +0 -0
  56. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_38_nines.py +0 -0
  57. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_all_cases.py +0 -0
  58. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_binary.py +0 -0
  59. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_correct_value.py +0 -0
  60. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/analyze_fields.py +0 -0
  61. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/check_decimal_errors.py +0 -0
  62. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/cleanup_test_files.py +0 -0
  63. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/conftest.py +0 -0
  64. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/debug_38_nines.py +0 -0
  65. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/debug_binary.py +0 -0
  66. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/final_test.py +0 -0
  67. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/__init__.py +0 -0
  68. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_preflight.py +0 -0
  69. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_result_batch_v2.py +0 -0
  70. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/benchmark_sync_async.py +0 -0
  71. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/expiry_preflight.py +0 -0
  72. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/result_batch_benchmark_support.py +0 -0
  73. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/support.py +0 -0
  74. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_connection.py +0 -0
  75. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_pool.py +0 -0
  76. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_queries.py +0 -0
  77. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_resume.py +0 -0
  78. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_sqlalchemy.py +0 -0
  79. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_async_token_lifecycle.py +0 -0
  80. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_auth_boundary.py +0 -0
  81. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_oauth_continuation.py +0 -0
  82. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_e2e_readonly_matrix.py +0 -0
  83. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_result_batch_v2.py +0 -0
  84. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_shared_token_issuance.py +0 -0
  85. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/test_sync_oauth_lifecycle.py +0 -0
  86. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/integration/token_observation.py +0 -0
  87. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/move_tests.py +0 -0
  88. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/quick_test.py +0 -0
  89. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_38_nines.py +0 -0
  90. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_all_decimal128_cases.py +0 -0
  91. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_efficiency.py +0 -0
  92. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_none_strategy.py +0 -0
  93. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_oauth.py +0 -0
  94. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_cluster_manager_strategy.py +0 -0
  95. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_code_optimizations.py +0 -0
  96. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_comprehensive.py +0 -0
  97. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool.py +0 -0
  98. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_connection_pool_e2e.py +0 -0
  99. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_current_implementation.py +0 -0
  100. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_binary_parsing.py +0 -0
  101. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_decimal128_parsing.py +0 -0
  102. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_dialect_columns.py +0 -0
  103. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_fix.py +0 -0
  104. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_improved_parsing.py +0 -0
  105. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_known_case.py +0 -0
  106. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_manual_analysis.py +0 -0
  107. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_mock_server.py +0 -0
  108. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_multiprocessing_fix.py +0 -0
  109. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_new_implementation.py +0 -0
  110. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_oauth_authentication.py +0 -0
  111. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_oauth_resume_integration.py +0 -0
  112. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_pool_concurrency_simple.py +0 -0
  113. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_pool_threading_multiprocessing.py +0 -0
  114. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_simple_connection_pool.py +0 -0
  115. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_specific_binary.py +0 -0
  116. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy.py +0 -0
  117. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy_logic.py +0 -0
  118. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy_persistence_fix.py +0 -0
  119. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_strategy_sharing_fix.py +0 -0
  120. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/test_user_binary_value.py +0 -0
  121. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/tests.py +0 -0
  122. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/tests_grpc.py +0 -0
  123. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/validate_decimal128.py +0 -0
  124. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/validate_implementation.py +0 -0
  125. {e6data_python_connector-3.0.1rc2 → e6data_python_connector-3.0.1rc4}/test/verify_decimal_fix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e6data-python-connector
3
- Version: 3.0.1rc2
3
+ Version: 3.0.1rc4
4
4
  Summary: Client for the e6data distributed SQL Engine.
5
5
  Home-page: https://github.com/e6x-labs/e6data-python-connector
6
6
  Author: e6data, Inc.
@@ -489,11 +489,15 @@ workers use the explicit `spawn` method; the application's global multiprocessin
489
489
  start method is unchanged. An unguarded main script can run its other import
490
490
  side effects, so keep all application startup inside the guard.
491
491
 
492
- Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
493
- custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
494
- flag-off path keeps its existing options. For async, set
495
- `max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
496
- Choose limits from measured envelope sizes and the client's memory budget.
492
+ Both APIs default to unlimited gRPC send and receive message sizes (`-1`).
493
+ Explicit positive byte limits are still honored. For sync, set
494
+ `max_receive_message_length` and `max_send_message_length` in `grpc_options`.
495
+ For async, set the receive limit with `max_receive_message_bytes` or
496
+ `max_receive_message_length` in `grpc_options`; if both are supplied, they must
497
+ agree. Set `max_send_message_length` in `grpc_options` for the send limit.
498
+ The receive limit applies to each message after transport decompression, not the
499
+ total query result. Unlimited receiving does not limit client memory use;
500
+ continuous downloading can retain serialized batches while decoding catches up.
497
501
 
498
502
  These examples use your existing `connection_options`, `sql`, and `consume`:
499
503
 
@@ -567,6 +571,15 @@ downloading is faster than decoding or application reads. Workers add Python
567
571
  objects and process copies; the receive limit is not a limit on total memory.
568
572
  `fetchall()` still retains the full result.
569
573
 
574
+ If a decoder worker exits or its local pipe breaks, the connector stops the
575
+ failed workers and decodes that complete serialized envelope once in the parent
576
+ process. It uses the same deadline and makes no extra result RPC. Continuous
577
+ downloading stays active and uncapped during this recovery. Later envelopes use
578
+ sequential decoding until the decoder runtime is replaced through its normal
579
+ lifecycle. Cancellation, expired deadlines, invalid worker replies, and errors
580
+ reported while decoding data are still failures. Their original cause is kept
581
+ on the first error delivered to the caller.
582
+
570
583
  Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
571
584
  optimizations. Other RPC failures reach the caller as the original
572
585
  `grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
@@ -443,11 +443,15 @@ workers use the explicit `spawn` method; the application's global multiprocessin
443
443
  start method is unchanged. An unguarded main script can run its other import
444
444
  side effects, so keep all application startup inside the guard.
445
445
 
446
- Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
447
- custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
448
- flag-off path keeps its existing options. For async, set
449
- `max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
450
- Choose limits from measured envelope sizes and the client's memory budget.
446
+ Both APIs default to unlimited gRPC send and receive message sizes (`-1`).
447
+ Explicit positive byte limits are still honored. For sync, set
448
+ `max_receive_message_length` and `max_send_message_length` in `grpc_options`.
449
+ For async, set the receive limit with `max_receive_message_bytes` or
450
+ `max_receive_message_length` in `grpc_options`; if both are supplied, they must
451
+ agree. Set `max_send_message_length` in `grpc_options` for the send limit.
452
+ The receive limit applies to each message after transport decompression, not the
453
+ total query result. Unlimited receiving does not limit client memory use;
454
+ continuous downloading can retain serialized batches while decoding catches up.
451
455
 
452
456
  These examples use your existing `connection_options`, `sql`, and `consume`:
453
457
 
@@ -521,6 +525,15 @@ downloading is faster than decoding or application reads. Workers add Python
521
525
  objects and process copies; the receive limit is not a limit on total memory.
522
526
  `fetchall()` still retains the full result.
523
527
 
528
+ If a decoder worker exits or its local pipe breaks, the connector stops the
529
+ failed workers and decodes that complete serialized envelope once in the parent
530
+ process. It uses the same deadline and makes no extra result RPC. Continuous
531
+ downloading stays active and uncapped during this recovery. Later envelopes use
532
+ sequential decoding until the decoder runtime is replaced through its normal
533
+ lifecycle. Cancellation, expired deadlines, invalid worker replies, and errors
534
+ reported while decoding data are still failures. Their original cause is kept
535
+ on the first error delivered to the caller.
536
+
524
537
  Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
525
538
  optimizations. Other RPC failures reach the caller as the original
526
539
  `grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
@@ -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
- limit = options.get('grpc.max_receive_message_length', 64 * 1024 * 1024)
127
- if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
128
- raise ValueError('A finite positive receive limit is required.')
129
- options['grpc.max_receive_message_length'] = limit
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,7 @@ 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=64 * 1024 * 1024,
56
+ max_receive_message_bytes=_UNSET_RECEIVE_MESSAGE_BYTES,
54
57
  enable_result_batch_v2=False):
55
58
  from .result_decode_worker import is_decode_worker
56
59
  if is_decode_worker():
@@ -98,14 +101,31 @@ class AsyncConnection:
98
101
  for name, value in [('operation_timeout', operation_timeout), ('oauth_timeout', oauth_timeout),
99
102
  ('cleanup_timeout', cleanup_timeout), ('auto_resume_timeout', auto_resume_timeout)]:
100
103
  settings[name] = validate_positive_timeout(value, name)
101
- if isinstance(max_receive_message_bytes, bool) or not isinstance(max_receive_message_bytes, int) or max_receive_message_bytes <= 0:
102
- raise ValueError('max_receive_message_bytes must be a positive integer.')
103
104
  options = dict(grpc_options or {})
104
- for name in ('max_receive_message_length', 'grpc.max_receive_message_length'):
105
- if name in options:
106
- if options[name] != max_receive_message_bytes:
107
- raise ValueError('Set the finite receive limit using max_receive_message_bytes.')
108
- del options[name]
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
109
129
  # Authority overrides can disable meaningful server identity validation.
110
130
  if any(name.removeprefix('grpc.') in ('ssl_target_name_override', 'default_authority') for name in options):
111
131
  raise ValueError('TLS authority overrides are unsupported.')
@@ -385,6 +405,8 @@ class AsyncConnection:
385
405
 
386
406
  self._cluster_manager = AsyncClusterManager(
387
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},
388
410
  secure_channel=self.secure, cluster_uuid=self.cluster_name,
389
411
  ssl_cert=self.ssl_cert, metadata_provider=metadata_provider if self._oauth_enabled else None,
390
412
  initial_strategy=self.strategy, auto_resume_timeout=self.auto_resume_timeout,
@@ -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,
@@ -129,7 +129,7 @@ class AsyncCursor:
129
129
  if self._state == 'CLOSED':
130
130
  raise ProgrammingError('Cursor is closed.')
131
131
  if fetch and self._failure is not None:
132
- raise self._failure
132
+ raise IncompleteResultError(self._failure.reason, query_id=self._failure.query_id)
133
133
  if fetch and self._state in ('EMPTY', 'SUBMISSION_UNKNOWN'):
134
134
  raise ProgrammingError('No complete query is available for fetching.')
135
135
 
@@ -172,12 +172,13 @@ class AsyncCursor:
172
172
  self._retire_prefetch()
173
173
  self._cancel_decode()
174
174
  if self._failure is None:
175
+ # Never raise this template: its traceback would retain decoded inputs.
175
176
  self._failure = IncompleteResultError(reason, query_id=self.query_id)
176
177
  if self._state != 'CLOSED':
177
178
  self._state = 'RESULT_FAILED'
178
179
  self._rows.clear()
179
180
  self._result_batches.clear()
180
- return self._failure
181
+ return IncompleteResultError(self._failure.reason, query_id=self._failure.query_id)
181
182
 
182
183
  def _accept_rows(self, rows):
183
184
  self._rows.extend(rows)
@@ -1,4 +1,6 @@
1
+ import hashlib
1
2
  import logging
3
+ import os
2
4
  import threading
3
5
  import time
4
6
  import e6data_python_connector.cluster_server.cluster_pb2 as cluster_pb2
@@ -74,6 +76,7 @@ class _StatusLock:
74
76
  self._status_multiprocessing_lock = (threading.Semaphore() if is_decode_worker()
75
77
  else multiprocessing.Semaphore())
76
78
  self._is_active = False
79
+ self._resume_completion = None
77
80
 
78
81
  @property
79
82
  def is_active(self) -> bool:
@@ -109,8 +112,14 @@ class _StatusLock:
109
112
  _StatusLock: The current instance of the lock, used for context management.
110
113
  """
111
114
 
112
- self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT)
113
- self._status_multiprocessing_lock.acquire(timeout=self._LOCK_TIMEOUT)
115
+ if not self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT):
116
+ raise TimeoutError('Cluster recovery timed out waiting for status lock.')
117
+ try:
118
+ if not self._status_multiprocessing_lock.acquire(timeout=self._LOCK_TIMEOUT):
119
+ raise TimeoutError('Cluster recovery timed out waiting for process lock.')
120
+ except BaseException:
121
+ self._status_thread_lock.release()
122
+ raise
114
123
  return self
115
124
 
116
125
  def __exit__(self, exc_type, exc_val, exc_tb):
@@ -127,8 +136,8 @@ class _StatusLock:
127
136
  exc_tb (Traceback): The traceback object of the exception (if any).
128
137
  """
129
138
 
130
- self._status_thread_lock.release()
131
139
  self._status_multiprocessing_lock.release()
140
+ self._status_thread_lock.release()
132
141
 
133
142
  @contextmanager
134
143
  def hold_until(self, deadline):
@@ -196,9 +205,10 @@ class ClusterManager:
196
205
  self._timeout = time.time() + timeout
197
206
  self._secure_channel = secure_channel
198
207
  self.cluster_uuid = cluster_uuid
199
- self._grpc_options = grpc_options
200
- if grpc_options is None:
201
- self._grpc_options = dict()
208
+ options = dict(grpc_options or ())
209
+ options.setdefault('grpc.max_receive_message_length', -1)
210
+ options.setdefault('grpc.max_send_message_length', -1)
211
+ self._grpc_options = list(options.items())
202
212
  self._debug = debug
203
213
  self._ssl_cert = ssl_cert
204
214
  self._metadata_provider = metadata_provider
@@ -465,6 +475,34 @@ class ClusterManager:
465
475
  logger.warning(f"Status check failed with error: {e}")
466
476
  yield None
467
477
 
478
+ def _legacy_resume_identity(self):
479
+ """Fingerprint exact connection inputs without retaining credentials.
480
+
481
+ Copy standard option containers. Unknown or mutable option values disable
482
+ sharing so their existing RPC behavior is preserved without guessing.
483
+ """
484
+ values = (self._host, self._port, self.cluster_uuid, self._secure_channel,
485
+ self._ssl_cert, self._user, self._password)
486
+ scalar_types = (type(None), str, bytes, bool, int)
487
+ if any(type(value) not in scalar_types for value in values):
488
+ return None
489
+ options = self._grpc_options
490
+ if type(options) is dict:
491
+ pairs = tuple(options.items())
492
+ elif type(options) in (list, tuple):
493
+ pairs = tuple(options)
494
+ else:
495
+ return None
496
+ frozen_options = []
497
+ for pair in pairs:
498
+ if (type(pair) not in (tuple, list) or len(pair) != 2
499
+ or any(type(value) not in scalar_types for value in pair)):
500
+ return None
501
+ frozen_options.append(tuple((type(value).__name__, value) for value in pair))
502
+ identity = (tuple((type(value).__name__, value) for value in values),
503
+ type(options).__name__, tuple(frozen_options))
504
+ return hashlib.sha256(repr(identity).encode('utf-8')).digest()
505
+
468
506
  def resume(self) -> bool:
469
507
  """
470
508
  Resumes the cluster if it is currently suspended or not in the 'active' state.
@@ -494,14 +532,21 @@ class ClusterManager:
494
532
  """
495
533
  if self._metadata_provider is not None:
496
534
  return self._resume_oauth()
535
+ call_started = time.monotonic()
497
536
  if self._debug:
498
537
  logger.info(f"Starting auto-resume for cluster {self.cluster_uuid} at {self._host}:{self._port}")
499
538
 
500
539
  with status_lock as lock:
501
- if lock.is_active:
502
- if self._debug:
503
- logger.info("Lock is already active, cluster appears to be running")
540
+ identity = self._legacy_resume_identity()
541
+ process_id = os.getpid()
542
+ completion = lock._resume_completion
543
+ if (identity is not None and completion is not None
544
+ and completion[0] == process_id and completion[1] == identity
545
+ and completion[2] > call_started):
504
546
  return True
547
+ # A fresh attempt can observe a new failure before older waiters run.
548
+ # Invalidate first, including when the attempt later raises an error.
549
+ lock._resume_completion = None
505
550
 
506
551
  # Retrieve the current cluster status with strategy header
507
552
  if self._debug:
@@ -530,6 +575,8 @@ class ClusterManager:
530
575
  elif current_status.status == 'active':
531
576
  if self._debug:
532
577
  logger.info("Cluster is already active, no action needed")
578
+ if identity is not None:
579
+ lock._resume_completion = (process_id, identity, time.monotonic())
533
580
  return True
534
581
  elif current_status.status != 'resuming':
535
582
  """
@@ -548,6 +595,8 @@ class ClusterManager:
548
595
  if status == 'active':
549
596
  if self._debug:
550
597
  logger.info(f"Cluster became active after {check_count} status checks")
598
+ if identity is not None:
599
+ lock._resume_completion = (process_id, identity, time.monotonic())
551
600
  return True
552
601
  elif status == 'failed':
553
602
  if self._debug:
@@ -527,9 +527,9 @@ class Connection(object):
527
527
  if key in normalized and normalized[key] != value:
528
528
  raise ValueError('Conflicting gRPC options after prefix normalization.')
529
529
  normalized[key] = value
530
- limit = normalized.get('max_receive_message_length', 64 * 1024 * 1024)
531
- if isinstance(limit, bool) or not isinstance(limit, int) or limit <= 0:
532
- raise ValueError('V2 receive limit must be a finite positive integer.')
530
+ limit = normalized.get('max_receive_message_length', -1)
531
+ if isinstance(limit, bool) or not isinstance(limit, int) or (limit != -1 and limit <= 0):
532
+ raise ValueError('V2 receive limit must be a positive integer or -1 for unlimited.')
533
533
  normalized['max_receive_message_length'] = limit
534
534
  grpc_options = normalized
535
535
  if enable_result_batch_v2 and grpc_options is not None and 'grpc_prepare_timeout' in grpc_options:
@@ -696,7 +696,7 @@ class Connection(object):
696
696
  default_options = {
697
697
  "keepalive_timeout_ms": 900000, # Time in milliseconds to keep the connection alive.
698
698
  "max_receive_message_length": -1, # Maximum size of received messages.
699
- "max_send_message_length": 300 * 1024 * 1024, # Maximum size of sent messages (300 MB).
699
+ "max_send_message_length": -1, # Unlimited sent message size.
700
700
  "grpc_prepare_timeout": self.grpc_prepare_timeout, # Timeout for prepare statement API call.
701
701
  "keepalive_time_ms": 30000, # Time in milliseconds between keep-alive pings.
702
702
  "keepalive_permit_without_calls": 1, # Allow keep-alives with no active RPCs.
@@ -1032,6 +1032,7 @@ class Connection(object):
1032
1032
  cluster_uuid=self.cluster_name,
1033
1033
  timeout=self.grpc_auto_resume_timeout_seconds,
1034
1034
  debug=self._debug,
1035
+ grpc_options=self._get_grpc_options,
1035
1036
  ssl_cert=self._ssl_cert
1036
1037
  ).resume()
1037
1038
  return status # Return boolean status directly
@@ -1709,7 +1710,9 @@ class Cursor(DBAPICursor):
1709
1710
  self._result_failure = IncompleteResultError(reason, query_id=self._query_id)
1710
1711
  self._data = None
1711
1712
  self._result_batches.clear()
1712
- return self._result_failure
1713
+ # Keep the cached template free of tracebacks and response frames.
1714
+ return IncompleteResultError(
1715
+ self._result_failure.reason, query_id=self._result_failure.query_id)
1713
1716
 
1714
1717
  def _check_result(self):
1715
1718
  if self._result_batch_v2_enabled and (self._result_pid != os.getpid() or
@@ -1720,7 +1723,8 @@ class Cursor(DBAPICursor):
1720
1723
  if self._closed:
1721
1724
  raise ProgrammingError('Cursor is closed.')
1722
1725
  if self._result_failure is not None:
1723
- raise self._result_failure
1726
+ raise IncompleteResultError(
1727
+ self._result_failure.reason, query_id=self._result_failure.query_id)
1724
1728
 
1725
1729
  @property
1726
1730
  def query_id(self):