velocity-python 0.0.35__py3-none-any.whl → 0.0.65__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.
@@ -1,11 +1,17 @@
1
+ import inspect
2
+ import sys
3
+ import re
4
+ import traceback
5
+ from functools import wraps
1
6
  from velocity.db import exceptions
2
7
  from velocity.db.core.transaction import Transaction
3
8
 
4
- from functools import wraps
5
- import inspect, sys, re, traceback
6
9
 
10
+ class Engine:
11
+ """
12
+ Encapsulates driver config, connection logic, error handling, and transaction decoration.
13
+ """
7
14
 
8
- class Engine(object):
9
15
  MAX_RETRIES = 100
10
16
 
11
17
  def __init__(self, driver, config, sql):
@@ -14,16 +20,12 @@ class Engine(object):
14
20
  self.__driver = driver
15
21
 
16
22
  def __str__(self):
17
- return """[{}] engine({})""".format(self.sql.server, self.config)
23
+ return f"[{self.sql.server}] engine({self.config})"
18
24
 
19
25
  def connect(self):
20
26
  """
21
27
  Connects to the database and returns the connection object.
22
-
23
- If the database is missing, it creates the database and then connects to it.
24
-
25
- Returns:
26
- conn: The connection object to the database.
28
+ If the database is missing, tries to create it, then reconnect.
27
29
  """
28
30
  try:
29
31
  conn = self.__connect()
@@ -36,158 +38,132 @@ class Engine(object):
36
38
 
37
39
  def __connect(self):
38
40
  """
39
- Connects to the database using the provided configuration.
40
-
41
- Returns:
42
- A connection object representing the connection to the database.
43
-
44
- Raises:
45
- Exception: If the configuration parameter is not handled properly.
46
- ProcessError is called to handle other exceptions.
41
+ Internal connection logic, raising suitable exceptions on error.
47
42
  """
48
43
  try:
49
44
  if isinstance(self.config, dict):
50
45
  return self.driver.connect(**self.config)
51
- elif isinstance(self.config, (tuple, list)):
46
+ if isinstance(self.config, (tuple, list)):
52
47
  return self.driver.connect(*self.config)
53
- elif isinstance(self.config, str):
48
+ if isinstance(self.config, str):
54
49
  return self.driver.connect(self.config)
55
- else:
56
- raise Exception("Unhandled configuration parameter")
50
+ raise Exception("Unhandled configuration parameter.")
57
51
  except:
58
- self.ProcessError()
52
+ self.process_error()
59
53
 
60
54
  def transaction(self, func_or_cls=None):
61
55
  """
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
56
+ Decorator that provides a Transaction. If `tx` is passed in, uses it; otherwise, creates a new one.
57
+ May also be used to decorate a class, in which case all methods are wrapped in a transaction if they accept `tx`.
58
+ With no arguments, returns a new Transaction directly.
59
+ """
60
+ # print("Transaction", func_or_cls.__name__, type(func_or_cls))
61
+
62
+ if func_or_cls is None:
63
+ return Transaction(self)
64
+
65
+ if isinstance(func_or_cls, classmethod):
66
+ return classmethod(self.transaction(func_or_cls.__func__))
67
+
89
68
  if inspect.isfunction(func_or_cls) or inspect.ismethod(func_or_cls):
69
+ names = list(inspect.signature(func_or_cls).parameters.keys())
70
+ # print(func_or_cls.__name__, names)
71
+ if "_tx" in names:
72
+ raise NameError(
73
+ f"In function {func_or_cls.__name__}, '_tx' is not allowed as a parameter."
74
+ )
90
75
 
91
76
  @wraps(func_or_cls)
92
- def NewFunction(*args, **kwds):
77
+ def new_function(*args, **kwds):
93
78
  tx = None
94
79
  names = list(inspect.signature(func_or_cls).parameters.keys())
95
- if "_tx" in names:
96
- raise NameError(
97
- f"In function named `{func_or_cls.__name__}` You may not name a paramater `_tx`"
98
- )
80
+
81
+ # print("inside", func_or_cls.__name__)
82
+ # print(names)
83
+ # print(args, kwds)
84
+
99
85
  if "tx" not in names:
86
+ # The function doesn't even declare a `tx` parameter, so run normally.
100
87
  return func_or_cls(*args, **kwds)
101
- elif "tx" in kwds:
88
+
89
+ if "tx" in kwds:
102
90
  if isinstance(kwds["tx"], Transaction):
103
91
  tx = kwds["tx"]
104
92
  else:
105
93
  raise TypeError(
106
- f"In function named `{func_or_cls.__name__}` keyword `tx` must be a Transaction object"
94
+ f"In function {func_or_cls.__name__}, keyword argument `tx` must be a Transaction object."
107
95
  )
108
- elif "tx" in names:
96
+ else:
97
+ # Might be in positional args
109
98
  pos = names.index("tx")
110
- if len(args) > pos and isinstance(args[pos], Transaction):
111
- tx = args[pos]
99
+ if len(args) > pos:
100
+ if isinstance(args[pos], Transaction):
101
+ tx = args[pos]
102
+
112
103
  if tx:
113
- return engine.exec_function(func_or_cls, tx, *args, **kwds)
114
- else:
115
- with Transaction(engine) as tx:
116
- pos = names.index("tx")
117
- args = list(args)
118
- args.insert(pos, tx)
119
- args = tuple(args)
120
- return engine.exec_function(func_or_cls, tx, *args, **kwds)
121
-
122
- return NewFunction
123
- elif inspect.isclass(func_or_cls):
124
-
125
- class NewCls(func_or_cls):
126
- def __getattribute__(self, key):
127
- attr = super(NewCls, self).__getattribute__(key)
128
- if key in ["start_response"]:
129
- return attr
130
- if inspect.ismethod(attr):
131
- return engine.transaction(attr)
132
- return attr
104
+ return self.exec_function(func_or_cls, tx, *args, **kwds)
105
+
106
+ with Transaction(self) as local_tx:
107
+ pos = names.index("tx")
108
+ new_args = args[:pos] + (local_tx,) + args[pos:]
109
+ return self.exec_function(func_or_cls, local_tx, *new_args, **kwds)
110
+
111
+ return new_function
112
+
113
+ if inspect.isclass(func_or_cls):
114
+
115
+ NewCls = type(func_or_cls.__name__, (func_or_cls,), {})
116
+
117
+ for attr_name in dir(func_or_cls):
118
+ # Optionally skip special methods
119
+ if attr_name.startswith("__") and attr_name.endswith("__"):
120
+ continue
121
+
122
+ attr = getattr(func_or_cls, attr_name)
123
+
124
+ if callable(attr):
125
+ setattr(NewCls, attr_name, self.transaction(attr))
133
126
 
134
127
  return NewCls
135
128
 
136
- return Transaction(engine)
129
+ return Transaction(self)
137
130
 
138
131
  def exec_function(self, function, _tx, *args, **kwds):
139
132
  """
140
- Executes the given function with the provided arguments and keyword arguments.
141
- If there is no transaction object, it executes the function without a transaction.
142
-
143
- If there is a transaction object, it executes the function within the transaction.
144
- If the function raises a `DbRetryTransaction` exception, it rolls back the transaction and retries.
145
- If the function raises a `DbLockTimeoutError` exception, it rolls back the transaction and retries.
146
- If any other exception occurs, it raises the exception.
147
-
148
- Args:
149
- function: The function to be executed.
150
- tx: The transaction object to be passed to the function.
151
- *args: Positional arguments to be passed to the function.
152
- **kwds: Keyword arguments to be passed to the function.
153
-
154
- Returns:
155
- The result of the function execution.
156
-
157
- Raises:
158
- DbRetryTransaction: If the maximum number of retries is exceeded.
159
- DbLockTimeoutError: If the maximum number of retries is exceeded.
160
- Any other exception raised by the function.
161
- """
162
- retry_count = 0
163
- tmout_count = 0
164
- if _tx is None:
165
- return function(*args, **kwds)
166
- else:
167
- while True:
168
- try:
169
- return function(*args, **kwds)
170
- except exceptions.DbRetryTransaction as e:
171
- if e.args and e.args[0]:
172
- print(e)
173
- print("**Retry Transaction. Rollback and start over")
133
+ Executes the given function inside the transaction `_tx`.
134
+ Retries if it raises DbRetryTransaction or DbLockTimeoutError, up to MAX_RETRIES times.
135
+ """
136
+ depth = getattr(_tx, "_exec_function_depth", 0)
137
+ setattr(_tx, "_exec_function_depth", depth + 1)
138
+
139
+ try:
140
+ if depth > 0:
141
+ # Not top-level. Just call the function.
142
+ return function(*args, **kwds)
143
+ else:
144
+ retry_count = 0
145
+ lock_timeout_count = 0
146
+ while True:
147
+ try:
148
+ return function(*args, **kwds)
149
+ except exceptions.DbRetryTransaction as e:
150
+ retry_count += 1
151
+ if retry_count > self.MAX_RETRIES:
152
+ raise
153
+ _tx.rollback()
154
+ except exceptions.DbLockTimeoutError as e:
155
+ lock_timeout_count += 1
156
+ if lock_timeout_count > self.MAX_RETRIES:
157
+ raise
174
158
  _tx.rollback()
175
159
  continue
176
- retry_count += 1
177
- if retry_count > self.MAX_RETRIES:
160
+ except:
178
161
  raise
179
- print("**Retry Transaction. Rollback and start over")
180
- _tx.rollback()
181
- continue
182
- except exceptions.DbLockTimeoutError:
183
- tmout_count += 1
184
- if tmout_count > self.MAX_RETRIES:
185
- raise
186
- print("**DbLockTimeoutError. Rollback and start over")
187
- _tx.rollback()
188
- continue
189
- except:
190
- raise
162
+ finally:
163
+ setattr(_tx, "_exec_function_depth", depth)
164
+ # or if depth was 0, you might delete the attribute:
165
+ # if depth == 0:
166
+ # delattr(_tx, "_exec_function_depth")
191
167
 
192
168
  @property
193
169
  def driver(self):
@@ -203,24 +179,36 @@ class Engine(object):
203
179
 
204
180
  @property
205
181
  def version(self):
182
+ """
183
+ Returns the DB server version.
184
+ """
206
185
  with Transaction(self) as tx:
207
186
  sql, vals = self.sql.version()
208
187
  return tx.execute(sql, vals).scalar()
209
188
 
210
189
  @property
211
190
  def timestamp(self):
191
+ """
192
+ Returns the current timestamp from the DB server.
193
+ """
212
194
  with Transaction(self) as tx:
213
195
  sql, vals = self.sql.timestamp()
214
196
  return tx.execute(sql, vals).scalar()
215
197
 
216
198
  @property
217
199
  def user(self):
200
+ """
201
+ Returns the current user as known by the DB server.
202
+ """
218
203
  with Transaction(self) as tx:
219
204
  sql, vals = self.sql.user()
220
205
  return tx.execute(sql, vals).scalar()
221
206
 
222
207
  @property
223
208
  def databases(self):
209
+ """
210
+ Returns a list of available databases.
211
+ """
224
212
  with Transaction(self) as tx:
225
213
  sql, vals = self.sql.databases()
226
214
  result = tx.execute(sql, vals)
@@ -228,13 +216,19 @@ class Engine(object):
228
216
 
229
217
  @property
230
218
  def current_database(self):
219
+ """
220
+ Returns the name of the current database.
221
+ """
231
222
  with Transaction(self) as tx:
232
223
  sql, vals = self.sql.current_database()
233
224
  return tx.execute(sql, vals).scalar()
234
225
 
235
226
  def create_database(self, name=None):
227
+ """
228
+ Creates a database if it doesn't exist, or does nothing if it does.
229
+ """
236
230
  old = None
237
- if name == None:
231
+ if name is None:
238
232
  old = self.config["database"]
239
233
  self.set_config({"database": "postgres"})
240
234
  name = old
@@ -246,19 +240,27 @@ class Engine(object):
246
240
  return self
247
241
 
248
242
  def switch_to_database(self, database):
243
+ """
244
+ Switch the config to use a different database name, closing any existing connection.
245
+ """
249
246
  conf = self.config
250
247
  if "database" in conf:
251
248
  conf["database"] = database
252
249
  if "dbname" in conf:
253
250
  conf["dbname"] = database
254
-
255
251
  return self
256
252
 
257
253
  def set_config(self, config):
254
+ """
255
+ Updates the internal config dictionary.
256
+ """
258
257
  self.config.update(config)
259
258
 
260
259
  @property
261
260
  def schemas(self):
261
+ """
262
+ Returns a list of schemas in the current database.
263
+ """
262
264
  with Transaction(self) as tx:
263
265
  sql, vals = self.sql.schemas()
264
266
  result = tx.execute(sql, vals)
@@ -266,107 +268,106 @@ class Engine(object):
266
268
 
267
269
  @property
268
270
  def current_schema(self):
271
+ """
272
+ Returns the current schema in use.
273
+ """
269
274
  with Transaction(self) as tx:
270
275
  sql, vals = self.sql.current_schema()
271
276
  return tx.execute(sql, vals).scalar()
272
277
 
273
278
  @property
274
279
  def tables(self):
280
+ """
281
+ Returns a list of 'schema.table' for all tables in the current DB.
282
+ """
275
283
  with Transaction(self) as tx:
276
284
  sql, vals = self.sql.tables()
277
285
  result = tx.execute(sql, vals)
278
- return ["%s.%s" % x for x in result.as_tuple()]
286
+ return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
279
287
 
280
288
  @property
281
289
  def views(self):
290
+ """
291
+ Returns a list of 'schema.view' for all views in the current DB.
292
+ """
282
293
  with Transaction(self) as tx:
283
294
  sql, vals = self.sql.views()
284
295
  result = tx.execute(sql, vals)
285
- return ["%s.%s" % x for x in result.as_tuple()]
296
+ return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
286
297
 
287
- def ProcessError(self, sql_stmt=None, sql_params=None):
288
- sql = self.sql
298
+ def process_error(self, sql_stmt=None, sql_params=None):
299
+ """
300
+ Central method to parse driver exceptions and re-raise them as our custom exceptions.
301
+ """
289
302
  e = sys.exc_info()[1]
290
303
  msg = str(e).strip().lower()
304
+
291
305
  if isinstance(e, exceptions.DbException):
292
306
  raise
293
- if hasattr(e, "pgcode"):
294
- error_code = e.pgcode
295
- error_mesg = e.pgerror
296
- elif (
297
- hasattr(e, "args") and isinstance(e.args, (tuple, list)) and len(e.args) > 1
298
- ):
299
- error_code = e[0]
300
- error_mesg = e[1]
301
- elif hasattr(e, "number") and hasattr(e, "text"):
302
- error_code = e.number
303
- error_mesg = e.text
304
- elif hasattr(e, "args") and hasattr(e, "message"):
305
- # SQLite3
306
- error_code = None
307
- error_mesg = e.message
308
- else:
309
- raise
310
- if error_code in sql.ApplicationErrorCodes:
307
+
308
+ error_code, error_mesg = self.sql.get_error(e)
309
+
310
+ if error_code in self.sql.ApplicationErrorCodes:
311
311
  raise exceptions.DbApplicationError(e)
312
- elif error_code in sql.ColumnMissingErrorCodes:
312
+ if error_code in self.sql.ColumnMissingErrorCodes:
313
313
  raise exceptions.DbColumnMissingError(e)
314
- elif error_code in sql.TableMissingErrorCodes:
314
+ if error_code in self.sql.TableMissingErrorCodes:
315
315
  raise exceptions.DbTableMissingError(e)
316
- elif error_code in sql.DatabaseMissingErrorCodes:
316
+ if error_code in self.sql.DatabaseMissingErrorCodes:
317
317
  raise exceptions.DbDatabaseMissingError(e)
318
- elif error_code in sql.ForeignKeyMissingErrorCodes:
318
+ if error_code in self.sql.ForeignKeyMissingErrorCodes:
319
319
  raise exceptions.DbForeignKeyMissingError(e)
320
- elif error_code in sql.TruncationErrorCodes:
320
+ if error_code in self.sql.TruncationErrorCodes:
321
321
  raise exceptions.DbTruncationError(e)
322
- elif error_code in sql.DataIntegrityErrorCodes:
322
+ if error_code in self.sql.DataIntegrityErrorCodes:
323
323
  raise exceptions.DbDataIntegrityError(e)
324
- elif error_code in sql.ConnectionErrorCodes:
324
+ if error_code in self.sql.ConnectionErrorCodes:
325
325
  raise exceptions.DbConnectionError(e)
326
- elif error_code in sql.DuplicateKeyErrorCodes:
326
+ if error_code in self.sql.DuplicateKeyErrorCodes:
327
327
  raise exceptions.DbDuplicateKeyError(e)
328
- elif re.search("key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
328
+ if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
329
329
  raise exceptions.DbDuplicateKeyError(e)
330
- elif error_code in sql.DatabaseObjectExistsErrorCodes:
330
+ if error_code in self.sql.DatabaseObjectExistsErrorCodes:
331
331
  raise exceptions.DbObjectExistsError(e)
332
- elif error_code in sql.LockTimeoutErrorCodes:
332
+ if error_code in self.sql.LockTimeoutErrorCodes:
333
333
  raise exceptions.DbLockTimeoutError(e)
334
- elif error_code in sql.RetryTransactionCodes:
334
+ if error_code in self.sql.RetryTransactionCodes:
335
335
  raise exceptions.DbRetryTransaction(e)
336
- elif re.findall("database.*does not exist", msg, re.M):
336
+ if re.findall(r"database.*does not exist", msg, re.M):
337
337
  raise exceptions.DbDatabaseMissingError(e)
338
- elif re.findall("no such database", msg, re.M):
338
+ if re.findall(r"no such database", msg, re.M):
339
339
  raise exceptions.DbDatabaseMissingError(e)
340
- elif re.findall("already exists", msg, re.M):
340
+ if re.findall(r"already exists", msg, re.M):
341
341
  raise exceptions.DbObjectExistsError(e)
342
- elif re.findall("server closed the connection unexpectedly", msg, re.M):
342
+ if re.findall(r"server closed the connection unexpectedly", msg, re.M):
343
343
  raise exceptions.DbConnectionError(e)
344
- elif re.findall("no connection to the server", msg, re.M):
344
+ if re.findall(r"no connection to the server", msg, re.M):
345
345
  raise exceptions.DbConnectionError(e)
346
- elif re.findall("connection timed out", msg, re.M):
346
+ if re.findall(r"connection timed out", msg, re.M):
347
347
  raise exceptions.DbConnectionError(e)
348
- elif re.findall("could not connect to server", msg, re.M):
348
+ if re.findall(r"could not connect to server", msg, re.M):
349
349
  raise exceptions.DbConnectionError(e)
350
- elif re.findall("cannot connect to server", msg, re.M):
350
+ if re.findall(r"cannot connect to server", msg, re.M):
351
351
  raise exceptions.DbConnectionError(e)
352
- elif re.findall("connection already closed", msg, re.M):
352
+ if re.findall(r"connection already closed", msg, re.M):
353
353
  raise exceptions.DbConnectionError(e)
354
- elif re.findall("cursor already closed", msg, re.M):
354
+ if re.findall(r"cursor already closed", msg, re.M):
355
355
  raise exceptions.DbConnectionError(e)
356
- # SQLite3 errors
357
- elif "no such table:" in msg:
356
+ if "no such table:" in msg:
358
357
  raise exceptions.DbTableMissingError(e)
359
- print("Unhandled/Unknown Error in connection.ProcessError")
360
- print("EXC_TYPE = {}".format(type(e)))
361
- print("EXC_MSG = {}".format(str(e).strip()))
362
- print("ERROR_CODE = {}".format(error_code))
363
- print("ERROR_MSG = {}".format(error_mesg))
364
- if sql_stmt:
365
- print("\n")
366
- print("sql_stmt [velocity.db.engine]: {}".format(sql_stmt))
367
- print("\n")
368
- if sql_params:
369
- print(sql_params)
370
- print("\n")
371
- traceback.print_exc()
358
+
359
+ msg = f"""
360
+ Unhandled/Unknown Error in engine.process_error
361
+ EXC_TYPE = {type(e)}
362
+ EXC_MSG = {str(e).strip()}
363
+
364
+ ERROR_CODE = {error_code}
365
+ ERROR_MSG = {error_mesg}
366
+
367
+ SQL_STMT = {sql_stmt}
368
+ SQL_PARAMS = {sql_params}
369
+
370
+ {traceback.format_exc()}
371
+ """
372
+ print(msg)
372
373
  raise