velocity-python 0.0.131__tar.gz → 0.0.152__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.
- {velocity_python-0.0.131 → velocity_python-0.0.152}/PKG-INFO +2 -2
- {velocity_python-0.0.131 → velocity_python-0.0.152}/README.md +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152}/pyproject.toml +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/__init__.py +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/orders.py +1 -3
- velocity_python-0.0.152/src/velocity/app/tests/__init__.py +1 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/activity_tracker.py +53 -14
- velocity_python-0.0.152/src/velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity_python-0.0.152/src/velocity/aws/tests/__init__.py +1 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/decorators.py +20 -3
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/engine.py +56 -8
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/row.py +12 -2
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/table.py +178 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/transaction.py +6 -10
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/exceptions.py +7 -0
- velocity_python-0.0.152/src/velocity/db/servers/base/__init__.py +9 -0
- velocity_python-0.0.152/src/velocity/db/servers/base/initializer.py +70 -0
- velocity_python-0.0.152/src/velocity/db/servers/base/operators.py +98 -0
- velocity_python-0.0.152/src/velocity/db/servers/base/sql.py +503 -0
- velocity_python-0.0.152/src/velocity/db/servers/base/types.py +135 -0
- velocity_python-0.0.152/src/velocity/db/servers/mysql/__init__.py +73 -0
- velocity_python-0.0.152/src/velocity/db/servers/mysql/operators.py +54 -0
- velocity_python-0.0.131/src/velocity/db/servers/mysql_reserved.py → velocity_python-0.0.152/src/velocity/db/servers/mysql/reserved.py +2 -14
- velocity_python-0.0.152/src/velocity/db/servers/mysql/sql.py +696 -0
- {velocity_python-0.0.131/src/velocity/db/servers/postgres → velocity_python-0.0.152/src/velocity/db/servers/mysql}/types.py +31 -33
- velocity_python-0.0.152/src/velocity/db/servers/postgres/__init__.py +67 -0
- velocity_python-0.0.152/src/velocity/db/servers/postgres/operators.py +57 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/postgres/sql.py +356 -95
- velocity_python-0.0.152/src/velocity/db/servers/postgres/types.py +195 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlite/__init__.py +61 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlite/operators.py +52 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlite/reserved.py +20 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlite/sql.py +653 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlite/types.py +92 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlserver/operators.py +47 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlserver/sql.py +786 -0
- velocity_python-0.0.152/src/velocity/db/servers/sqlserver/types.py +114 -0
- velocity_python-0.0.152/src/velocity/db/tests/__init__.py +1 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/__init__.py +1 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/common.py +49 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_column.py +29 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_connections.py +25 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_database.py +21 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_engine.py +205 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_imports.py +8 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_result.py +19 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_row.py +137 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_table.py +101 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_table_comprehensive.py +644 -0
- velocity_python-0.0.152/src/velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity_python-0.0.152/src/velocity/db/tests/sql/__init__.py +1 -0
- velocity_python-0.0.152/src/velocity/db/tests/sql/common.py +177 -0
- velocity_python-0.0.152/src/velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity_python-0.0.152/src/velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity_python-0.0.152/src/velocity/db/tests/test_postgres.py +448 -0
- velocity_python-0.0.152/src/velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity_python-0.0.152/src/velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity_python-0.0.152/src/velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity_python-0.0.152/src/velocity/misc/__init__.py +0 -0
- velocity_python-0.0.152/src/velocity/misc/tests/__init__.py +1 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_db.py +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_format.py +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_iconv.py +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_merge.py +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_oconv.py +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_timer.py +1 -1
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity_python.egg-info/PKG-INFO +2 -2
- velocity_python-0.0.152/src/velocity_python.egg-info/SOURCES.txt +134 -0
- velocity_python-0.0.152/tests/test_sys_modified_count_postgres_demo.py +77 -0
- velocity_python-0.0.152/tests/test_where_clause_validation.py +194 -0
- velocity_python-0.0.131/src/velocity/db/servers/mysql.py +0 -640
- velocity_python-0.0.131/src/velocity/db/servers/postgres/__init__.py +0 -17
- velocity_python-0.0.131/src/velocity/db/servers/postgres/operators.py +0 -23
- velocity_python-0.0.131/src/velocity/db/servers/sqlite.py +0 -968
- velocity_python-0.0.131/src/velocity/db/servers/sqlite_reserved.py +0 -208
- velocity_python-0.0.131/src/velocity/db/servers/sqlserver.py +0 -921
- velocity_python-0.0.131/src/velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.131/src/velocity_python.egg-info/SOURCES.txt +0 -87
- velocity_python-0.0.131/tests/test_postgres.py +0 -212
- {velocity_python-0.0.131 → velocity_python-0.0.152}/LICENSE +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/setup.cfg +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/app}/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/app}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/app}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/error_handler.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/legacy_mixin.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/standard_mixin.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/aws}/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/aws}/tests/test_response.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/tablehelper.py +0 -0
- /velocity_python-0.0.131/src/velocity/misc/__init__.py → /velocity_python-0.0.152/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_db_utils.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_result_caching.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_tablehelper.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_fix.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_original_error.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.152
|
|
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
|
|
@@ -497,7 +497,7 @@ def update_user(tx):
|
|
|
497
497
|
# Find and update using dictionary syntax
|
|
498
498
|
user = users.find(123) # Returns a row that behaves like a dict
|
|
499
499
|
user['name'] = 'Updated Name' # Direct assignment like a dict
|
|
500
|
-
user['
|
|
500
|
+
user['important_date'] = datetime.now() # No special methods needed
|
|
501
501
|
|
|
502
502
|
# Check if columns exist before updating
|
|
503
503
|
if 'phone' in user:
|
|
@@ -447,7 +447,7 @@ def update_user(tx):
|
|
|
447
447
|
# Find and update using dictionary syntax
|
|
448
448
|
user = users.find(123) # Returns a row that behaves like a dict
|
|
449
449
|
user['name'] = 'Updated Name' # Direct assignment like a dict
|
|
450
|
-
user['
|
|
450
|
+
user['important_date'] = datetime.now() # No special methods needed
|
|
451
451
|
|
|
452
452
|
# Check if columns exist before updating
|
|
453
453
|
if 'phone' in user:
|
|
@@ -115,9 +115,7 @@ class Order:
|
|
|
115
115
|
for key, default in defaults.items():
|
|
116
116
|
if key not in target:
|
|
117
117
|
target[key] = default() if callable(default) else default
|
|
118
|
-
|
|
119
|
-
# Always update updated_at if present
|
|
120
|
-
target[key] = default() if callable(default) else default
|
|
118
|
+
|
|
121
119
|
|
|
122
120
|
def _validate(self):
|
|
123
121
|
self._apply_defaults()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# App module tests
|
|
@@ -9,33 +9,34 @@ import copy
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
import time
|
|
12
|
-
from abc import ABC
|
|
13
|
-
from
|
|
12
|
+
from abc import ABC
|
|
13
|
+
from datetime import date, datetime
|
|
14
|
+
from typing import Dict, Any
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ActivityTracker(ABC):
|
|
17
18
|
"""
|
|
18
19
|
Mixin class providing standardized activity tracking for Lambda handlers.
|
|
19
|
-
|
|
20
|
+
|
|
20
21
|
Tracks API calls to the aws_api_activity table with consistent data structure
|
|
21
22
|
and automatic duration calculation.
|
|
22
23
|
"""
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
def __init__(self, *args, **kwargs):
|
|
25
26
|
super().__init__(*args, **kwargs)
|
|
26
27
|
self.activity_log_key = None
|
|
27
28
|
self.start_time = None
|
|
28
29
|
self.end_time = None
|
|
29
30
|
self.activity_data = {}
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
def track_activity_start(self, tx, context):
|
|
32
33
|
"""Start tracking activity for the current request"""
|
|
33
34
|
self.start_time = time.time()
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
# Gather common activity data
|
|
36
37
|
postdata = context.postdata()
|
|
37
38
|
payload = context.payload()
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
self.activity_data = {
|
|
40
41
|
"action": context.action(),
|
|
41
42
|
"args": json.dumps(context.args()),
|
|
@@ -45,21 +46,34 @@ class ActivityTracker(ABC):
|
|
|
45
46
|
"user_branch": os.environ.get("USER_BRANCH", "Unknown"),
|
|
46
47
|
"start_timestamp": self.start_time,
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
# Add user information if available
|
|
50
51
|
user_info = self._extract_user_info(payload)
|
|
51
52
|
if user_info:
|
|
52
53
|
self.activity_data.update(user_info)
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
# Add session information
|
|
55
56
|
session_data = context.session()
|
|
56
57
|
if session_data:
|
|
57
|
-
self.activity_data.update(
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
self.activity_data.update(self._sanitize_session_data(session_data))
|
|
59
|
+
|
|
60
|
+
# Ensure all values are serializable before persisting
|
|
61
|
+
self.activity_data = {
|
|
62
|
+
key: self._normalize_activity_value(value)
|
|
63
|
+
for key, value in self.activity_data.items()
|
|
64
|
+
if value is not None
|
|
65
|
+
}
|
|
66
|
+
|
|
60
67
|
# Create the activity record
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
try:
|
|
69
|
+
self.activity_log_key = tx.table("aws_api_activity").new(self.activity_data).pk
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
context.log(
|
|
72
|
+
f"ActivityTracker.track_activity_start failed: {exc}; keys={list(self.activity_data.keys())}",
|
|
73
|
+
"ActivityTracker.track_activity_start",
|
|
74
|
+
)
|
|
75
|
+
raise
|
|
76
|
+
|
|
63
77
|
return self.activity_log_key
|
|
64
78
|
|
|
65
79
|
def track_activity_success(self, tx, context):
|
|
@@ -140,3 +154,28 @@ class ActivityTracker(ABC):
|
|
|
140
154
|
pass
|
|
141
155
|
|
|
142
156
|
return user_info
|
|
157
|
+
|
|
158
|
+
def _sanitize_session_data(self, session: Dict[str, Any]) -> Dict[str, Any]:
|
|
159
|
+
"""Remove sensitive session keys and normalize value types"""
|
|
160
|
+
sanitized = {}
|
|
161
|
+
|
|
162
|
+
for key, value in session.items():
|
|
163
|
+
if key == "cognito_user":
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
sanitized[key] = self._normalize_activity_value(value)
|
|
167
|
+
|
|
168
|
+
return sanitized
|
|
169
|
+
|
|
170
|
+
def _normalize_activity_value(self, value: Any) -> Any:
|
|
171
|
+
"""Convert activity data values into types acceptable by psycopg2"""
|
|
172
|
+
if isinstance(value, (dict, list, tuple, set)):
|
|
173
|
+
try:
|
|
174
|
+
return json.dumps(value)
|
|
175
|
+
except (TypeError, ValueError):
|
|
176
|
+
return str(value)
|
|
177
|
+
if isinstance(value, (datetime, date)):
|
|
178
|
+
return value.isoformat()
|
|
179
|
+
if isinstance(value, (bytes, bytearray)):
|
|
180
|
+
return value.decode("utf-8", errors="ignore")
|
|
181
|
+
return value
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error Handler Mixin for Lambda Handlers.
|
|
3
|
+
|
|
4
|
+
Provides standardized error handling, logging, and notification functionality
|
|
5
|
+
for Lambda handlers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import copy
|
|
9
|
+
import os
|
|
10
|
+
import pprint
|
|
11
|
+
import time
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AwsSessionMixin(ABC):
|
|
17
|
+
"""
|
|
18
|
+
Mixin class providing standardized error handling for Lambda handlers.
|
|
19
|
+
|
|
20
|
+
Handles error logging to sys_log table, email notifications to administrators,
|
|
21
|
+
and error metrics collection.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def handle_standard_error(self, tx, context, exception: Exception, tb_string: str):
|
|
25
|
+
"""Handle errors with consistent logging and notification patterns"""
|
|
26
|
+
|
|
27
|
+
# Log to sys_log for centralized logging
|
|
28
|
+
self.log_error_to_system(tx, context, exception, tb_string)
|
|
29
|
+
|
|
30
|
+
# Determine if this error requires notification
|
|
31
|
+
if self._should_notify_error(exception):
|
|
32
|
+
self.send_error_notification(tx, context, exception, tb_string)
|
|
33
|
+
|
|
34
|
+
# Log error metrics for monitoring
|
|
35
|
+
self.log_error_metrics(tx, context, exception)
|
|
36
|
+
|
|
37
|
+
def log_error_to_system(self, tx, context, exception: Exception, tb_string: str):
|
|
38
|
+
"""Log error to sys_log table"""
|
|
39
|
+
error_data = {
|
|
40
|
+
"level": "ERROR",
|
|
41
|
+
"message": str(exception),
|
|
42
|
+
"function": f"{self.__class__.__name__}.{context.action()}",
|
|
43
|
+
"traceback": tb_string,
|
|
44
|
+
"exception_type": exception.__class__.__name__,
|
|
45
|
+
"handler_name": self.__class__.__name__,
|
|
46
|
+
"action": context.action(),
|
|
47
|
+
"user_branch": os.environ.get("USER_BRANCH", "Unknown"),
|
|
48
|
+
"function_name": os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "Unknown"),
|
|
49
|
+
"app_name": os.environ.get("ProjectName", "Unknown"),
|
|
50
|
+
"user_agent": "AWS Lambda",
|
|
51
|
+
"device_type": "Lambda",
|
|
52
|
+
"sys_modified_by": "Lambda",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Add user context if available
|
|
56
|
+
try:
|
|
57
|
+
if hasattr(self, 'current_user') and self.current_user:
|
|
58
|
+
error_data["user_email"] = self.current_user.get("email_address")
|
|
59
|
+
except:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
tx.table("sys_log").insert(error_data)
|
|
63
|
+
|
|
64
|
+
def send_error_notification(self, tx, context, exception: Exception, tb_string: str):
|
|
65
|
+
"""Send error notification email to administrators"""
|
|
66
|
+
try:
|
|
67
|
+
# Import here to avoid circular dependency
|
|
68
|
+
from support.app import helpers
|
|
69
|
+
|
|
70
|
+
environment = os.environ.get('USER_BRANCH', 'Unknown').title()
|
|
71
|
+
function_name = os.environ.get('AWS_LAMBDA_FUNCTION_NAME', 'Unknown')
|
|
72
|
+
|
|
73
|
+
subject = f"{environment} Lambda Error - {function_name}"
|
|
74
|
+
|
|
75
|
+
body = f"""
|
|
76
|
+
Error Details:
|
|
77
|
+
- Handler: {self.__class__.__name__}
|
|
78
|
+
- Action: {context.action()}
|
|
79
|
+
- Exception: {exception.__class__.__name__}
|
|
80
|
+
- Message: {str(exception)}
|
|
81
|
+
- Environment: {environment}
|
|
82
|
+
- Function: {function_name}
|
|
83
|
+
|
|
84
|
+
Full Traceback:
|
|
85
|
+
{tb_string}
|
|
86
|
+
|
|
87
|
+
Request Details:
|
|
88
|
+
{self._get_error_context(context)}
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
sender = self._get_error_notification_sender()
|
|
92
|
+
recipients = self._get_error_notification_recipients()
|
|
93
|
+
|
|
94
|
+
helpers.sendmail(
|
|
95
|
+
tx,
|
|
96
|
+
subject=subject,
|
|
97
|
+
body=body,
|
|
98
|
+
html=None,
|
|
99
|
+
sender=sender,
|
|
100
|
+
recipient=recipients[0],
|
|
101
|
+
cc=recipients[1:] if len(recipients) > 1 else None,
|
|
102
|
+
bcc=None,
|
|
103
|
+
email_settings_id=1001,
|
|
104
|
+
)
|
|
105
|
+
except Exception as email_error:
|
|
106
|
+
print(f"Failed to send error notification email: {email_error}")
|
|
107
|
+
|
|
108
|
+
def _should_notify_error(self, exception: Exception) -> bool:
|
|
109
|
+
"""Determine if an error should trigger email notifications"""
|
|
110
|
+
# Don't notify for user authentication errors or validation errors
|
|
111
|
+
non_notification_types = [
|
|
112
|
+
"AuthenticationError",
|
|
113
|
+
"ValidationError",
|
|
114
|
+
"ValueError",
|
|
115
|
+
"AlertError"
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
exception_name = exception.__class__.__name__
|
|
119
|
+
|
|
120
|
+
# Check for authentication-related exceptions
|
|
121
|
+
if "Authentication" in exception_name or "Auth" in exception_name:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
return exception_name not in non_notification_types
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def _get_error_notification_recipients(self) -> list:
|
|
128
|
+
"""
|
|
129
|
+
Get list of email recipients for error notifications.
|
|
130
|
+
|
|
131
|
+
Must be implemented by the handler class.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of email addresses to notify when errors occur
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
return ["admin@company.com", "devops@company.com"]
|
|
138
|
+
"""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@abstractmethod
|
|
142
|
+
def _get_error_notification_sender(self) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Get email sender for error notifications.
|
|
145
|
+
|
|
146
|
+
Must be implemented by the handler class.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Email address to use as sender for error notifications
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
return "no-reply@company.com"
|
|
153
|
+
"""
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
def _get_error_context(self, context) -> str:
|
|
157
|
+
"""Get sanitized request context for error reporting"""
|
|
158
|
+
try:
|
|
159
|
+
postdata = context.postdata()
|
|
160
|
+
sanitized = copy.deepcopy(postdata)
|
|
161
|
+
|
|
162
|
+
# Remove sensitive data
|
|
163
|
+
if "payload" in sanitized and isinstance(sanitized["payload"], dict):
|
|
164
|
+
sanitized["payload"].pop("cognito_user", None)
|
|
165
|
+
|
|
166
|
+
return pprint.pformat(sanitized)
|
|
167
|
+
except:
|
|
168
|
+
return "Unable to retrieve request context"
|
|
169
|
+
|
|
170
|
+
def log_error_metrics(self, tx, context, exception: Exception):
|
|
171
|
+
"""Log error metrics for monitoring and alerting"""
|
|
172
|
+
try:
|
|
173
|
+
metrics_data = {
|
|
174
|
+
"metric_type": "error_count",
|
|
175
|
+
"handler_name": self.__class__.__name__,
|
|
176
|
+
"action": context.action(),
|
|
177
|
+
"exception_type": exception.__class__.__name__,
|
|
178
|
+
"environment": os.environ.get("USER_BRANCH", "Unknown"),
|
|
179
|
+
"function_name": os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "Unknown"),
|
|
180
|
+
"timestamp": time.time(),
|
|
181
|
+
"sys_modified_by": "Lambda"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Try to insert into metrics table if it exists
|
|
185
|
+
try:
|
|
186
|
+
tx.table("lambda_metrics").insert(metrics_data)
|
|
187
|
+
except:
|
|
188
|
+
# Metrics table might not exist yet, don't fail error handler
|
|
189
|
+
pass
|
|
190
|
+
except:
|
|
191
|
+
# Don't fail the error handler if metrics logging fails
|
|
192
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# AWS module tests
|
|
@@ -99,7 +99,8 @@ def return_default(
|
|
|
99
99
|
|
|
100
100
|
def create_missing(func):
|
|
101
101
|
"""
|
|
102
|
-
If the function call fails with DbColumnMissingError or DbTableMissingError,
|
|
102
|
+
If the function call fails with DbColumnMissingError or DbTableMissingError,
|
|
103
|
+
tries to create them and re-run (only if schema is not locked).
|
|
103
104
|
"""
|
|
104
105
|
|
|
105
106
|
@wraps(func)
|
|
@@ -109,8 +110,16 @@ def create_missing(func):
|
|
|
109
110
|
result = func(self, *args, **kwds)
|
|
110
111
|
self.tx.release_savepoint(sp, cursor=self.cursor())
|
|
111
112
|
return result
|
|
112
|
-
except exceptions.DbColumnMissingError:
|
|
113
|
+
except exceptions.DbColumnMissingError as e:
|
|
113
114
|
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
115
|
+
|
|
116
|
+
# Check if schema is locked
|
|
117
|
+
if self.tx.engine.schema_locked:
|
|
118
|
+
raise exceptions.DbSchemaLockedError(
|
|
119
|
+
f"Cannot create missing column: schema is locked. Original error: {e}"
|
|
120
|
+
) from e
|
|
121
|
+
|
|
122
|
+
# Existing logic for automatic creation
|
|
114
123
|
data = {}
|
|
115
124
|
if "pk" in kwds:
|
|
116
125
|
data.update(kwds["pk"])
|
|
@@ -121,8 +130,16 @@ def create_missing(func):
|
|
|
121
130
|
data.update(arg)
|
|
122
131
|
self.alter(data)
|
|
123
132
|
return func(self, *args, **kwds)
|
|
124
|
-
except exceptions.DbTableMissingError:
|
|
133
|
+
except exceptions.DbTableMissingError as e:
|
|
125
134
|
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
135
|
+
|
|
136
|
+
# Check if schema is locked
|
|
137
|
+
if self.tx.engine.schema_locked:
|
|
138
|
+
raise exceptions.DbSchemaLockedError(
|
|
139
|
+
f"Cannot create missing table: schema is locked. Original error: {e}"
|
|
140
|
+
) from e
|
|
141
|
+
|
|
142
|
+
# Existing logic for automatic creation
|
|
126
143
|
data = {}
|
|
127
144
|
if "pk" in kwds:
|
|
128
145
|
data.update(kwds["pk"])
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import re
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import contextmanager
|
|
3
5
|
from functools import wraps
|
|
4
6
|
from velocity.db import exceptions
|
|
5
7
|
from velocity.db.core.transaction import Transaction
|
|
@@ -17,11 +19,12 @@ class Engine:
|
|
|
17
19
|
|
|
18
20
|
MAX_RETRIES = 100
|
|
19
21
|
|
|
20
|
-
def __init__(self, driver, config, sql, connect_timeout=5):
|
|
22
|
+
def __init__(self, driver, config, sql, connect_timeout=5, schema_locked=False):
|
|
21
23
|
self.__config = config
|
|
22
24
|
self.__sql = sql
|
|
23
25
|
self.__driver = driver
|
|
24
26
|
self.__connect_timeout = connect_timeout
|
|
27
|
+
self.__schema_locked = schema_locked
|
|
25
28
|
|
|
26
29
|
def __str__(self):
|
|
27
30
|
return f"[{self.sql.server}] engine({self.config})"
|
|
@@ -205,6 +208,29 @@ class Engine:
|
|
|
205
208
|
def sql(self):
|
|
206
209
|
return self.__sql
|
|
207
210
|
|
|
211
|
+
@property
|
|
212
|
+
def schema_locked(self):
|
|
213
|
+
"""Returns True if schema modifications are locked."""
|
|
214
|
+
return self.__schema_locked
|
|
215
|
+
|
|
216
|
+
def lock_schema(self):
|
|
217
|
+
"""Lock schema to prevent automatic modifications."""
|
|
218
|
+
self.__schema_locked = True
|
|
219
|
+
|
|
220
|
+
def unlock_schema(self):
|
|
221
|
+
"""Unlock schema to allow automatic modifications."""
|
|
222
|
+
self.__schema_locked = False
|
|
223
|
+
|
|
224
|
+
@contextmanager
|
|
225
|
+
def unlocked_schema(self):
|
|
226
|
+
"""Temporarily unlock schema for automatic creation."""
|
|
227
|
+
original_state = self.__schema_locked
|
|
228
|
+
self.__schema_locked = False
|
|
229
|
+
try:
|
|
230
|
+
yield self
|
|
231
|
+
finally:
|
|
232
|
+
self.__schema_locked = original_state
|
|
233
|
+
|
|
208
234
|
@property
|
|
209
235
|
def version(self):
|
|
210
236
|
"""
|
|
@@ -341,24 +367,46 @@ class Engine:
|
|
|
341
367
|
|
|
342
368
|
msg = str(exception).strip().lower()
|
|
343
369
|
|
|
344
|
-
# Create enhanced error message with SQL query
|
|
370
|
+
# Create enhanced error message with SQL query and context
|
|
345
371
|
enhanced_message = str(exception)
|
|
372
|
+
|
|
373
|
+
# Add specific guidance for common WHERE clause errors
|
|
374
|
+
exception_str_lower = str(exception).lower()
|
|
375
|
+
if "argument of where must be type boolean" in exception_str_lower:
|
|
376
|
+
enhanced_message += (
|
|
377
|
+
"\n\n*** WHERE CLAUSE ERROR ***\n"
|
|
378
|
+
"This error typically occurs when a WHERE clause contains a bare value "
|
|
379
|
+
"instead of a proper boolean expression.\n"
|
|
380
|
+
"Common fixes:\n"
|
|
381
|
+
" - Change WHERE 1001 to WHERE sys_id = 1001\n"
|
|
382
|
+
" - Change WHERE {'column': value} format in dictionaries\n"
|
|
383
|
+
" - Ensure string WHERE clauses are complete SQL expressions"
|
|
384
|
+
)
|
|
385
|
+
|
|
346
386
|
if sql:
|
|
347
387
|
enhanced_message += (
|
|
348
388
|
f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
|
|
349
389
|
)
|
|
390
|
+
|
|
391
|
+
# Add call stack context for better debugging
|
|
392
|
+
import traceback
|
|
393
|
+
stack_trace = traceback.format_stack()
|
|
394
|
+
# Get the last few frames that aren't in the error handling itself
|
|
395
|
+
relevant_frames = [frame for frame in stack_trace if 'process_error' not in frame and 'logging' not in frame][-3:]
|
|
396
|
+
if relevant_frames:
|
|
397
|
+
enhanced_message += "\n\nCall Context:\n" + "".join(relevant_frames)
|
|
350
398
|
|
|
351
399
|
# Format SQL for logging
|
|
352
400
|
formatted_sql_info = ""
|
|
353
401
|
if sql:
|
|
354
402
|
formatted_sql_info = f" sql={self._format_sql_with_params(sql, parameters)}"
|
|
355
403
|
|
|
356
|
-
logger.warning(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
)
|
|
404
|
+
# logger.warning(
|
|
405
|
+
# "Database error caught. Attempting to transform: code=%s message=%s%s",
|
|
406
|
+
# error_code,
|
|
407
|
+
# error_message,
|
|
408
|
+
# formatted_sql_info,
|
|
409
|
+
# )
|
|
362
410
|
|
|
363
411
|
# Direct error code mapping
|
|
364
412
|
if error_code in self.sql.ApplicationErrorCodes:
|
|
@@ -44,7 +44,12 @@ class Row:
|
|
|
44
44
|
def __setitem__(self, key, val):
|
|
45
45
|
if key in self.pk:
|
|
46
46
|
raise Exception("Cannot update a primary key.")
|
|
47
|
-
self.table
|
|
47
|
+
if hasattr(self.table, "updins"):
|
|
48
|
+
self.table.updins({key: val}, pk=self.pk)
|
|
49
|
+
elif hasattr(self.table, "upsert"):
|
|
50
|
+
self.table.upsert({key: val}, pk=self.pk)
|
|
51
|
+
else:
|
|
52
|
+
self.table.update({key: val}, pk=self.pk)
|
|
48
53
|
|
|
49
54
|
def __delitem__(self, key):
|
|
50
55
|
if key in self.pk:
|
|
@@ -121,7 +126,12 @@ class Row:
|
|
|
121
126
|
if kwds:
|
|
122
127
|
data.update(kwds)
|
|
123
128
|
if data:
|
|
124
|
-
self.table
|
|
129
|
+
if hasattr(self.table, "updins"):
|
|
130
|
+
self.table.updins(data, pk=self.pk)
|
|
131
|
+
elif hasattr(self.table, "upsert"):
|
|
132
|
+
self.table.upsert(data, pk=self.pk)
|
|
133
|
+
else:
|
|
134
|
+
self.table.update(data, pk=self.pk)
|
|
125
135
|
return self
|
|
126
136
|
|
|
127
137
|
def __cmp__(self, other):
|