e6data-python-connector 2.3.12rc11__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.12rc11/e6data_python_connector.egg-info → e6data_python_connector-2.3.12rc12}/PKG-INFO +78 -2
  2. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/README.md +77 -1
  3. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_manager.py +76 -2
  4. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6data_grpc.py +74 -52
  5. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12/e6data_python_connector.egg-info}/PKG-INFO +78 -2
  6. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/setup.py +1 -1
  7. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/LICENSE +0 -0
  8. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/MANIFEST.in +0 -0
  9. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/__init__.py +0 -0
  10. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_server/__init__.py +0 -0
  11. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  12. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  13. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/common.py +0 -0
  14. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/connection_pool.py +0 -0
  15. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/constants.py +0 -0
  16. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/datainputstream.py +0 -0
  17. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/date_time_utils.py +0 -0
  18. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/dialect.py +0 -0
  19. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  20. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6x_vector/constants.py +0 -0
  21. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  22. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/exceptions.py +0 -0
  23. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/QueryEngineService.py +0 -0
  24. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/__init__.py +0 -0
  25. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/constants.py +0 -0
  26. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  27. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  28. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  29. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/server/ttypes.py +0 -0
  30. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/strategy.py +0 -0
  31. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector/typeId.py +0 -0
  32. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/SOURCES.txt +0 -0
  33. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  34. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  35. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/requires.txt +0 -0
  36. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/e6data_python_connector.egg-info/top_level.txt +0 -0
  37. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/gen-py/__init__.py +0 -0
  38. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/gen-py/e6x_vector/__init__.py +0 -0
  39. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/gen-py/e6x_vector/constants.py +0 -0
  40. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/gen-py/e6x_vector/ttypes.py +0 -0
  41. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/pyproject.toml +0 -0
  42. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/setup.cfg +0 -0
  43. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/__init__.py +0 -0
  44. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/analyze_38_nines.py +0 -0
  45. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/analyze_all_cases.py +0 -0
  46. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/analyze_binary.py +0 -0
  47. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/analyze_correct_value.py +0 -0
  48. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/analyze_fields.py +0 -0
  49. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/check_decimal_errors.py +0 -0
  50. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/cleanup_test_files.py +0 -0
  51. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/debug_38_nines.py +0 -0
  52. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/debug_binary.py +0 -0
  53. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/final_test.py +0 -0
  54. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/move_tests.py +0 -0
  55. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/quick_test.py +0 -0
  56. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_38_nines.py +0 -0
  57. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_all_decimal128_cases.py +0 -0
  58. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_cluster_manager_efficiency.py +0 -0
  59. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_cluster_manager_none_strategy.py +0 -0
  60. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_cluster_manager_strategy.py +0 -0
  61. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_comprehensive.py +0 -0
  62. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_connection_pool.py +0 -0
  63. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_connection_pool_e2e.py +0 -0
  64. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_current_implementation.py +0 -0
  65. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_decimal128_binary_parsing.py +0 -0
  66. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_decimal128_parsing.py +0 -0
  67. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_fix.py +0 -0
  68. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_improved_parsing.py +0 -0
  69. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_known_case.py +0 -0
  70. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_manual_analysis.py +0 -0
  71. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_mock_server.py +0 -0
  72. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_multiprocessing_fix.py +0 -0
  73. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_new_implementation.py +0 -0
  74. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_pool_concurrency_simple.py +0 -0
  75. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_pool_threading_multiprocessing.py +0 -0
  76. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_simple_connection_pool.py +0 -0
  77. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_specific_binary.py +0 -0
  78. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_strategy.py +0 -0
  79. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_strategy_logic.py +0 -0
  80. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_strategy_persistence_fix.py +0 -0
  81. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_strategy_sharing_fix.py +0 -0
  82. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/test_user_binary_value.py +0 -0
  83. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/tests.py +0 -0
  84. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/tests_grpc.py +0 -0
  85. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/validate_decimal128.py +0 -0
  86. {e6data_python_connector-2.3.12rc11 → e6data_python_connector-2.3.12rc12}/test/validate_implementation.py +0 -0
  87. {e6data_python_connector-2.3.12rc11 → 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.12rc11
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.12rc11-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.12rc11-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,6 +392,7 @@ 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
 
@@ -483,6 +494,60 @@ class Connection(object):
483
494
 
484
495
  return self._cached_grpc_options
485
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
+
486
551
  def _create_client(self):
487
552
  """
488
553
  Creates a gRPC client for the connection.
@@ -497,13 +562,19 @@ class Connection(object):
497
562
 
498
563
  Raises:
499
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.
500
567
  """
501
568
 
502
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
503
574
  self._channel = grpc.secure_channel(
504
575
  target='{}:{}'.format(self._host, self._port),
505
576
  options=self._get_grpc_options,
506
- credentials=grpc.ssl_channel_credentials()
577
+ credentials=credentials
507
578
  )
508
579
  else:
509
580
  self._channel = grpc.insecure_channel(
@@ -685,7 +756,8 @@ class Connection(object):
685
756
  secure_channel=self._secure_channel,
686
757
  cluster_uuid=self.cluster_name,
687
758
  timeout=self.grpc_auto_resume_timeout_seconds,
688
- debug=self._debug
759
+ debug=self._debug,
760
+ ssl_cert=self._ssl_cert
689
761
  ).resume()
690
762
  return status # Return boolean status directly
691
763
  else:
@@ -853,56 +925,6 @@ class Connection(object):
853
925
  )
854
926
  return dry_run_response.dryrunValue
855
927
 
856
- def get_logical_plan(self, query, catalog=None, schema=None):
857
- """
858
- Gets the logical execution plan for a query without executing it.
859
- This method performs syntax, parsing, and plan compilation, then returns the logical plan.
860
-
861
- Args:
862
- query (str): The SQL query to get the plan for.
863
- catalog (str, optional): Catalog name. Uses connection's catalog if not provided.
864
- schema (str, optional): Schema/database name. Uses connection's database if not provided.
865
-
866
- Returns:
867
- dict: A dictionary with:
868
- - 'success' (bool): True if plan generation succeeded, False otherwise
869
- - 'plan' (str, optional): The logical plan string if successful
870
- - 'error' (str, optional): Error message if plan generation failed
871
-
872
- Example:
873
- >>> result = conn.get_logical_plan("SELECT * FROM table")
874
- >>> if result['success']:
875
- ... print(f"Plan: {result['plan']}")
876
- >>> else:
877
- ... print(f"Error: {result['error']}")
878
- """
879
- catalog_to_use = catalog if catalog is not None else self.catalog_name
880
- schema_to_use = schema if schema is not None else self.database
881
-
882
- try:
883
- dry_run_request = e6x_engine_pb2.DryRunRequestV2(
884
- sessionId=self.get_session_id,
885
- schema=schema_to_use,
886
- catalog=catalog_to_use,
887
- queryString=query
888
- )
889
-
890
- dry_run_response = self._client.dryRunV2(
891
- dry_run_request,
892
- metadata=_get_grpc_header(cluster=self.cluster_name, strategy=_get_active_strategy())
893
- )
894
-
895
- return {
896
- 'success': True,
897
- 'plan': dry_run_response.dryrunValue
898
- }
899
-
900
- except Exception as e:
901
- return {
902
- 'success': False,
903
- 'error': str(e)
904
- }
905
-
906
928
  def get_tables(self, catalog, database):
907
929
  """
908
930
  Retrieves the list of tables from the specified catalog and database.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: e6data-python-connector
3
- Version: 2.3.12rc11
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.12rc11-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,'rc11')
15
+ VERSION = (2, 3, 12, 'rc12',)
16
16
 
17
17
 
18
18
  def get_long_desc():