e6data-python-connector 3.0.2__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.2/e6data_python_connector.egg-info → e6data_python_connector-3.0.3rc1}/PKG-INFO +1 -1
  2. {e6data_python_connector-3.0.2 → 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.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_manager.py +2 -0
  5. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/datainputstream.py +15 -0
  6. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6x_vector/ttypes.py +133 -1
  7. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1/e6data_python_connector.egg-info}/PKG-INFO +1 -1
  8. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/SOURCES.txt +2 -0
  9. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/setup.py +1 -1
  10. e6data_python_connector-3.0.3rc1/test/integration/test_cluster_resume_errors.py +179 -0
  11. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/LICENSE +0 -0
  12. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/MANIFEST.in +0 -0
  13. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/README.md +0 -0
  14. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/__init__.py +0 -0
  15. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/aio.py +0 -0
  16. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_connection.py +0 -0
  17. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_connection_pool.py +0 -0
  18. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_cursor.py +0 -0
  19. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_dialect.py +0 -0
  20. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_oauth.py +0 -0
  21. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/async_work.py +0 -0
  22. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_server/__init__.py +0 -0
  23. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  24. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  25. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/common.py +0 -0
  26. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/connection_pool.py +0 -0
  27. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/constants.py +0 -0
  28. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/date_time_utils.py +0 -0
  29. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/dialect.py +0 -0
  30. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6data_grpc.py +0 -0
  31. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  32. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/e6x_vector/constants.py +0 -0
  33. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/exceptions.py +0 -0
  34. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/oauth.py +0 -0
  35. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/oauth_cache.py +0 -0
  36. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/oauth_common.py +0 -0
  37. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_batch.py +0 -0
  38. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_decode.py +0 -0
  39. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_decode_worker.py +0 -0
  40. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/result_prefetch.py +0 -0
  41. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/QueryEngineService.py +0 -0
  42. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/__init__.py +0 -0
  43. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/constants.py +0 -0
  44. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  45. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  46. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  47. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/server/ttypes.py +0 -0
  48. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/strategy.py +0 -0
  49. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector/typeId.py +0 -0
  50. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  51. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  52. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/requires.txt +0 -0
  53. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/e6data_python_connector.egg-info/top_level.txt +0 -0
  54. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/pyproject.toml +0 -0
  55. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/setup.cfg +0 -0
  56. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/__init__.py +0 -0
  57. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/analyze_38_nines.py +0 -0
  58. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/analyze_all_cases.py +0 -0
  59. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/analyze_binary.py +0 -0
  60. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/analyze_correct_value.py +0 -0
  61. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/analyze_fields.py +0 -0
  62. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/check_decimal_errors.py +0 -0
  63. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/cleanup_test_files.py +0 -0
  64. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/conftest.py +0 -0
  65. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/debug_38_nines.py +0 -0
  66. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/debug_binary.py +0 -0
  67. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/final_test.py +0 -0
  68. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/__init__.py +0 -0
  69. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/benchmark_preflight.py +0 -0
  70. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/benchmark_result_batch_v2.py +0 -0
  71. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/benchmark_sync_async.py +0 -0
  72. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/expiry_preflight.py +0 -0
  73. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/result_batch_benchmark_support.py +0 -0
  74. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/support.py +0 -0
  75. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_connection.py +0 -0
  76. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_pool.py +0 -0
  77. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_queries.py +0 -0
  78. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_resume.py +0 -0
  79. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_sqlalchemy.py +0 -0
  80. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_async_token_lifecycle.py +0 -0
  81. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_e2e_auth_boundary.py +0 -0
  82. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_e2e_oauth_continuation.py +0 -0
  83. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_e2e_readonly_matrix.py +0 -0
  84. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_result_batch_v2.py +0 -0
  85. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_shared_token_issuance.py +0 -0
  86. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/test_sync_oauth_lifecycle.py +0 -0
  87. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/integration/token_observation.py +0 -0
  88. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/move_tests.py +0 -0
  89. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/quick_test.py +0 -0
  90. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_38_nines.py +0 -0
  91. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_all_decimal128_cases.py +0 -0
  92. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_efficiency.py +0 -0
  93. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_none_strategy.py +0 -0
  94. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_oauth.py +0 -0
  95. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_cluster_manager_strategy.py +0 -0
  96. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_code_optimizations.py +0 -0
  97. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_comprehensive.py +0 -0
  98. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_connection_pool.py +0 -0
  99. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_connection_pool_e2e.py +0 -0
  100. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_current_implementation.py +0 -0
  101. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_decimal128_binary_parsing.py +0 -0
  102. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_decimal128_parsing.py +0 -0
  103. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_dialect_columns.py +0 -0
  104. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_fix.py +0 -0
  105. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_improved_parsing.py +0 -0
  106. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_known_case.py +0 -0
  107. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_manual_analysis.py +0 -0
  108. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_mock_server.py +0 -0
  109. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_multiprocessing_fix.py +0 -0
  110. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_new_implementation.py +0 -0
  111. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_oauth_authentication.py +0 -0
  112. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_oauth_auto_resume.py +0 -0
  113. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_oauth_resume_integration.py +0 -0
  114. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_pool_concurrency_simple.py +0 -0
  115. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_pool_threading_multiprocessing.py +0 -0
  116. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_simple_connection_pool.py +0 -0
  117. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_specific_binary.py +0 -0
  118. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_strategy.py +0 -0
  119. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_strategy_logic.py +0 -0
  120. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_strategy_persistence_fix.py +0 -0
  121. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_strategy_sharing_fix.py +0 -0
  122. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/test_user_binary_value.py +0 -0
  123. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/tests.py +0 -0
  124. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/tests_grpc.py +0 -0
  125. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/validate_decimal128.py +0 -0
  126. {e6data_python_connector-3.0.2 → e6data_python_connector-3.0.3rc1}/test/validate_implementation.py +0 -0
  127. {e6data_python_connector-3.0.2 → 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.2
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,8 @@ 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()))
481
484
  elif dtype == "INTERVAL_DAY_SECOND":
482
485
  value_array.append(timedelta(microseconds=dis.read_long()))
483
486
  elif dtype == "DATE":
@@ -584,6 +587,18 @@ def get_column_from_chunk(vector: Vector, strict=False) -> list:
584
587
  continue
585
588
  value_array.append(vector.data.int64Data.data[
586
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)))
587
602
  elif d_type == VectorType.INTERVAL_DAY_SECOND:
588
603
  for row in range(vector.size):
589
604
  if get_null(vector, row):
@@ -36,6 +36,7 @@ class VectorType(object):
36
36
  TIMESTAMP_TZ = 15
37
37
  DECIMAL128 = 16
38
38
  INTERVAL_DAY_SECOND = 17
39
+ CALENDAR_INTERVAL = 18
39
40
 
40
41
  _VALUES_TO_NAMES = {
41
42
  0: "BINARY",
@@ -56,6 +57,7 @@ class VectorType(object):
56
57
  15: "TIMESTAMP_TZ",
57
58
  16: "DECIMAL128",
58
59
  17: "INTERVAL_DAY_SECOND",
60
+ 18: "CALENDAR_INTERVAL",
59
61
  }
60
62
 
61
63
  _NAMES_TO_VALUES = {
@@ -77,6 +79,7 @@ class VectorType(object):
77
79
  "TIMESTAMP_TZ": 15,
78
80
  "DECIMAL128": 16,
79
81
  "INTERVAL_DAY_SECOND": 17,
82
+ "CALENDAR_INTERVAL": 18,
80
83
  }
81
84
 
82
85
 
@@ -235,12 +238,13 @@ class Data(object):
235
238
  - timeData
236
239
  - decimal128Data
237
240
  - numericDecimal128ConstantData
241
+ - calendarIntervalData
238
242
 
239
243
  """
240
244
  thrift_spec = None
241
245
 
242
246
 
243
- 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,):
244
248
  self.boolData = boolData
245
249
  self.int32Data = int32Data
246
250
  self.int64Data = int64Data
@@ -259,6 +263,7 @@ class Data(object):
259
263
  self.timeData = timeData
260
264
  self.decimal128Data = decimal128Data
261
265
  self.numericDecimal128ConstantData = numericDecimal128ConstantData
266
+ self.calendarIntervalData = calendarIntervalData
262
267
 
263
268
  def read(self, iprot):
264
269
  if iprot._fast_decode is not None and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None:
@@ -377,6 +382,12 @@ class Data(object):
377
382
  self.numericDecimal128ConstantData.read(iprot)
378
383
  else:
379
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)
380
391
  else:
381
392
  iprot.skip(ftype)
382
393
  iprot.readFieldEnd()
@@ -460,6 +471,10 @@ class Data(object):
460
471
  oprot.writeFieldBegin('numericDecimal128ConstantData', TType.STRUCT, 18)
461
472
  self.numericDecimal128ConstantData.write(oprot)
462
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()
463
478
  oprot.writeFieldStop()
464
479
  oprot.writeStructEnd()
465
480
 
@@ -1741,6 +1756,111 @@ class Chunk(object):
1741
1756
 
1742
1757
  def __ne__(self, other):
1743
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)
1744
1864
  all_structs.append(Vector)
1745
1865
  Vector.thrift_spec = (
1746
1866
  None, # 0
@@ -1773,6 +1893,11 @@ Data.thrift_spec = (
1773
1893
  (16, TType.STRUCT, 'timeData', [TimeData, None], None, ), # 16
1774
1894
  (17, TType.STRUCT, 'decimal128Data', [Decimal128Data, None], None, ), # 17
1775
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
1776
1901
  )
1777
1902
  all_structs.append(BoolData)
1778
1903
  BoolData.thrift_spec = (
@@ -1874,5 +1999,12 @@ Chunk.thrift_spec = (
1874
1999
  (1, TType.I32, 'size', None, None, ), # 1
1875
2000
  (2, TType.LIST, 'vectors', (TType.STRUCT, [Vector, None], False), None, ), # 2
1876
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
+ )
1877
2009
  fix_spec(all_structs)
1878
2010
  del all_structs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e6data-python-connector
3
- Version: 3.0.2
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, 2,)
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())