e6data-python-connector 3.0.1rc4__tar.gz → 3.0.3rc1__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 (127) hide show
  1. {e6data_python_connector-3.0.1rc4/e6data_python_connector.egg-info → e6data_python_connector-3.0.3rc1}/PKG-INFO +1 -1
  2. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_cluster_manager.py +7 -0
  3. e6data_python_connector-3.0.3rc1/e6data_python_connector/calendar_interval.py +29 -0
  4. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_manager.py +2 -0
  5. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/datainputstream.py +26 -0
  6. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6data_grpc.py +8 -7
  7. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6x_vector/ttypes.py +136 -1
  8. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1/e6data_python_connector.egg-info}/PKG-INFO +1 -1
  9. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/SOURCES.txt +2 -0
  10. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/setup.py +1 -1
  11. e6data_python_connector-3.0.3rc1/test/integration/test_cluster_resume_errors.py +179 -0
  12. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/LICENSE +0 -0
  13. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/MANIFEST.in +0 -0
  14. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/README.md +0 -0
  15. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/__init__.py +0 -0
  16. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/aio.py +0 -0
  17. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_connection.py +0 -0
  18. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_connection_pool.py +0 -0
  19. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_cursor.py +0 -0
  20. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_dialect.py +0 -0
  21. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_oauth.py +0 -0
  22. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_work.py +0 -0
  23. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_server/__init__.py +0 -0
  24. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  25. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  26. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/common.py +0 -0
  27. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/connection_pool.py +0 -0
  28. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/constants.py +0 -0
  29. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/date_time_utils.py +0 -0
  30. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/dialect.py +0 -0
  31. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  32. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6x_vector/constants.py +0 -0
  33. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/exceptions.py +0 -0
  34. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/oauth.py +0 -0
  35. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/oauth_cache.py +0 -0
  36. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/oauth_common.py +0 -0
  37. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_batch.py +0 -0
  38. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_decode.py +0 -0
  39. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_decode_worker.py +0 -0
  40. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_prefetch.py +0 -0
  41. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/QueryEngineService.py +0 -0
  42. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/__init__.py +0 -0
  43. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/constants.py +0 -0
  44. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  45. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  46. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  47. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/ttypes.py +0 -0
  48. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/strategy.py +0 -0
  49. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/typeId.py +0 -0
  50. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  51. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  52. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/requires.txt +0 -0
  53. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/top_level.txt +0 -0
  54. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/pyproject.toml +0 -0
  55. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/setup.cfg +0 -0
  56. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/__init__.py +0 -0
  57. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/analyze_38_nines.py +0 -0
  58. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/analyze_all_cases.py +0 -0
  59. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/analyze_binary.py +0 -0
  60. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/analyze_correct_value.py +0 -0
  61. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/analyze_fields.py +0 -0
  62. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/check_decimal_errors.py +0 -0
  63. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/cleanup_test_files.py +0 -0
  64. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/conftest.py +0 -0
  65. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/debug_38_nines.py +0 -0
  66. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/debug_binary.py +0 -0
  67. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/final_test.py +0 -0
  68. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/__init__.py +0 -0
  69. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/benchmark_preflight.py +0 -0
  70. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/benchmark_result_batch_v2.py +0 -0
  71. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/benchmark_sync_async.py +0 -0
  72. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/expiry_preflight.py +0 -0
  73. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/result_batch_benchmark_support.py +0 -0
  74. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/support.py +0 -0
  75. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_connection.py +0 -0
  76. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_pool.py +0 -0
  77. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_queries.py +0 -0
  78. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_resume.py +0 -0
  79. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_sqlalchemy.py +0 -0
  80. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_token_lifecycle.py +0 -0
  81. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_e2e_auth_boundary.py +0 -0
  82. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_e2e_oauth_continuation.py +0 -0
  83. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_e2e_readonly_matrix.py +0 -0
  84. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_result_batch_v2.py +0 -0
  85. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_shared_token_issuance.py +0 -0
  86. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/test_sync_oauth_lifecycle.py +0 -0
  87. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/integration/token_observation.py +0 -0
  88. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/move_tests.py +0 -0
  89. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/quick_test.py +0 -0
  90. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_38_nines.py +0 -0
  91. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_all_decimal128_cases.py +0 -0
  92. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_efficiency.py +0 -0
  93. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_none_strategy.py +0 -0
  94. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_oauth.py +0 -0
  95. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_strategy.py +0 -0
  96. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_code_optimizations.py +0 -0
  97. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_comprehensive.py +0 -0
  98. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_connection_pool.py +0 -0
  99. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_connection_pool_e2e.py +0 -0
  100. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_current_implementation.py +0 -0
  101. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_decimal128_binary_parsing.py +0 -0
  102. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_decimal128_parsing.py +0 -0
  103. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_dialect_columns.py +0 -0
  104. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_fix.py +0 -0
  105. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_improved_parsing.py +0 -0
  106. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_known_case.py +0 -0
  107. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_manual_analysis.py +0 -0
  108. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_mock_server.py +0 -0
  109. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_multiprocessing_fix.py +0 -0
  110. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_new_implementation.py +0 -0
  111. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_oauth_authentication.py +0 -0
  112. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_oauth_auto_resume.py +0 -0
  113. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_oauth_resume_integration.py +0 -0
  114. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_pool_concurrency_simple.py +0 -0
  115. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_pool_threading_multiprocessing.py +0 -0
  116. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_simple_connection_pool.py +0 -0
  117. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_specific_binary.py +0 -0
  118. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_strategy.py +0 -0
  119. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_strategy_logic.py +0 -0
  120. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_strategy_persistence_fix.py +0 -0
  121. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_strategy_sharing_fix.py +0 -0
  122. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/test_user_binary_value.py +0 -0
  123. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/tests.py +0 -0
  124. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/tests_grpc.py +0 -0
  125. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/validate_decimal128.py +0 -0
  126. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/test/validate_implementation.py +0 -0
  127. {e6data_python_connector-3.0.1rc4 → e6data_python_connector-3.0.3rc1}/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.1rc4
3
+ Version: 3.0.3rc1
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.
@@ -50,6 +50,9 @@ class _ResumeState:
50
50
  def acknowledged(self):
51
51
  self.pending = 'acknowledged'
52
52
 
53
+ def rejected(self):
54
+ self.pending = None
55
+
53
56
 
54
57
  class _ResumeFlight:
55
58
  """One phase deadline, independently bounded shielded waiters."""
@@ -167,6 +170,8 @@ class AsyncClusterManager:
167
170
  _remaining(deadline)
168
171
  return await self._flight.wait(self._recover, deadline)
169
172
  except (TimeoutError, grpc.RpcError) as error:
173
+ if isinstance(error, grpc.RpcError) and error.code() == grpc.StatusCode.FAILED_PRECONDITION:
174
+ raise OperationalError('Cluster recovery failed: {}'.format(error.details())) from error
170
175
  raise OperationalError('Cluster recovery failed; a dispatched resume may still complete.') from error
171
176
 
172
177
  async def _metadata(self, strategy, deadline, rejected_token=None):
@@ -199,6 +204,8 @@ class AsyncClusterManager:
199
204
  self.last_successful_strategy = strategy
200
205
  return response
201
206
  except grpc.RpcError as error:
207
+ if operation == 'resume' and error.code() == grpc.StatusCode.FAILED_PRECONDITION:
208
+ self._mutation.rejected()
202
209
  if (error.code() == grpc.StatusCode.UNAUTHENTICATED and not refreshed
203
210
  and self._metadata_provider is not None):
204
211
  refreshed = True
@@ -0,0 +1,29 @@
1
+ """Lossless calendar intervals returned by MAKE_INTERVAL."""
2
+ from typing import NamedTuple
3
+
4
+
5
+ class CalendarInterval(NamedTuple):
6
+ """Months and calendar days are independent of elapsed microseconds."""
7
+
8
+ months: int
9
+ days: int
10
+ microseconds: int
11
+
12
+ def __str__(self):
13
+ month_sign = -1 if self.months < 0 else 1
14
+ time_sign = -1 if self.microseconds < 0 else 1
15
+ months = abs(self.months)
16
+ micros = abs(self.microseconds)
17
+ values = (month_sign * (months // 12), month_sign * (months % 12),
18
+ self.days, time_sign * (micros // 3600000000),
19
+ time_sign * (micros // 60000000 % 60))
20
+ parts = [f"{value} {unit}" for value, unit in
21
+ zip(values, ("years", "months", "days", "hours", "minutes")) if value]
22
+ seconds = micros % 60000000
23
+ if seconds:
24
+ whole, fraction = divmod(seconds, 1000000)
25
+ text = str(whole)
26
+ if fraction:
27
+ text += f".{fraction:06d}".rstrip('0')
28
+ parts.append(f"{'-' if time_sign < 0 else ''}{text} seconds")
29
+ return ' '.join(parts) if parts else '0 seconds'
@@ -571,6 +571,8 @@ class ClusterManager:
571
571
  except _InactiveRpcError as e:
572
572
  if self._debug:
573
573
  logger.error(f"Failed to send resume request: {e}")
574
+ if e.code() == grpc.StatusCode.FAILED_PRECONDITION:
575
+ raise
574
576
  return False
575
577
  elif current_status.status == 'active':
576
578
  if self._debug:
@@ -10,6 +10,7 @@ from thrift.transport import TTransport
10
10
 
11
11
  from e6data_python_connector.e6x_vector.ttypes import Chunk, Vector, VectorType
12
12
  from e6data_python_connector.constants import ZONE
13
+ from e6data_python_connector.calendar_interval import CalendarInterval
13
14
  from e6data_python_connector.date_time_utils import (
14
15
  floor_div,
15
16
  floor_mod,
@@ -478,6 +479,10 @@ def read_values_from_array(query_columns_description: list, dis: DataInputStream
478
479
  try:
479
480
  if dtype == "LONG":
480
481
  value_array.append(dis.read_long())
482
+ elif dtype == "CALENDAR_INTERVAL":
483
+ value_array.append(CalendarInterval(dis.read_int(), dis.read_int(), dis.read_long()))
484
+ elif dtype == "INTERVAL_DAY_SECOND":
485
+ value_array.append(timedelta(microseconds=dis.read_long()))
481
486
  elif dtype == "DATE":
482
487
  value_array.append(format_iso_date_from_epoch_micros(dis.read_long()))
483
488
  elif dtype == "DATETIME":
@@ -582,6 +587,27 @@ def get_column_from_chunk(vector: Vector, strict=False) -> list:
582
587
  continue
583
588
  value_array.append(vector.data.int64Data.data[
584
589
  row] if not vector.isConstantVector else vector.data.numericConstantData.data)
590
+ elif d_type == VectorType.CALENDAR_INTERVAL:
591
+ payload = getattr(vector.data, 'calendarIntervalData', None)
592
+ for row in range(vector.size):
593
+ if get_null(vector, row):
594
+ value_array.append(None)
595
+ continue
596
+ index = 0 if vector.isConstantVector else row
597
+ components = (payload.months, payload.days, payload.microseconds) if payload else ()
598
+ if not components or any(values is None or index >= len(values) or values[index] is None
599
+ for values in components):
600
+ raise ValueError("Incomplete CALENDAR_INTERVAL vector payload")
601
+ value_array.append(CalendarInterval(*(values[index] for values in components)))
602
+ elif d_type == VectorType.INTERVAL_DAY_SECOND:
603
+ for row in range(vector.size):
604
+ if get_null(vector, row):
605
+ value_array.append(None)
606
+ continue
607
+ microseconds = (vector.data.int64Data.data[row]
608
+ if not vector.isConstantVector
609
+ else vector.data.numericConstantData.data)
610
+ value_array.append(timedelta(microseconds=microseconds))
585
611
  elif d_type == VectorType.DATE:
586
612
  # Use the JDBC-parity formatter so years > 9999 emit "+YYYYY-MM-DD"
587
613
  # instead of raising. Per-row try/except keeps a single bad value
@@ -521,17 +521,18 @@ class Connection(object):
521
521
  if enable_result_batch_v2:
522
522
  from .result_decode import validate_decode_runtime
523
523
  validate_decode_runtime()
524
- normalized = {}
525
- for key, value in (grpc_options or {}).items():
526
- key = key[5:] if key.startswith('grpc.') else key
527
- if key in normalized and normalized[key] != value:
528
- raise ValueError('Conflicting gRPC options after prefix normalization.')
529
- normalized[key] = value
524
+ normalized = {}
525
+ for key, value in (grpc_options or {}).items():
526
+ key = key[5:] if key.startswith('grpc.') else key
527
+ if key in normalized and normalized[key] != value:
528
+ raise ValueError('Conflicting gRPC options after prefix normalization.')
529
+ normalized[key] = value
530
+ grpc_options = normalized
531
+ if enable_result_batch_v2:
530
532
  limit = normalized.get('max_receive_message_length', -1)
531
533
  if isinstance(limit, bool) or not isinstance(limit, int) or (limit != -1 and limit <= 0):
532
534
  raise ValueError('V2 receive limit must be a positive integer or -1 for unlimited.')
533
535
  normalized['max_receive_message_length'] = limit
534
- grpc_options = normalized
535
536
  if enable_result_batch_v2 and grpc_options is not None and 'grpc_prepare_timeout' in grpc_options:
536
537
  timeout = grpc_options['grpc_prepare_timeout']
537
538
  if (isinstance(timeout, bool) or not isinstance(timeout, (int, float))
@@ -35,6 +35,8 @@ class VectorType(object):
35
35
  NULL = 14
36
36
  TIMESTAMP_TZ = 15
37
37
  DECIMAL128 = 16
38
+ INTERVAL_DAY_SECOND = 17
39
+ CALENDAR_INTERVAL = 18
38
40
 
39
41
  _VALUES_TO_NAMES = {
40
42
  0: "BINARY",
@@ -54,6 +56,8 @@ class VectorType(object):
54
56
  14: "NULL",
55
57
  15: "TIMESTAMP_TZ",
56
58
  16: "DECIMAL128",
59
+ 17: "INTERVAL_DAY_SECOND",
60
+ 18: "CALENDAR_INTERVAL",
57
61
  }
58
62
 
59
63
  _NAMES_TO_VALUES = {
@@ -74,6 +78,8 @@ class VectorType(object):
74
78
  "NULL": 14,
75
79
  "TIMESTAMP_TZ": 15,
76
80
  "DECIMAL128": 16,
81
+ "INTERVAL_DAY_SECOND": 17,
82
+ "CALENDAR_INTERVAL": 18,
77
83
  }
78
84
 
79
85
 
@@ -232,12 +238,13 @@ class Data(object):
232
238
  - timeData
233
239
  - decimal128Data
234
240
  - numericDecimal128ConstantData
241
+ - calendarIntervalData
235
242
 
236
243
  """
237
244
  thrift_spec = None
238
245
 
239
246
 
240
- def __init__(self, boolData = None, int32Data = None, int64Data = None, dateData = None, float32Data = None, float64Data = None, varcharData = None, boolConstantData = None, dateConstantData = None, nullConstantData = None, numericConstantData = None, numericDecimalConstantData = None, temporalIntervalConstantData = None, timeConstantData = None, varcharConstantData = None, timeData = None, decimal128Data = None, numericDecimal128ConstantData = None,):
247
+ def __init__(self, boolData = None, int32Data = None, int64Data = None, dateData = None, float32Data = None, float64Data = None, varcharData = None, boolConstantData = None, dateConstantData = None, nullConstantData = None, numericConstantData = None, numericDecimalConstantData = None, temporalIntervalConstantData = None, timeConstantData = None, varcharConstantData = None, timeData = None, decimal128Data = None, numericDecimal128ConstantData = None, calendarIntervalData = None,):
241
248
  self.boolData = boolData
242
249
  self.int32Data = int32Data
243
250
  self.int64Data = int64Data
@@ -256,6 +263,7 @@ class Data(object):
256
263
  self.timeData = timeData
257
264
  self.decimal128Data = decimal128Data
258
265
  self.numericDecimal128ConstantData = numericDecimal128ConstantData
266
+ self.calendarIntervalData = calendarIntervalData
259
267
 
260
268
  def read(self, iprot):
261
269
  if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None:
@@ -374,6 +382,12 @@ class Data(object):
374
382
  self.numericDecimal128ConstantData.read(iprot)
375
383
  else:
376
384
  iprot.skip(ftype)
385
+ elif fid == 23:
386
+ if ftype == TType.STRUCT:
387
+ self.calendarIntervalData = CalendarIntervalData()
388
+ self.calendarIntervalData.read(iprot)
389
+ else:
390
+ iprot.skip(ftype)
377
391
  else:
378
392
  iprot.skip(ftype)
379
393
  iprot.readFieldEnd()
@@ -457,6 +471,10 @@ class Data(object):
457
471
  oprot.writeFieldBegin('numericDecimal128ConstantData', TType.STRUCT, 18)
458
472
  self.numericDecimal128ConstantData.write(oprot)
459
473
  oprot.writeFieldEnd()
474
+ if self.calendarIntervalData is not None:
475
+ oprot.writeFieldBegin('calendarIntervalData', TType.STRUCT, 23)
476
+ self.calendarIntervalData.write(oprot)
477
+ oprot.writeFieldEnd()
460
478
  oprot.writeFieldStop()
461
479
  oprot.writeStructEnd()
462
480
 
@@ -1738,6 +1756,111 @@ class Chunk(object):
1738
1756
 
1739
1757
  def __ne__(self, other):
1740
1758
  return not (self == other)
1759
+
1760
+
1761
+ class CalendarIntervalData(object):
1762
+ """
1763
+ Attributes:
1764
+ - months
1765
+ - days
1766
+ - microseconds
1767
+
1768
+ """
1769
+ thrift_spec = None
1770
+
1771
+
1772
+ def __init__(self, months = None, days = None, microseconds = None,):
1773
+ self.months = months
1774
+ self.days = days
1775
+ self.microseconds = microseconds
1776
+
1777
+ def read(self, iprot):
1778
+ if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None:
1779
+ iprot._fast_decode(self, iprot, [self.__class__, self.thrift_spec])
1780
+ return
1781
+ iprot.readStructBegin()
1782
+ while True:
1783
+ (fname, ftype, fid) = iprot.readFieldBegin()
1784
+ if ftype == TType.STOP:
1785
+ break
1786
+ if fid == 1:
1787
+ if ftype == TType.LIST:
1788
+ self.months = []
1789
+ (_etype87, _size84) = iprot.readListBegin()
1790
+ for _i88 in range(_size84):
1791
+ _elem89 = iprot.readI32()
1792
+ self.months.append(_elem89)
1793
+ iprot.readListEnd()
1794
+ else:
1795
+ iprot.skip(ftype)
1796
+ elif fid == 2:
1797
+ if ftype == TType.LIST:
1798
+ self.days = []
1799
+ (_etype93, _size90) = iprot.readListBegin()
1800
+ for _i94 in range(_size90):
1801
+ _elem95 = iprot.readI32()
1802
+ self.days.append(_elem95)
1803
+ iprot.readListEnd()
1804
+ else:
1805
+ iprot.skip(ftype)
1806
+ elif fid == 3:
1807
+ if ftype == TType.LIST:
1808
+ self.microseconds = []
1809
+ (_etype99, _size96) = iprot.readListBegin()
1810
+ for _i100 in range(_size96):
1811
+ _elem101 = iprot.readI64()
1812
+ self.microseconds.append(_elem101)
1813
+ iprot.readListEnd()
1814
+ else:
1815
+ iprot.skip(ftype)
1816
+ else:
1817
+ iprot.skip(ftype)
1818
+ iprot.readFieldEnd()
1819
+ iprot.readStructEnd()
1820
+
1821
+ def write(self, oprot):
1822
+ self.validate()
1823
+ if oprot._fast_encode is not None and self.thrift_spec is not None:
1824
+ oprot.trans.write(oprot._fast_encode(self, [self.__class__, self.thrift_spec]))
1825
+ return
1826
+ oprot.writeStructBegin('CalendarIntervalData')
1827
+ if self.months is not None:
1828
+ oprot.writeFieldBegin('months', TType.LIST, 1)
1829
+ oprot.writeListBegin(TType.I32, len(self.months))
1830
+ for iter102 in self.months:
1831
+ oprot.writeI32(iter102)
1832
+ oprot.writeListEnd()
1833
+ oprot.writeFieldEnd()
1834
+ if self.days is not None:
1835
+ oprot.writeFieldBegin('days', TType.LIST, 2)
1836
+ oprot.writeListBegin(TType.I32, len(self.days))
1837
+ for iter103 in self.days:
1838
+ oprot.writeI32(iter103)
1839
+ oprot.writeListEnd()
1840
+ oprot.writeFieldEnd()
1841
+ if self.microseconds is not None:
1842
+ oprot.writeFieldBegin('microseconds', TType.LIST, 3)
1843
+ oprot.writeListBegin(TType.I64, len(self.microseconds))
1844
+ for iter104 in self.microseconds:
1845
+ oprot.writeI64(iter104)
1846
+ oprot.writeListEnd()
1847
+ oprot.writeFieldEnd()
1848
+ oprot.writeFieldStop()
1849
+ oprot.writeStructEnd()
1850
+
1851
+ def validate(self):
1852
+ return
1853
+
1854
+ def __repr__(self):
1855
+ L = ['%s=%r' % (key, value)
1856
+ for key, value in self.__dict__.items()]
1857
+ return '%s(%s)' % (self.__class__.__name__, ', '.join(L))
1858
+
1859
+ def __eq__(self, other):
1860
+ return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
1861
+
1862
+ def __ne__(self, other):
1863
+ return not (self == other)
1741
1864
  all_structs.append(Vector)
1742
1865
  Vector.thrift_spec = (
1743
1866
  None, # 0
@@ -1770,6 +1893,11 @@ Data.thrift_spec = (
1770
1893
  (16, TType.STRUCT, 'timeData', [TimeData, None], None, ), # 16
1771
1894
  (17, TType.STRUCT, 'decimal128Data', [Decimal128Data, None], None, ), # 17
1772
1895
  (18, TType.STRUCT, 'numericDecimal128ConstantData', [NumericDecimal128ConstantData, None], None, ), # 18
1896
+ None, # 19
1897
+ None, # 20
1898
+ None, # 21
1899
+ None, # 22
1900
+ (23, TType.STRUCT, 'calendarIntervalData', [CalendarIntervalData, None], None, ), # 23
1773
1901
  )
1774
1902
  all_structs.append(BoolData)
1775
1903
  BoolData.thrift_spec = (
@@ -1871,5 +1999,12 @@ Chunk.thrift_spec = (
1871
1999
  (1, TType.I32, 'size', None, None, ), # 1
1872
2000
  (2, TType.LIST, 'vectors', (TType.STRUCT, [Vector, None], False), None, ), # 2
1873
2001
  )
2002
+ all_structs.append(CalendarIntervalData)
2003
+ CalendarIntervalData.thrift_spec = (
2004
+ None, # 0
2005
+ (1, TType.LIST, 'months', (TType.I32, None, False), None, ), # 1
2006
+ (2, TType.LIST, 'days', (TType.I32, None, False), None, ), # 2
2007
+ (3, TType.LIST, 'microseconds', (TType.I64, None, False), None, ), # 3
2008
+ )
1874
2009
  fix_spec(all_structs)
1875
2010
  del all_structs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e6data-python-connector
3
- Version: 3.0.1rc4
3
+ Version: 3.0.3rc1
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.
@@ -13,6 +13,7 @@ e6data_python_connector/async_cursor.py
13
13
  e6data_python_connector/async_dialect.py
14
14
  e6data_python_connector/async_oauth.py
15
15
  e6data_python_connector/async_work.py
16
+ e6data_python_connector/calendar_interval.py
16
17
  e6data_python_connector/cluster_manager.py
17
18
  e6data_python_connector/common.py
18
19
  e6data_python_connector/connection_pool.py
@@ -115,6 +116,7 @@ test/integration/test_async_queries.py
115
116
  test/integration/test_async_resume.py
116
117
  test/integration/test_async_sqlalchemy.py
117
118
  test/integration/test_async_token_lifecycle.py
119
+ test/integration/test_cluster_resume_errors.py
118
120
  test/integration/test_e2e_auth_boundary.py
119
121
  test/integration/test_e2e_oauth_continuation.py
120
122
  test/integration/test_e2e_readonly_matrix.py
@@ -12,7 +12,7 @@
12
12
 
13
13
  import setuptools
14
14
 
15
- VERSION = (3, 0, '1rc4',)
15
+ VERSION = (3, 0, '3rc1',)
16
16
 
17
17
 
18
18
  def get_long_desc():
@@ -0,0 +1,179 @@
1
+ """Real RPC regressions on an explicitly prepared, exclusive-use cluster.
2
+
3
+ The operator must suspend the cluster and disable auto-resume before opting in.
4
+ These tests observe real requests and never alter responses or cluster settings.
5
+ """
6
+ import asyncio
7
+ import os
8
+ from pathlib import Path
9
+
10
+ import grpc
11
+ import pytest
12
+
13
+ from e6data_python_connector import Connection
14
+ from e6data_python_connector.aio import AsyncConnection
15
+ from e6data_python_connector.async_cluster_manager import AsyncClusterManager
16
+ from e6data_python_connector.cluster_manager import ClusterManager
17
+ from e6data_python_connector.exceptions import OperationalError
18
+
19
+
20
+ pytestmark = pytest.mark.integration
21
+
22
+
23
+ @pytest.fixture
24
+ def rejected_resume_config(live_config):
25
+ if os.environ.get('E6_RESUME_REJECTION_AUTHORIZED') != '1':
26
+ pytest.skip('Requires an exclusive suspended target with auto-resume disabled')
27
+ if not live_config.connection_kwargs.get('password'):
28
+ pytest.skip('This regression requires the legacy password authentication path')
29
+ return live_config
30
+
31
+
32
+ def _manager_options(config):
33
+ settings = config.connection_kwargs
34
+ return dict(host=settings['host'], port=settings['port'],
35
+ user=settings['username'], password=settings['password'],
36
+ secure_channel=settings.get('secure', False),
37
+ cluster_uuid=settings.get('cluster_name'),
38
+ ssl_cert=settings.get('ssl_cert'),
39
+ grpc_options=settings.get('grpc_options'), timeout=15)
40
+
41
+
42
+ class _ObservedSyncManager(ClusterManager):
43
+ """Observe inherited real RPCs without replacing the transport."""
44
+
45
+ def _try_cluster_request(self, operation):
46
+ self.operations.append(operation)
47
+ try:
48
+ return super()._try_cluster_request(operation)
49
+ except grpc.RpcError as error:
50
+ self.errors.append(error)
51
+ raise
52
+
53
+
54
+ class _ObservedAsyncManager(AsyncClusterManager):
55
+ """Observe inherited real RPCs without replacing the transport."""
56
+
57
+ async def _request(self, client, operation, deadline):
58
+ self.operations.append(operation)
59
+ try:
60
+ return await super()._request(client, operation, deadline)
61
+ except grpc.RpcError as error:
62
+ self.errors.append(error)
63
+ raise
64
+
65
+
66
+ def _observed_manager(manager_type, config, *, timeout=15, debug=False):
67
+ options = _manager_options(config)
68
+ options.update(timeout=timeout, debug=debug)
69
+ manager = manager_type(**options)
70
+ manager.operations = []
71
+ manager.errors = []
72
+ return manager
73
+
74
+
75
+ def _assert_disabled(error):
76
+ assert error.code() == grpc.StatusCode.FAILED_PRECONDITION
77
+ assert 'disabled' in error.details().lower()
78
+
79
+
80
+ def test_sync_active_cluster_does_not_dispatch_resume(live_config):
81
+ if os.environ.get('E6_RESUME_ACTIVE_AUTHORIZED') != '1':
82
+ pytest.skip('Requires an explicitly prepared active target')
83
+ if not live_config.connection_kwargs.get('password'):
84
+ pytest.skip('This regression requires the legacy password authentication path')
85
+ manager = _observed_manager(_ObservedSyncManager, live_config, debug=True)
86
+ try:
87
+ assert manager.resume() is True
88
+ assert manager.operations == ['status']
89
+ assert manager.errors == []
90
+ finally:
91
+ if hasattr(manager, '_channel'):
92
+ manager._channel.close()
93
+
94
+
95
+ def test_sync_resume_preserves_original_terminal_error(rejected_resume_config):
96
+ manager = _observed_manager(_ObservedSyncManager, rejected_resume_config)
97
+ try:
98
+ with pytest.raises(grpc.RpcError) as raised:
99
+ manager.resume()
100
+ _assert_disabled(raised.value)
101
+ assert raised.value is manager.errors[-1]
102
+ assert manager.operations == ['status', 'resume']
103
+ finally:
104
+ if hasattr(manager, '_channel'):
105
+ manager._channel.close()
106
+
107
+
108
+ def test_sync_query_preserves_disabled_resume_error(rejected_resume_config):
109
+ options = dict(rejected_resume_config.connection_kwargs, auto_resume=True)
110
+ with pytest.raises(grpc.RpcError) as raised:
111
+ with Connection(**options) as connection:
112
+ with connection.cursor() as cursor:
113
+ cursor.execute(rejected_resume_config.read_only_sql)
114
+ _assert_disabled(raised.value)
115
+
116
+
117
+ def test_async_resume_preserves_terminal_reason_and_cause(rejected_resume_config):
118
+ async def run():
119
+ manager = _observed_manager(_ObservedAsyncManager, rejected_resume_config)
120
+ try:
121
+ with pytest.raises(OperationalError) as raised:
122
+ await manager.resume()
123
+ cause = raised.value.__cause__
124
+ _assert_disabled(cause)
125
+ assert cause is manager.errors[-1]
126
+ assert cause.details() in str(raised.value)
127
+ assert manager.operations == ['status', 'resume']
128
+ finally:
129
+ await manager.close()
130
+ asyncio.run(run())
131
+
132
+
133
+ def test_async_rejected_resume_allows_a_later_attempt(rejected_resume_config):
134
+ async def run():
135
+ manager = _observed_manager(_ObservedAsyncManager, rejected_resume_config)
136
+ try:
137
+ for _ in range(2):
138
+ with pytest.raises(OperationalError) as raised:
139
+ await manager.resume()
140
+ _assert_disabled(raised.value.__cause__)
141
+ assert manager._mutation.pending is None
142
+ assert manager.operations == ['status', 'resume', 'status', 'resume']
143
+ finally:
144
+ await manager.close()
145
+ asyncio.run(run())
146
+
147
+
148
+ def test_async_same_manager_recovers_after_reenable(rejected_resume_config):
149
+ signal_value = os.environ.get('E6_RESUME_REENABLE_SIGNAL')
150
+ if not signal_value:
151
+ pytest.skip('Requires operator re-enable coordination through a fresh signal path')
152
+ signal = Path(signal_value)
153
+ assert signal.is_absolute()
154
+ rejected, enabled = Path(str(signal) + '.rejected'), Path(str(signal) + '.enabled')
155
+ assert not rejected.exists() and not enabled.exists(), 'Use a fresh signal path'
156
+
157
+ async def run():
158
+ manager = _observed_manager(_ObservedAsyncManager, rejected_resume_config, timeout=300)
159
+ try:
160
+ with pytest.raises(OperationalError) as raised:
161
+ await manager.resume()
162
+ _assert_disabled(raised.value.__cause__)
163
+ assert manager._mutation.pending is None
164
+ rejected.touch(mode=0o600)
165
+ async with asyncio.timeout(180):
166
+ while not enabled.exists():
167
+ await asyncio.sleep(.25)
168
+ assert await manager.resume()
169
+ assert manager._mutation.pending is None
170
+ assert manager.operations.count('resume') == 2
171
+ async with AsyncConnection(**rejected_resume_config.connection_kwargs) as connection:
172
+ async with connection.cursor() as cursor:
173
+ await cursor.execute(rejected_resume_config.read_only_sql)
174
+ assert [list(row) for row in await cursor.fetchall()] == rejected_resume_config.expected_rows
175
+ finally:
176
+ await manager.close()
177
+ rejected.unlink(missing_ok=True)
178
+ enabled.unlink(missing_ok=True)
179
+ asyncio.run(run())