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.
- e6data_python_connector-1.0.0.dist-info/LICENSE +202 -0
- e6data_python_connector-1.0.0.dist-info/METADATA +205 -0
- e6data_python_connector-1.0.0.dist-info/RECORD +19 -0
- e6data_python_connector-1.0.0.dist-info/WHEEL +5 -0
- e6data_python_connector-1.0.0.dist-info/entry_points.txt +2 -0
- e6data_python_connector-1.0.0.dist-info/top_level.txt +1 -0
- e6xdb/__init__.py +10 -0
- e6xdb/common.py +269 -0
- e6xdb/constants.py +32 -0
- e6xdb/datainputstream.py +170 -0
- e6xdb/date_time_utils.py +167 -0
- e6xdb/e6x.py +467 -0
- e6xdb/exceptions.py +15 -0
- e6xdb/server/QueryEngineService.py +3776 -0
- e6xdb/server/__init__.py +1 -0
- e6xdb/server/constants.py +14 -0
- e6xdb/server/ttypes.py +342 -0
- e6xdb/sqlalchemy_e6x.py +396 -0
- e6xdb/typeId.py +73 -0
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
|