velocity-python 0.0.143__tar.gz → 0.0.158__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.
Files changed (137) hide show
  1. {velocity_python-0.0.143 → velocity_python-0.0.158}/PKG-INFO +2 -2
  2. {velocity_python-0.0.143 → velocity_python-0.0.158}/README.md +1 -1
  3. {velocity_python-0.0.143 → velocity_python-0.0.158}/pyproject.toml +1 -1
  4. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/__init__.py +1 -1
  5. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/orders.py +1 -3
  6. velocity_python-0.0.158/src/velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
  7. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/decorators.py +27 -8
  8. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/engine.py +3 -2
  9. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/row.py +12 -2
  10. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/table.py +262 -48
  11. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/transaction.py +28 -15
  12. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/mysql/sql.py +32 -10
  13. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/postgres/sql.py +321 -145
  14. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlite/sql.py +34 -10
  15. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlserver/sql.py +31 -10
  16. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_table_comprehensive.py +3 -1
  17. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_db_utils.py +49 -0
  18. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_postgres.py +189 -0
  19. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/utils.py +67 -4
  20. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity_python.egg-info/PKG-INFO +2 -2
  21. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  22. velocity_python-0.0.158/tests/test_decorators.py +157 -0
  23. {velocity_python-0.0.143 → velocity_python-0.0.158}/tests/test_sys_modified_count_postgres_demo.py +2 -2
  24. {velocity_python-0.0.143 → velocity_python-0.0.158}/tests/test_where_clause_validation.py +0 -4
  25. {velocity_python-0.0.143 → velocity_python-0.0.158}/LICENSE +0 -0
  26. {velocity_python-0.0.143 → velocity_python-0.0.158}/setup.cfg +0 -0
  27. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/__init__.py +0 -0
  28. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/invoices.py +0 -0
  29. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/payments.py +0 -0
  30. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/purchase_orders.py +0 -0
  31. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/tests/__init__.py +0 -0
  32. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/tests/test_email_processing.py +0 -0
  33. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  34. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  35. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/__init__.py +0 -0
  36. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/amplify.py +0 -0
  37. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/__init__.py +0 -0
  38. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/base_handler.py +0 -0
  39. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/context.py +0 -0
  40. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/exceptions.py +0 -0
  41. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  42. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  43. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/mixins/activity_tracker.py +0 -0
  44. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/mixins/error_handler.py +0 -0
  45. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/mixins/legacy_mixin.py +0 -0
  46. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/mixins/standard_mixin.py +0 -0
  47. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/response.py +0 -0
  48. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  49. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/tests/__init__.py +0 -0
  50. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  51. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/aws/tests/test_response.py +0 -0
  52. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/__init__.py +0 -0
  53. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/__init__.py +0 -0
  54. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/column.py +0 -0
  55. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/database.py +0 -0
  56. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/result.py +0 -0
  57. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/core/sequence.py +0 -0
  58. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/exceptions.py +0 -0
  59. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/__init__.py +0 -0
  60. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/base/__init__.py +0 -0
  61. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/base/initializer.py +0 -0
  62. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/base/operators.py +0 -0
  63. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/base/sql.py +0 -0
  64. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/base/types.py +0 -0
  65. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/mysql/__init__.py +0 -0
  66. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/mysql/operators.py +0 -0
  67. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/mysql/reserved.py +0 -0
  68. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/mysql/types.py +0 -0
  69. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/postgres/__init__.py +0 -0
  70. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/postgres/operators.py +0 -0
  71. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/postgres/reserved.py +0 -0
  72. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/postgres/types.py +0 -0
  73. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  74. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlite/operators.py +0 -0
  75. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  76. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlite/types.py +0 -0
  77. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  78. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  79. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  80. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/sqlserver/types.py +0 -0
  81. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/servers/tablehelper.py +0 -0
  82. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/__init__.py +0 -0
  83. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/common_db_test.py +0 -0
  84. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/__init__.py +0 -0
  85. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/common.py +0 -0
  86. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_column.py +0 -0
  87. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  88. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_database.py +0 -0
  89. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  90. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  91. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  92. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_result.py +0 -0
  93. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_row.py +0 -0
  94. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  95. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  96. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  97. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  98. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  99. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_table.py +0 -0
  100. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  101. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/sql/__init__.py +0 -0
  102. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/sql/common.py +0 -0
  103. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  104. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  105. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  106. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  107. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  108. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_result_caching.py +0 -0
  109. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  110. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  111. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  112. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  113. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_sql_builder.py +0 -0
  114. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/db/tests/test_tablehelper.py +0 -0
  115. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/__init__.py +0 -0
  116. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/conv/__init__.py +0 -0
  117. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/conv/iconv.py +0 -0
  118. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/conv/oconv.py +0 -0
  119. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/db.py +0 -0
  120. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/export.py +0 -0
  121. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/format.py +0 -0
  122. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/mail.py +0 -0
  123. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/merge.py +0 -0
  124. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/__init__.py +0 -0
  125. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_db.py +0 -0
  126. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_fix.py +0 -0
  127. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_format.py +0 -0
  128. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_iconv.py +0 -0
  129. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_merge.py +0 -0
  130. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_oconv.py +0 -0
  131. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_original_error.py +0 -0
  132. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tests/test_timer.py +0 -0
  133. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/timer.py +0 -0
  134. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity/misc/tools.py +0 -0
  135. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  136. {velocity_python-0.0.143 → velocity_python-0.0.158}/src/velocity_python.egg-info/requires.txt +0 -0
  137. {velocity_python-0.0.143 → velocity_python-0.0.158}/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.143
3
+ Version: 0.0.158
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.143"
7
+ version = "0.0.158"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.143"
1
+ __version__ = version = "0.0.158"
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,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
@@ -4,10 +4,27 @@ from functools import wraps
4
4
  from velocity.db import exceptions
5
5
 
6
6
 
7
+ _PRIMARY_KEY_PATTERNS = (
8
+ "primary key",
9
+ "key 'primary'",
10
+ 'key "primary"',
11
+ )
12
+
13
+
14
+ def _is_primary_key_duplicate(error):
15
+ """Return True when the duplicate-key error is targeting the primary key."""
16
+
17
+ message = str(error or "")
18
+ lowered = message.lower()
19
+
20
+ if "sys_id" in lowered:
21
+ return True
22
+
23
+ return any(pattern in lowered for pattern in _PRIMARY_KEY_PATTERNS)
24
+
25
+
7
26
  def retry_on_dup_key(func):
8
- """
9
- Retries a function call if it raises DbDuplicateKeyError, up to max_retries.
10
- """
27
+ """Retry when insert/update fails because the primary key already exists."""
11
28
 
12
29
  @wraps(func)
13
30
  def retry_decorator(self, *args, **kwds):
@@ -19,10 +36,12 @@ def retry_on_dup_key(func):
19
36
  result = func(self, *args, **kwds)
20
37
  self.tx.release_savepoint(sp, cursor=self.cursor())
21
38
  return result
22
- except exceptions.DbDuplicateKeyError:
39
+ except exceptions.DbDuplicateKeyError as error:
23
40
  self.tx.rollback_savepoint(sp, cursor=self.cursor())
24
41
  if "sys_id" in kwds.get("data", {}):
25
42
  raise
43
+ if not _is_primary_key_duplicate(error):
44
+ raise
26
45
  retries += 1
27
46
  if retries >= max_retries:
28
47
  raise
@@ -34,9 +53,7 @@ def retry_on_dup_key(func):
34
53
 
35
54
 
36
55
  def reset_id_on_dup_key(func):
37
- """
38
- Wraps an INSERT/UPSERT to reset the sys_id sequence on duplicate key collisions.
39
- """
56
+ """Retry sys_id sequence bump only when the primary key collides."""
40
57
 
41
58
  @wraps(func)
42
59
  def reset_decorator(self, *args, retries=0, **kwds):
@@ -45,10 +62,12 @@ def reset_id_on_dup_key(func):
45
62
  result = func(self, *args, **kwds)
46
63
  self.tx.release_savepoint(sp, cursor=self.cursor())
47
64
  return result
48
- except exceptions.DbDuplicateKeyError:
65
+ except exceptions.DbDuplicateKeyError as error:
49
66
  self.tx.rollback_savepoint(sp, cursor=self.cursor())
50
67
  if "sys_id" in kwds.get("data", {}):
51
68
  raise
69
+ if not _is_primary_key_duplicate(error):
70
+ raise
52
71
  if retries < 3:
53
72
  backoff_time = (2**retries) * 0.01 + random.uniform(0, 0.02)
54
73
  time.sleep(backoff_time)
@@ -1,10 +1,10 @@
1
1
  import inspect
2
2
  import re
3
- import os
4
3
  from contextlib import contextmanager
5
4
  from functools import wraps
6
5
  from velocity.db import exceptions
7
6
  from velocity.db.core.transaction import Transaction
7
+ from velocity.db.utils import mask_config_for_display
8
8
 
9
9
  import logging
10
10
 
@@ -27,7 +27,8 @@ class Engine:
27
27
  self.__schema_locked = schema_locked
28
28
 
29
29
  def __str__(self):
30
- return f"[{self.sql.server}] engine({self.config})"
30
+ safe_config = mask_config_for_display(self.config)
31
+ return f"[{self.sql.server}] engine({safe_config})"
31
32
 
32
33
  def connect(self):
33
34
  """
@@ -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):