e6data-python-connector 2.3.12rc32__tar.gz → 2.3.13rc1__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 (92) hide show
  1. {e6data_python_connector-2.3.12rc32/e6data_python_connector.egg-info → e6data_python_connector-2.3.13rc1}/PKG-INFO +6 -2
  2. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/README.md +5 -1
  3. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/cluster_manager.py +3 -60
  4. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/common.py +44 -0
  5. e6data_python_connector-2.3.13rc1/e6data_python_connector/constants.py +56 -0
  6. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/datainputstream.py +46 -36
  7. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/date_time_utils.py +95 -0
  8. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/e6data_grpc.py +14 -57
  9. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1/e6data_python_connector.egg-info}/PKG-INFO +6 -2
  10. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector.egg-info/SOURCES.txt +4 -0
  11. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/setup.py +1 -1
  12. e6data_python_connector-2.3.13rc1/test/benchmark_fastbinary_performance.py +303 -0
  13. e6data_python_connector-2.3.13rc1/test/test_code_optimizations.py +322 -0
  14. e6data_python_connector-2.3.13rc1/test/test_optimizations_integration.py +443 -0
  15. e6data_python_connector-2.3.13rc1/test/test_ssl_cert.py +548 -0
  16. e6data_python_connector-2.3.12rc32/e6data_python_connector/constants.py +0 -32
  17. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/LICENSE +0 -0
  18. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/MANIFEST.in +0 -0
  19. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/__init__.py +0 -0
  20. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/cluster_server/__init__.py +0 -0
  21. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  22. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  23. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/connection_pool.py +0 -0
  24. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/dialect.py +0 -0
  25. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  26. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/e6x_vector/constants.py +0 -0
  27. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  28. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/exceptions.py +0 -0
  29. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/server/QueryEngineService.py +0 -0
  30. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/server/__init__.py +0 -0
  31. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/server/constants.py +0 -0
  32. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  33. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  34. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  35. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/server/ttypes.py +0 -0
  36. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/strategy.py +0 -0
  37. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector/typeId.py +0 -0
  38. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  39. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  40. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector.egg-info/requires.txt +0 -0
  41. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/e6data_python_connector.egg-info/top_level.txt +0 -0
  42. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/gen-py/__init__.py +0 -0
  43. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/gen-py/e6x_vector/__init__.py +0 -0
  44. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/gen-py/e6x_vector/constants.py +0 -0
  45. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/gen-py/e6x_vector/ttypes.py +0 -0
  46. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/pyproject.toml +0 -0
  47. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/setup.cfg +0 -0
  48. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/__init__.py +0 -0
  49. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/analyze_38_nines.py +0 -0
  50. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/analyze_all_cases.py +0 -0
  51. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/analyze_binary.py +0 -0
  52. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/analyze_correct_value.py +0 -0
  53. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/analyze_fields.py +0 -0
  54. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/check_decimal_errors.py +0 -0
  55. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/cleanup_test_files.py +0 -0
  56. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/debug_38_nines.py +0 -0
  57. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/debug_binary.py +0 -0
  58. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/final_test.py +0 -0
  59. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/move_tests.py +0 -0
  60. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/quick_test.py +0 -0
  61. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_38_nines.py +0 -0
  62. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_all_decimal128_cases.py +0 -0
  63. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_cluster_manager_efficiency.py +0 -0
  64. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_cluster_manager_none_strategy.py +0 -0
  65. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_cluster_manager_strategy.py +0 -0
  66. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_comprehensive.py +0 -0
  67. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_connection_pool.py +0 -0
  68. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_connection_pool_e2e.py +0 -0
  69. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_current_implementation.py +0 -0
  70. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_decimal128_binary_parsing.py +0 -0
  71. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_decimal128_parsing.py +0 -0
  72. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_fix.py +0 -0
  73. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_improved_parsing.py +0 -0
  74. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_known_case.py +0 -0
  75. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_manual_analysis.py +0 -0
  76. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_mock_server.py +0 -0
  77. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_multiprocessing_fix.py +0 -0
  78. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_new_implementation.py +0 -0
  79. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_pool_concurrency_simple.py +0 -0
  80. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_pool_threading_multiprocessing.py +0 -0
  81. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_simple_connection_pool.py +0 -0
  82. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_specific_binary.py +0 -0
  83. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_strategy.py +0 -0
  84. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_strategy_logic.py +0 -0
  85. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_strategy_persistence_fix.py +0 -0
  86. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_strategy_sharing_fix.py +0 -0
  87. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/test_user_binary_value.py +0 -0
  88. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/tests.py +0 -0
  89. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/tests_grpc.py +0 -0
  90. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/validate_decimal128.py +0 -0
  91. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/validate_implementation.py +0 -0
  92. {e6data_python_connector-2.3.12rc32 → e6data_python_connector-2.3.13rc1}/test/verify_decimal_fix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: e6data-python-connector
3
- Version: 2.3.12rc32
3
+ Version: 2.3.13rc1
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.
@@ -39,7 +39,7 @@ Dynamic: summary
39
39
 
40
40
  # e6data Python Connector
41
41
 
42
- ![version](https://img.shields.io/badge/version-2.3.12rc32-blue.svg)
42
+ ![version](https://img.shields.io/badge/version-2.3.13rc1-blue.svg)
43
43
 
44
44
  ## Introduction
45
45
 
@@ -54,6 +54,10 @@ yum install python3-devel gcc-c++ -y
54
54
  # Ubuntu/Debian dependencies
55
55
  apt install python3-dev g++ -y
56
56
 
57
+ # Windows dependencies
58
+ # Install Visual C++ Build Tools from:
59
+ # https://visualstudio.microsoft.com/visual-cpp-build-tools/
60
+ # Select the "Desktop Development with C++" option during installation.
57
61
 
58
62
  # Pip dependencies
59
63
  pip install wheel
@@ -1,6 +1,6 @@
1
1
  # e6data Python Connector
2
2
 
3
- ![version](https://img.shields.io/badge/version-2.3.12rc32-blue.svg)
3
+ ![version](https://img.shields.io/badge/version-2.3.13rc1-blue.svg)
4
4
 
5
5
  ## Introduction
6
6
 
@@ -15,6 +15,10 @@ yum install python3-devel gcc-c++ -y
15
15
  # Ubuntu/Debian dependencies
16
16
  apt install python3-dev g++ -y
17
17
 
18
+ # Windows dependencies
19
+ # Install Visual C++ Build Tools from:
20
+ # https://visualstudio.microsoft.com/visual-cpp-build-tools/
21
+ # Select the "Desktop Development with C++" option during installation.
18
22
 
19
23
  # Pip dependencies
20
24
  pip install wheel
@@ -9,27 +9,9 @@ import multiprocessing
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
+ from e6data_python_connector.common import get_ssl_credentials
12
13
  from e6data_python_connector.strategy import _get_active_strategy, _set_active_strategy, _set_pending_strategy, \
13
- _get_grpc_header as _get_strategy_header
14
-
15
-
16
- def _get_grpc_header(engine_ip=None, cluster=None, strategy=None):
17
- """
18
- Generate gRPC metadata headers for the request.
19
-
20
- This function creates a list of metadata headers to be used in gRPC requests.
21
- It includes optional headers for the engine IP, cluster UUID, and deployment strategy.
22
-
23
- Args:
24
- engine_ip (str, optional): The IP address of the engine. Defaults to None.
25
- cluster (str, optional): The UUID of the cluster. Defaults to None.
26
- strategy (str, optional): The deployment strategy (blue/green). Defaults to None.
27
-
28
- Returns:
29
- list: A list of tuples representing the gRPC metadata headers.
30
- """
31
- # Use the strategy module's implementation
32
- return _get_strategy_header(engine_ip=engine_ip, cluster=cluster, strategy=strategy)
14
+ _get_grpc_header
33
15
 
34
16
 
35
17
  class _StatusLock:
@@ -173,45 +155,6 @@ class ClusterManager:
173
155
  self._debug = debug
174
156
  self._ssl_cert = ssl_cert
175
157
 
176
- def _get_ssl_credentials(self):
177
- """
178
- Get SSL credentials for secure gRPC channel.
179
-
180
- Handles three scenarios:
181
- 1. ssl_cert is a string (file path): Read the PEM certificate from the file
182
- 2. ssl_cert is bytes: Use the certificate content directly
183
- 3. ssl_cert is None: Use system default CA bundle
184
-
185
- Returns:
186
- grpc.ChannelCredentials: SSL credentials for secure channel
187
-
188
- Raises:
189
- FileNotFoundError: If ssl_cert is a file path but the file doesn't exist
190
- IOError: If ssl_cert file cannot be read
191
- """
192
- if self._ssl_cert is None:
193
- # Use system default CA bundle
194
- return grpc.ssl_channel_credentials()
195
- elif isinstance(self._ssl_cert, str):
196
- # ssl_cert is a file path - read the certificate from file
197
- try:
198
- with open(self._ssl_cert, 'rb') as cert_file:
199
- root_ca_cert = cert_file.read()
200
- return grpc.ssl_channel_credentials(root_certificates=root_ca_cert)
201
- except FileNotFoundError:
202
- logger.error(f"SSL certificate file not found: {self._ssl_cert}")
203
- raise
204
- except IOError as e:
205
- logger.error(f"Failed to read SSL certificate file {self._ssl_cert}: {e}")
206
- raise
207
- elif isinstance(self._ssl_cert, bytes):
208
- # ssl_cert is certificate content as bytes
209
- return grpc.ssl_channel_credentials(root_certificates=self._ssl_cert)
210
- else:
211
- # Invalid type - log warning and use system default
212
- logger.warning(f"Invalid ssl_cert type: {type(self._ssl_cert)}. Using system default CA bundle.")
213
- return grpc.ssl_channel_credentials()
214
-
215
158
  @property
216
159
  def _get_connection(self):
217
160
  """
@@ -226,7 +169,7 @@ class ClusterManager:
226
169
  self._channel = grpc.secure_channel(
227
170
  target='{}:{}'.format(self._host, self._port),
228
171
  options=self._grpc_options,
229
- credentials=self._get_ssl_credentials()
172
+ credentials=get_ssl_credentials(self._ssl_cert)
230
173
  )
231
174
  else:
232
175
  self._channel = grpc.insecure_channel(
@@ -15,6 +15,7 @@ from builtins import int
15
15
  from builtins import object
16
16
  from builtins import str
17
17
 
18
+ import grpc
18
19
  from future.utils import with_metaclass
19
20
  from past.builtins import basestring
20
21
 
@@ -267,3 +268,46 @@ class UniversalSet(object):
267
268
 
268
269
  def __contains__(self, item):
269
270
  return True
271
+
272
+
273
+ def get_ssl_credentials(ssl_cert):
274
+ """
275
+ Get SSL credentials for secure gRPC channel.
276
+
277
+ Handles three scenarios:
278
+ 1. ssl_cert is a string (file path): Read the PEM certificate from the file
279
+ 2. ssl_cert is bytes: Use the certificate content directly
280
+ 3. ssl_cert is None: Use system default CA bundle
281
+
282
+ Parameters:
283
+ ssl_cert (str or bytes or None): SSL certificate as file path, bytes, or None
284
+
285
+ Returns:
286
+ grpc.ChannelCredentials: SSL credentials for gRPC channel
287
+
288
+ Raises:
289
+ FileNotFoundError: If ssl_cert is a file path but the file doesn't exist
290
+ IOError: If ssl_cert file cannot be read
291
+ """
292
+ if ssl_cert is None:
293
+ # Use system default CA bundle
294
+ return grpc.ssl_channel_credentials()
295
+ elif isinstance(ssl_cert, str):
296
+ # ssl_cert is a file path - read the certificate from file
297
+ try:
298
+ with open(ssl_cert, 'rb') as cert_file:
299
+ root_ca_cert = cert_file.read()
300
+ return grpc.ssl_channel_credentials(root_certificates=root_ca_cert)
301
+ except FileNotFoundError:
302
+ _logger.error("SSL certificate file not found: {}".format(ssl_cert))
303
+ raise
304
+ except IOError as e:
305
+ _logger.error("Failed to read SSL certificate file {}: {}".format(ssl_cert, e))
306
+ raise
307
+ elif isinstance(ssl_cert, bytes):
308
+ # ssl_cert is certificate content as bytes
309
+ return grpc.ssl_channel_credentials(root_certificates=ssl_cert)
310
+ else:
311
+ # Invalid type - fall back to system default CA bundle with warning
312
+ _logger.warning("Invalid ssl_cert type: {}. Using system default CA bundle.".format(type(ssl_cert)))
313
+ return grpc.ssl_channel_credentials()
@@ -0,0 +1,56 @@
1
+ import pytz
2
+
3
+ # Type definitions
4
+ PRIMITIVE_TYPES = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 17, 18, 19, 20, 21}
5
+ COMPLEX_TYPES = {10, 11, 12, 13, 14}
6
+ COLLECTION_TYPES = {10, 11}
7
+ TYPE_NAMES = {
8
+ 0: "BOOLEAN",
9
+ 1: "TINYINT",
10
+ 2: "SMALLINT",
11
+ 3: "INT",
12
+ 4: "BIGINT",
13
+ 5: "FLOAT",
14
+ 6: "DOUBLE",
15
+ 7: "STRING",
16
+ 8: "TIMESTAMP",
17
+ 9: "BINARY",
18
+ 10: "ARRAY",
19
+ 11: "MAP",
20
+ 12: "STRUCT",
21
+ 13: "UNIONTYPE",
22
+ 15: "DECIMAL",
23
+ 16: "NULL",
24
+ 17: "DATE",
25
+ 18: "VARCHAR",
26
+ 19: "CHAR",
27
+ 20: "INTERVAL_YEAR_MONTH",
28
+ 21: "INTERVAL_DAY_TIME",
29
+ }
30
+ CHARACTER_MAXIMUM_LENGTH = "characterMaximumLength"
31
+ PRECISION = "precision"
32
+ SCALE = "scale"
33
+ ZONE = pytz.timezone('UTC')
34
+
35
+ # Retry and timeout constants
36
+ MAX_RETRY_ATTEMPTS = 5
37
+ RETRY_SLEEP_SECONDS = 0.2
38
+ STRATEGY_CACHE_TIMEOUT_SECONDS = 300 # 5 minutes
39
+ DEFAULT_GRPC_PREPARE_TIMEOUT_SECONDS = 600 # 10 minutes
40
+ DEFAULT_AUTO_RESUME_TIMEOUT_SECONDS = 300 # 5 minutes
41
+ CLUSTER_STATUS_CHECK_SLEEP_SECONDS = 5
42
+ LOCK_TIMEOUT_MS = 500
43
+
44
+ # Connection pool constants
45
+ POOL_GET_TIMEOUT_SECONDS = 0.1
46
+ POOL_RETRY_SLEEP_SECONDS = 0.1
47
+
48
+ # Blue-green deployment strategy constants
49
+ STRATEGY_BLUE = 'blue'
50
+ STRATEGY_GREEN = 'green'
51
+ VALID_STRATEGIES = {STRATEGY_BLUE, STRATEGY_GREEN}
52
+
53
+ # gRPC error codes and messages
54
+ GRPC_ERROR_STRATEGY_MISMATCH = 'status: 456'
55
+ GRPC_ERROR_SERVICE_UNAVAILABLE = 'status: 503'
56
+ GRPC_ERROR_ACCESS_DENIED = 'Access denied'
@@ -10,7 +10,13 @@ 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.date_time_utils import floor_div, floor_mod, timezone_from_offset
13
+ from e6data_python_connector.date_time_utils import (
14
+ floor_div,
15
+ floor_mod,
16
+ timezone_from_offset,
17
+ format_iso_date_from_epoch_micros,
18
+ format_iso_datetime_from_epoch_micros,
19
+ )
14
20
 
15
21
  # Try to import fastbinary - it's optional but provides better performance
16
22
  _fastbinary_available = False
@@ -463,16 +469,11 @@ def read_values_from_array(query_columns_description: list, dis: DataInputStream
463
469
  if dtype == "LONG":
464
470
  value_array.append(dis.read_long())
465
471
  elif dtype == "DATE":
466
- epoch_seconds = floor_div(dis.read_long(), 1000_000)
467
- date = datetime.fromtimestamp(epoch_seconds, ZONE)
468
- value_array.append(date.strftime("%Y-%m-%d"))
472
+ value_array.append(format_iso_date_from_epoch_micros(dis.read_long()))
469
473
  elif dtype == "DATETIME":
470
- epoch_micros = dis.read_long()
471
- epoch_seconds = floor_div(epoch_micros, 1000_000)
472
- micros_of_the_day = floor_mod(epoch_micros, 1000_000)
473
- date_time = datetime.fromtimestamp(epoch_seconds, ZONE)
474
- date_time = date_time + timedelta(microseconds=micros_of_the_day)
475
- value_array.append(date_time.strftime("%Y-%m-%d %H:%M:%S"))
474
+ value_array.append(format_iso_datetime_from_epoch_micros(
475
+ dis.read_long(), tz=ZONE, separator=' ',
476
+ include_millis=False, include_offset=False))
476
477
  elif dtype == "STRING" or dtype == "ARRAY" or dtype == "MAP" or dtype == "STRUCT":
477
478
  value_array.append(dis.read_utf().decode())
478
479
  elif dtype == "INT":
@@ -564,23 +565,27 @@ def get_column_from_chunk(vector: Vector) -> list:
564
565
  if get_null(vector, row):
565
566
  value_array.append(None)
566
567
  continue
567
- epoch_seconds = floor_div(vector.data.dateData.data[
568
- row] if not vector.isConstantVector else vector.data.dateConstantData.data,
569
- 1000_000)
570
- date = datetime.fromtimestamp(epoch_seconds, zone)
571
- value_array.append(date.strftime("%Y-%m-%d"))
568
+ try:
569
+ raw = (vector.data.dateData.data[row]
570
+ if not vector.isConstantVector
571
+ else vector.data.dateConstantData.data)
572
+ value_array.append(format_iso_date_from_epoch_micros(raw))
573
+ except Exception as e:
574
+ _logger.error("Failed to parse DATE row=%s: %s", row, e)
575
+ value_array.append('Failed to parse.')
572
576
  elif d_type == VectorType.DATETIME:
573
577
  for row in range(vector.size):
574
578
  if get_null(vector, row):
575
579
  value_array.append(None)
576
580
  continue
577
- epoch_micros = vector.data.timeData.data[
578
- row] if not vector.isConstantVector else vector.data.timeConstantData.data
579
- epoch_seconds = floor_div(epoch_micros, 1000_000)
580
- micros_of_the_day = floor_mod(epoch_micros, 1000_000)
581
- date_time = datetime.fromtimestamp(epoch_seconds, zone)
582
- date_time = date_time + timedelta(microseconds=micros_of_the_day)
583
- value_array.append(date_time.isoformat(timespec='milliseconds'))
581
+ try:
582
+ epoch_micros = (vector.data.timeData.data[row]
583
+ if not vector.isConstantVector
584
+ else vector.data.timeConstantData.data)
585
+ value_array.append(format_iso_datetime_from_epoch_micros(epoch_micros, tz=zone))
586
+ except Exception as e:
587
+ _logger.error("Failed to parse DATETIME row=%s: %s", row, e)
588
+ value_array.append('Failed to parse.')
584
589
  elif d_type == VectorType.STRING or d_type == VectorType.ARRAY or d_type == VectorType.MAP or d_type == VectorType.STRUCT:
585
590
  for row in range(vector.size):
586
591
  if get_null(vector, row):
@@ -631,18 +636,21 @@ def get_column_from_chunk(vector: Vector) -> list:
631
636
  if get_null(vector, row):
632
637
  value_array.append(None)
633
638
  continue
634
- epoch_micros = vector.data.timeData.data[
635
- row] if not vector.isConstantVector else vector.data.timeConstantData.data
636
- if ((vector.isConstantVector and vector.data.timeConstantData.zoneData is not None) or
637
- (not vector.isConstantVector and vector.data.timeData.zoneData is not None)):
638
- zone_id = vector.data.timeData.zoneData[
639
- row] if not vector.isConstantVector else vector.data.timeConstantData.zoneData
640
- zone = timezone_from_offset(zone_id)
641
- epoch_seconds = floor_div(epoch_micros, 1000_000)
642
- micros_of_the_day = floor_mod(epoch_micros, 1000_000)
643
- date_time = datetime.fromtimestamp(epoch_seconds, zone)
644
- date_time = date_time + timedelta(microseconds=micros_of_the_day)
645
- value_array.append(date_time.isoformat(timespec='milliseconds'))
639
+ try:
640
+ epoch_micros = (vector.data.timeData.data[row]
641
+ if not vector.isConstantVector
642
+ else vector.data.timeConstantData.data)
643
+ row_zone = zone
644
+ if ((vector.isConstantVector and vector.data.timeConstantData.zoneData is not None) or
645
+ (not vector.isConstantVector and vector.data.timeData.zoneData is not None)):
646
+ zone_id = (vector.data.timeData.zoneData[row]
647
+ if not vector.isConstantVector
648
+ else vector.data.timeConstantData.zoneData)
649
+ row_zone = timezone_from_offset(zone_id)
650
+ value_array.append(format_iso_datetime_from_epoch_micros(epoch_micros, tz=row_zone))
651
+ except Exception as e:
652
+ _logger.error("Failed to parse TIMESTAMP_TZ row=%s: %s", row, e)
653
+ value_array.append('Failed to parse.')
646
654
  elif d_type == VectorType.DECIMAL128:
647
655
  # Handle both constant and non-constant vectors following Java implementation
648
656
  if vector.isConstantVector:
@@ -679,6 +687,8 @@ def get_column_from_chunk(vector: Vector) -> list:
679
687
  else:
680
688
  value_array.append(None)
681
689
  except Exception as e:
682
- _logger.error(e)
683
- value_array.append('Failed to parse.')
690
+ _logger.error("get_column_from_chunk failed (vectorType=%s, parsed=%s/%s): %s",
691
+ d_type, len(value_array), vector.size, e)
692
+ while len(value_array) < vector.size:
693
+ value_array.append('Failed to parse.')
684
694
  return value_array
@@ -160,6 +160,101 @@ def floor_mod(x, y):
160
160
  return r
161
161
 
162
162
 
163
+ _SECONDS_PER_DAY = 86400
164
+
165
+
166
+ def _civil_from_days(z):
167
+ """
168
+ Convert days since 1970-01-01 to proleptic Gregorian (year, month, day).
169
+
170
+ Howard Hinnant's algorithm. Supports any year representable by a Python int,
171
+ so it does not overflow like datetime.fromtimestamp (capped at year 9999).
172
+ """
173
+ z = z + 719468
174
+ era = (z if z >= 0 else z - 146096) // 146097
175
+ doe = z - era * 146097
176
+ yoe = (doe - doe // 1460 + doe // 36524 - doe // 146096) // 365
177
+ y = yoe + era * 400
178
+ doy = doe - (365 * yoe + yoe // 4 - yoe // 100)
179
+ mp = (5 * doy + 2) // 153
180
+ d = doy - (153 * mp + 2) // 5 + 1
181
+ m = mp + 3 if mp < 10 else mp - 9
182
+ if m <= 2:
183
+ y += 1
184
+ return y, m, d
185
+
186
+
187
+ def _format_iso_year(year):
188
+ """Java LocalDate-style year: 0000-9999 unprefixed; outside range gets +/- prefix."""
189
+ if 0 <= year <= 9999:
190
+ return "{:04d}".format(year)
191
+ if year > 9999:
192
+ return "+{}".format(year)
193
+ return "-{:04d}".format(abs(year)) if abs(year) <= 9999 else "-{}".format(abs(year))
194
+
195
+
196
+ def _tz_offset_minutes(tz, ref_seconds=0):
197
+ """Best-effort fixed-offset extraction. Returns 0 if unknown."""
198
+ if tz is None:
199
+ return 0
200
+ try:
201
+ offset = tz.utcoffset(None)
202
+ if offset is not None:
203
+ return int(offset.total_seconds() // 60)
204
+ except (TypeError, AttributeError, ValueError):
205
+ pass
206
+ # Fallback for named timezones: probe with a reference datetime.
207
+ try:
208
+ from datetime import datetime as _dt
209
+ probe = _dt.utcfromtimestamp(max(0, min(ref_seconds, 253370764800))) # clamp to year 9999
210
+ offset = tz.utcoffset(probe)
211
+ if offset is not None:
212
+ return int(offset.total_seconds() // 60)
213
+ except Exception:
214
+ pass
215
+ return 0
216
+
217
+
218
+ def format_iso_date_from_epoch_micros(epoch_micros):
219
+ """
220
+ Format epoch microseconds (UTC) as ISO-8601 date with Java LocalDate-style
221
+ expanded year support (e.g. '+11756-12-31'). Never raises on year overflow.
222
+ """
223
+ epoch_seconds = floor_div(epoch_micros, 1_000_000)
224
+ days = floor_div(epoch_seconds, _SECONDS_PER_DAY)
225
+ y, mo, d = _civil_from_days(days)
226
+ return "{}-{:02d}-{:02d}".format(_format_iso_year(y), mo, d)
227
+
228
+
229
+ def format_iso_datetime_from_epoch_micros(epoch_micros, tz=None, separator='T',
230
+ include_millis=True, include_offset=True):
231
+ """
232
+ Format epoch microseconds as ISO-8601 datetime with expanded year support.
233
+ Mirrors Java's LocalDateTime/OffsetDateTime toString() semantics so out-of-range
234
+ years are emitted instead of raising.
235
+ """
236
+ epoch_seconds_utc = floor_div(epoch_micros, 1_000_000)
237
+ offset_minutes = _tz_offset_minutes(tz, ref_seconds=epoch_seconds_utc)
238
+ local_micros = epoch_micros + offset_minutes * 60 * 1_000_000
239
+ epoch_seconds = floor_div(local_micros, 1_000_000)
240
+ micros_of_second = floor_mod(local_micros, 1_000_000)
241
+ days = floor_div(epoch_seconds, _SECONDS_PER_DAY)
242
+ sec_of_day = floor_mod(epoch_seconds, _SECONDS_PER_DAY)
243
+ y, mo, d = _civil_from_days(days)
244
+ h = sec_of_day // 3600
245
+ mi = (sec_of_day % 3600) // 60
246
+ s = sec_of_day % 60
247
+ out = "{}-{:02d}-{:02d}{}{:02d}:{:02d}:{:02d}".format(
248
+ _format_iso_year(y), mo, d, separator, h, mi, s)
249
+ if include_millis:
250
+ out += ".{:03d}".format(micros_of_second // 1000)
251
+ if include_offset:
252
+ sign = '+' if offset_minutes >= 0 else '-'
253
+ tot = abs(offset_minutes)
254
+ out += "{}{:02d}:{:02d}".format(sign, tot // 60, tot % 60)
255
+ return out
256
+
257
+
163
258
  def timezone_from_offset(offset_string) -> pytz:
164
259
  # Parse the offset string into hours and minutes
165
260
  sign = -1 if offset_string[0] == "-" else 1
@@ -9,25 +9,27 @@ from __future__ import unicode_literals
9
9
  import datetime
10
10
  import logging
11
11
  import os
12
-
13
12
  import re
14
13
  import sys
14
+ import threading
15
15
  import time
16
16
  from decimal import Decimal
17
17
  from io import BytesIO
18
18
  from ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
19
- import threading
20
- import multiprocessing
21
19
 
22
20
  import grpc
23
21
  from grpc._channel import _InactiveRpcError
24
22
 
25
23
  from e6data_python_connector.cluster_manager import ClusterManager
26
- from e6data_python_connector.strategy import _get_grpc_header as _get_strategy_header
27
- from e6data_python_connector.common import DBAPITypeObject, ParamEscaper, DBAPICursor
28
- from e6data_python_connector.constants import *
29
- from e6data_python_connector.datainputstream import get_query_columns_info, read_rows_from_chunk, is_fastbinary_available
24
+ from e6data_python_connector.common import DBAPITypeObject, ParamEscaper, DBAPICursor, get_ssl_credentials
25
+ from e6data_python_connector.constants import (
26
+ MAX_RETRY_ATTEMPTS, RETRY_SLEEP_SECONDS, GRPC_ERROR_STRATEGY_MISMATCH, GRPC_ERROR_ACCESS_DENIED,
27
+ PRIMITIVE_TYPES
28
+ )
29
+ from e6data_python_connector.datainputstream import get_query_columns_info, read_rows_from_chunk, \
30
+ is_fastbinary_available
30
31
  from e6data_python_connector.server import e6x_engine_pb2_grpc, e6x_engine_pb2
32
+ from e6data_python_connector.strategy import _get_grpc_header
31
33
  from e6data_python_connector.typeId import *
32
34
 
33
35
  apilevel = '2.0'
@@ -71,7 +73,7 @@ TYPES_CONVERTER = {
71
73
 
72
74
  def re_auth(func):
73
75
  def wrapper(self, *args, **kwargs):
74
- max_retry = 5
76
+ max_retry = MAX_RETRY_ATTEMPTS
75
77
  current_retry = 0
76
78
  while current_retry < max_retry:
77
79
  try:
@@ -80,10 +82,10 @@ def re_auth(func):
80
82
  current_retry += 1
81
83
  if current_retry == max_retry:
82
84
  raise e
83
- if e.code() == grpc.StatusCode.INTERNAL and 'Access denied' in e.details():
84
- time.sleep(0.2)
85
+ if e.code() == grpc.StatusCode.INTERNAL and GRPC_ERROR_ACCESS_DENIED in e.details():
86
+ time.sleep(RETRY_SLEEP_SECONDS)
85
87
  self.connection.get_re_authenticate_session_id()
86
- elif 'status: 456' in e.details():
88
+ elif GRPC_ERROR_STRATEGY_MISMATCH in e.details():
87
89
  # Strategy changed, clear cache and retry
88
90
  _clear_strategy_cache()
89
91
  # Force re-authentication which will detect new strategy
@@ -304,12 +306,6 @@ def _get_strategy_debug_info():
304
306
  }
305
307
 
306
308
 
307
- def _get_grpc_header(engine_ip=None, cluster=None, strategy=None):
308
- """Generate gRPC metadata headers for the request."""
309
- # Use the strategy module's implementation
310
- return _get_strategy_header(engine_ip=engine_ip, cluster=cluster, strategy=strategy)
311
-
312
-
313
309
  def connect(*args, **kwargs):
314
310
  """Constructor for creating a connection to the database. See class :py:class:`Connection` for
315
311
  arguments.
@@ -516,45 +512,6 @@ class Connection(object):
516
512
 
517
513
  return self._cached_grpc_options
518
514
 
519
- def _get_ssl_credentials(self):
520
- """
521
- Get SSL credentials for secure gRPC channel.
522
-
523
- Handles three scenarios:
524
- 1. ssl_cert is a string (file path): Read the PEM certificate from the file
525
- 2. ssl_cert is bytes: Use the certificate content directly
526
- 3. ssl_cert is None: Use system default CA bundle
527
-
528
- Returns:
529
- grpc.ChannelCredentials: SSL credentials for secure channel
530
-
531
- Raises:
532
- FileNotFoundError: If ssl_cert is a file path but the file doesn't exist
533
- IOError: If ssl_cert file cannot be read
534
- """
535
- if self._ssl_cert is None:
536
- # Use system default CA bundle
537
- return grpc.ssl_channel_credentials()
538
- elif isinstance(self._ssl_cert, str):
539
- # ssl_cert is a file path - read the certificate from file
540
- try:
541
- with open(self._ssl_cert, 'rb') as cert_file:
542
- root_ca_cert = cert_file.read()
543
- return grpc.ssl_channel_credentials(root_certificates=root_ca_cert)
544
- except FileNotFoundError:
545
- logger.error(f"SSL certificate file not found: {self._ssl_cert}")
546
- raise
547
- except IOError as e:
548
- logger.error(f"Failed to read SSL certificate file {self._ssl_cert}: {e}")
549
- raise
550
- elif isinstance(self._ssl_cert, bytes):
551
- # ssl_cert is certificate content as bytes
552
- return grpc.ssl_channel_credentials(root_certificates=self._ssl_cert)
553
- else:
554
- # Invalid type - log warning and use system default
555
- logger.warning(f"Invalid ssl_cert type: {type(self._ssl_cert)}. Using system default CA bundle.")
556
- return grpc.ssl_channel_credentials()
557
-
558
515
  def _create_client(self):
559
516
  """
560
517
  Creates a gRPC client for the connection.
@@ -575,7 +532,7 @@ class Connection(object):
575
532
  self._channel = grpc.secure_channel(
576
533
  target='{}:{}'.format(self._host, self._port),
577
534
  options=self._get_grpc_options,
578
- credentials=self._get_ssl_credentials()
535
+ credentials=get_ssl_credentials(self._ssl_cert)
579
536
  )
580
537
  else:
581
538
  self._channel = grpc.insecure_channel(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: e6data-python-connector
3
- Version: 2.3.12rc32
3
+ Version: 2.3.13rc1
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.
@@ -39,7 +39,7 @@ Dynamic: summary
39
39
 
40
40
  # e6data Python Connector
41
41
 
42
- ![version](https://img.shields.io/badge/version-2.3.12rc32-blue.svg)
42
+ ![version](https://img.shields.io/badge/version-2.3.13rc1-blue.svg)
43
43
 
44
44
  ## Introduction
45
45
 
@@ -54,6 +54,10 @@ yum install python3-devel gcc-c++ -y
54
54
  # Ubuntu/Debian dependencies
55
55
  apt install python3-dev g++ -y
56
56
 
57
+ # Windows dependencies
58
+ # Install Visual C++ Build Tools from:
59
+ # https://visualstudio.microsoft.com/visual-cpp-build-tools/
60
+ # Select the "Desktop Development with C++" option during installation.
57
61
 
58
62
  # Pip dependencies
59
63
  pip install wheel
@@ -45,6 +45,7 @@ test/analyze_all_cases.py
45
45
  test/analyze_binary.py
46
46
  test/analyze_correct_value.py
47
47
  test/analyze_fields.py
48
+ test/benchmark_fastbinary_performance.py
48
49
  test/check_decimal_errors.py
49
50
  test/cleanup_test_files.py
50
51
  test/debug_38_nines.py
@@ -57,6 +58,7 @@ test/test_all_decimal128_cases.py
57
58
  test/test_cluster_manager_efficiency.py
58
59
  test/test_cluster_manager_none_strategy.py
59
60
  test/test_cluster_manager_strategy.py
61
+ test/test_code_optimizations.py
60
62
  test/test_comprehensive.py
61
63
  test/test_connection_pool.py
62
64
  test/test_connection_pool_e2e.py
@@ -70,10 +72,12 @@ test/test_manual_analysis.py
70
72
  test/test_mock_server.py
71
73
  test/test_multiprocessing_fix.py
72
74
  test/test_new_implementation.py
75
+ test/test_optimizations_integration.py
73
76
  test/test_pool_concurrency_simple.py
74
77
  test/test_pool_threading_multiprocessing.py
75
78
  test/test_simple_connection_pool.py
76
79
  test/test_specific_binary.py
80
+ test/test_ssl_cert.py
77
81
  test/test_strategy.py
78
82
  test/test_strategy_logic.py
79
83
  test/test_strategy_persistence_fix.py
@@ -12,7 +12,7 @@
12
12
 
13
13
  import setuptools
14
14
 
15
- VERSION = (2, 3, 12, 'rc32',)
15
+ VERSION = (2, 3, 13, 'rc1')
16
16
 
17
17
 
18
18
  def get_long_desc():