e6data-python-connector 1.0.0__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.
e6xdb/e6x.py ADDED
@@ -0,0 +1,467 @@
1
+ """DB-API implementation backed by HiveServer2 (Thrift API)
2
+ See http://www.python.org/dev/peps/pep-0249/
3
+ Many docstrings in this file are based on the PEP, which is in the public domain.
4
+ """
5
+
6
+ from __future__ import absolute_import
7
+ from __future__ import unicode_literals
8
+
9
+ import datetime
10
+ # Make all exceptions visible in this e6xdb per DB-API
11
+ import logging
12
+ import re
13
+ import sys
14
+ from decimal import Decimal
15
+ from io import BytesIO
16
+ from ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
17
+
18
+ from e6xdb.server import QueryEngineService
19
+ from thrift.protocol import TBinaryProtocol, TMultiplexedProtocol
20
+ from thrift.transport import TSocket
21
+ from thrift.transport import TTransport
22
+
23
+ from e6xdb.common import DBAPITypeObject, ParamEscaper, DBAPICursor
24
+ from e6xdb.constants import *
25
+ from e6xdb.datainputstream import DataInputStream, get_query_columns_info, read_rows_from_batch, read_values_from_array
26
+ from e6xdb.typeId import *
27
+
28
+ apilevel = '2.0'
29
+ threadsafety = 2 # Threads may share the e6xdb and connections.
30
+ paramstyle = 'pyformat' # Python extended format codes, e.g. ...WHERE name=%(name)s
31
+
32
+ _logger = logging.getLogger(__name__)
33
+
34
+ _TIMESTAMP_PATTERN = re.compile(r'(\d+-\d+-\d+ \d+:\d+:\d+(\.\d{,6})?)')
35
+
36
+ ssl_cert_parameter_map = {
37
+ "none": CERT_NONE,
38
+ "optional": CERT_OPTIONAL,
39
+ "required": CERT_REQUIRED,
40
+ }
41
+
42
+
43
+ def _parse_timestamp(value):
44
+ if value:
45
+ match = _TIMESTAMP_PATTERN.match(value)
46
+ if match:
47
+ if match.group(2):
48
+ format = '%Y-%m-%d %H:%M:%S.%f'
49
+ # use the pattern to truncate the value
50
+ value = match.group()
51
+ else:
52
+ format = '%Y-%m-%d %H:%M:%S'
53
+ value = datetime.datetime.strptime(value, format)
54
+ else:
55
+ raise Exception(
56
+ 'Cannot convert "{}" into a datetime'.format(value))
57
+ else:
58
+ value = None
59
+ return value
60
+
61
+
62
+ TYPES_CONVERTER = {"DECIMAL_TYPE": Decimal,
63
+ "TIMESTAMP_TYPE": _parse_timestamp}
64
+
65
+
66
+ class HiveParamEscaper(ParamEscaper):
67
+ def escape_string(self, item):
68
+ # backslashes and single quotes need to be escaped
69
+ # TODO verify against parser
70
+ # Need to decode UTF-8 because of old sqlalchemy.
71
+ # Newer SQLAlchemy checks dialect.supports_unicode_binds before encoding Unicode strings
72
+ # as byte strings. The old version always encodes Unicode as byte strings, which breaks
73
+ # string formatting here.
74
+ if isinstance(item, bytes):
75
+ item = item.decode('utf-8')
76
+ return "'{}'".format(
77
+ item
78
+ .replace('\\', '\\\\')
79
+ .replace("'", "\\'")
80
+ .replace('\r', '\\r')
81
+ .replace('\n', '\\n')
82
+ .replace('\t', '\\t')
83
+ )
84
+
85
+
86
+ _escaper = HiveParamEscaper()
87
+
88
+
89
+ def connect(*args, **kwargs):
90
+ """Constructor for creating a connection to the database. See class :py:class:`Connection` for
91
+ arguments.
92
+ :returns: a :py:class:`Connection` object.
93
+ """
94
+ return Connection(*args, **kwargs)
95
+
96
+
97
+ class Connection(object):
98
+ """Wraps a http e6xdb session"""
99
+
100
+ def __init__(
101
+ self,
102
+ host=None,
103
+ port=None,
104
+ scheme='e6xdb',
105
+ username=None,
106
+ database='default',
107
+ auth=None,
108
+ configuration=None,
109
+ kerberos_service_name=None,
110
+ password=None,
111
+ check_hostname=None,
112
+ ssl_cert=None,
113
+ thrift_transport=None
114
+ ):
115
+ self.__username = username
116
+ self.__password = password
117
+ self._database = database
118
+ self._session_id = None
119
+
120
+ # service_name = 'E6x' # E6x QueryExecutor
121
+ service_name = 'QueryEngine' # E6x QueryExecutor
122
+
123
+ if scheme != "e6xdb":
124
+ raise ValueError("scheme is not e6xdb")
125
+
126
+ if not self.__username or not self.__password:
127
+ raise ValueError("username or password cannot be empty.")
128
+ if port is None:
129
+ port = 9000
130
+ self._transport = TSocket.TSocket(host, port)
131
+ self._transport = TTransport.TBufferedTransport(self._transport)
132
+
133
+ protocol = TBinaryProtocol.TBinaryProtocol(self._transport)
134
+ protocol = TMultiplexedProtocol.TMultiplexedProtocol(protocol, service_name)
135
+ self._client = QueryEngineService.Client(protocol)
136
+ self._transport.open()
137
+
138
+ @property
139
+ def get_session_id(self):
140
+ """
141
+ To get the session id, if user is not authorised, first authenticate the user.
142
+ """
143
+ if not self._session_id:
144
+ try:
145
+ self._session_id = self._client.authenticate(self.__username, self.__password)
146
+ if not self._session_id:
147
+ raise ValueError("Invalid credentials.")
148
+ # self._client.setSchema(database)
149
+ except Exception as e:
150
+ self._transport.close()
151
+ raise e
152
+ return self._session_id
153
+
154
+ def update_users(self, user_info):
155
+ self.client.updateUsers(userInfo=user_info)
156
+
157
+ def set_prop_map(self, prop_map: str):
158
+ """
159
+ To enable to disable the caches.
160
+ :param prop_map: To set engine props
161
+ """
162
+ self._client.setProps(sessionId=self.get_session_id, propMap=prop_map)
163
+
164
+ def __enter__(self):
165
+ """Transport should already be opened by __init__"""
166
+ return self
167
+
168
+ def __exit__(self, exc_type, exc_val, exc_tb):
169
+ """Call close"""
170
+ self.close()
171
+
172
+ def close(self):
173
+ self._transport.close()
174
+
175
+ def check_connection(self):
176
+ return self._transport.isOpen()
177
+
178
+ def clear(self, query_id):
179
+ self._client.clear(sessionId=self.get_session_id, queryId=query_id)
180
+ self._session_id = None
181
+
182
+ def reopen(self):
183
+ self._transport.close()
184
+ self._transport.open()
185
+
186
+ def query_cancel(self, query_id):
187
+ self._client.cancelQuery(sessionId=self.get_session_id, queryId=query_id)
188
+
189
+ def dry_run(self, query):
190
+ return self._client.dryRun(sessionId=self.get_session_id, sSchema=self._database, sQueryString=query)
191
+
192
+ def get_tables(self, database):
193
+ return self._client.getTables(sessionId=self.get_session_id, schema=database)
194
+
195
+ def get_columns(self, database, table):
196
+ return self._client.getColumns(sessionId=self.get_session_id, schema=database, table=table)
197
+
198
+ def get_schema_names(self):
199
+ return self._client.getSchemaNames(sessionId=self.get_session_id)
200
+
201
+ def commit(self):
202
+ """We do not support transactions, so this does nothing."""
203
+ pass
204
+
205
+ def cursor(self, db_name=None):
206
+ """Return a new :py:class:`Cursor` object using the connection."""
207
+ return Cursor(self, database=db_name)
208
+
209
+ def rollback(self):
210
+ raise Exception("e6xdb does not support transactions") # pragma: no cover
211
+
212
+ @property
213
+ def client(self):
214
+ return self._client
215
+
216
+
217
+ class Cursor(DBAPICursor):
218
+ """These objects represent a database cursor, which is used to manage the context of a fetch
219
+ operation.
220
+ Cursors are not isolated, i.e., any changes done to the database by a cursor are immediately
221
+ visible by other cursors or connections.
222
+ """
223
+ rows_count = 0
224
+
225
+ def __init__(self, connection, arraysize=1000, database=None):
226
+ super(Cursor, self).__init__()
227
+ self._arraysize = arraysize
228
+ self.connection = connection
229
+ self._data = None
230
+ self._query_columns_description = None
231
+ self._is_metadata_updated = False
232
+ self._description = None
233
+ self._query_id = None
234
+ self._batch = list()
235
+ self._rowcount = 0
236
+ self._database = self.connection._database if database is None else database
237
+
238
+ def _reset_state(self):
239
+ """Reset state about the previous query in preparation for running another query"""
240
+ pass
241
+
242
+ @property
243
+ def arraysize(self):
244
+ return self._arraysize
245
+
246
+ @arraysize.setter
247
+ def arraysize(self, value):
248
+ """Array size cannot be None, and should be an integer"""
249
+ default_arraysize = 1000
250
+ try:
251
+ self._arraysize = int(value) or default_arraysize
252
+ except TypeError:
253
+ self._arraysize = default_arraysize
254
+
255
+ @property
256
+ def description(self):
257
+ """This read-only attribute is a sequence of 7-item sequences.
258
+ Each of these sequences contains information describing one result column:
259
+ - name
260
+ - type_code
261
+ - display_size (None in current implementation)
262
+ - internal_size (None in current implementation)
263
+ - precision (None in current implementation)
264
+ - scale (None in current implementation)
265
+ - null_ok (always True in current implementation)
266
+ This attribute will be ``None`` for operations that do not return rows or if the cursor has
267
+ not had an operation invoked via the :py:meth:`execute` method yet.
268
+ The ``type_code`` can be interpreted by comparing it to the Type Objects specified in the
269
+ section below.
270
+ """
271
+ if self._description is None:
272
+ self._description = []
273
+ for col in self._query_columns_description:
274
+ type_code = col.get_field_type()
275
+ column_name = col.get_name()
276
+ self._description.append((
277
+ column_name,
278
+ type_code,
279
+ None, None, None, None, True
280
+ ))
281
+ return self._description
282
+
283
+ def __enter__(self):
284
+ return self
285
+
286
+ def __exit__(self, exc_type, exc_val, exc_tb):
287
+ self.close()
288
+
289
+ def close(self):
290
+ """Close the operation handle"""
291
+ self.connection.close()
292
+ self._arraysize = None
293
+ self.connection = None
294
+ self._data = None
295
+ self._query_columns_description = None
296
+ self._description = None
297
+ self._query_id = None
298
+ self._batch = None
299
+ self._rowcount = None
300
+ self._database = None
301
+
302
+ def get_tables(self):
303
+ schema = self.connection._database
304
+ return self.connection.get_tables(database=schema)
305
+
306
+ def get_columns(self, table):
307
+ schema = self.connection._database
308
+ return self.connection.get_columns(database=schema, table=table)
309
+
310
+ def clear(self):
311
+ """Clears the tmp data"""
312
+ self.connection.clear(self._query_id)
313
+
314
+ def cancel(self, query_id):
315
+ _logger.info("Cancelling query")
316
+ self.connection.query_cancel(query_id)
317
+
318
+ def execute(self, operation, parameters=None, **kwargs):
319
+ """Prepare and execute a database operation (query or command).
320
+ Return values are not defined.
321
+ """
322
+ """
323
+ Semicolon is now not supported. So removing it from query end.
324
+ """
325
+ if operation.endswith(';'):
326
+ operation = operation[:-1]
327
+
328
+ # Prepare statement
329
+ if parameters is None:
330
+ sql = operation
331
+ else:
332
+ sql = operation % _escaper.escape_args(parameters)
333
+
334
+ client = self.connection.client
335
+ self._query_id = client.prepareStatement(
336
+ self.connection.get_session_id,
337
+ self._database,
338
+ sql
339
+ )
340
+ client.executeStatement(self.connection.get_session_id, self._query_id)
341
+ self.update_mete_data()
342
+ return self._query_id
343
+
344
+ def rowcount(self):
345
+ return self._rowcount
346
+
347
+ def update_mete_data(self):
348
+ buffer = self.connection.client.getResultMetadata(self.connection.get_session_id, self._query_id)
349
+ buffer = BytesIO(buffer)
350
+ self._rowcount, self._query_columns_description = get_query_columns_info(buffer)
351
+
352
+ def _fetch_more(self):
353
+ # _logger.info("fetching batch")
354
+ batch_size = self._arraysize
355
+ self._data = list()
356
+ for i in range(batch_size):
357
+ rows = self.fetch_batch()
358
+ if rows is None:
359
+ return
360
+ self._data = self._data + rows
361
+
362
+ # _logger.info("fetched batch of {num}".format(num=len(self._data)))
363
+ return self._data
364
+
365
+ def _fetch_all(self):
366
+ self._data = list()
367
+ while True:
368
+ rows = self.fetch_batch()
369
+ if rows is None:
370
+ break
371
+ self._data = self._data + rows
372
+ rows = self._data
373
+ self._data = None
374
+ return rows
375
+
376
+ def fetch_batch(self):
377
+ # _logger.debug("fetching next batch from e6data")
378
+ client = self.connection.client
379
+ buffer = client.getNextResultBatch(self.connection.get_session_id, self._query_id)
380
+ if not self._is_metadata_updated:
381
+ self.update_mete_data()
382
+ self._is_metadata_updated = True
383
+ if not buffer:
384
+ return None
385
+ buffer = BytesIO(buffer)
386
+ dis = DataInputStream(buffer)
387
+ # one batch retrieves the predefined set of rows
388
+ return read_rows_from_batch(self._query_columns_description, dis)
389
+
390
+ def fetchall(self):
391
+ return self._fetch_all()
392
+
393
+ def fetchmany(self, size=None):
394
+ # _logger.info("fetching all from overriden method")
395
+ if size is None:
396
+ size = self.arraysize
397
+ if self._data is None:
398
+ self._data = list()
399
+ while len(self._data) < size:
400
+ # _logger.info("fetching next batch from fetch many")
401
+ rows = self.fetch_batch()
402
+ if rows is None:
403
+ break
404
+ self._data += rows
405
+ _logger.info(len(self._data))
406
+ if len(self._data) <= size:
407
+ rows = self._data
408
+ self._data = None
409
+ return rows
410
+ rows = self._data[:size]
411
+ self._data = self._data[size:]
412
+ return rows
413
+
414
+ def fetchone(self):
415
+ # _logger.info("fetch One returning the batch itself which is limited by predefined no.of rows")
416
+ rows_to_return = []
417
+ client = self.connection.client
418
+ buffer = client.getNextResultRow(self.connection.get_session_id, self._query_id)
419
+ if not self._is_metadata_updated:
420
+ self.update_mete_data()
421
+ self._is_metadata_updated = True
422
+ if not buffer:
423
+ return None
424
+ buffer = BytesIO(buffer)
425
+ dis = DataInputStream(buffer)
426
+ rows_to_return.append(read_values_from_array(self._query_columns_description, dis))
427
+ return rows_to_return
428
+
429
+ def explain(self):
430
+ return self.connection.client.explain(self.connection.get_session_id, self._query_id)
431
+
432
+ def explain_analyse(self):
433
+ return self.connection.client.explainAnalyze(self.connection.get_session_id, self._query_id)
434
+
435
+
436
+ def poll(self, get_progress_update=True):
437
+ """Poll for and return the raw status data provided by the Hive Thrift REST API.
438
+ :returns: ``ttypes.TGetOperationStatusResp``
439
+ :raises: ``ProgrammingError`` when no query has been started
440
+ .. note::
441
+ This is not a part of DB-API.
442
+ """
443
+ pass
444
+
445
+
446
+ def fetch_logs(self):
447
+ """Retrieve the logs produced by the execution of the query.
448
+ Can be called multiple times to fetch the logs produced after the previous call.
449
+ :returns: list<str>
450
+ :raises: ``ProgrammingError`` when no query has been started
451
+ .. note::
452
+ This is not a part of DB-API.
453
+ """
454
+ pass
455
+
456
+
457
+ class Error(Exception):
458
+ pass
459
+
460
+
461
+ #
462
+ # Type Objects and Constructors
463
+ #
464
+
465
+ for type_id in PRIMITIVE_TYPES:
466
+ name = TypeId._VALUES_TO_NAMES[type_id]
467
+ setattr(sys.modules[__name__], name, DBAPITypeObject([name]))
e6xdb/exceptions.py ADDED
@@ -0,0 +1,15 @@
1
+ class NotSupportedError(Exception):
2
+ """Raised when op not supported by e6xdb"""
3
+ pass
4
+
5
+ class ProgrammingError(Exception):
6
+ """Raised when op not supported by e6xdb"""
7
+ pass
8
+
9
+ class DataError(Exception):
10
+ """Raised when there are inherent data issues"""
11
+ pass
12
+
13
+ class OperationalError(Exception):
14
+ """Raised when there are operational issues in Uniphi"""
15
+ pass