velocity-python 0.0.1__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.

Potentially problematic release.


This version of velocity-python might be problematic. Click here for more details.

@@ -0,0 +1,367 @@
1
+ from velocity.db import exceptions
2
+ from velocity.db.core.transaction import Transaction
3
+
4
+ from functools import wraps
5
+ import inspect, sys, re, traceback
6
+
7
+ class Engine(object):
8
+ MAX_RETRIES = 100
9
+
10
+ def __init__(self, driver, config, sql):
11
+ self.__config = config
12
+ self.__sql = sql
13
+ self.__driver = driver
14
+
15
+ def __str__(self):
16
+ return """[{}] engine({})""".format(self.sql.server, self.config)
17
+
18
+ def connect(self):
19
+ """
20
+ Connects to the database and returns the connection object.
21
+
22
+ If the database is missing, it creates the database and then connects to it.
23
+
24
+ Returns:
25
+ conn: The connection object to the database.
26
+ """
27
+ try:
28
+ conn = self.__connect()
29
+ except exceptions.DbDatabaseMissingError:
30
+ self.create_database()
31
+ conn = self.__connect()
32
+ if self.sql.server == 'SQLite3':
33
+ conn.isolation_level = None
34
+ return conn
35
+
36
+ def __connect(self):
37
+ """
38
+ Connects to the database using the provided configuration.
39
+
40
+ Returns:
41
+ A connection object representing the connection to the database.
42
+
43
+ Raises:
44
+ Exception: If the configuration parameter is not handled properly.
45
+ ProcessError is called to handle other exceptions.
46
+ """
47
+ try:
48
+ if isinstance(self.config, dict):
49
+ return self.driver.connect(**self.config)
50
+ elif isinstance(self.config, (tuple, list)):
51
+ return self.driver.connect(*self.config)
52
+ elif isinstance(self.config, str):
53
+ return self.driver.connect(self.config)
54
+ else:
55
+ raise Exception("Unhandled configuration parameter")
56
+ except:
57
+ self.ProcessError()
58
+
59
+
60
+ def transaction(self, func_or_cls=None):
61
+ """
62
+ Decorator for defining a transaction. Use this to wrap a function, method, or class to automatically
63
+ start a transaction if necessary. If the function, method or class is called with a `tx` keyword argument,
64
+ it will use that transaction object instead of creating a new one. If the function, method or class
65
+ is called with a `tx` positional argument, it will use that transaction object instead of creating a new one.
66
+ If the function, method or class is called with a `tx` positional argument and a `tx` keyword argument, it will use the positional
67
+ argument and ignore the keyword argument. If the function, method or class is called without a `tx` argument,
68
+ it will create a new transaction object and use that.
69
+ Args:
70
+ func_or_cls: The function or class to be decorated.
71
+
72
+ Returns:
73
+ If `func_or_cls` is a function or method, returns a wrapped version of the function or method that
74
+ automatically starts a transaction if necessary. If `func_or_cls` is a class, returns a subclass of
75
+ `func_or_cls` that wraps all its methods with the transaction decorator.
76
+
77
+
78
+
79
+ If `func_or_cls` is not provided, returns a new `Transaction` object associated with the engine.
80
+ """
81
+ # If you are having trouble passing TWO transaction objects, for
82
+ # example as a source database to draw data from, pass the second as
83
+ # a keyword. For example:
84
+ # @engine.transaction
85
+ # def function(tx, src=src) <-- pass second as a kwd and tx will populate correctly.
86
+ # ...
87
+ #
88
+ engine = self
89
+ if inspect.isfunction(func_or_cls) \
90
+ or inspect.ismethod(func_or_cls):
91
+ @wraps(func_or_cls)
92
+ def NewFunction(*args, **kwds):
93
+ tx = None
94
+ names = list(inspect.signature(func_or_cls).parameters.keys())
95
+ if '_tx' in names:
96
+ raise NameError(f'In function named `{func_or_cls.__name__}` You may not name a paramater `_tx`')
97
+ if 'tx' not in names:
98
+ return func_or_cls(*args, **kwds)
99
+ elif 'tx' in kwds:
100
+ if isinstance(kwds['tx'], Transaction):
101
+ tx = kwds['tx']
102
+ else:
103
+ raise TypeError(f'In function named `{func_or_cls.__name__}` keyword `tx` must be a Transaction object')
104
+ elif 'tx' in names:
105
+ pos = names.index('tx')
106
+ if len(args) > pos \
107
+ and isinstance(args[pos], Transaction):
108
+ tx = args[pos]
109
+ if tx:
110
+ return engine.exec_function(func_or_cls, tx, *args, **kwds)
111
+ else:
112
+ with Transaction(engine) as tx:
113
+ pos = names.index('tx')
114
+ args = list(args)
115
+ args.insert(pos, tx)
116
+ args = tuple(args)
117
+ return engine.exec_function(func_or_cls, tx, *args, **kwds)
118
+ return NewFunction
119
+ elif inspect.isclass(func_or_cls):
120
+ class NewCls(func_or_cls):
121
+ def __getattribute__(self, key):
122
+ attr = super(NewCls, self).__getattribute__(key)
123
+ if key in ['start_response']:
124
+ return attr
125
+ if inspect.ismethod(attr):
126
+ return engine.transaction(attr)
127
+ return attr
128
+ return NewCls
129
+
130
+ return Transaction(engine)
131
+
132
+
133
+ def exec_function(self, function, _tx, *args, **kwds):
134
+ """
135
+ Executes the given function with the provided arguments and keyword arguments.
136
+ If there is no transaction object, it executes the function without a transaction.
137
+
138
+ If there is a transaction object, it executes the function within the transaction.
139
+ If the function raises a `DbRetryTransaction` exception, it rolls back the transaction and retries.
140
+ If the function raises a `DbLockTimeoutError` exception, it rolls back the transaction and retries.
141
+ If any other exception occurs, it raises the exception.
142
+
143
+ Args:
144
+ function: The function to be executed.
145
+ tx: The transaction object to be passed to the function.
146
+ *args: Positional arguments to be passed to the function.
147
+ **kwds: Keyword arguments to be passed to the function.
148
+
149
+ Returns:
150
+ The result of the function execution.
151
+
152
+ Raises:
153
+ DbRetryTransaction: If the maximum number of retries is exceeded.
154
+ DbLockTimeoutError: If the maximum number of retries is exceeded.
155
+ Any other exception raised by the function.
156
+ """
157
+ retry_count = 0
158
+ tmout_count = 0
159
+ if _tx is None:
160
+ return function(*args, **kwds)
161
+ else:
162
+ while True:
163
+ try:
164
+ return function(*args, **kwds)
165
+ except exceptions.DbRetryTransaction as e:
166
+ if e.args and e.args[0]:
167
+ _tx.rollback()
168
+ continue
169
+ retry_count += 1
170
+ if retry_count > self.MAX_RETRIES:
171
+ raise
172
+ print('**Retry Transaction. Rollback and start over')
173
+ _tx.rollback()
174
+ continue
175
+ except exceptions.DbLockTimeoutError:
176
+ tmout_count += 1
177
+ if tmout_count > self.MAX_RETRIES:
178
+ raise
179
+ print('**DbLockTimeoutError. Rollback and start over')
180
+ _tx.rollback()
181
+ continue
182
+ except:
183
+ raise
184
+
185
+ @property
186
+ def driver(self):
187
+ return self.__driver
188
+
189
+ @property
190
+ def config(self):
191
+ return self.__config
192
+
193
+ @property
194
+ def sql(self):
195
+ return self.__sql
196
+
197
+ @property
198
+ def version(self):
199
+ with Transaction(self) as tx:
200
+ sql, vals = self.sql.version()
201
+ return tx.execute(sql, vals).scalar()
202
+
203
+ @property
204
+ def timestamp(self):
205
+ with Transaction(self) as tx:
206
+ sql, vals = self.sql.timestamp()
207
+ return tx.execute(sql, vals).scalar()
208
+
209
+ @property
210
+ def user(self):
211
+ with Transaction(self) as tx:
212
+ sql, vals = self.sql.user()
213
+ return tx.execute(sql, vals).scalar()
214
+
215
+ @property
216
+ def databases(self):
217
+ with Transaction(self) as tx:
218
+ sql, vals = self.sql.databases()
219
+ result = tx.execute(sql, vals)
220
+ return [x[0] for x in result.as_tuple()]
221
+
222
+ @property
223
+ def current_database(self):
224
+ with Transaction(self) as tx:
225
+ sql, vals = self.sql.current_database()
226
+ return tx.execute(sql, vals).scalar()
227
+
228
+ def create_database(self, name=None):
229
+ old = None
230
+ if name == None:
231
+ old = self.config['database']
232
+ self.set_config({'database':'postgres'})
233
+ name = old
234
+ with Transaction(self) as tx:
235
+ sql, vals = self.sql.create_database(name)
236
+ tx.execute(sql, vals, single=True)
237
+ if old:
238
+ self.set_config({'database':old})
239
+ return self
240
+
241
+ def switch_to_database(self, database):
242
+ conf = self.config
243
+ if 'database' in conf:
244
+ conf['database'] = database
245
+ if 'dbname' in conf:
246
+ conf['dbname'] = database
247
+ return self
248
+
249
+ def set_config(self, config):
250
+ self.config.update(config)
251
+
252
+ @property
253
+ def schemas(self):
254
+ with Transaction(self) as tx:
255
+ sql, vals = self.sql.schemas()
256
+ result = tx.execute(sql, vals)
257
+ return [x[0] for x in result.as_tuple()]
258
+
259
+ @property
260
+ def current_schema(self):
261
+ with Transaction(self) as tx:
262
+ sql, vals = self.sql.current_schema()
263
+ return tx.execute(sql, vals).scalar()
264
+
265
+ @property
266
+ def tables(self):
267
+ with Transaction(self) as tx:
268
+ sql, vals = self.sql.tables()
269
+ result = tx.execute(sql, vals)
270
+ return ["%s.%s" % x for x in result.as_tuple()]
271
+
272
+ @property
273
+ def views(self):
274
+ with Transaction(self) as tx:
275
+ sql, vals = self.sql.views()
276
+ result = tx.execute(sql, vals)
277
+ return ["%s.%s" % x for x in result.as_tuple()]
278
+
279
+
280
+ def ProcessError(self, sql_stmt=None, sql_params=None):
281
+ sql = self.sql
282
+ e = sys.exc_info()[1]
283
+ msg = str(e).strip().lower()
284
+ if isinstance(e,exceptions.DbException):
285
+ raise
286
+ if hasattr(e,'pgcode'):
287
+ error_code = e.pgcode
288
+ error_mesg = e.pgerror
289
+ elif hasattr(e,'args') \
290
+ and isinstance(e.args,(tuple,list)) \
291
+ and len(e.args) > 1:
292
+ error_code = e[0]
293
+ error_mesg = e[1]
294
+ elif hasattr(e,'number') \
295
+ and hasattr(e,'text'):
296
+ error_code = e.number
297
+ error_mesg = e.text
298
+ elif hasattr(e,'args') \
299
+ and hasattr(e,'message'):
300
+ # SQLite3
301
+ error_code = None
302
+ error_mesg = e.message
303
+ else:
304
+ raise
305
+ if error_code in sql.ApplicationErrorCodes:
306
+ raise exceptions.DbApplicationError(e)
307
+ elif error_code in sql.ColumnMissingErrorCodes:
308
+ raise exceptions.DbColumnMissingError(e)
309
+ elif error_code in sql.TableMissingErrorCodes:
310
+ raise exceptions.DbTableMissingError(e)
311
+ elif error_code in sql.DatabaseMissingErrorCodes:
312
+ raise exceptions.DbDatabaseMissingError(e)
313
+ elif error_code in sql.ForeignKeyMissingErrorCodes:
314
+ raise exceptions.DbForeignKeyMissingError(e)
315
+ elif error_code in sql.TruncationErrorCodes:
316
+ raise exceptions.DbTruncationError(e)
317
+ elif error_code in sql.DataIntegrityErrorCodes:
318
+ raise exceptions.DbDataIntegrityError(e)
319
+ elif error_code in sql.ConnectionErrorCodes:
320
+ raise exceptions.DbConnectionError(e)
321
+ elif error_code in sql.DuplicateKeyErrorCodes:
322
+ raise exceptions.DbDuplicateKeyError(e)
323
+ elif re.search('key \(sys_id\)=\(\d+\) already exists.', msg, re.M):
324
+ raise exceptions.DbDuplicateKeyError(e)
325
+ elif error_code in sql.DatabaseObjectExistsErrorCodes:
326
+ raise exceptions.DbObjectExistsError(e)
327
+ elif error_code in sql.LockTimeoutErrorCodes:
328
+ raise exceptions.DbLockTimeoutError(e)
329
+ elif error_code in sql.RetryTransactionCodes:
330
+ raise exceptions.DbRetryTransaction(e)
331
+ elif re.findall('database.*does not exist', msg, re.M):
332
+ raise exceptions.DbDatabaseMissingError(e)
333
+ elif re.findall('no such database', msg, re.M):
334
+ raise exceptions.DbDatabaseMissingError(e)
335
+ elif re.findall('already exists', msg, re.M):
336
+ raise exceptions.DbObjectExistsError(e)
337
+ elif re.findall('server closed the connection unexpectedly', msg, re.M):
338
+ raise exceptions.DbConnectionError(e)
339
+ elif re.findall('no connection to the server', msg, re.M):
340
+ raise exceptions.DbConnectionError(e)
341
+ elif re.findall('connection timed out', msg, re.M):
342
+ raise exceptions.DbConnectionError(e)
343
+ elif re.findall('could not connect to server', msg, re.M):
344
+ raise exceptions.DbConnectionError(e)
345
+ elif re.findall('cannot connect to server', msg, re.M):
346
+ raise exceptions.DbConnectionError(e)
347
+ elif re.findall('connection already closed', msg, re.M):
348
+ raise exceptions.DbConnectionError(e)
349
+ elif re.findall('cursor already closed', msg, re.M):
350
+ raise exceptions.DbConnectionError(e)
351
+ # SQLite3 errors
352
+ elif 'no such table:' in msg:
353
+ raise exceptions.DbTableMissingError(e)
354
+ print("Unhandled/Unknown Error in connection.ProcessError")
355
+ print('EXC_TYPE = {}'.format(type(e)))
356
+ print('EXC_MSG = {}'.format(str(e).strip()))
357
+ print('ERROR_CODE = {}'.format(error_code))
358
+ print('ERROR_MSG = {}'.format(error_mesg))
359
+ if sql_stmt:
360
+ print("\n")
361
+ print("sql_stmt [velocity.db.engine]: {}".format(sql_stmt))
362
+ print("\n")
363
+ if sql_params:
364
+ print(sql_params)
365
+ print("\n")
366
+ traceback.print_exc()
367
+ raise
@@ -0,0 +1,31 @@
1
+ class DbException(Exception):
2
+ pass
3
+
4
+ class DbApplicationError(DbException):
5
+ pass
6
+
7
+ class DbForeignKeyMissingError(DbException):
8
+ pass
9
+ class DbDatabaseMissingError(DbException):
10
+ pass
11
+ class DbTableMissingError(DbException):
12
+ pass
13
+ class DbColumnMissingError(DbException):
14
+ pass
15
+
16
+ class DbTruncationError(DbException):
17
+ pass
18
+ class DbConnectionError(DbException):
19
+ pass
20
+ class DbDuplicateKeyError(DbException):
21
+ pass
22
+ class DbObjectExistsError(DbException):
23
+ pass
24
+ class DbLockTimeoutError(DbException):
25
+ pass
26
+ class DbRetryTransaction(DbException):
27
+ pass
28
+ class DbDataIntegrityError(DbException):
29
+ pass
30
+ class DuplicateRowsFoundError(Exception):
31
+ pass
@@ -0,0 +1,137 @@
1
+ from velocity.misc.format import to_json
2
+
3
+ class Result(object):
4
+ def __init__(self, cursor=None):
5
+ self._cursor = cursor
6
+ if hasattr(cursor,'description') and cursor.description:
7
+ self._headers = [x[0].lower() for x in cursor.description]
8
+ else:
9
+ self._headers = []
10
+ self.as_dict()
11
+ self.__as_strings = False
12
+ self.__enumerate = False
13
+ self.__count = -1
14
+
15
+ @property
16
+ def headers(self):
17
+ if not self._headers:
18
+ if self._cursor and hasattr(self._cursor, 'description'):
19
+ self._headers = [x[0].lower() for x in self._cursor.description]
20
+ return self._headers
21
+
22
+ def __str__(self):
23
+ return repr(self.all())
24
+
25
+ def __enter__(self):
26
+ return self
27
+
28
+ def __exit__(self, exc_type, exc_val, exc_tb):
29
+ if not exc_type:
30
+ self.close()
31
+
32
+ def __next__(self):
33
+ if self._cursor:
34
+ row = self._cursor.fetchone()
35
+ if row:
36
+ if self.__as_strings:
37
+ row = ['' if x is None else str(x) for x in row]
38
+ if self.__enumerate:
39
+ self.__count += 1
40
+ return (self.__count, self.transform(row))
41
+ else:
42
+ return self.transform(row)
43
+ raise StopIteration
44
+
45
+ def batch(self, qty=1):
46
+ results = []
47
+ while True:
48
+ try:
49
+ results.append(next(self))
50
+ except StopIteration:
51
+ if results:
52
+ yield results
53
+ results = []
54
+ continue
55
+ raise
56
+ if len(results) == qty:
57
+ yield results
58
+ results = []
59
+
60
+ def all(self):
61
+ results = []
62
+ while True:
63
+ try:
64
+ results.append(next(self))
65
+ except StopIteration:
66
+ break
67
+ return results
68
+
69
+ def __iter__(self):
70
+ return self
71
+
72
+ @property
73
+ def cursor(self):
74
+ return self._cursor
75
+
76
+ def close(self):
77
+ self._cursor.close()
78
+
79
+ def as_dict(self):
80
+ self.transform = lambda row: dict(list(zip(self.headers,row)))
81
+ return self
82
+
83
+ def as_json(self):
84
+ self.transform = lambda row: to_json(dict(list(zip(self.headers,row))))
85
+ return self
86
+
87
+ def as_named_tuple(self):
88
+ self.transform = lambda row: list(zip(self.headers,row))
89
+ return self
90
+
91
+ def as_list(self):
92
+ self.transform = lambda row: list(row)
93
+ return self
94
+
95
+ def as_tuple(self):
96
+ self.transform = lambda row: row
97
+ return self
98
+
99
+ def as_simple_list(self, pos=0):
100
+ self.transform = lambda row: row[pos]
101
+ return self
102
+
103
+ def strings(self, as_strings=True):
104
+ self.__as_strings = as_strings
105
+ return self
106
+
107
+ def scalar(self, default=None):
108
+ if not self._cursor:
109
+ return None
110
+ val = self._cursor.fetchone()
111
+ self._cursor.fetchall()
112
+ return val[0] if val else default
113
+
114
+ def one(self,default=None):
115
+ try:
116
+ return next(self)
117
+ except StopIteration:
118
+ return default
119
+ finally:
120
+ if self._cursor:
121
+ self._cursor.fetchall()
122
+
123
+ def get_table_data(self, headers=True, strings=True):
124
+ self.as_list()
125
+ rows = []
126
+ for row in self:
127
+ rows.append(['' if x is None else str(x) for x in row])
128
+ if isinstance(headers,list):
129
+ rows.insert(0,[x.replace('_',' ').title() for x in headers])
130
+ elif headers:
131
+ rows.insert(0,[x.replace('_',' ').title() for x in self.headers])
132
+ return rows
133
+
134
+ def enum(self):
135
+ self.__enumerate = True
136
+ return self
137
+ enumerate = enum