velocity-python 0.0.35__py3-none-any.whl → 0.0.64__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.

@@ -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,126 @@ 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
+ if func_or_cls is None:
61
+ return Transaction(self)
62
+
63
+ if isinstance(func_or_cls, classmethod):
64
+ return classmethod(self.transaction(func_or_cls.__func__))
65
+
89
66
  if inspect.isfunction(func_or_cls) or inspect.ismethod(func_or_cls):
90
67
 
91
68
  @wraps(func_or_cls)
92
- def NewFunction(*args, **kwds):
69
+ def new_function(*args, **kwds):
93
70
  tx = None
94
71
  names = list(inspect.signature(func_or_cls).parameters.keys())
95
72
  if "_tx" in names:
96
73
  raise NameError(
97
- f"In function named `{func_or_cls.__name__}` You may not name a paramater `_tx`"
74
+ f"In function {func_or_cls.__name__}, '_tx' is not allowed as a parameter."
98
75
  )
76
+
99
77
  if "tx" not in names:
78
+ # The function doesn't even declare a `tx` parameter, so run normally.
100
79
  return func_or_cls(*args, **kwds)
101
- elif "tx" in kwds:
80
+
81
+ if "tx" in kwds:
102
82
  if isinstance(kwds["tx"], Transaction):
103
83
  tx = kwds["tx"]
104
84
  else:
105
85
  raise TypeError(
106
- f"In function named `{func_or_cls.__name__}` keyword `tx` must be a Transaction object"
86
+ f"In function {func_or_cls.__name__}, keyword argument `tx` must be a Transaction object."
107
87
  )
108
- elif "tx" in names:
88
+ else:
89
+ # Might be in positional args
109
90
  pos = names.index("tx")
110
- if len(args) > pos and isinstance(args[pos], Transaction):
111
- tx = args[pos]
91
+ if len(args) > pos:
92
+ if isinstance(args[pos], Transaction):
93
+ tx = args[pos]
94
+ else:
95
+ raise TypeError(
96
+ f"In function {func_or_cls.__name__}, positional argument `tx` must be a Transaction object."
97
+ )
112
98
  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)
99
+ return self.exec_function(func_or_cls, tx, *args, **kwds)
100
+ with Transaction(self) as local_tx:
101
+ pos = names.index("tx")
102
+ args = list(args)
103
+ args.insert(pos, local_tx)
104
+ args = tuple(args)
105
+ return self.exec_function(func_or_cls, local_tx, *args, **kwds)
121
106
 
122
- return NewFunction
123
- elif inspect.isclass(func_or_cls):
107
+ return new_function
124
108
 
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
109
+ if inspect.isclass(func_or_cls):
133
110
 
111
+ class NewCls(func_or_cls):
112
+ pass
113
+
114
+ for attr_name, attr_value in func_or_cls.__dict__.items():
115
+ if (
116
+ isinstance(attr_value, classmethod)
117
+ or inspect.isfunction(attr_value)
118
+ or inspect.ismethod(attr_value)
119
+ ):
120
+ setattr(NewCls, attr_name, self.transaction(attr_value))
134
121
  return NewCls
135
122
 
136
- return Transaction(engine)
123
+ return Transaction(self)
137
124
 
138
125
  def exec_function(self, function, _tx, *args, **kwds):
139
126
  """
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")
127
+ Executes the given function inside the transaction `_tx`.
128
+ Retries if it raises DbRetryTransaction or DbLockTimeoutError, up to MAX_RETRIES times.
129
+ """
130
+ depth = getattr(_tx, "_exec_function_depth", 0)
131
+ setattr(_tx, "_exec_function_depth", depth + 1)
132
+
133
+ try:
134
+ if depth > 0:
135
+ # Not top-level. Just call the function.
136
+ return function(*args, **kwds)
137
+ else:
138
+ retry_count = 0
139
+ lock_timeout_count = 0
140
+ while True:
141
+ try:
142
+ return function(*args, **kwds)
143
+ except exceptions.DbRetryTransaction as e:
144
+ retry_count += 1
145
+ if retry_count > self.MAX_RETRIES:
146
+ raise
147
+ _tx.rollback()
148
+ except exceptions.DbLockTimeoutError as e:
149
+ lock_timeout_count += 1
150
+ if lock_timeout_count > self.MAX_RETRIES:
151
+ raise
174
152
  _tx.rollback()
175
153
  continue
176
- retry_count += 1
177
- if retry_count > self.MAX_RETRIES:
154
+ except:
178
155
  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
156
+ finally:
157
+ setattr(_tx, "_exec_function_depth", depth)
158
+ # or if depth was 0, you might delete the attribute:
159
+ # if depth == 0:
160
+ # delattr(_tx, "_exec_function_depth")
191
161
 
192
162
  @property
193
163
  def driver(self):
@@ -203,24 +173,36 @@ class Engine(object):
203
173
 
204
174
  @property
205
175
  def version(self):
176
+ """
177
+ Returns the DB server version.
178
+ """
206
179
  with Transaction(self) as tx:
207
180
  sql, vals = self.sql.version()
208
181
  return tx.execute(sql, vals).scalar()
209
182
 
210
183
  @property
211
184
  def timestamp(self):
185
+ """
186
+ Returns the current timestamp from the DB server.
187
+ """
212
188
  with Transaction(self) as tx:
213
189
  sql, vals = self.sql.timestamp()
214
190
  return tx.execute(sql, vals).scalar()
215
191
 
216
192
  @property
217
193
  def user(self):
194
+ """
195
+ Returns the current user as known by the DB server.
196
+ """
218
197
  with Transaction(self) as tx:
219
198
  sql, vals = self.sql.user()
220
199
  return tx.execute(sql, vals).scalar()
221
200
 
222
201
  @property
223
202
  def databases(self):
203
+ """
204
+ Returns a list of available databases.
205
+ """
224
206
  with Transaction(self) as tx:
225
207
  sql, vals = self.sql.databases()
226
208
  result = tx.execute(sql, vals)
@@ -228,13 +210,19 @@ class Engine(object):
228
210
 
229
211
  @property
230
212
  def current_database(self):
213
+ """
214
+ Returns the name of the current database.
215
+ """
231
216
  with Transaction(self) as tx:
232
217
  sql, vals = self.sql.current_database()
233
218
  return tx.execute(sql, vals).scalar()
234
219
 
235
220
  def create_database(self, name=None):
221
+ """
222
+ Creates a database if it doesn't exist, or does nothing if it does.
223
+ """
236
224
  old = None
237
- if name == None:
225
+ if name is None:
238
226
  old = self.config["database"]
239
227
  self.set_config({"database": "postgres"})
240
228
  name = old
@@ -246,19 +234,27 @@ class Engine(object):
246
234
  return self
247
235
 
248
236
  def switch_to_database(self, database):
237
+ """
238
+ Switch the config to use a different database name, closing any existing connection.
239
+ """
249
240
  conf = self.config
250
241
  if "database" in conf:
251
242
  conf["database"] = database
252
243
  if "dbname" in conf:
253
244
  conf["dbname"] = database
254
-
255
245
  return self
256
246
 
257
247
  def set_config(self, config):
248
+ """
249
+ Updates the internal config dictionary.
250
+ """
258
251
  self.config.update(config)
259
252
 
260
253
  @property
261
254
  def schemas(self):
255
+ """
256
+ Returns a list of schemas in the current database.
257
+ """
262
258
  with Transaction(self) as tx:
263
259
  sql, vals = self.sql.schemas()
264
260
  result = tx.execute(sql, vals)
@@ -266,107 +262,106 @@ class Engine(object):
266
262
 
267
263
  @property
268
264
  def current_schema(self):
265
+ """
266
+ Returns the current schema in use.
267
+ """
269
268
  with Transaction(self) as tx:
270
269
  sql, vals = self.sql.current_schema()
271
270
  return tx.execute(sql, vals).scalar()
272
271
 
273
272
  @property
274
273
  def tables(self):
274
+ """
275
+ Returns a list of 'schema.table' for all tables in the current DB.
276
+ """
275
277
  with Transaction(self) as tx:
276
278
  sql, vals = self.sql.tables()
277
279
  result = tx.execute(sql, vals)
278
- return ["%s.%s" % x for x in result.as_tuple()]
280
+ return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
279
281
 
280
282
  @property
281
283
  def views(self):
284
+ """
285
+ Returns a list of 'schema.view' for all views in the current DB.
286
+ """
282
287
  with Transaction(self) as tx:
283
288
  sql, vals = self.sql.views()
284
289
  result = tx.execute(sql, vals)
285
- return ["%s.%s" % x for x in result.as_tuple()]
290
+ return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
286
291
 
287
- def ProcessError(self, sql_stmt=None, sql_params=None):
288
- sql = self.sql
292
+ def process_error(self, sql_stmt=None, sql_params=None):
293
+ """
294
+ Central method to parse driver exceptions and re-raise them as our custom exceptions.
295
+ """
289
296
  e = sys.exc_info()[1]
290
297
  msg = str(e).strip().lower()
298
+
291
299
  if isinstance(e, exceptions.DbException):
292
300
  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:
301
+
302
+ error_code, error_mesg = self.sql.get_error(e)
303
+
304
+ if error_code in self.sql.ApplicationErrorCodes:
311
305
  raise exceptions.DbApplicationError(e)
312
- elif error_code in sql.ColumnMissingErrorCodes:
306
+ if error_code in self.sql.ColumnMissingErrorCodes:
313
307
  raise exceptions.DbColumnMissingError(e)
314
- elif error_code in sql.TableMissingErrorCodes:
308
+ if error_code in self.sql.TableMissingErrorCodes:
315
309
  raise exceptions.DbTableMissingError(e)
316
- elif error_code in sql.DatabaseMissingErrorCodes:
310
+ if error_code in self.sql.DatabaseMissingErrorCodes:
317
311
  raise exceptions.DbDatabaseMissingError(e)
318
- elif error_code in sql.ForeignKeyMissingErrorCodes:
312
+ if error_code in self.sql.ForeignKeyMissingErrorCodes:
319
313
  raise exceptions.DbForeignKeyMissingError(e)
320
- elif error_code in sql.TruncationErrorCodes:
314
+ if error_code in self.sql.TruncationErrorCodes:
321
315
  raise exceptions.DbTruncationError(e)
322
- elif error_code in sql.DataIntegrityErrorCodes:
316
+ if error_code in self.sql.DataIntegrityErrorCodes:
323
317
  raise exceptions.DbDataIntegrityError(e)
324
- elif error_code in sql.ConnectionErrorCodes:
318
+ if error_code in self.sql.ConnectionErrorCodes:
325
319
  raise exceptions.DbConnectionError(e)
326
- elif error_code in sql.DuplicateKeyErrorCodes:
320
+ if error_code in self.sql.DuplicateKeyErrorCodes:
327
321
  raise exceptions.DbDuplicateKeyError(e)
328
- elif re.search("key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
322
+ if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
329
323
  raise exceptions.DbDuplicateKeyError(e)
330
- elif error_code in sql.DatabaseObjectExistsErrorCodes:
324
+ if error_code in self.sql.DatabaseObjectExistsErrorCodes:
331
325
  raise exceptions.DbObjectExistsError(e)
332
- elif error_code in sql.LockTimeoutErrorCodes:
326
+ if error_code in self.sql.LockTimeoutErrorCodes:
333
327
  raise exceptions.DbLockTimeoutError(e)
334
- elif error_code in sql.RetryTransactionCodes:
328
+ if error_code in self.sql.RetryTransactionCodes:
335
329
  raise exceptions.DbRetryTransaction(e)
336
- elif re.findall("database.*does not exist", msg, re.M):
330
+ if re.findall(r"database.*does not exist", msg, re.M):
337
331
  raise exceptions.DbDatabaseMissingError(e)
338
- elif re.findall("no such database", msg, re.M):
332
+ if re.findall(r"no such database", msg, re.M):
339
333
  raise exceptions.DbDatabaseMissingError(e)
340
- elif re.findall("already exists", msg, re.M):
334
+ if re.findall(r"already exists", msg, re.M):
341
335
  raise exceptions.DbObjectExistsError(e)
342
- elif re.findall("server closed the connection unexpectedly", msg, re.M):
336
+ if re.findall(r"server closed the connection unexpectedly", msg, re.M):
343
337
  raise exceptions.DbConnectionError(e)
344
- elif re.findall("no connection to the server", msg, re.M):
338
+ if re.findall(r"no connection to the server", msg, re.M):
345
339
  raise exceptions.DbConnectionError(e)
346
- elif re.findall("connection timed out", msg, re.M):
340
+ if re.findall(r"connection timed out", msg, re.M):
347
341
  raise exceptions.DbConnectionError(e)
348
- elif re.findall("could not connect to server", msg, re.M):
342
+ if re.findall(r"could not connect to server", msg, re.M):
349
343
  raise exceptions.DbConnectionError(e)
350
- elif re.findall("cannot connect to server", msg, re.M):
344
+ if re.findall(r"cannot connect to server", msg, re.M):
351
345
  raise exceptions.DbConnectionError(e)
352
- elif re.findall("connection already closed", msg, re.M):
346
+ if re.findall(r"connection already closed", msg, re.M):
353
347
  raise exceptions.DbConnectionError(e)
354
- elif re.findall("cursor already closed", msg, re.M):
348
+ if re.findall(r"cursor already closed", msg, re.M):
355
349
  raise exceptions.DbConnectionError(e)
356
- # SQLite3 errors
357
- elif "no such table:" in msg:
350
+ if "no such table:" in msg:
358
351
  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()
352
+
353
+ msg = f"""
354
+ Unhandled/Unknown Error in engine.process_error
355
+ EXC_TYPE = {type(e)}
356
+ EXC_MSG = {str(e).strip()}
357
+
358
+ ERROR_CODE = {error_code}
359
+ ERROR_MSG = {error_mesg}
360
+
361
+ SQL_STMT = {sql_stmt}
362
+ SQL_PARAMS = {sql_params}
363
+
364
+ {traceback.format_exc()}
365
+ """
366
+ print(msg)
372
367
  raise