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.

Files changed (145) hide show
  1. {velocity_python-0.0.131 → velocity_python-0.0.152}/PKG-INFO +2 -2
  2. {velocity_python-0.0.131 → velocity_python-0.0.152}/README.md +1 -1
  3. {velocity_python-0.0.131 → velocity_python-0.0.152}/pyproject.toml +1 -1
  4. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/__init__.py +1 -1
  5. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/orders.py +1 -3
  6. velocity_python-0.0.152/src/velocity/app/tests/__init__.py +1 -0
  7. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/activity_tracker.py +53 -14
  8. velocity_python-0.0.152/src/velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
  9. velocity_python-0.0.152/src/velocity/aws/tests/__init__.py +1 -0
  10. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/decorators.py +20 -3
  11. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/engine.py +56 -8
  12. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/row.py +12 -2
  13. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/table.py +178 -0
  14. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/transaction.py +6 -10
  15. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/exceptions.py +7 -0
  16. velocity_python-0.0.152/src/velocity/db/servers/base/__init__.py +9 -0
  17. velocity_python-0.0.152/src/velocity/db/servers/base/initializer.py +70 -0
  18. velocity_python-0.0.152/src/velocity/db/servers/base/operators.py +98 -0
  19. velocity_python-0.0.152/src/velocity/db/servers/base/sql.py +503 -0
  20. velocity_python-0.0.152/src/velocity/db/servers/base/types.py +135 -0
  21. velocity_python-0.0.152/src/velocity/db/servers/mysql/__init__.py +73 -0
  22. velocity_python-0.0.152/src/velocity/db/servers/mysql/operators.py +54 -0
  23. 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
  24. velocity_python-0.0.152/src/velocity/db/servers/mysql/sql.py +696 -0
  25. {velocity_python-0.0.131/src/velocity/db/servers/postgres → velocity_python-0.0.152/src/velocity/db/servers/mysql}/types.py +31 -33
  26. velocity_python-0.0.152/src/velocity/db/servers/postgres/__init__.py +67 -0
  27. velocity_python-0.0.152/src/velocity/db/servers/postgres/operators.py +57 -0
  28. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/postgres/sql.py +356 -95
  29. velocity_python-0.0.152/src/velocity/db/servers/postgres/types.py +195 -0
  30. velocity_python-0.0.152/src/velocity/db/servers/sqlite/__init__.py +61 -0
  31. velocity_python-0.0.152/src/velocity/db/servers/sqlite/operators.py +52 -0
  32. velocity_python-0.0.152/src/velocity/db/servers/sqlite/reserved.py +20 -0
  33. velocity_python-0.0.152/src/velocity/db/servers/sqlite/sql.py +653 -0
  34. velocity_python-0.0.152/src/velocity/db/servers/sqlite/types.py +92 -0
  35. velocity_python-0.0.152/src/velocity/db/servers/sqlserver/__init__.py +73 -0
  36. velocity_python-0.0.152/src/velocity/db/servers/sqlserver/operators.py +47 -0
  37. velocity_python-0.0.152/src/velocity/db/servers/sqlserver/reserved.py +32 -0
  38. velocity_python-0.0.152/src/velocity/db/servers/sqlserver/sql.py +786 -0
  39. velocity_python-0.0.152/src/velocity/db/servers/sqlserver/types.py +114 -0
  40. velocity_python-0.0.152/src/velocity/db/tests/__init__.py +1 -0
  41. velocity_python-0.0.152/src/velocity/db/tests/postgres/__init__.py +1 -0
  42. velocity_python-0.0.152/src/velocity/db/tests/postgres/common.py +49 -0
  43. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_column.py +29 -0
  44. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_connections.py +25 -0
  45. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_database.py +21 -0
  46. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_engine.py +205 -0
  47. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_general_usage.py +88 -0
  48. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_imports.py +8 -0
  49. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_result.py +19 -0
  50. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_row.py +137 -0
  51. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
  52. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_schema_locking.py +335 -0
  53. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  54. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_sequence.py +34 -0
  55. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
  56. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_table.py +101 -0
  57. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_table_comprehensive.py +644 -0
  58. velocity_python-0.0.152/src/velocity/db/tests/postgres/test_transaction.py +106 -0
  59. velocity_python-0.0.152/src/velocity/db/tests/sql/__init__.py +1 -0
  60. velocity_python-0.0.152/src/velocity/db/tests/sql/common.py +177 -0
  61. velocity_python-0.0.152/src/velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  62. velocity_python-0.0.152/src/velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  63. velocity_python-0.0.152/src/velocity/db/tests/test_postgres.py +448 -0
  64. velocity_python-0.0.152/src/velocity/db/tests/test_postgres_unchanged.py +81 -0
  65. velocity_python-0.0.152/src/velocity/db/tests/test_schema_locking_initializers.py +226 -0
  66. velocity_python-0.0.152/src/velocity/db/tests/test_schema_locking_simple.py +97 -0
  67. velocity_python-0.0.152/src/velocity/misc/__init__.py +0 -0
  68. velocity_python-0.0.152/src/velocity/misc/tests/__init__.py +1 -0
  69. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_db.py +1 -1
  70. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_format.py +1 -1
  71. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_iconv.py +1 -1
  72. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_merge.py +1 -1
  73. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_oconv.py +1 -1
  74. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_timer.py +1 -1
  75. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity_python.egg-info/PKG-INFO +2 -2
  76. velocity_python-0.0.152/src/velocity_python.egg-info/SOURCES.txt +134 -0
  77. velocity_python-0.0.152/tests/test_sys_modified_count_postgres_demo.py +77 -0
  78. velocity_python-0.0.152/tests/test_where_clause_validation.py +194 -0
  79. velocity_python-0.0.131/src/velocity/db/servers/mysql.py +0 -640
  80. velocity_python-0.0.131/src/velocity/db/servers/postgres/__init__.py +0 -17
  81. velocity_python-0.0.131/src/velocity/db/servers/postgres/operators.py +0 -23
  82. velocity_python-0.0.131/src/velocity/db/servers/sqlite.py +0 -968
  83. velocity_python-0.0.131/src/velocity/db/servers/sqlite_reserved.py +0 -208
  84. velocity_python-0.0.131/src/velocity/db/servers/sqlserver.py +0 -921
  85. velocity_python-0.0.131/src/velocity/db/servers/sqlserver_reserved.py +0 -314
  86. velocity_python-0.0.131/src/velocity_python.egg-info/SOURCES.txt +0 -87
  87. velocity_python-0.0.131/tests/test_postgres.py +0 -212
  88. {velocity_python-0.0.131 → velocity_python-0.0.152}/LICENSE +0 -0
  89. {velocity_python-0.0.131 → velocity_python-0.0.152}/setup.cfg +0 -0
  90. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/__init__.py +0 -0
  91. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/invoices.py +0 -0
  92. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/payments.py +0 -0
  93. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/app/purchase_orders.py +0 -0
  94. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/app}/tests/test_email_processing.py +0 -0
  95. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/app}/tests/test_payment_profile_sorting.py +0 -0
  96. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/app}/tests/test_spreadsheet_functions.py +0 -0
  97. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/__init__.py +0 -0
  98. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/amplify.py +0 -0
  99. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/__init__.py +0 -0
  100. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/base_handler.py +0 -0
  101. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/context.py +0 -0
  102. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/exceptions.py +0 -0
  103. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  104. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  105. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/error_handler.py +0 -0
  106. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/legacy_mixin.py +0 -0
  107. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/mixins/standard_mixin.py +0 -0
  108. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/response.py +0 -0
  109. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  110. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/aws}/tests/test_lambda_handler_json_serialization.py +0 -0
  111. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/aws}/tests/test_response.py +0 -0
  112. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/__init__.py +0 -0
  113. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/__init__.py +0 -0
  114. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/column.py +0 -0
  115. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/database.py +0 -0
  116. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/result.py +0 -0
  117. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/core/sequence.py +0 -0
  118. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/__init__.py +0 -0
  119. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/postgres/reserved.py +0 -0
  120. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/servers/tablehelper.py +0 -0
  121. /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
  122. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_cursor_rowcount_fix.py +0 -0
  123. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_db_utils.py +0 -0
  124. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_process_error_robustness.py +0 -0
  125. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_result_caching.py +0 -0
  126. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_result_sql_aware.py +0 -0
  127. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_row_get_missing_column.py +0 -0
  128. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_sql_builder.py +0 -0
  129. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/db}/tests/test_tablehelper.py +0 -0
  130. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/db/utils.py +0 -0
  131. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/conv/__init__.py +0 -0
  132. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/conv/iconv.py +0 -0
  133. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/conv/oconv.py +0 -0
  134. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/db.py +0 -0
  135. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/export.py +0 -0
  136. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/format.py +0 -0
  137. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/mail.py +0 -0
  138. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/merge.py +0 -0
  139. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_fix.py +0 -0
  140. {velocity_python-0.0.131 → velocity_python-0.0.152/src/velocity/misc}/tests/test_original_error.py +0 -0
  141. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/timer.py +0 -0
  142. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity/misc/tools.py +0 -0
  143. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  144. {velocity_python-0.0.131 → velocity_python-0.0.152}/src/velocity_python.egg-info/requires.txt +0 -0
  145. {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.131
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['updated_at'] = datetime.now() # No special methods needed
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['updated_at'] = datetime.now() # No special methods needed
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:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.131"
7
+ version = "0.0.152"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.131"
1
+ __version__ = version = "0.0.152"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -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
- elif key == "updated_at":
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, abstractmethod
13
- from typing import Dict, Any, Optional
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({k: v for k, v in session_data.items()
58
- if k not in ['cognito_user']})
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
- self.activity_log_key = tx.table("aws_api_activity").new(self.activity_data).pk
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, tries to create them and re-run.
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
- "Database error caught. Attempting to transform: code=%s message=%s%s",
358
- error_code,
359
- error_message,
360
- formatted_sql_info,
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.upsert({key: val}, self.pk)
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.upsert(data, self.pk)
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):