velocity-python 0.0.145__tar.gz → 0.0.147__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.145 → velocity_python-0.0.147}/PKG-INFO +1 -1
- {velocity_python-0.0.145 → velocity_python-0.0.147}/pyproject.toml +1 -1
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/__init__.py +1 -1
- velocity_python-0.0.147/src/velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/table.py +158 -2
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/mysql/sql.py +1 -1
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/postgres/sql.py +162 -37
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlite/sql.py +1 -1
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlserver/sql.py +3 -1
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_postgres.py +189 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity_python.egg-info/SOURCES.txt +1 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/LICENSE +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/README.md +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/setup.cfg +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/mixins/activity_tracker.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/mixins/error_handler.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/mixins/legacy_mixin.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/mixins/standard_mixin.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.0.145 → velocity_python-0.0.147}/tests/test_where_clause_validation.py +0 -0
|
@@ -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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sqlparse
|
|
2
|
+
from collections.abc import Iterable, Mapping
|
|
2
3
|
from velocity.db import exceptions
|
|
3
4
|
from velocity.db.core.row import Row
|
|
4
5
|
from velocity.db.core.result import Result
|
|
@@ -119,6 +120,59 @@ class Table:
|
|
|
119
120
|
return sql, vals
|
|
120
121
|
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
121
122
|
|
|
123
|
+
def create_indexes(self, indexes, **kwds):
|
|
124
|
+
"""
|
|
125
|
+
Convenience wrapper to create multiple indexes in order.
|
|
126
|
+
|
|
127
|
+
Accepts an iterable of definitions. Each definition may be either:
|
|
128
|
+
- Mapping with a required "columns" entry plus optional "unique",
|
|
129
|
+
"direction", "where", and "lower" keys.
|
|
130
|
+
- A simple sequence/string of columns, in which case defaults apply.
|
|
131
|
+
|
|
132
|
+
When sql_only=True, a list of (sql, params) tuples is returned.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if indexes is None:
|
|
136
|
+
return [] if kwds.get("sql_only", False) else None
|
|
137
|
+
|
|
138
|
+
if not isinstance(indexes, Iterable) or isinstance(indexes, (str, bytes)):
|
|
139
|
+
raise TypeError("indexes must be an iterable of index definitions")
|
|
140
|
+
|
|
141
|
+
sql_only = kwds.get("sql_only", False)
|
|
142
|
+
statements = []
|
|
143
|
+
|
|
144
|
+
for definition in indexes:
|
|
145
|
+
if isinstance(definition, Mapping):
|
|
146
|
+
columns = definition.get("columns")
|
|
147
|
+
if not columns:
|
|
148
|
+
raise ValueError("Index definition requires a non-empty 'columns' entry")
|
|
149
|
+
params = {
|
|
150
|
+
"unique": definition.get("unique", False),
|
|
151
|
+
"direction": definition.get("direction"),
|
|
152
|
+
"where": definition.get("where"),
|
|
153
|
+
"lower": definition.get("lower"),
|
|
154
|
+
}
|
|
155
|
+
else:
|
|
156
|
+
columns = definition
|
|
157
|
+
params = {
|
|
158
|
+
"unique": False,
|
|
159
|
+
"direction": None,
|
|
160
|
+
"where": None,
|
|
161
|
+
"lower": None,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if isinstance(columns, str):
|
|
165
|
+
columns = [columns]
|
|
166
|
+
|
|
167
|
+
if not columns:
|
|
168
|
+
raise ValueError("Index columns cannot be empty")
|
|
169
|
+
|
|
170
|
+
result = self.create_index(columns, **params, **kwds)
|
|
171
|
+
if sql_only:
|
|
172
|
+
statements.append(result)
|
|
173
|
+
|
|
174
|
+
return statements if sql_only else None
|
|
175
|
+
|
|
122
176
|
@return_default(None)
|
|
123
177
|
def drop_index(self, columns, **kwds):
|
|
124
178
|
"""
|
|
@@ -177,10 +231,14 @@ class Table:
|
|
|
177
231
|
columns = []
|
|
178
232
|
|
|
179
233
|
has_column = "sys_modified_count" in columns
|
|
180
|
-
|
|
234
|
+
has_row_column = "sys_modified_row" in columns
|
|
235
|
+
|
|
236
|
+
if has_column and has_row_column and not force:
|
|
181
237
|
return
|
|
182
238
|
|
|
183
|
-
sql, vals = self.sql.ensure_sys_modified_count(
|
|
239
|
+
sql, vals = self.sql.ensure_sys_modified_count(
|
|
240
|
+
self.name, has_column=has_column, has_row_column=has_row_column
|
|
241
|
+
)
|
|
184
242
|
if kwds.get("sql_only", False):
|
|
185
243
|
return sql, vals
|
|
186
244
|
self.tx.execute(sql, vals, cursor=self.cursor())
|
|
@@ -487,6 +545,104 @@ class Table:
|
|
|
487
545
|
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
488
546
|
return result.cursor.rowcount if result.cursor else 0
|
|
489
547
|
|
|
548
|
+
@create_missing
|
|
549
|
+
def update_or_insert(self, update_data, insert_data=None, where=None, pk=None, **kwds):
|
|
550
|
+
"""
|
|
551
|
+
Attempts an UPDATE first; if no rows change, performs an INSERT guarded by NOT EXISTS.
|
|
552
|
+
|
|
553
|
+
:param update_data: Mapping of columns to update.
|
|
554
|
+
:param insert_data: Optional mapping used for the INSERT. When omitted, values are
|
|
555
|
+
derived from update_data combined with simple equality predicates
|
|
556
|
+
from ``where`` and primary key values.
|
|
557
|
+
:param where: Criteria for the UPDATE and existence check.
|
|
558
|
+
:param pk: Optional primary key mapping for UPDATE (merged into WHERE) and INSERT.
|
|
559
|
+
:param sql_only: When True, return the SQL/parameter tuples for both phases instead of executing.
|
|
560
|
+
:return: Number of rows affected, or a dict with ``update``/``insert`` entries when sql_only=True.
|
|
561
|
+
"""
|
|
562
|
+
sql_only = kwds.get("sql_only", False)
|
|
563
|
+
if not isinstance(update_data, Mapping) or not update_data:
|
|
564
|
+
raise ValueError("update_data must be a non-empty mapping of column-value pairs.")
|
|
565
|
+
if where is None and pk is None:
|
|
566
|
+
raise ValueError("Either where or pk must be provided for update_or_insert.")
|
|
567
|
+
|
|
568
|
+
update_stmt = None
|
|
569
|
+
if sql_only:
|
|
570
|
+
update_stmt = self.update(update_data, where=where, pk=pk, sql_only=True)
|
|
571
|
+
else:
|
|
572
|
+
updated = self.update(update_data, where=where, pk=pk)
|
|
573
|
+
if updated:
|
|
574
|
+
return updated
|
|
575
|
+
|
|
576
|
+
if insert_data is not None:
|
|
577
|
+
if not isinstance(insert_data, Mapping):
|
|
578
|
+
raise ValueError("insert_data must be a mapping when provided.")
|
|
579
|
+
insert_payload = dict(insert_data)
|
|
580
|
+
else:
|
|
581
|
+
insert_payload = dict(update_data)
|
|
582
|
+
if isinstance(where, Mapping):
|
|
583
|
+
for key, val in where.items():
|
|
584
|
+
if not isinstance(key, str):
|
|
585
|
+
continue
|
|
586
|
+
if set("<>!=%").intersection(key):
|
|
587
|
+
continue
|
|
588
|
+
insert_payload.setdefault(key, val)
|
|
589
|
+
if isinstance(pk, Mapping):
|
|
590
|
+
for key, val in pk.items():
|
|
591
|
+
insert_payload.setdefault(key, val)
|
|
592
|
+
|
|
593
|
+
if not insert_payload:
|
|
594
|
+
raise ValueError("Unable to derive insert payload for update_or_insert.")
|
|
595
|
+
|
|
596
|
+
exists_where = None
|
|
597
|
+
if where is not None and pk is not None:
|
|
598
|
+
if isinstance(where, Mapping) and isinstance(pk, Mapping):
|
|
599
|
+
combined = dict(where)
|
|
600
|
+
combined.update(pk)
|
|
601
|
+
exists_where = combined
|
|
602
|
+
else:
|
|
603
|
+
exists_where = where
|
|
604
|
+
elif where is not None:
|
|
605
|
+
exists_where = where
|
|
606
|
+
else:
|
|
607
|
+
exists_where = pk
|
|
608
|
+
|
|
609
|
+
ins_builder = getattr(self.sql, "insnx", None) or getattr(
|
|
610
|
+
self.sql, "insert_if_not_exists", None
|
|
611
|
+
)
|
|
612
|
+
if ins_builder is None:
|
|
613
|
+
raise NotImplementedError(
|
|
614
|
+
"Current SQL dialect does not support insert-if-not-exists operations."
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
sql, vals = ins_builder(self.tx, self.name, insert_payload, exists_where)
|
|
618
|
+
if sql_only:
|
|
619
|
+
return {"update": update_stmt, "insert": (sql, vals)}
|
|
620
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
621
|
+
return result.cursor.rowcount if result.cursor else 0
|
|
622
|
+
|
|
623
|
+
updins = update_or_insert
|
|
624
|
+
|
|
625
|
+
@create_missing
|
|
626
|
+
def insert_if_not_exists(self, data, where=None, **kwds):
|
|
627
|
+
"""
|
|
628
|
+
Inserts `data` into the table only if the existence check (`where`) does not match any rows.
|
|
629
|
+
|
|
630
|
+
Usage:
|
|
631
|
+
table.insert_if_not_exists({'key_col': 'k', 'value': 'v'}, where={'key_col': 'k'})
|
|
632
|
+
|
|
633
|
+
:param data: dict of column -> value for insert
|
|
634
|
+
:param where: mapping/list/str used for the EXISTS check; if None primary keys are used and
|
|
635
|
+
must be present in `data`.
|
|
636
|
+
:return: rowcount (0 or 1) or (sql, params) when sql_only=True
|
|
637
|
+
"""
|
|
638
|
+
sql, vals = self.sql.insert_if_not_exists(self.tx, self.name, data, where)
|
|
639
|
+
if kwds.get("sql_only", False):
|
|
640
|
+
return sql, vals
|
|
641
|
+
result = self.tx.execute(sql, vals, cursor=self.cursor())
|
|
642
|
+
return result.cursor.rowcount if result.cursor else 0
|
|
643
|
+
|
|
644
|
+
insnx = insert_if_not_exists
|
|
645
|
+
|
|
490
646
|
upsert = merge
|
|
491
647
|
indate = merge
|
|
492
648
|
|
|
@@ -450,7 +450,7 @@ END;
|
|
|
450
450
|
return "\n".join(statements), tuple()
|
|
451
451
|
|
|
452
452
|
@classmethod
|
|
453
|
-
def ensure_sys_modified_count(cls, name):
|
|
453
|
+
def ensure_sys_modified_count(cls, name, has_column=False, has_row_column=False):
|
|
454
454
|
"""Ensure sys_modified_count column and associated triggers exist for the table."""
|
|
455
455
|
table_identifier = quote(name)
|
|
456
456
|
base_name = name.split(".")[-1].replace("`", "")
|