e6data-python-connector 2.3.12rc2__tar.gz → 2.3.12rc12__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 (87) hide show
  1. {e6data_python_connector-2.3.12rc2/e6data_python_connector.egg-info → e6data_python_connector-2.3.12rc12}/PKG-INFO +78 -2
  2. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/README.md +77 -1
  3. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_manager.py +76 -2
  4. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6data_grpc.py +78 -17
  5. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12/e6data_python_connector.egg-info}/PKG-INFO +78 -2
  6. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/setup.py +1 -1
  7. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/LICENSE +0 -0
  8. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/MANIFEST.in +0 -0
  9. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/__init__.py +0 -0
  10. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_server/__init__.py +0 -0
  11. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  12. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  13. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/common.py +0 -0
  14. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/connection_pool.py +0 -0
  15. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/constants.py +0 -0
  16. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/datainputstream.py +0 -0
  17. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/date_time_utils.py +0 -0
  18. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/dialect.py +0 -0
  19. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  20. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6x_vector/constants.py +0 -0
  21. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  22. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/exceptions.py +0 -0
  23. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/QueryEngineService.py +0 -0
  24. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/__init__.py +0 -0
  25. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/constants.py +0 -0
  26. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  27. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  28. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  29. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/ttypes.py +0 -0
  30. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/strategy.py +0 -0
  31. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/typeId.py +0 -0
  32. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/SOURCES.txt +0 -0
  33. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  34. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  35. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/requires.txt +0 -0
  36. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/top_level.txt +0 -0
  37. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/gen-py/__init__.py +0 -0
  38. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/gen-py/e6x_vector/__init__.py +0 -0
  39. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/gen-py/e6x_vector/constants.py +0 -0
  40. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/gen-py/e6x_vector/ttypes.py +0 -0
  41. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/pyproject.toml +0 -0
  42. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/setup.cfg +0 -0
  43. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/__init__.py +0 -0
  44. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/analyze_38_nines.py +0 -0
  45. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/analyze_all_cases.py +0 -0
  46. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/analyze_binary.py +0 -0
  47. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/analyze_correct_value.py +0 -0
  48. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/analyze_fields.py +0 -0
  49. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/check_decimal_errors.py +0 -0
  50. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/cleanup_test_files.py +0 -0
  51. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/debug_38_nines.py +0 -0
  52. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/debug_binary.py +0 -0
  53. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/final_test.py +0 -0
  54. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/move_tests.py +0 -0
  55. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/quick_test.py +0 -0
  56. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_38_nines.py +0 -0
  57. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_all_decimal128_cases.py +0 -0
  58. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_cluster_manager_efficiency.py +0 -0
  59. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_cluster_manager_none_strategy.py +0 -0
  60. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_cluster_manager_strategy.py +0 -0
  61. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_comprehensive.py +0 -0
  62. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_connection_pool.py +0 -0
  63. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_connection_pool_e2e.py +0 -0
  64. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_current_implementation.py +0 -0
  65. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_decimal128_binary_parsing.py +0 -0
  66. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_decimal128_parsing.py +0 -0
  67. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_fix.py +0 -0
  68. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_improved_parsing.py +0 -0
  69. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_known_case.py +0 -0
  70. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_manual_analysis.py +0 -0
  71. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_mock_server.py +0 -0
  72. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_multiprocessing_fix.py +0 -0
  73. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_new_implementation.py +0 -0
  74. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_pool_concurrency_simple.py +0 -0
  75. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_pool_threading_multiprocessing.py +0 -0
  76. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_simple_connection_pool.py +0 -0
  77. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_specific_binary.py +0 -0
  78. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_strategy.py +0 -0
  79. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_strategy_logic.py +0 -0
  80. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_strategy_persistence_fix.py +0 -0
  81. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_strategy_sharing_fix.py +0 -0
  82. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/test_user_binary_value.py +0 -0
  83. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/tests.py +0 -0
  84. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/tests_grpc.py +0 -0
  85. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/validate_decimal128.py +0 -0
  86. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/test/validate_implementation.py +0 -0
  87. {e6data_python_connector-2.3.12rc2 → e6data_python_connector-2.3.12rc12}/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.12rc2
3
+ Version: 2.3.12rc12
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.12rc2-blue.svg)
42
+ ![version](https://img.shields.io/badge/version-2.3.12rc12-blue.svg)
43
43
 
44
44
  ## Introduction
45
45
 
@@ -125,6 +125,7 @@ The `Connection` class supports the following parameters:
125
125
  | `auto_resume` | bool | No | True | Automatically resume cluster if suspended |
126
126
  | `grpc_options` | dict | No | None | Additional gRPC configuration options |
127
127
  | `debug` | bool | No | False | Enable debug logging for troubleshooting |
128
+ | `ssl_cert` | str/bytes | No | None | Path to CA certificate (PEM) or certificate bytes for HTTPS connections |
128
129
 
129
130
  #### Secure Connection Example
130
131
 
@@ -142,6 +143,52 @@ conn = Connection(
142
143
  )
143
144
  ```
144
145
 
146
+ #### HTTPS Connection with HAProxy
147
+
148
+ When connecting through HAProxy with HTTPS/TLS and custom CA certificates:
149
+
150
+ ```python
151
+ from e6data_python_connector import Connection
152
+
153
+ # Option 1: Using CA certificate file path
154
+ conn = Connection(
155
+ host=host,
156
+ port=port,
157
+ username=username,
158
+ password=password,
159
+ database=database,
160
+ cluster_name='analytics-cluster-01', # Specify cluster name
161
+ secure=True,
162
+ ssl_cert='/path/to/ca-cert.pem' # Path to CA certificate
163
+ )
164
+
165
+ # Option 2: Reading certificate content as bytes
166
+ with open('/path/to/ca-cert.pem', 'rb') as f:
167
+ ca_cert = f.read()
168
+
169
+ conn = Connection(
170
+ host=host,
171
+ port=port,
172
+ username=username,
173
+ password=password,
174
+ database=database,
175
+ secure=True,
176
+ ssl_cert=ca_cert # Certificate as bytes
177
+ )
178
+
179
+ # Option 3: System CA bundle (for publicly signed certificates)
180
+ conn = Connection(
181
+ host=host,
182
+ port=port,
183
+ username=username,
184
+ password=password,
185
+ database=database,
186
+ secure=True # Uses system CA bundle
187
+ )
188
+ ```
189
+
190
+ **Note:** Certificate verification is always enabled for security. For self-signed certificates, provide the CA certificate via the `ssl_cert` parameter.
191
+
145
192
  #### Cluster-Specific Connection
146
193
 
147
194
  When working with multiple clusters, specify the cluster name:
@@ -504,6 +551,35 @@ pool.return_connection(conn)
504
551
  pool.close_all()
505
552
  ```
506
553
 
554
+ ##### Connection Pool with HTTPS (HAProxy)
555
+
556
+ ```python
557
+ from e6data_python_connector import ConnectionPool
558
+
559
+ # Connection pool with HTTPS and custom CA certificate
560
+ pool = ConnectionPool(
561
+ min_size=2,
562
+ max_size=10,
563
+ host=host,
564
+ port=port,
565
+ username=username,
566
+ password=password,
567
+ database=database,
568
+ secure=True,
569
+ ssl_cert='/path/to/ca-cert.pem' # CA certificate for HTTPS
570
+ )
571
+
572
+ # Use the pool with context manager for automatic cleanup
573
+ with pool.get_connection_context() as conn:
574
+ cursor = conn.cursor()
575
+ cursor.execute("SELECT * FROM table")
576
+ results = cursor.fetchall()
577
+ print(f"Retrieved {len(results)} rows")
578
+
579
+ # Clean up pool when done
580
+ pool.close_all()
581
+ ```
582
+
507
583
  ##### Using Context Manager (Recommended)
508
584
 
509
585
  The context manager pattern ensures connections are automatically returned to the pool:
@@ -1,6 +1,6 @@
1
1
  # e6data Python Connector
2
2
 
3
- ![version](https://img.shields.io/badge/version-2.3.12rc2-blue.svg)
3
+ ![version](https://img.shields.io/badge/version-2.3.12rc12-blue.svg)
4
4
 
5
5
  ## Introduction
6
6
 
@@ -86,6 +86,7 @@ The `Connection` class supports the following parameters:
86
86
  | `auto_resume` | bool | No | True | Automatically resume cluster if suspended |
87
87
  | `grpc_options` | dict | No | None | Additional gRPC configuration options |
88
88
  | `debug` | bool | No | False | Enable debug logging for troubleshooting |
89
+ | `ssl_cert` | str/bytes | No | None | Path to CA certificate (PEM) or certificate bytes for HTTPS connections |
89
90
 
90
91
  #### Secure Connection Example
91
92
 
@@ -103,6 +104,52 @@ conn = Connection(
103
104
  )
104
105
  ```
105
106
 
107
+ #### HTTPS Connection with HAProxy
108
+
109
+ When connecting through HAProxy with HTTPS/TLS and custom CA certificates:
110
+
111
+ ```python
112
+ from e6data_python_connector import Connection
113
+
114
+ # Option 1: Using CA certificate file path
115
+ conn = Connection(
116
+ host=host,
117
+ port=port,
118
+ username=username,
119
+ password=password,
120
+ database=database,
121
+ cluster_name='analytics-cluster-01', # Specify cluster name
122
+ secure=True,
123
+ ssl_cert='/path/to/ca-cert.pem' # Path to CA certificate
124
+ )
125
+
126
+ # Option 2: Reading certificate content as bytes
127
+ with open('/path/to/ca-cert.pem', 'rb') as f:
128
+ ca_cert = f.read()
129
+
130
+ conn = Connection(
131
+ host=host,
132
+ port=port,
133
+ username=username,
134
+ password=password,
135
+ database=database,
136
+ secure=True,
137
+ ssl_cert=ca_cert # Certificate as bytes
138
+ )
139
+
140
+ # Option 3: System CA bundle (for publicly signed certificates)
141
+ conn = Connection(
142
+ host=host,
143
+ port=port,
144
+ username=username,
145
+ password=password,
146
+ database=database,
147
+ secure=True # Uses system CA bundle
148
+ )
149
+ ```
150
+
151
+ **Note:** Certificate verification is always enabled for security. For self-signed certificates, provide the CA certificate via the `ssl_cert` parameter.
152
+
106
153
  #### Cluster-Specific Connection
107
154
 
108
155
  When working with multiple clusters, specify the cluster name:
@@ -465,6 +512,35 @@ pool.return_connection(conn)
465
512
  pool.close_all()
466
513
  ```
467
514
 
515
+ ##### Connection Pool with HTTPS (HAProxy)
516
+
517
+ ```python
518
+ from e6data_python_connector import ConnectionPool
519
+
520
+ # Connection pool with HTTPS and custom CA certificate
521
+ pool = ConnectionPool(
522
+ min_size=2,
523
+ max_size=10,
524
+ host=host,
525
+ port=port,
526
+ username=username,
527
+ password=password,
528
+ database=database,
529
+ secure=True,
530
+ ssl_cert='/path/to/ca-cert.pem' # CA certificate for HTTPS
531
+ )
532
+
533
+ # Use the pool with context manager for automatic cleanup
534
+ with pool.get_connection_context() as conn:
535
+ cursor = conn.cursor()
536
+ cursor.execute("SELECT * FROM table")
537
+ results = cursor.fetchall()
538
+ print(f"Retrieved {len(results)} rows")
539
+
540
+ # Clean up pool when done
541
+ pool.close_all()
542
+ ```
543
+
468
544
  ##### Using Context Manager (Recommended)
469
545
 
470
546
  The context manager pattern ensures connections are automatically returned to the pool:
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import threading
3
3
  import time
4
+ from typing import Optional, Union
4
5
  import e6data_python_connector.cluster_server.cluster_pb2 as cluster_pb2
5
6
  import e6data_python_connector.cluster_server.cluster_pb2_grpc as cluster_pb2_grpc
6
7
  import grpc
@@ -140,7 +141,7 @@ class ClusterManager:
140
141
  """
141
142
 
142
143
  def __init__(self, host: str, port: int, user: str, password: str, secure_channel: bool = False, timeout=60 * 5,
143
- cluster_uuid=None, grpc_options=None, debug=False):
144
+ cluster_uuid=None, grpc_options=None, debug=False, ssl_cert: Optional[Union[str, bytes]] = None):
144
145
  """
145
146
  Initializes a new instance of the ClusterManager class.
146
147
 
@@ -155,7 +156,12 @@ class ClusterManager:
155
156
  defaults to 5 minutes.
156
157
  cluster_uuid (str, optional): The unique identifier for the target cluster;
157
158
  defaults to None.
159
+ grpc_options (dict, optional): Additional gRPC configuration options;
160
+ defaults to None.
158
161
  debug (bool, optional): Enable debug logging; defaults to False.
162
+ ssl_cert (str or bytes, optional): Path to CA certificate file (PEM format)
163
+ or certificate content as bytes. Used when secure_channel=True.
164
+ If not provided and secure_channel=True, system default CA bundle will be used.
159
165
  """
160
166
 
161
167
  self._host = host
@@ -164,12 +170,67 @@ class ClusterManager:
164
170
  self._password = password
165
171
  self._timeout = time.time() + timeout
166
172
  self._secure_channel = secure_channel
173
+ self._ssl_cert = ssl_cert
167
174
  self.cluster_uuid = cluster_uuid
168
175
  self._grpc_options = grpc_options
169
176
  if grpc_options is None:
170
177
  self._grpc_options = dict()
171
178
  self._debug = debug
172
179
 
180
+ def _get_ssl_credentials(self):
181
+ """
182
+ Creates SSL/TLS credentials for secure gRPC connections.
183
+
184
+ This method handles loading CA certificates from various sources:
185
+ - File path (str): Reads the certificate from the specified file
186
+ - Bytes: Uses the certificate content directly
187
+ - None: Uses system default CA bundle
188
+
189
+ Returns:
190
+ grpc.ChannelCredentials: SSL credentials for secure channel creation
191
+
192
+ Raises:
193
+ FileNotFoundError: If ssl_cert is a file path and the file doesn't exist
194
+ ValueError: If ssl_cert format is invalid or file cannot be read
195
+ """
196
+ root_certificates = None
197
+
198
+ if self._ssl_cert:
199
+ # Custom CA certificate provided
200
+ if isinstance(self._ssl_cert, str):
201
+ # It's a file path - read the certificate
202
+ try:
203
+ with open(self._ssl_cert, 'rb') as f:
204
+ root_certificates = f.read()
205
+ if self._debug:
206
+ logger.info(f"ClusterManager: Loaded CA certificate from file: {self._ssl_cert}")
207
+ except FileNotFoundError:
208
+ raise FileNotFoundError(
209
+ f"CA certificate file not found: {self._ssl_cert}. "
210
+ f"Please ensure the file exists and the path is correct."
211
+ )
212
+ except Exception as e:
213
+ raise ValueError(
214
+ f"Failed to read CA certificate from {self._ssl_cert}: {str(e)}"
215
+ )
216
+ elif isinstance(self._ssl_cert, bytes):
217
+ # Certificate content provided as bytes
218
+ root_certificates = self._ssl_cert
219
+ if self._debug:
220
+ logger.info("ClusterManager: Using CA certificate provided as bytes")
221
+ else:
222
+ raise ValueError(
223
+ f"ssl_cert must be either a file path (str) or certificate content (bytes), "
224
+ f"got {type(self._ssl_cert)}"
225
+ )
226
+ else:
227
+ # No custom CA certificate - use system default CA bundle
228
+ if self._debug:
229
+ logger.info("ClusterManager: Using system default CA bundle for SSL/TLS")
230
+
231
+ # Create and return SSL credentials with certificate verification enabled
232
+ return grpc.ssl_channel_credentials(root_certificates=root_certificates)
233
+
173
234
  @property
174
235
  def _get_connection(self):
175
236
  """
@@ -181,16 +242,29 @@ class ClusterManager:
181
242
  """
182
243
 
183
244
  if self._secure_channel:
245
+ # Get SSL credentials (handles CA cert loading and validation)
246
+ credentials = self._get_ssl_credentials()
247
+
184
248
  self._channel = grpc.secure_channel(
185
249
  target='{}:{}'.format(self._host, self._port),
186
250
  options=self._grpc_options,
187
- credentials=grpc.ssl_channel_credentials()
251
+ credentials=credentials
188
252
  )
253
+
254
+ if self._debug:
255
+ logger.info(
256
+ f"ClusterManager: Created secure gRPC channel to {self._host}:{self._port} "
257
+ f"with SSL/TLS certificate verification enabled"
258
+ )
189
259
  else:
190
260
  self._channel = grpc.insecure_channel(
191
261
  target='{}:{}'.format(self._host, self._port),
192
262
  options=self._grpc_options
193
263
  )
264
+
265
+ if self._debug:
266
+ logger.info(f"ClusterManager: Created insecure gRPC channel to {self._host}:{self._port}")
267
+
194
268
  return cluster_pb2_grpc.ClusterServiceStub(self._channel)
195
269
 
196
270
  def _try_cluster_request(self, request_type, payload=None):
@@ -16,6 +16,7 @@ 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
+ from typing import Union, Optional
19
20
  import threading
20
21
  import multiprocessing
21
22
 
@@ -335,6 +336,7 @@ class Connection(object):
335
336
  scheme: str = 'e6data',
336
337
  grpc_options: dict = None,
337
338
  debug: bool = False,
339
+ ssl_cert: Optional[Union[str, bytes]] = None,
338
340
  ):
339
341
  """
340
342
  Parameters
@@ -368,6 +370,14 @@ class Connection(object):
368
370
  - keepalive_time_ms: This parameter defines the time, in milliseconds, Default to 30 seconds
369
371
  debug: bool, Optional
370
372
  Flag to enable debug logging for blue-green deployment strategy changes
373
+ ssl_cert: str or bytes, Optional
374
+ Path to CA certificate file (PEM format) or certificate content as bytes.
375
+ Used when connecting via HTTPS .
376
+ If not provided and secure=True, system default CA bundle will be used.
377
+ For self-signed certificates, provide the CA certificate via this parameter.
378
+ Examples:
379
+ - File path: ssl_cert='/path/to/ca-cert.pem'
380
+ - Bytes: ssl_cert=open('/path/to/ca-cert.pem', 'rb').read()
371
381
  """
372
382
  if not username or not password:
373
383
  raise ValueError("username or password cannot be empty.")
@@ -382,13 +392,13 @@ class Connection(object):
382
392
  self._port = port
383
393
 
384
394
  self._secure_channel = secure
395
+ self._ssl_cert = ssl_cert
385
396
 
386
397
  self.catalog_name = catalog
387
398
 
388
399
  self._auto_resume = auto_resume
389
400
 
390
401
  self._grpc_options = grpc_options
391
-
392
402
  if self._grpc_options is None:
393
403
  self._grpc_options = dict()
394
404
  self.grpc_prepare_timeout = self._grpc_options.get('grpc_prepare_timeout') or 10 * 60 # 10 minutes
@@ -398,15 +408,7 @@ class Connection(object):
398
408
  The default maximum time on client side to wait for the cluster to resume is 5 minutes.
399
409
  """
400
410
  self.grpc_auto_resume_timeout_seconds = self._grpc_options.pop('grpc_auto_resume_timeout_seconds')
401
-
402
- self.timeout = 500
403
- if 'timeout' in self._grpc_options:
404
- """
405
- The default maximum time on client side to wait for the cluster to resume is 5 minutes.
406
- """
407
- self.timeout = self._grpc_options.pop('timeout')
408
-
409
-
411
+
410
412
  # Store debug flag and register with debug connections
411
413
  self._debug = debug
412
414
  if self._debug:
@@ -492,6 +494,60 @@ class Connection(object):
492
494
 
493
495
  return self._cached_grpc_options
494
496
 
497
+ def _get_ssl_credentials(self):
498
+ """
499
+ Creates SSL/TLS credentials for secure gRPC connections.
500
+
501
+ This method handles loading CA certificates from various sources:
502
+ - File path (str): Reads the certificate from the specified file
503
+ - Bytes: Uses the certificate content directly
504
+ - None: Uses system default CA bundle
505
+
506
+ Returns:
507
+ grpc.ChannelCredentials: SSL credentials for secure channel creation
508
+
509
+ Raises:
510
+ FileNotFoundError: If ssl_cert is a file path and the file doesn't exist
511
+ ValueError: If ssl_cert format is invalid or file cannot be read
512
+ """
513
+ root_certificates = None
514
+
515
+ if self._ssl_cert:
516
+ # Custom CA certificate provided
517
+ if isinstance(self._ssl_cert, str):
518
+ # It's a file path - read the certificate
519
+ try:
520
+ with open(self._ssl_cert, 'rb') as f:
521
+ root_certificates = f.read()
522
+ if self._debug:
523
+ logger.info(f"Loaded CA certificate from file: {self._ssl_cert}")
524
+ except FileNotFoundError:
525
+ raise FileNotFoundError(
526
+ f"CA certificate file not found: {self._ssl_cert}. "
527
+ f"Please ensure the file exists and the path is correct."
528
+ )
529
+ except Exception as e:
530
+ raise ValueError(
531
+ f"Failed to read CA certificate from {self._ssl_cert}: {str(e)}"
532
+ )
533
+ elif isinstance(self._ssl_cert, bytes):
534
+ # Certificate content provided as bytes
535
+ root_certificates = self._ssl_cert
536
+ if self._debug:
537
+ logger.info("Using CA certificate provided as bytes")
538
+ else:
539
+ raise ValueError(
540
+ f"ssl_cert must be either a file path (str) or certificate content (bytes), "
541
+ f"got {type(self._ssl_cert)}"
542
+ )
543
+ else:
544
+ # No custom CA certificate - use system default CA bundle
545
+ if self._debug:
546
+ logger.info("Using system default CA bundle for SSL/TLS")
547
+
548
+ # Create and return SSL credentials with certificate verification enabled
549
+ return grpc.ssl_channel_credentials(root_certificates=root_certificates)
550
+
495
551
  def _create_client(self):
496
552
  """
497
553
  Creates a gRPC client for the connection.
@@ -506,13 +562,19 @@ class Connection(object):
506
562
 
507
563
  Raises:
508
564
  grpc.RpcError: If there is an error in creating the gRPC channel or client stub.
565
+ FileNotFoundError: If ssl_cert is a file path and the file doesn't exist.
566
+ ValueError: If ssl_cert format is invalid.
509
567
  """
510
568
 
511
569
  if self._secure_channel:
570
+ # Get SSL credentials (handles CA cert loading and validation)
571
+ credentials = self._get_ssl_credentials()
572
+
573
+ # Create secure channel
512
574
  self._channel = grpc.secure_channel(
513
575
  target='{}:{}'.format(self._host, self._port),
514
576
  options=self._get_grpc_options,
515
- credentials=grpc.ssl_channel_credentials()
577
+ credentials=credentials
516
578
  )
517
579
  else:
518
580
  self._channel = grpc.insecure_channel(
@@ -694,7 +756,8 @@ class Connection(object):
694
756
  secure_channel=self._secure_channel,
695
757
  cluster_uuid=self.cluster_name,
696
758
  timeout=self.grpc_auto_resume_timeout_seconds,
697
- debug=self._debug
759
+ debug=self._debug,
760
+ ssl_cert=self._ssl_cert
698
761
  ).resume()
699
762
  return status # Return boolean status directly
700
763
  else:
@@ -1343,8 +1406,7 @@ class Cursor(DBAPICursor):
1343
1406
  client = self.connection.client
1344
1407
  execute_response = client.executeStatementV2(
1345
1408
  execute_statement_request,
1346
- metadata=self.metadata,
1347
- timeout=self.connection.timeout
1409
+ metadata=self.metadata
1348
1410
  )
1349
1411
 
1350
1412
  # Check for new strategy in execute response
@@ -1453,14 +1515,13 @@ class Cursor(DBAPICursor):
1453
1515
  get_next_result_batch_request = e6x_engine_pb2.GetNextResultBatchRequest(
1454
1516
  engineIP=self._engine_ip,
1455
1517
  sessionId=self.connection.get_session_id,
1456
- queryId=self._query_id,
1518
+ queryId=self._query_id
1457
1519
  )
1458
1520
  # Get fresh client after session access (may have been invalidated)
1459
1521
  client = self.connection.client
1460
1522
  get_next_result_batch_response = client.getNextResultBatch(
1461
1523
  get_next_result_batch_request,
1462
- metadata=self.metadata,
1463
- timeout=self.connection.timeout
1524
+ metadata=self.metadata
1464
1525
  )
1465
1526
 
1466
1527
  # Check for new strategy in batch response
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: e6data-python-connector
3
- Version: 2.3.12rc2
3
+ Version: 2.3.12rc12
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.12rc2-blue.svg)
42
+ ![version](https://img.shields.io/badge/version-2.3.12rc12-blue.svg)
43
43
 
44
44
  ## Introduction
45
45
 
@@ -125,6 +125,7 @@ The `Connection` class supports the following parameters:
125
125
  | `auto_resume` | bool | No | True | Automatically resume cluster if suspended |
126
126
  | `grpc_options` | dict | No | None | Additional gRPC configuration options |
127
127
  | `debug` | bool | No | False | Enable debug logging for troubleshooting |
128
+ | `ssl_cert` | str/bytes | No | None | Path to CA certificate (PEM) or certificate bytes for HTTPS connections |
128
129
 
129
130
  #### Secure Connection Example
130
131
 
@@ -142,6 +143,52 @@ conn = Connection(
142
143
  )
143
144
  ```
144
145
 
146
+ #### HTTPS Connection with HAProxy
147
+
148
+ When connecting through HAProxy with HTTPS/TLS and custom CA certificates:
149
+
150
+ ```python
151
+ from e6data_python_connector import Connection
152
+
153
+ # Option 1: Using CA certificate file path
154
+ conn = Connection(
155
+ host=host,
156
+ port=port,
157
+ username=username,
158
+ password=password,
159
+ database=database,
160
+ cluster_name='analytics-cluster-01', # Specify cluster name
161
+ secure=True,
162
+ ssl_cert='/path/to/ca-cert.pem' # Path to CA certificate
163
+ )
164
+
165
+ # Option 2: Reading certificate content as bytes
166
+ with open('/path/to/ca-cert.pem', 'rb') as f:
167
+ ca_cert = f.read()
168
+
169
+ conn = Connection(
170
+ host=host,
171
+ port=port,
172
+ username=username,
173
+ password=password,
174
+ database=database,
175
+ secure=True,
176
+ ssl_cert=ca_cert # Certificate as bytes
177
+ )
178
+
179
+ # Option 3: System CA bundle (for publicly signed certificates)
180
+ conn = Connection(
181
+ host=host,
182
+ port=port,
183
+ username=username,
184
+ password=password,
185
+ database=database,
186
+ secure=True # Uses system CA bundle
187
+ )
188
+ ```
189
+
190
+ **Note:** Certificate verification is always enabled for security. For self-signed certificates, provide the CA certificate via the `ssl_cert` parameter.
191
+
145
192
  #### Cluster-Specific Connection
146
193
 
147
194
  When working with multiple clusters, specify the cluster name:
@@ -504,6 +551,35 @@ pool.return_connection(conn)
504
551
  pool.close_all()
505
552
  ```
506
553
 
554
+ ##### Connection Pool with HTTPS (HAProxy)
555
+
556
+ ```python
557
+ from e6data_python_connector import ConnectionPool
558
+
559
+ # Connection pool with HTTPS and custom CA certificate
560
+ pool = ConnectionPool(
561
+ min_size=2,
562
+ max_size=10,
563
+ host=host,
564
+ port=port,
565
+ username=username,
566
+ password=password,
567
+ database=database,
568
+ secure=True,
569
+ ssl_cert='/path/to/ca-cert.pem' # CA certificate for HTTPS
570
+ )
571
+
572
+ # Use the pool with context manager for automatic cleanup
573
+ with pool.get_connection_context() as conn:
574
+ cursor = conn.cursor()
575
+ cursor.execute("SELECT * FROM table")
576
+ results = cursor.fetchall()
577
+ print(f"Retrieved {len(results)} rows")
578
+
579
+ # Clean up pool when done
580
+ pool.close_all()
581
+ ```
582
+
507
583
  ##### Using Context Manager (Recommended)
508
584
 
509
585
  The context manager pattern ensures connections are automatically returned to the pool:
@@ -12,7 +12,7 @@
12
12
 
13
13
  import setuptools
14
14
 
15
- VERSION = (2, 3, 12, 'rc2',)
15
+ VERSION = (2, 3, 12, 'rc12',)
16
16
 
17
17
 
18
18
  def get_long_desc():