e6data-python-connector 2.3.12rc31__tar.gz → 2.3.13__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 (89) hide show
  1. {e6data_python_connector-2.3.12rc31/e6data_python_connector.egg-info → e6data_python_connector-2.3.13}/PKG-INFO +73 -2
  2. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/README.md +72 -1
  3. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/cluster_manager.py +7 -22
  4. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/common.py +44 -0
  5. e6data_python_connector-2.3.13/e6data_python_connector/constants.py +56 -0
  6. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/datainputstream.py +59 -36
  7. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/date_time_utils.py +171 -0
  8. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/e6data_grpc.py +21 -19
  9. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13/e6data_python_connector.egg-info}/PKG-INFO +73 -2
  10. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector.egg-info/SOURCES.txt +1 -0
  11. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/setup.py +1 -1
  12. e6data_python_connector-2.3.13/test/test_code_optimizations.py +322 -0
  13. e6data_python_connector-2.3.12rc31/e6data_python_connector/constants.py +0 -32
  14. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/LICENSE +0 -0
  15. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/MANIFEST.in +0 -0
  16. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/__init__.py +0 -0
  17. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/cluster_server/__init__.py +0 -0
  18. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
  19. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
  20. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/connection_pool.py +0 -0
  21. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/dialect.py +0 -0
  22. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/e6x_vector/__init__.py +0 -0
  23. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/e6x_vector/constants.py +0 -0
  24. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
  25. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/exceptions.py +0 -0
  26. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/server/QueryEngineService.py +0 -0
  27. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/server/__init__.py +0 -0
  28. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/server/constants.py +0 -0
  29. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/server/e6x_engine_pb2.py +0 -0
  30. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
  31. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +0 -0
  32. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/server/ttypes.py +0 -0
  33. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/strategy.py +0 -0
  34. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector/typeId.py +0 -0
  35. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
  36. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector.egg-info/entry_points.txt +0 -0
  37. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector.egg-info/requires.txt +0 -0
  38. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/e6data_python_connector.egg-info/top_level.txt +0 -0
  39. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/gen-py/__init__.py +0 -0
  40. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/gen-py/e6x_vector/__init__.py +0 -0
  41. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/gen-py/e6x_vector/constants.py +0 -0
  42. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/gen-py/e6x_vector/ttypes.py +0 -0
  43. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/pyproject.toml +0 -0
  44. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/setup.cfg +0 -0
  45. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/__init__.py +0 -0
  46. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/analyze_38_nines.py +0 -0
  47. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/analyze_all_cases.py +0 -0
  48. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/analyze_binary.py +0 -0
  49. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/analyze_correct_value.py +0 -0
  50. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/analyze_fields.py +0 -0
  51. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/check_decimal_errors.py +0 -0
  52. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/cleanup_test_files.py +0 -0
  53. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/debug_38_nines.py +0 -0
  54. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/debug_binary.py +0 -0
  55. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/final_test.py +0 -0
  56. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/move_tests.py +0 -0
  57. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/quick_test.py +0 -0
  58. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_38_nines.py +0 -0
  59. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_all_decimal128_cases.py +0 -0
  60. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_cluster_manager_efficiency.py +0 -0
  61. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_cluster_manager_none_strategy.py +0 -0
  62. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_cluster_manager_strategy.py +0 -0
  63. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_comprehensive.py +0 -0
  64. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_connection_pool.py +0 -0
  65. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_connection_pool_e2e.py +0 -0
  66. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_current_implementation.py +0 -0
  67. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_decimal128_binary_parsing.py +0 -0
  68. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_decimal128_parsing.py +0 -0
  69. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_fix.py +0 -0
  70. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_improved_parsing.py +0 -0
  71. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_known_case.py +0 -0
  72. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_manual_analysis.py +0 -0
  73. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_mock_server.py +0 -0
  74. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_multiprocessing_fix.py +0 -0
  75. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_new_implementation.py +0 -0
  76. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_pool_concurrency_simple.py +0 -0
  77. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_pool_threading_multiprocessing.py +0 -0
  78. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_simple_connection_pool.py +0 -0
  79. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_specific_binary.py +0 -0
  80. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_strategy.py +0 -0
  81. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_strategy_logic.py +0 -0
  82. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_strategy_persistence_fix.py +0 -0
  83. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_strategy_sharing_fix.py +0 -0
  84. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/test_user_binary_value.py +0 -0
  85. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/tests.py +0 -0
  86. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/tests_grpc.py +0 -0
  87. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/validate_decimal128.py +0 -0
  88. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/test/validate_implementation.py +0 -0
  89. {e6data_python_connector-2.3.12rc31 → e6data_python_connector-2.3.13}/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.12rc31
3
+ Version: 2.3.13
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.12rc31-blue.svg)
42
+ ![version](https://img.shields.io/badge/version-2.3.13-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
@@ -122,6 +126,7 @@ The `Connection` class supports the following parameters:
122
126
  | `catalog` | str | No | None | Catalog name |
123
127
  | `cluster_name` | str | No | None | Name of the cluster for cluster-specific operations |
124
128
  | `secure` | bool | No | False | Enable SSL/TLS for secure connections |
129
+ | `ssl_cert` | str/bytes | No | None | Path to CA certificate (PEM) or certificate bytes for HTTPS connections |
125
130
  | `auto_resume` | bool | No | True | Automatically resume cluster if suspended |
126
131
  | `grpc_options` | dict | No | None | Additional gRPC configuration options |
127
132
  | `debug` | bool | No | False | Enable debug logging for troubleshooting |
@@ -159,6 +164,72 @@ conn = Connection(
159
164
  )
160
165
  ```
161
166
 
167
+ #### HTTPS Connection with HAProxy
168
+
169
+ When connecting through HAProxy with HTTPS, you can provide a custom CA certificate for secure connections. The `ssl_cert` parameter accepts either a file path to a PEM certificate or the certificate content as bytes.
170
+
171
+ **Using a CA certificate file path:**
172
+
173
+ ```python
174
+ conn = Connection(
175
+ host=host,
176
+ port=443,
177
+ username=username,
178
+ password=password,
179
+ database=database,
180
+ secure=True,
181
+ ssl_cert='/path/to/ca-cert.pem' # Path to your CA certificate
182
+ )
183
+ ```
184
+
185
+ **Reading certificate content as bytes:**
186
+
187
+ ```python
188
+ # Read certificate file and pass as bytes
189
+ with open('/path/to/ca-cert.pem', 'rb') as cert_file:
190
+ cert_data = cert_file.read()
191
+
192
+ conn = Connection(
193
+ host=host,
194
+ port=443,
195
+ username=username,
196
+ password=password,
197
+ database=database,
198
+ secure=True,
199
+ ssl_cert=cert_data # Certificate content as bytes
200
+ )
201
+ ```
202
+
203
+ **Using system CA bundle for publicly signed certificates:**
204
+
205
+ ```python
206
+ # When ssl_cert is None, system default CA bundle is used
207
+ conn = Connection(
208
+ host=host,
209
+ port=443,
210
+ username=username,
211
+ password=password,
212
+ database=database,
213
+ secure=True # Uses system CA bundle by default
214
+ )
215
+ ```
216
+
217
+ **Connection pooling with custom CA certificate:**
218
+
219
+ ```python
220
+ pool = ConnectionPool(
221
+ min_size=2,
222
+ max_size=10,
223
+ host=host,
224
+ port=443,
225
+ username=username,
226
+ password=password,
227
+ database=database,
228
+ secure=True,
229
+ ssl_cert='/path/to/ca-cert.pem' # Custom CA certificate for pool connections
230
+ )
231
+ ```
232
+
162
233
  #### Handling Missing System Dependencies
163
234
 
164
235
  The e6data connector uses the `fastbinary` module (from Apache Thrift) for optimal performance when deserializing data. This module requires system-level dependencies (`python3-devel` and `gcc-c++`) to be installed.
@@ -1,6 +1,6 @@
1
1
  # e6data Python Connector
2
2
 
3
- ![version](https://img.shields.io/badge/version-2.3.12rc31-blue.svg)
3
+ ![version](https://img.shields.io/badge/version-2.3.13-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
@@ -83,6 +87,7 @@ The `Connection` class supports the following parameters:
83
87
  | `catalog` | str | No | None | Catalog name |
84
88
  | `cluster_name` | str | No | None | Name of the cluster for cluster-specific operations |
85
89
  | `secure` | bool | No | False | Enable SSL/TLS for secure connections |
90
+ | `ssl_cert` | str/bytes | No | None | Path to CA certificate (PEM) or certificate bytes for HTTPS connections |
86
91
  | `auto_resume` | bool | No | True | Automatically resume cluster if suspended |
87
92
  | `grpc_options` | dict | No | None | Additional gRPC configuration options |
88
93
  | `debug` | bool | No | False | Enable debug logging for troubleshooting |
@@ -120,6 +125,72 @@ conn = Connection(
120
125
  )
121
126
  ```
122
127
 
128
+ #### HTTPS Connection with HAProxy
129
+
130
+ When connecting through HAProxy with HTTPS, you can provide a custom CA certificate for secure connections. The `ssl_cert` parameter accepts either a file path to a PEM certificate or the certificate content as bytes.
131
+
132
+ **Using a CA certificate file path:**
133
+
134
+ ```python
135
+ conn = Connection(
136
+ host=host,
137
+ port=443,
138
+ username=username,
139
+ password=password,
140
+ database=database,
141
+ secure=True,
142
+ ssl_cert='/path/to/ca-cert.pem' # Path to your CA certificate
143
+ )
144
+ ```
145
+
146
+ **Reading certificate content as bytes:**
147
+
148
+ ```python
149
+ # Read certificate file and pass as bytes
150
+ with open('/path/to/ca-cert.pem', 'rb') as cert_file:
151
+ cert_data = cert_file.read()
152
+
153
+ conn = Connection(
154
+ host=host,
155
+ port=443,
156
+ username=username,
157
+ password=password,
158
+ database=database,
159
+ secure=True,
160
+ ssl_cert=cert_data # Certificate content as bytes
161
+ )
162
+ ```
163
+
164
+ **Using system CA bundle for publicly signed certificates:**
165
+
166
+ ```python
167
+ # When ssl_cert is None, system default CA bundle is used
168
+ conn = Connection(
169
+ host=host,
170
+ port=443,
171
+ username=username,
172
+ password=password,
173
+ database=database,
174
+ secure=True # Uses system CA bundle by default
175
+ )
176
+ ```
177
+
178
+ **Connection pooling with custom CA certificate:**
179
+
180
+ ```python
181
+ pool = ConnectionPool(
182
+ min_size=2,
183
+ max_size=10,
184
+ host=host,
185
+ port=443,
186
+ username=username,
187
+ password=password,
188
+ database=database,
189
+ secure=True,
190
+ ssl_cert='/path/to/ca-cert.pem' # Custom CA certificate for pool connections
191
+ )
192
+ ```
193
+
123
194
  #### Handling Missing System Dependencies
124
195
 
125
196
  The e6data connector uses the `fastbinary` module (from Apache Thrift) for optimal performance when deserializing data. This module requires system-level dependencies (`python3-devel` and `gcc-c++`) to be installed.
@@ -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:
@@ -140,7 +122,7 @@ class ClusterManager:
140
122
  """
141
123
 
142
124
  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):
125
+ cluster_uuid=None, grpc_options=None, debug=False, ssl_cert=None):
144
126
  """
145
127
  Initializes a new instance of the ClusterManager class.
146
128
 
@@ -156,6 +138,8 @@ class ClusterManager:
156
138
  cluster_uuid (str, optional): The unique identifier for the target cluster;
157
139
  defaults to None.
158
140
  debug (bool, optional): Enable debug logging; defaults to False.
141
+ ssl_cert (str or bytes, optional): Path to CA certificate file (PEM format) or
142
+ certificate content as bytes for secure connections; defaults to None.
159
143
  """
160
144
 
161
145
  self._host = host
@@ -169,6 +153,7 @@ class ClusterManager:
169
153
  if grpc_options is None:
170
154
  self._grpc_options = dict()
171
155
  self._debug = debug
156
+ self._ssl_cert = ssl_cert
172
157
 
173
158
  @property
174
159
  def _get_connection(self):
@@ -184,7 +169,7 @@ class ClusterManager:
184
169
  self._channel = grpc.secure_channel(
185
170
  target='{}:{}'.format(self._host, self._port),
186
171
  options=self._grpc_options,
187
- credentials=grpc.ssl_channel_credentials()
172
+ credentials=get_ssl_credentials(self._ssl_cert)
188
173
  )
189
174
  else:
190
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":
@@ -560,27 +561,38 @@ def get_column_from_chunk(vector: Vector) -> list:
560
561
  value_array.append(vector.data.int64Data.data[
561
562
  row] if not vector.isConstantVector else vector.data.numericConstantData.data)
562
563
  elif d_type == VectorType.DATE:
564
+ # Use the JDBC-parity formatter so years > 9999 emit "+YYYYY-MM-DD"
565
+ # instead of raising. Per-row try/except keeps a single bad value
566
+ # from truncating the column (which previously cascaded to an
567
+ # IndexError in read_rows_from_chunk).
563
568
  for row in range(vector.size):
564
569
  if get_null(vector, row):
565
570
  value_array.append(None)
566
571
  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"))
572
+ try:
573
+ raw = (vector.data.dateData.data[row]
574
+ if not vector.isConstantVector
575
+ else vector.data.dateConstantData.data)
576
+ value_array.append(format_iso_date_from_epoch_micros(raw))
577
+ except Exception as e:
578
+ _logger.error("Failed to parse DATE row=%s: %s", row, e)
579
+ value_array.append('Failed to parse.')
572
580
  elif d_type == VectorType.DATETIME:
581
+ # Default formatter knobs reproduce the prior datetime.isoformat
582
+ # output ("YYYY-MM-DDTHH:MM:SS.sss+HH:MM"). See DATE branch above
583
+ # for the rationale on per-row try/except.
573
584
  for row in range(vector.size):
574
585
  if get_null(vector, row):
575
586
  value_array.append(None)
576
587
  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'))
588
+ try:
589
+ epoch_micros = (vector.data.timeData.data[row]
590
+ if not vector.isConstantVector
591
+ else vector.data.timeConstantData.data)
592
+ value_array.append(format_iso_datetime_from_epoch_micros(epoch_micros, tz=zone))
593
+ except Exception as e:
594
+ _logger.error("Failed to parse DATETIME row=%s: %s", row, e)
595
+ value_array.append('Failed to parse.')
584
596
  elif d_type == VectorType.STRING or d_type == VectorType.ARRAY or d_type == VectorType.MAP or d_type == VectorType.STRUCT:
585
597
  for row in range(vector.size):
586
598
  if get_null(vector, row):
@@ -627,22 +639,27 @@ def get_column_from_chunk(vector: Vector) -> list:
627
639
  for row in range(vector.size):
628
640
  value_array.append(None)
629
641
  elif d_type == VectorType.TIMESTAMP_TZ:
642
+ # row_zone is scoped to the row (not the outer `zone`) so a
643
+ # per-row zone resolution doesn't leak into later iterations.
630
644
  for row in range(vector.size):
631
645
  if get_null(vector, row):
632
646
  value_array.append(None)
633
647
  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'))
648
+ try:
649
+ epoch_micros = (vector.data.timeData.data[row]
650
+ if not vector.isConstantVector
651
+ else vector.data.timeConstantData.data)
652
+ row_zone = zone
653
+ if ((vector.isConstantVector and vector.data.timeConstantData.zoneData is not None) or
654
+ (not vector.isConstantVector and vector.data.timeData.zoneData is not None)):
655
+ zone_id = (vector.data.timeData.zoneData[row]
656
+ if not vector.isConstantVector
657
+ else vector.data.timeConstantData.zoneData)
658
+ row_zone = timezone_from_offset(zone_id)
659
+ value_array.append(format_iso_datetime_from_epoch_micros(epoch_micros, tz=row_zone))
660
+ except Exception as e:
661
+ _logger.error("Failed to parse TIMESTAMP_TZ row=%s: %s", row, e)
662
+ value_array.append('Failed to parse.')
646
663
  elif d_type == VectorType.DECIMAL128:
647
664
  # Handle both constant and non-constant vectors following Java implementation
648
665
  if vector.isConstantVector:
@@ -679,6 +696,12 @@ def get_column_from_chunk(vector: Vector) -> list:
679
696
  else:
680
697
  value_array.append(None)
681
698
  except Exception as e:
682
- _logger.error(e)
683
- value_array.append('Failed to parse.')
699
+ # Safety net: if anything escapes the per-row try/excepts above (or
700
+ # comes from a branch without one), pad value_array to vector.size so
701
+ # read_rows_from_chunk's columns[colIndex][rowIndex] access can never
702
+ # IndexError on a short column.
703
+ _logger.error("get_column_from_chunk failed (vectorType=%s, parsed=%s/%s): %s",
704
+ d_type, len(value_array), vector.size, e)
705
+ while len(value_array) < vector.size:
706
+ value_array.append('Failed to parse.')
684
707
  return value_array