velocity-python 0.0.110__tar.gz → 0.0.113__tar.gz

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.

Files changed (83) hide show
  1. {velocity_python-0.0.110/src/velocity_python.egg-info → velocity_python-0.0.113}/PKG-INFO +1 -1
  2. {velocity_python-0.0.110 → velocity_python-0.0.113}/pyproject.toml +1 -1
  3. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/aws/handlers/lambda_handler.py +1 -1
  5. velocity_python-0.0.113/src/velocity/db/core/engine.py +481 -0
  6. {velocity_python-0.0.110 → velocity_python-0.0.113/src/velocity_python.egg-info}/PKG-INFO +1 -1
  7. velocity_python-0.0.110/src/velocity/db/core/engine.py +0 -1091
  8. {velocity_python-0.0.110 → velocity_python-0.0.113}/LICENSE +0 -0
  9. {velocity_python-0.0.110 → velocity_python-0.0.113}/README.md +0 -0
  10. {velocity_python-0.0.110 → velocity_python-0.0.113}/setup.cfg +0 -0
  11. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/app/__init__.py +0 -0
  12. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/app/invoices.py +0 -0
  13. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/app/orders.py +0 -0
  14. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/app/payments.py +0 -0
  15. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/app/purchase_orders.py +0 -0
  16. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/aws/__init__.py +0 -0
  17. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/aws/amplify.py +0 -0
  18. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/aws/handlers/__init__.py +0 -0
  19. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/aws/handlers/context.py +0 -0
  20. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/aws/handlers/response.py +0 -0
  21. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  22. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/__init__.py +0 -0
  23. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/__init__.py +0 -0
  24. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/column.py +0 -0
  25. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/database.py +0 -0
  26. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/decorators.py +0 -0
  27. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/exceptions.py +0 -0
  28. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/result.py +0 -0
  29. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/row.py +0 -0
  30. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/sequence.py +0 -0
  31. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/table.py +0 -0
  32. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/core/transaction.py +0 -0
  33. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/exceptions.py +0 -0
  34. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/__init__.py +0 -0
  35. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/mysql.py +0 -0
  36. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/mysql_reserved.py +0 -0
  37. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/postgres/__init__.py +0 -0
  38. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/postgres/operators.py +0 -0
  39. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/postgres/reserved.py +0 -0
  40. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/postgres/sql.py +0 -0
  41. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/postgres/types.py +0 -0
  42. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/sqlite.py +0 -0
  43. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/sqlite_reserved.py +0 -0
  44. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/sqlserver.py +0 -0
  45. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
  46. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/servers/tablehelper.py +0 -0
  47. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/db/utils.py +0 -0
  48. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/__init__.py +0 -0
  49. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/conv/__init__.py +0 -0
  50. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/conv/iconv.py +0 -0
  51. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/conv/oconv.py +0 -0
  52. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/db.py +0 -0
  53. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/export.py +0 -0
  54. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/format.py +0 -0
  55. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/mail.py +0 -0
  56. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/merge.py +0 -0
  57. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/timer.py +0 -0
  58. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity/misc/tools.py +0 -0
  59. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  60. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  61. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity_python.egg-info/requires.txt +0 -0
  62. {velocity_python-0.0.110 → velocity_python-0.0.113}/src/velocity_python.egg-info/top_level.txt +0 -0
  63. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_cursor_rowcount_fix.py +0 -0
  64. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_db.py +0 -0
  65. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_db_utils.py +0 -0
  66. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_email_processing.py +0 -0
  67. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_fix.py +0 -0
  68. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_format.py +0 -0
  69. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_iconv.py +0 -0
  70. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_merge.py +0 -0
  71. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_oconv.py +0 -0
  72. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_original_error.py +0 -0
  73. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_payment_profile_sorting.py +0 -0
  74. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_postgres.py +0 -0
  75. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_process_error_robustness.py +0 -0
  76. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_response.py +0 -0
  77. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_result_caching.py +0 -0
  78. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_result_sql_aware.py +0 -0
  79. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_row_get_missing_column.py +0 -0
  80. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_spreadsheet_functions.py +0 -0
  81. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_sql_builder.py +0 -0
  82. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_tablehelper.py +0 -0
  83. {velocity_python-0.0.110 → velocity_python-0.0.113}/tests/test_timer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.110
3
+ Version: 0.0.113
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.110"
7
+ version = "0.0.113"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.110"
1
+ __version__ = version = "0.0.113"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -166,4 +166,4 @@ class LambdaHandler:
166
166
  self.track(tx, context.payload().get("data", {}))
167
167
 
168
168
  def enqueue(self, tx, action, payload={}):
169
- enqueue(tx, action, payload, self.session["email_address"])
169
+ return enqueue(tx, action, payload, self.session["email_address"])
@@ -0,0 +1,481 @@
1
+ import inspect
2
+ import sys
3
+ import re
4
+ import traceback
5
+ from functools import wraps
6
+ from velocity.db import exceptions
7
+ from velocity.db.core.transaction import Transaction
8
+
9
+ import logging
10
+
11
+ logger = logging.getLogger("velocity.db.engine")
12
+ logger.setLevel(logging.INFO) # Or DEBUG for more verbosity
13
+
14
+
15
+ class Engine:
16
+ """
17
+ Encapsulates driver config, connection logic, error handling, and transaction decoration.
18
+ """
19
+
20
+ MAX_RETRIES = 100
21
+
22
+ def __init__(self, driver, config, sql, connect_timeout=5):
23
+ self.__config = config
24
+ self.__sql = sql
25
+ self.__driver = driver
26
+ self.__connect_timeout = connect_timeout
27
+
28
+ def __str__(self):
29
+ return f"[{self.sql.server}] engine({self.config})"
30
+
31
+ def connect(self):
32
+ """
33
+ Connects to the database and returns the connection object.
34
+ If the database is missing, tries to create it, then reconnect.
35
+ """
36
+ try:
37
+ conn = self.__connect()
38
+ except exceptions.DbDatabaseMissingError:
39
+ self.create_database()
40
+ conn = self.__connect()
41
+ if self.sql.server == "SQLite3":
42
+ conn.isolation_level = None
43
+ return conn
44
+
45
+ def __connect(self):
46
+ """
47
+ Internal connection logic, raising suitable exceptions on error.
48
+ Enforces a connect timeout and handles different config types.
49
+ """
50
+ server = self.sql.server.lower()
51
+ timeout_key = "timeout" if "sqlite" in server else "connect_timeout"
52
+ timeout_val = self.__connect_timeout
53
+
54
+ try:
55
+ if isinstance(self.config, dict):
56
+ config = self.config.copy()
57
+ if timeout_key not in config:
58
+ config[timeout_key] = timeout_val
59
+ return self.driver.connect(**config)
60
+
61
+ elif isinstance(self.config, str):
62
+ conn_str = self.config
63
+ if timeout_key not in conn_str:
64
+ conn_str += f" {timeout_key}={timeout_val}"
65
+ return self.driver.connect(conn_str)
66
+
67
+ elif isinstance(self.config, (tuple, list)):
68
+ config_args = list(self.config)
69
+ if config_args and isinstance(config_args[-1], dict):
70
+ if timeout_key not in config_args[-1]:
71
+ config_args[-1][timeout_key] = timeout_val
72
+ else:
73
+ config_args.append({timeout_key: timeout_val})
74
+ return self.driver.connect(*config_args)
75
+
76
+ else:
77
+ raise TypeError(
78
+ f"Unhandled configuration parameter type: {type(self.config)}"
79
+ )
80
+
81
+ except Exception as e:
82
+ raise self.process_error(e)
83
+
84
+ def transaction(self, func_or_cls=None):
85
+ """
86
+ Decorator that provides a Transaction. If `tx` is passed in, uses it; otherwise, creates a new one.
87
+ May also be used to decorate a class, in which case all methods are wrapped in a transaction if they accept `tx`.
88
+ With no arguments, returns a new Transaction directly.
89
+ """
90
+ # print("Transaction", func_or_cls.__name__, type(func_or_cls))
91
+
92
+ if func_or_cls is None:
93
+ return Transaction(self)
94
+
95
+ if isinstance(func_or_cls, classmethod):
96
+ return classmethod(self.transaction(func_or_cls.__func__))
97
+
98
+ if inspect.isfunction(func_or_cls) or inspect.ismethod(func_or_cls):
99
+ names = list(inspect.signature(func_or_cls).parameters.keys())
100
+ # print(func_or_cls.__name__, names)
101
+ if "_tx" in names:
102
+ raise NameError(
103
+ f"In function {func_or_cls.__name__}, '_tx' is not allowed as a parameter."
104
+ )
105
+
106
+ @wraps(func_or_cls)
107
+ def new_function(*args, **kwds):
108
+ tx = None
109
+ names = list(inspect.signature(func_or_cls).parameters.keys())
110
+
111
+ # print("inside", func_or_cls.__name__)
112
+ # print(names)
113
+ # print(args, kwds)
114
+
115
+ if "tx" not in names:
116
+ # The function doesn't even declare a `tx` parameter, so run normally.
117
+ return func_or_cls(*args, **kwds)
118
+
119
+ if "tx" in kwds:
120
+ if isinstance(kwds["tx"], Transaction):
121
+ tx = kwds["tx"]
122
+ else:
123
+ raise TypeError(
124
+ f"In function {func_or_cls.__name__}, keyword argument `tx` must be a Transaction object."
125
+ )
126
+ else:
127
+ # Might be in positional args
128
+ pos = names.index("tx")
129
+ if len(args) > pos:
130
+ if isinstance(args[pos], Transaction):
131
+ tx = args[pos]
132
+
133
+ if tx:
134
+ return self.exec_function(func_or_cls, tx, *args, **kwds)
135
+
136
+ with Transaction(self) as local_tx:
137
+ pos = names.index("tx")
138
+ new_args = args[:pos] + (local_tx,) + args[pos:]
139
+ return self.exec_function(func_or_cls, local_tx, *new_args, **kwds)
140
+
141
+ return new_function
142
+
143
+ if inspect.isclass(func_or_cls):
144
+
145
+ NewCls = type(func_or_cls.__name__, (func_or_cls,), {})
146
+
147
+ for attr_name in dir(func_or_cls):
148
+ # Optionally skip special methods
149
+ if attr_name.startswith("__") and attr_name.endswith("__"):
150
+ continue
151
+
152
+ attr = getattr(func_or_cls, attr_name)
153
+
154
+ if callable(attr):
155
+ setattr(NewCls, attr_name, self.transaction(attr))
156
+
157
+ return NewCls
158
+
159
+ return Transaction(self)
160
+
161
+ def exec_function(self, function, _tx, *args, **kwds):
162
+ """
163
+ Executes the given function inside the transaction `_tx`.
164
+ Retries if it raises DbRetryTransaction or DbLockTimeoutError, up to MAX_RETRIES times.
165
+ """
166
+ depth = getattr(_tx, "_exec_function_depth", 0)
167
+ setattr(_tx, "_exec_function_depth", depth + 1)
168
+
169
+ try:
170
+ if depth > 0:
171
+ # Not top-level. Just call the function.
172
+ return function(*args, **kwds)
173
+ else:
174
+ retry_count = 0
175
+ lock_timeout_count = 0
176
+ while True:
177
+ try:
178
+ return function(*args, **kwds)
179
+ except exceptions.DbRetryTransaction as e:
180
+ retry_count += 1
181
+ if retry_count > self.MAX_RETRIES:
182
+ raise
183
+ _tx.rollback()
184
+ except exceptions.DbLockTimeoutError as e:
185
+ lock_timeout_count += 1
186
+ if lock_timeout_count > self.MAX_RETRIES:
187
+ raise
188
+ _tx.rollback()
189
+ continue
190
+ except:
191
+ raise
192
+ finally:
193
+ setattr(_tx, "_exec_function_depth", depth)
194
+ # or if depth was 0, you might delete the attribute:
195
+ # if depth == 0:
196
+ # delattr(_tx, "_exec_function_depth")
197
+
198
+ @property
199
+ def driver(self):
200
+ return self.__driver
201
+
202
+ @property
203
+ def config(self):
204
+ return self.__config
205
+
206
+ @property
207
+ def sql(self):
208
+ return self.__sql
209
+
210
+ @property
211
+ def version(self):
212
+ """
213
+ Returns the DB server version.
214
+ """
215
+ with Transaction(self) as tx:
216
+ sql, vals = self.sql.version()
217
+ return tx.execute(sql, vals).scalar()
218
+
219
+ @property
220
+ def timestamp(self):
221
+ """
222
+ Returns the current timestamp from the DB server.
223
+ """
224
+ with Transaction(self) as tx:
225
+ sql, vals = self.sql.timestamp()
226
+ return tx.execute(sql, vals).scalar()
227
+
228
+ @property
229
+ def user(self):
230
+ """
231
+ Returns the current user as known by the DB server.
232
+ """
233
+ with Transaction(self) as tx:
234
+ sql, vals = self.sql.user()
235
+ return tx.execute(sql, vals).scalar()
236
+
237
+ @property
238
+ def databases(self):
239
+ """
240
+ Returns a list of available databases.
241
+ """
242
+ with Transaction(self) as tx:
243
+ sql, vals = self.sql.databases()
244
+ result = tx.execute(sql, vals)
245
+ return [x[0] for x in result.as_tuple()]
246
+
247
+ @property
248
+ def current_database(self):
249
+ """
250
+ Returns the name of the current database.
251
+ """
252
+ with Transaction(self) as tx:
253
+ sql, vals = self.sql.current_database()
254
+ return tx.execute(sql, vals).scalar()
255
+
256
+ def create_database(self, name=None):
257
+ """
258
+ Creates a database if it doesn't exist, or does nothing if it does.
259
+ """
260
+ old = None
261
+ if name is None:
262
+ old = self.config["database"]
263
+ self.set_config({"database": "postgres"})
264
+ name = old
265
+ with Transaction(self) as tx:
266
+ sql, vals = self.sql.create_database(name)
267
+ tx.execute(sql, vals, single=True)
268
+ if old:
269
+ self.set_config({"database": old})
270
+ return self
271
+
272
+ def switch_to_database(self, database):
273
+ """
274
+ Switch the config to use a different database name, closing any existing connection.
275
+ """
276
+ conf = self.config
277
+ if "database" in conf:
278
+ conf["database"] = database
279
+ if "dbname" in conf:
280
+ conf["dbname"] = database
281
+ return self
282
+
283
+ def set_config(self, config):
284
+ """
285
+ Updates the internal config dictionary.
286
+ """
287
+ self.config.update(config)
288
+
289
+ @property
290
+ def schemas(self):
291
+ """
292
+ Returns a list of schemas in the current database.
293
+ """
294
+ with Transaction(self) as tx:
295
+ sql, vals = self.sql.schemas()
296
+ result = tx.execute(sql, vals)
297
+ return [x[0] for x in result.as_tuple()]
298
+
299
+ @property
300
+ def current_schema(self):
301
+ """
302
+ Returns the current schema in use.
303
+ """
304
+ with Transaction(self) as tx:
305
+ sql, vals = self.sql.current_schema()
306
+ return tx.execute(sql, vals).scalar()
307
+
308
+ @property
309
+ def tables(self):
310
+ """
311
+ Returns a list of 'schema.table' for all tables in the current DB.
312
+ """
313
+ with Transaction(self) as tx:
314
+ sql, vals = self.sql.tables()
315
+ result = tx.execute(sql, vals)
316
+ return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
317
+
318
+ @property
319
+ def views(self):
320
+ """
321
+ Returns a list of 'schema.view' for all views in the current DB.
322
+ """
323
+ with Transaction(self) as tx:
324
+ sql, vals = self.sql.views()
325
+ result = tx.execute(sql, vals)
326
+ return [f"{x[0]}.{x[1]}" for x in result.as_tuple()]
327
+
328
+ def process_error(self, exception, sql=None, parameters=None):
329
+ """
330
+ Central method to parse driver exceptions and re-raise them as our custom exceptions.
331
+ """
332
+ logger = logging.getLogger(__name__)
333
+
334
+ # If it's already a velocity exception, just re-raise it
335
+ if isinstance(exception, exceptions.DbException):
336
+ raise exception
337
+
338
+ # Get error code and message from the SQL driver
339
+ try:
340
+ error_code, error_message = self.sql.get_error(exception)
341
+ except Exception:
342
+ error_code, error_message = None, str(exception)
343
+
344
+ msg = str(exception).strip().lower()
345
+
346
+ # Create enhanced error message with SQL query
347
+ enhanced_message = str(exception)
348
+ if sql:
349
+ enhanced_message += f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
350
+
351
+ logger.warning(
352
+ "Database error caught. Attempting to transform: code=%s message=%s",
353
+ error_code,
354
+ error_message,
355
+ )
356
+
357
+ # Direct error code mapping
358
+ if error_code in self.sql.ApplicationErrorCodes:
359
+ raise exceptions.DbApplicationError(enhanced_message) from exception
360
+ if error_code in self.sql.ColumnMissingErrorCodes:
361
+ raise exceptions.DbColumnMissingError(enhanced_message) from exception
362
+ if error_code in self.sql.TableMissingErrorCodes:
363
+ raise exceptions.DbTableMissingError(enhanced_message) from exception
364
+ if error_code in self.sql.DatabaseMissingErrorCodes:
365
+ raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
366
+ if error_code in self.sql.ForeignKeyMissingErrorCodes:
367
+ raise exceptions.DbForeignKeyMissingError(enhanced_message) from exception
368
+ if error_code in self.sql.TruncationErrorCodes:
369
+ raise exceptions.DbTruncationError(enhanced_message) from exception
370
+ if error_code in self.sql.DataIntegrityErrorCodes:
371
+ raise exceptions.DbDataIntegrityError(enhanced_message) from exception
372
+ if error_code in self.sql.ConnectionErrorCodes:
373
+ raise exceptions.DbConnectionError(enhanced_message) from exception
374
+ if error_code in self.sql.DuplicateKeyErrorCodes:
375
+ raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
376
+ if error_code in self.sql.DatabaseObjectExistsErrorCodes:
377
+ raise exceptions.DbObjectExistsError(enhanced_message) from exception
378
+ if error_code in self.sql.LockTimeoutErrorCodes:
379
+ raise exceptions.DbLockTimeoutError(enhanced_message) from exception
380
+ if error_code in self.sql.RetryTransactionCodes:
381
+ raise exceptions.DbRetryTransaction(enhanced_message) from exception
382
+
383
+ # Regex-based fallback patterns
384
+ if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
385
+ raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
386
+ if re.findall(r"database.*does not exist", msg, re.M):
387
+ raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
388
+ if re.findall(r"no such database", msg, re.M):
389
+ raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
390
+ if re.findall(r"already exists", msg, re.M):
391
+ raise exceptions.DbObjectExistsError(enhanced_message) from exception
392
+ if re.findall(r"server closed the connection unexpectedly", msg, re.M):
393
+ raise exceptions.DbConnectionError(enhanced_message) from exception
394
+ if re.findall(r"no connection to the server", msg, re.M):
395
+ raise exceptions.DbConnectionError(enhanced_message) from exception
396
+ if re.findall(r"connection timed out", msg, re.M):
397
+ raise exceptions.DbConnectionError(enhanced_message) from exception
398
+ if re.findall(r"could not connect to server", msg, re.M):
399
+ raise exceptions.DbConnectionError(enhanced_message) from exception
400
+ if re.findall(r"cannot connect to server", msg, re.M):
401
+ raise exceptions.DbConnectionError(enhanced_message) from exception
402
+ if re.findall(r"connection already closed", msg, re.M):
403
+ raise exceptions.DbConnectionError(enhanced_message) from exception
404
+ if re.findall(r"cursor already closed", msg, re.M):
405
+ raise exceptions.DbConnectionError(enhanced_message) from exception
406
+ if "no such table:" in msg:
407
+ raise exceptions.DbTableMissingError(enhanced_message) from exception
408
+
409
+ logger.error(
410
+ "Unhandled/Unknown Error in engine.process_error",
411
+ exc_info=True,
412
+ extra={
413
+ "error_code": error_code,
414
+ "error_msg": error_message,
415
+ "sql_stmt": sql,
416
+ "sql_params": parameters,
417
+ },
418
+ )
419
+
420
+ # If we can't classify it, re-raise with enhanced message
421
+ raise type(exception)(enhanced_message) from exception
422
+
423
+ def _format_sql_with_params(self, sql, parameters):
424
+ """
425
+ Format SQL query with parameters merged for easy copy-paste debugging.
426
+ """
427
+ if not sql:
428
+ return "No SQL provided"
429
+
430
+ if not parameters:
431
+ return sql
432
+
433
+ try:
434
+ # Handle different parameter formats
435
+ if isinstance(parameters, (list, tuple)):
436
+ # Convert parameters to strings and handle None values
437
+ formatted_params = []
438
+ for param in parameters:
439
+ if param is None:
440
+ formatted_params.append('NULL')
441
+ elif isinstance(param, str):
442
+ # Escape single quotes and wrap in quotes
443
+ escaped = param.replace("'", "''")
444
+ formatted_params.append(f"'{escaped}'")
445
+ elif isinstance(param, bool):
446
+ formatted_params.append('TRUE' if param else 'FALSE')
447
+ else:
448
+ formatted_params.append(str(param))
449
+
450
+ # Replace %s placeholders with actual values
451
+ formatted_sql = sql
452
+ for param in formatted_params:
453
+ formatted_sql = formatted_sql.replace('%s', param, 1)
454
+
455
+ return formatted_sql
456
+
457
+ elif isinstance(parameters, dict):
458
+ # Handle named parameters
459
+ formatted_sql = sql
460
+ for key, value in parameters.items():
461
+ if value is None:
462
+ replacement = 'NULL'
463
+ elif isinstance(value, str):
464
+ escaped = value.replace("'", "''")
465
+ replacement = f"'{escaped}'"
466
+ elif isinstance(value, bool):
467
+ replacement = 'TRUE' if value else 'FALSE'
468
+ else:
469
+ replacement = str(value)
470
+
471
+ # Replace %(key)s or :key patterns
472
+ formatted_sql = formatted_sql.replace(f'%({key})s', replacement)
473
+ formatted_sql = formatted_sql.replace(f':{key}', replacement)
474
+
475
+ return formatted_sql
476
+ else:
477
+ return f"{sql}\n-- Parameters: {parameters}"
478
+
479
+ except Exception as e:
480
+ # If formatting fails, return original SQL with parameters shown separately
481
+ return f"{sql}\n-- Parameters (formatting failed): {parameters}\n-- Error: {e}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.110
3
+ Version: 0.0.113
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT