e6data-python-connector 2.2.1rc4__py3-none-any.whl → 2.2.2__py3-none-any.whl

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.
@@ -7,27 +7,97 @@ from grpc._channel import _InactiveRpcError
7
7
  import multiprocessing
8
8
 
9
9
 
10
+ def _get_grpc_header(engine_ip=None, cluster=None):
11
+ metadata = []
12
+ if engine_ip:
13
+ metadata.append(('plannerip', engine_ip))
14
+ if cluster:
15
+ metadata.append(('cluster-uuid', cluster))
16
+ return metadata
17
+
18
+
10
19
  class _StatusLock:
20
+ """
21
+ A thread-safe and process-safe lock manager designed for managing
22
+ concurrent access protection in multithreaded and multiprocessing environments.
23
+
24
+ This class encapsulates locking mechanisms using threading and multiprocessing
25
+ modules to ensure an operation's atomicity and handle shared resources safely.
26
+
27
+ Attributes:
28
+ _LOCK_TIMEOUT (int): The maximum timeout (in milliseconds) to try
29
+ acquiring a lock before raising an error.
30
+ _status_thread_lock (threading.Lock): A thread-level lock to synchronize
31
+ access among threads in the same process.
32
+ _status_multiprocessing_lock (multiprocessing.Semaphore): A
33
+ process-level lock to synchronize access across different processes.
34
+ _is_active (bool): A boolean flag indicating whether the lock
35
+ is currently active (True) or not (False).
36
+ """
37
+
11
38
  _LOCK_TIMEOUT = 500
12
39
 
13
40
  def __init__(self):
41
+ """
42
+ Initializes the _StatusLock instance with its respective
43
+ thread and multiprocessing locks and sets the active flag to False.
44
+ """
14
45
  self._status_thread_lock = threading.Lock()
15
46
  self._status_multiprocessing_lock = multiprocessing.Semaphore()
16
47
  self._is_active = False
17
48
 
18
49
  @property
19
50
  def is_active(self) -> bool:
51
+ """
52
+ Checks if the lock is currently active.
53
+
54
+ Returns:
55
+ bool: True if the lock is active, False otherwise.
56
+ """
57
+
20
58
  return self._is_active
21
59
 
22
60
  def set_active(self):
61
+ """
62
+ Activates the lock by setting the `_is_active` flag to True.
63
+ This can be used for manually marking the state of the lock
64
+ as active during synchronization operations.
65
+ """
66
+
23
67
  self._is_active = True
24
68
 
25
69
  def __enter__(self):
70
+ """
71
+ Enters a context-managed locking block.
72
+
73
+ Acquires both the thread-level lock and process-level semaphore
74
+ to ensure the current operation can safely access shared resources.
75
+
76
+ Raises:
77
+ TimeoutError: If the lock cannot be acquired within the timeout period.
78
+
79
+ Returns:
80
+ _StatusLock: The current instance of the lock, used for context management.
81
+ """
82
+
26
83
  self._status_thread_lock.acquire(timeout=self._LOCK_TIMEOUT)
27
84
  self._status_multiprocessing_lock.acquire(timeout=self._LOCK_TIMEOUT)
28
85
  return self
29
86
 
30
87
  def __exit__(self, exc_type, exc_val, exc_tb):
88
+ """
89
+ Exits a context-managed locking block.
90
+
91
+ Releases both the thread-level lock and process-level semaphore
92
+ to allow other operations to acquire the lock. Handles cleanup
93
+ regardless of whether an exception occurred.
94
+
95
+ Args:
96
+ exc_type (Type[BaseException]): The type of exception raised (if any).
97
+ exc_val (BaseException): The exception instance raised (if any).
98
+ exc_tb (Traceback): The traceback object of the exception (if any).
99
+ """
100
+
31
101
  self._status_thread_lock.release()
32
102
  self._status_multiprocessing_lock.release()
33
103
 
@@ -36,16 +106,58 @@ status_lock = _StatusLock()
36
106
 
37
107
 
38
108
  class ClusterManager:
39
- def __init__(self, host: str, port: int, user: str, password: str, secure_channel: bool = False, timeout=60 * 3):
109
+ """
110
+ A manager for handling operations and connections with a remote cluster infrastructure.
111
+ Provides methods to manage cluster states, such as resuming or suspending its activity,
112
+ by interacting with a gRPC-based remote service.
113
+
114
+ Attributes:
115
+ _host (str): The hostname or IP address of the cluster service.
116
+ _port (int): The port number used to connect to the cluster service.
117
+ _user (str): The username for authentication with the cluster service.
118
+ _password (str): The password for authentication with the cluster service.
119
+ _timeout (float): The timeout threshold (as an epoch timestamp) for operations,
120
+ defaulting to 3 minutes from initialization.
121
+ _secure_channel (bool): Indicates whether a secure gRPC channel
122
+ (SSL/TLS) should be used for communication; defaults to False.
123
+ cluster_uuid (str): The unique identifier for the target cluster.
124
+ """
125
+
126
+ def __init__(self, host: str, port: int, user: str, password: str, secure_channel: bool = False, timeout=60 * 3, cluster_uuid=None):
127
+ """
128
+ Initializes a new instance of the ClusterManager class.
129
+
130
+ Args:
131
+ host (str): The hostname or IP address of the cluster service.
132
+ port (int): The port number for accessing the cluster service.
133
+ user (str): The username used for connecting to the cluster service.
134
+ password (str): The password used for connecting to the cluster service.
135
+ secure_channel (bool, optional): Whether to use a secure
136
+ gRPC channel for communication; defaults to False.
137
+ timeout (int, optional): The timeout duration (in seconds) for operations;
138
+ defaults to 3 minutes.
139
+ cluster_uuid (str, optional): The unique identifier for the target cluster;
140
+ defaults to None.
141
+ """
142
+
40
143
  self._host = host
41
144
  self._port = port
42
145
  self._user = user
43
146
  self._password = password
44
147
  self._timeout = time.time() + timeout
45
148
  self._secure_channel = secure_channel
149
+ self.cluster_uuid = cluster_uuid
46
150
 
47
151
  @property
48
152
  def _get_connection(self):
153
+ """
154
+ Dynamically establishes a gRPC connection to the cluster service.
155
+
156
+ Returns:
157
+ cluster_pb2_grpc.ClusterServiceStub: A gRPC client stub
158
+ for interacting with the cluster service methods.
159
+ """
160
+
49
161
  if self._secure_channel:
50
162
  self._channel = grpc.secure_channel(
51
163
  target='{}:{}'.format(self._host, self._port),
@@ -59,37 +171,75 @@ class ClusterManager:
59
171
 
60
172
  def resume(self) -> bool:
61
173
  """
62
- Return True if cluster came to active state else False.
174
+ Resumes the cluster if it is currently suspended or not in the 'active' state.
175
+
176
+ This method interacts with the remote cluster service to verify its current
177
+ status. If suspended, it sends a resume request and monitors
178
+ the cluster's state until it becomes active or fails.
179
+
180
+ The operation uses a locking mechanism (`status_lock`) to ensure thread-safe
181
+ and process-safe state transitions.
182
+
183
+ Returns:
184
+ bool: True if the cluster resumes successfully and becomes active;
185
+ False if the cluster cannot be resumed, remains suspended, or fails.
186
+
187
+ Raises:
188
+ _InactiveRpcError: An exception raised if there is a communication error
189
+ while interacting with the remote cluster service.
190
+
191
+ Notes:
192
+ - If the cluster is already active, this method completes successfully
193
+ without further actions.
194
+ - If the cluster is in a 'resuming' state, this method waits for the
195
+ cluster to transition to 'active' or any terminal state (e.g., 'failed').
196
+ - The operation is subject to the `_timeout` threshold;
197
+ if the timeout expires, the method returns False.
63
198
  """
199
+
64
200
  with status_lock as lock:
65
201
  if lock.is_active:
66
202
  return True
203
+
204
+ # Retrieve the current cluster status
67
205
  status_payload = cluster_pb2.ClusterStatusRequest(
68
206
  user=self._user,
69
207
  password=self._password
70
208
  )
71
- current_status = self._get_connection.status(status_payload)
209
+ current_status = self._get_connection.status(
210
+ status_payload,
211
+ metadata=_get_grpc_header(cluster=self.cluster_uuid)
212
+ )
72
213
  if current_status.status == 'suspended':
214
+ # Send the resume request
73
215
  payload = cluster_pb2.ResumeRequest(
74
216
  user=self._user,
75
217
  password=self._password
76
218
  )
77
- response = self._get_connection.resume(payload)
219
+ response = self._get_connection.resume(
220
+ payload,
221
+ metadata=_get_grpc_header(cluster=self.cluster_uuid)
222
+ )
78
223
  elif current_status.status == 'active':
79
224
  return True
80
225
  elif current_status.status != 'resuming':
81
226
  """
82
- If cluster is in resuming state already, start watching for the status.
83
- Cluster is in different state, cannot resume.
84
- """
227
+ If cluster cannot be resumed due to its current state,
228
+ or already in a process of resuming, terminate the operation.
229
+ """
85
230
  return False
231
+
232
+ # Wait for the cluster to become active
86
233
  while True:
87
234
  try:
88
235
  status_payload = cluster_pb2.ClusterStatusRequest(
89
236
  user=self._user,
90
237
  password=self._password
91
238
  )
92
- response = self._get_connection.status(status_payload)
239
+ response = self._get_connection.status(
240
+ status_payload,
241
+ metadata=_get_grpc_header(cluster=self.cluster_uuid)
242
+ )
93
243
  if response.status == 'active':
94
244
  lock.set_active()
95
245
  return True
@@ -102,4 +252,10 @@ class ClusterManager:
102
252
  time.sleep(5)
103
253
 
104
254
  def suspend(self):
255
+ """
256
+ Suspends the cluster operations (not implemented).
257
+
258
+ Placeholder method to be implemented for suspending a cluster,
259
+ typically interacting with the remote cluster service API.
260
+ """
105
261
  pass
@@ -53,14 +53,35 @@ def _parse_timestamp(value):
53
53
  value = datetime.datetime.strptime(value, format)
54
54
  else:
55
55
  raise Exception(
56
- 'Cannot convert "{}" into a datetime'.format(value))
56
+ 'Cannot convert "{}" into a datetime'.format(value)
57
+ )
57
58
  else:
58
59
  value = None
59
60
  return value
60
61
 
61
62
 
62
- TYPES_CONVERTER = {"DECIMAL_TYPE": Decimal,
63
- "TIMESTAMP_TYPE": _parse_timestamp}
63
+ TYPES_CONVERTER = {
64
+ "DECIMAL_TYPE": Decimal,
65
+ "TIMESTAMP_TYPE": _parse_timestamp
66
+ }
67
+
68
+
69
+ def re_auth(func):
70
+ def wrapper(self, *args, **kwargs):
71
+ try:
72
+ return func(self, *args, **kwargs)
73
+ except _InactiveRpcError as e:
74
+ print(f'RE_AUTH: Function Name: {func}')
75
+ print(f'RE_AUTH: Error Found {e}')
76
+ if e.code() == grpc.StatusCode.INTERNAL and 'Access denied' in e.details():
77
+ print('RE_AUTH: Initialising re-authentication.')
78
+ self.connection.get_re_authenticate_session_id()
79
+ print(f'RE_AUTH: Re-auth successful.')
80
+ return func(self, *args, **kwargs)
81
+ else:
82
+ raise e
83
+
84
+ return wrapper
64
85
 
65
86
 
66
87
  class HiveParamEscaper(ParamEscaper):
@@ -202,6 +223,10 @@ class Connection(object):
202
223
  )
203
224
  self._client = e6x_engine_pb2_grpc.QueryEngineServiceStub(self._channel)
204
225
 
226
+ def get_re_authenticate_session_id(self):
227
+ self._session_id = None
228
+ return self.get_session_id
229
+
205
230
  @property
206
231
  def get_session_id(self):
207
232
  """
@@ -228,7 +253,8 @@ class Connection(object):
228
253
  port=self._port,
229
254
  user=self.__username,
230
255
  password=self.__password,
231
- secure_channel=self._secure_channel
256
+ secure_channel=self._secure_channel,
257
+ cluster_uuid=self.cluster_uuid
232
258
  ).resume()
233
259
  if status:
234
260
  authenticate_request = e6x_engine_pb2.AuthenticateRequest(
@@ -496,6 +522,7 @@ class Cursor(DBAPICursor):
496
522
  )
497
523
  return self.connection.client.status(status_request, metadata=self.metadata)
498
524
 
525
+ @re_auth
499
526
  def execute(self, operation, parameters=None, **kwargs):
500
527
  """Prepare and execute a database operation (query or command).
501
528
  Return values are not defined.
@@ -551,6 +578,7 @@ class Cursor(DBAPICursor):
551
578
 
552
579
  self._query_id = prepare_statement_response.queryId
553
580
  self._engine_ip = prepare_statement_response.engineIP
581
+
554
582
  execute_statement_request = e6x_engine_pb2.ExecuteStatementV2Request(
555
583
  engineIP=self._engine_ip,
556
584
  sessionId=self.connection.get_session_id,
@@ -635,7 +663,6 @@ class Cursor(DBAPICursor):
635
663
  return self._fetch_all()
636
664
 
637
665
  def fetchmany(self, size: int = None):
638
- # _logger.info("fetching all from overriden method")
639
666
  if size is None:
640
667
  size = self.arraysize
641
668
  if self._data is None:
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: e6data-python-connector
3
- Version: 2.2.1rc4
3
+ Version: 2.2.2
4
4
  Summary: Client for the e6data distributed SQL Engine.
5
5
  Home-page: https://github.com/e6x-labs/e6data-python-connector
6
- Author: Uniphi, Inc.
6
+ Author: e6data, Inc.
7
7
  Author-email: info@e6data.com
8
8
  License: Apache 2.0
9
9
  Classifier: Operating System :: POSIX :: Linux
@@ -18,18 +18,27 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: sqlalchemy >=1.0.0
22
- Requires-Dist: future
23
- Requires-Dist: python-dateutil
24
- Requires-Dist: pycryptodome
25
- Requires-Dist: pytz
26
- Requires-Dist: thrift
27
- Requires-Dist: grpcio >=1.65.1
21
+ Requires-Dist: sqlalchemy>=1.0.0
22
+ Requires-Dist: future==1.0.0
23
+ Requires-Dist: python-dateutil==2.9.0.post0
24
+ Requires-Dist: pycryptodome==3.19.1
25
+ Requires-Dist: pytz==2024.1
26
+ Requires-Dist: thrift==0.20.0
27
+ Requires-Dist: grpcio==1.65.1
28
28
  Requires-Dist: grpcio-tools
29
+ Dynamic: author
30
+ Dynamic: author-email
31
+ Dynamic: classifier
32
+ Dynamic: description
33
+ Dynamic: description-content-type
34
+ Dynamic: home-page
35
+ Dynamic: license
36
+ Dynamic: requires-dist
37
+ Dynamic: summary
29
38
 
30
39
  # e6data Python Connector
31
40
 
32
- ![version](https://img.shields.io/badge/version-2.2.1rc4-blue.svg)
41
+ ![version](https://img.shields.io/badge/version-2.2.2-blue.svg)
33
42
 
34
43
  ## Introduction
35
44
 
@@ -1,11 +1,11 @@
1
1
  e6data_python_connector/__init__.py,sha256=x_VwhPQ7XLlthR-NJL9Vl7lkLBRCG2pSjmyZSBHOBpM,103
2
- e6data_python_connector/cluster_manager.py,sha256=21UW7Sr4t5_ZlnxmcutLp-OI2jB1ldGnUmfRsa4qbxA,3635
2
+ e6data_python_connector/cluster_manager.py,sha256=ZKxsMDgUuEKuRxNxo6vFhV8q4-Il3Rg-hX8niaACMso,10163
3
3
  e6data_python_connector/common.py,sha256=nk0CDQ5j1iu8DctBOXX4QypPYmxSgIgoA-Cn1UZXLic,9976
4
4
  e6data_python_connector/constants.py,sha256=h5w0lWiFsxz__zUZRlo1f72-8mT_vt9gcWvtWh0viFI,682
5
5
  e6data_python_connector/datainputstream.py,sha256=2XKW__PCGfTQ55Puqgie2LZdk_WU-YKwpEIUOQEF-sY,12592
6
6
  e6data_python_connector/date_time_utils.py,sha256=zmIwRhEGJwDvCOUjAl6VTDWGppluA-DPl_ioHTNpJKI,14413
7
7
  e6data_python_connector/dialect.py,sha256=hkVRjXxx9DhS8-aHpSoyMVhhHxVs3jdTE60x2aOAf5I,11312
8
- e6data_python_connector/e6data_grpc.py,sha256=4EEZEmPt1I6xcxX_Z9RlaQ6b1Wi0u-X9YjDfPiIezSQ,26203
8
+ e6data_python_connector/e6data_grpc.py,sha256=B5MsAKsI6Syx9QGDpDOXxQn4tjEwiByA4HevrSczbFs,26981
9
9
  e6data_python_connector/exceptions.py,sha256=vrUqdzMq3LjbCmvr5_TcRq9eKmdqGfe-uVjR0BJQf5U,382
10
10
  e6data_python_connector/typeId.py,sha256=2EMNWK2SO5CBReNuJphma6xp-69r9GDwwys66t-kSk0,1777
11
11
  e6data_python_connector/cluster_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -21,9 +21,9 @@ e6data_python_connector/server/e6x_engine_pb2.py,sha256=BxJjbijNrEqXQTWEhzLm54Fq
21
21
  e6data_python_connector/server/e6x_engine_pb2.pyi,sha256=_aQDLFbSCQ7ubK0Z5ZKh9_VAHFaXbMV55MXYXr85srg,18693
22
22
  e6data_python_connector/server/e6x_engine_pb2_grpc.py,sha256=n-eTCsAjsmpgPA_bDansKZq_zMmqkK6TlQGn6IdT6L4,46671
23
23
  e6data_python_connector/server/ttypes.py,sha256=uWxwSp4m6e-7SeSEckbZtSduGYmfzXWM09iy5eltBPA,21227
24
- e6data_python_connector-2.2.1rc4.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
25
- e6data_python_connector-2.2.1rc4.dist-info/METADATA,sha256=wVWEWNeVKi2K34L0LumEupWVZZY4AmzPdUIML5JavN8,7084
26
- e6data_python_connector-2.2.1rc4.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
27
- e6data_python_connector-2.2.1rc4.dist-info/entry_points.txt,sha256=MDK4cAOND0preLvq2Tlpnz00wXnP_Y_UopqJsAf3U7Y,77
28
- e6data_python_connector-2.2.1rc4.dist-info/top_level.txt,sha256=ChOS6qLf-SQycXr0uBiVxomIfYfbH2SgwrOWhW79ab0,24
29
- e6data_python_connector-2.2.1rc4.dist-info/RECORD,,
24
+ e6data_python_connector-2.2.2.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
25
+ e6data_python_connector-2.2.2.dist-info/METADATA,sha256=SDpjML-yRccpZOxJSqLr_L1O9Ph7YHkokCdwCYrjRzk,7309
26
+ e6data_python_connector-2.2.2.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
27
+ e6data_python_connector-2.2.2.dist-info/entry_points.txt,sha256=MDK4cAOND0preLvq2Tlpnz00wXnP_Y_UopqJsAf3U7Y,77
28
+ e6data_python_connector-2.2.2.dist-info/top_level.txt,sha256=ChOS6qLf-SQycXr0uBiVxomIfYfbH2SgwrOWhW79ab0,24
29
+ e6data_python_connector-2.2.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5