velocity-python 0.0.105__py3-none-any.whl → 0.0.155__py3-none-any.whl
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.
- velocity/__init__.py +3 -1
- velocity/app/orders.py +3 -4
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- velocity/aws/__init__.py +3 -0
- velocity/aws/amplify.py +10 -6
- velocity/aws/handlers/__init__.py +2 -0
- velocity/aws/handlers/base_handler.py +248 -0
- velocity/aws/handlers/context.py +167 -2
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +24 -85
- velocity/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +181 -0
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/aws/handlers/mixins/error_handler.py +192 -0
- velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
- velocity/aws/handlers/mixins/standard_mixin.py +73 -0
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +28 -143
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/__init__.py +16 -4
- velocity/db/core/decorators.py +20 -4
- velocity/db/core/engine.py +185 -792
- velocity/db/core/result.py +36 -22
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +283 -44
- velocity/db/core/transaction.py +19 -11
- velocity/db/exceptions.py +42 -18
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +718 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +59 -11
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +474 -120
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +677 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +805 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +221 -0
- velocity/db/tests/test_postgres.py +448 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/db/utils.py +62 -47
- velocity/misc/conv/__init__.py +2 -0
- velocity/misc/conv/iconv.py +5 -4
- velocity/misc/export.py +1 -4
- velocity/misc/merge.py +1 -1
- velocity/misc/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- velocity/misc/tools.py +0 -1
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
- velocity_python-0.0.155.dist-info/RECORD +129 -0
- velocity/db/core/exceptions.py +0 -70
- velocity/db/servers/mysql.py +0 -641
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.105.dist-info/RECORD +0 -56
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Handler Module
|
|
3
|
+
|
|
4
|
+
This module provides a base class for handling AWS Lambda events.
|
|
5
|
+
It includes common functionality shared between LambdaHandler and SqsHandler.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from velocity.aws.handlers import context as VelocityContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseHandler:
|
|
17
|
+
"""
|
|
18
|
+
Base class for handling AWS Lambda events.
|
|
19
|
+
|
|
20
|
+
Provides common functionality including action routing, logging,
|
|
21
|
+
and error handling hooks that can be shared across different handler types.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
aws_event: Dict[str, Any],
|
|
27
|
+
aws_context: Any,
|
|
28
|
+
context_class=VelocityContext.Context,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the base handler.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
aws_event: The AWS Lambda event
|
|
35
|
+
aws_context: The AWS Lambda context object
|
|
36
|
+
context_class: The context class to use for processing
|
|
37
|
+
"""
|
|
38
|
+
self.aws_event = aws_event
|
|
39
|
+
self.aws_context = aws_context
|
|
40
|
+
self.serve_action_default = True # Set to False to disable OnActionDefault
|
|
41
|
+
self.skip_action = False # Set to True to skip all actions
|
|
42
|
+
self.ContextClass = context_class
|
|
43
|
+
|
|
44
|
+
# Configure SSL certificates for HTTPS requests
|
|
45
|
+
self._update_lambda_ca_certificates()
|
|
46
|
+
|
|
47
|
+
def _update_lambda_ca_certificates(self):
|
|
48
|
+
"""Configure SSL certificates for HTTPS requests in Lambda environments."""
|
|
49
|
+
# This is helpful for running HTTPS clients on lambda.
|
|
50
|
+
if os.path.exists("/opt/python/ca-certificates.crt"):
|
|
51
|
+
os.environ["REQUESTS_CA_BUNDLE"] = "/opt/python/ca-certificates.crt"
|
|
52
|
+
elif os.path.exists("/home/ubuntu/PyLibLayer/ca-certificates.crt"):
|
|
53
|
+
os.environ["REQUESTS_CA_BUNDLE"] = (
|
|
54
|
+
"/home/ubuntu/PyLibLayer/ca-certificates.crt"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def get_calling_function(self) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Get the name of the calling function by inspecting the call stack.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The name of the calling function or "<Unknown>" if not found
|
|
63
|
+
"""
|
|
64
|
+
skip_functions = {"x", "log", "_transaction", "get_calling_function"}
|
|
65
|
+
|
|
66
|
+
for idx in range(10): # Limit search to prevent infinite loops
|
|
67
|
+
try:
|
|
68
|
+
frame = sys._getframe(idx)
|
|
69
|
+
function_name = frame.f_code.co_name
|
|
70
|
+
|
|
71
|
+
if function_name not in skip_functions:
|
|
72
|
+
return function_name
|
|
73
|
+
|
|
74
|
+
except ValueError:
|
|
75
|
+
# No more frames in the stack
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
return "<Unknown>"
|
|
79
|
+
|
|
80
|
+
def format_action_name(self, action: str) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Format an action string into a method name.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
action: The raw action string (e.g., "create-user", "delete_item")
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Formatted method name (e.g., "OnActionCreateUser", "OnActionDeleteItem")
|
|
89
|
+
"""
|
|
90
|
+
if not action:
|
|
91
|
+
return ""
|
|
92
|
+
|
|
93
|
+
formatted = action.replace("-", " ").replace("_", " ")
|
|
94
|
+
return f"on action {formatted}".title().replace(" ", "")
|
|
95
|
+
|
|
96
|
+
def get_actions_to_execute(self, action: Optional[str]) -> List[str]:
|
|
97
|
+
"""
|
|
98
|
+
Get the list of actions to execute.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
action: The specific action to execute, if any
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of action method names to try executing
|
|
105
|
+
"""
|
|
106
|
+
actions = []
|
|
107
|
+
|
|
108
|
+
# Add specific action if available
|
|
109
|
+
if action:
|
|
110
|
+
action_method = self.format_action_name(action)
|
|
111
|
+
actions.append(action_method)
|
|
112
|
+
|
|
113
|
+
# Add default action if enabled
|
|
114
|
+
if self.serve_action_default:
|
|
115
|
+
actions.append("OnActionDefault")
|
|
116
|
+
|
|
117
|
+
return actions
|
|
118
|
+
|
|
119
|
+
def execute_actions(self, tx, local_context, actions: List[str]) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Execute the appropriate actions for the given context.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
tx: Database transaction object
|
|
125
|
+
local_context: The context object
|
|
126
|
+
actions: List of action method names to try
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if an action was executed, False otherwise
|
|
130
|
+
"""
|
|
131
|
+
# Execute beforeAction hook if available
|
|
132
|
+
if hasattr(self, "beforeAction"):
|
|
133
|
+
self.beforeAction(tx, local_context)
|
|
134
|
+
|
|
135
|
+
action_executed = False
|
|
136
|
+
|
|
137
|
+
# Execute the first matching action
|
|
138
|
+
for action in actions:
|
|
139
|
+
if self.skip_action:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
if hasattr(self, action):
|
|
143
|
+
result = getattr(self, action)(tx, local_context)
|
|
144
|
+
|
|
145
|
+
# Check for deprecated return values (LambdaHandler specific)
|
|
146
|
+
if result is not None and hasattr(self, "_check_deprecated_return"):
|
|
147
|
+
self._check_deprecated_return(action, result)
|
|
148
|
+
|
|
149
|
+
action_executed = True
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
# Execute afterAction hook if available
|
|
153
|
+
if hasattr(self, "afterAction"):
|
|
154
|
+
self.afterAction(tx, local_context)
|
|
155
|
+
|
|
156
|
+
return action_executed
|
|
157
|
+
|
|
158
|
+
def handle_error(self, tx, local_context, exception: Exception):
|
|
159
|
+
"""
|
|
160
|
+
Handle errors that occur during action execution.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
tx: Database transaction object
|
|
164
|
+
local_context: The context object
|
|
165
|
+
exception: The exception that occurred
|
|
166
|
+
"""
|
|
167
|
+
if hasattr(self, "onError"):
|
|
168
|
+
self.onError(
|
|
169
|
+
tx,
|
|
170
|
+
local_context,
|
|
171
|
+
exc=exception.__class__.__name__,
|
|
172
|
+
tb=traceback.format_exc(),
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
# Re-raise if no error handler is defined
|
|
176
|
+
raise exception
|
|
177
|
+
|
|
178
|
+
def log(self, tx, message: str, function: Optional[str] = None):
|
|
179
|
+
"""
|
|
180
|
+
Log a message to the system log table.
|
|
181
|
+
This is a base implementation that should be overridden by subclasses
|
|
182
|
+
to provide handler-specific logging details.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
tx: Database transaction object
|
|
186
|
+
message: The message to log
|
|
187
|
+
function: Optional function name, auto-detected if not provided
|
|
188
|
+
"""
|
|
189
|
+
if not function:
|
|
190
|
+
function = self.get_calling_function()
|
|
191
|
+
|
|
192
|
+
# Base log data - subclasses should extend this
|
|
193
|
+
data = {
|
|
194
|
+
"app_name": os.environ.get("ProjectName", "Unknown"),
|
|
195
|
+
"function": function,
|
|
196
|
+
"message": message,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Let subclasses add their specific fields
|
|
200
|
+
self._extend_log_data(data)
|
|
201
|
+
|
|
202
|
+
tx.table("sys_log").insert(data)
|
|
203
|
+
|
|
204
|
+
def _extend_log_data(self, data: Dict[str, Any]):
|
|
205
|
+
"""
|
|
206
|
+
Extend log data with handler-specific fields.
|
|
207
|
+
To be overridden by subclasses.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
data: The base log data dictionary to extend
|
|
211
|
+
"""
|
|
212
|
+
# Default implementation adds basic Lambda info
|
|
213
|
+
data.update(
|
|
214
|
+
{
|
|
215
|
+
"user_agent": "AWS Lambda",
|
|
216
|
+
"device_type": "Lambda",
|
|
217
|
+
"sys_modified_by": "Lambda",
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def OnActionDefault(self, local_context):
|
|
222
|
+
"""
|
|
223
|
+
Default action handler when no specific action is found.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
local_context: The context object
|
|
227
|
+
"""
|
|
228
|
+
action = getattr(local_context, "action", lambda: "unknown")()
|
|
229
|
+
warning_message = (
|
|
230
|
+
f"[Warn] Action handler not found. Calling default action "
|
|
231
|
+
f"`{self.__class__.__name__}.OnActionDefault` with the following parameters:\n"
|
|
232
|
+
f" - action: {action}\n"
|
|
233
|
+
f" - handler: {self.__class__.__name__}"
|
|
234
|
+
)
|
|
235
|
+
print(warning_message)
|
|
236
|
+
|
|
237
|
+
def get_context_args(self) -> Dict[str, Any]:
|
|
238
|
+
"""
|
|
239
|
+
Get the arguments to pass to the context constructor.
|
|
240
|
+
To be implemented by subclasses based on their specific needs.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Dictionary of arguments for context initialization
|
|
244
|
+
"""
|
|
245
|
+
return {
|
|
246
|
+
"aws_event": self.aws_event,
|
|
247
|
+
"aws_context": self.aws_context,
|
|
248
|
+
}
|
velocity/aws/handlers/context.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import boto3
|
|
3
|
+
import uuid
|
|
1
4
|
import support.app
|
|
5
|
+
from velocity.misc.format import to_json
|
|
6
|
+
from velocity.misc.merge import deep_merge
|
|
7
|
+
from datetime import datetime
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
import velocity.db
|
|
10
|
+
|
|
11
|
+
engine = velocity.db.postgres.initialize()
|
|
4
12
|
|
|
5
13
|
|
|
6
14
|
@engine.transaction
|
|
@@ -12,7 +20,7 @@ class Context:
|
|
|
12
20
|
self.__args = args
|
|
13
21
|
self.__postdata = postdata
|
|
14
22
|
self.__response = response
|
|
15
|
-
self.__session = session
|
|
23
|
+
self.__session = {} if session is None else session
|
|
16
24
|
self.__aws_event = aws_event
|
|
17
25
|
self.__aws_context = aws_context
|
|
18
26
|
self.__log = log
|
|
@@ -68,3 +76,160 @@ class Context:
|
|
|
68
76
|
print(f"{function}: {message}")
|
|
69
77
|
else:
|
|
70
78
|
print(f"{message}")
|
|
79
|
+
|
|
80
|
+
def update_job(self, tx, data=None):
|
|
81
|
+
"""Update job status and message in aws_job_activity table.
|
|
82
|
+
|
|
83
|
+
This method only UPDATES existing jobs. For creating new jobs, use create_job.
|
|
84
|
+
"""
|
|
85
|
+
if not data:
|
|
86
|
+
return
|
|
87
|
+
if self.postdata("job_id"):
|
|
88
|
+
# Sanitize data before storing in database
|
|
89
|
+
sanitized_data = self._sanitize_job_data(data)
|
|
90
|
+
tx.table("aws_job_activity").update(
|
|
91
|
+
sanitized_data, {"job_id": self.postdata("job_id")}
|
|
92
|
+
)
|
|
93
|
+
tx.commit()
|
|
94
|
+
|
|
95
|
+
def create_job(self, tx, job_data=None):
|
|
96
|
+
"""Create a new job record in aws_job_activity table using independent transaction."""
|
|
97
|
+
if not job_data:
|
|
98
|
+
return
|
|
99
|
+
sanitized_data = self._sanitize_job_data(job_data)
|
|
100
|
+
tx.table("aws_job_activity").insert(sanitized_data)
|
|
101
|
+
tx.commit()
|
|
102
|
+
|
|
103
|
+
def _sanitize_job_data(self, data):
|
|
104
|
+
"""Sanitize sensitive data before storing in aws_job_activity table."""
|
|
105
|
+
if not isinstance(data, dict):
|
|
106
|
+
return data
|
|
107
|
+
|
|
108
|
+
sanitized = {}
|
|
109
|
+
|
|
110
|
+
# List of sensitive field patterns to sanitize
|
|
111
|
+
sensitive_patterns = [
|
|
112
|
+
"password",
|
|
113
|
+
"token",
|
|
114
|
+
"secret",
|
|
115
|
+
"key",
|
|
116
|
+
"credential",
|
|
117
|
+
"auth",
|
|
118
|
+
"cognito_user",
|
|
119
|
+
"session",
|
|
120
|
+
"cookie",
|
|
121
|
+
"authorization",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
for key, value in data.items():
|
|
125
|
+
# Check if key contains sensitive patterns
|
|
126
|
+
if any(pattern in key.lower() for pattern in sensitive_patterns):
|
|
127
|
+
sanitized[key] = "[REDACTED]" if value else value
|
|
128
|
+
elif key == "error" and value:
|
|
129
|
+
# Sanitize error messages - keep first 500 chars and remove potential sensitive info
|
|
130
|
+
error_str = str(value)[:500]
|
|
131
|
+
for pattern in sensitive_patterns:
|
|
132
|
+
if pattern in error_str.lower():
|
|
133
|
+
# Replace potential sensitive values with placeholder
|
|
134
|
+
import re
|
|
135
|
+
|
|
136
|
+
# Remove patterns like password=value, token=value, etc.
|
|
137
|
+
error_str = re.sub(
|
|
138
|
+
rf"{pattern}[=:]\s*[^\s,\]}}]+",
|
|
139
|
+
f"{pattern}=[REDACTED]",
|
|
140
|
+
error_str,
|
|
141
|
+
flags=re.IGNORECASE,
|
|
142
|
+
)
|
|
143
|
+
sanitized[key] = error_str
|
|
144
|
+
elif key == "traceback" and value:
|
|
145
|
+
# Sanitize traceback - keep structure but remove sensitive values
|
|
146
|
+
tb_str = str(value)
|
|
147
|
+
for pattern in sensitive_patterns:
|
|
148
|
+
if pattern in tb_str.lower():
|
|
149
|
+
import re
|
|
150
|
+
|
|
151
|
+
# Remove patterns like password=value, token=value, etc.
|
|
152
|
+
tb_str = re.sub(
|
|
153
|
+
rf"{pattern}[=:]\s*[^\s,\]}}]+",
|
|
154
|
+
f"{pattern}=[REDACTED]",
|
|
155
|
+
tb_str,
|
|
156
|
+
flags=re.IGNORECASE,
|
|
157
|
+
)
|
|
158
|
+
# Limit traceback size to prevent DB bloat
|
|
159
|
+
sanitized[key] = tb_str[:2000]
|
|
160
|
+
elif key == "message" and value:
|
|
161
|
+
# Sanitize message field
|
|
162
|
+
message_str = str(value)
|
|
163
|
+
for pattern in sensitive_patterns:
|
|
164
|
+
if pattern in message_str.lower():
|
|
165
|
+
import re
|
|
166
|
+
|
|
167
|
+
message_str = re.sub(
|
|
168
|
+
rf"{pattern}[=:]\s*[^\s,\]}}]+",
|
|
169
|
+
f"{pattern}=[REDACTED]",
|
|
170
|
+
message_str,
|
|
171
|
+
flags=re.IGNORECASE,
|
|
172
|
+
)
|
|
173
|
+
sanitized[key] = message_str[:1000] # Limit message size
|
|
174
|
+
else:
|
|
175
|
+
# For other fields, copy as-is but check for nested dicts
|
|
176
|
+
if isinstance(value, dict):
|
|
177
|
+
sanitized[key] = self._sanitize_job_data(value)
|
|
178
|
+
elif isinstance(value, str) and len(value) > 5000:
|
|
179
|
+
# Limit very large string fields
|
|
180
|
+
sanitized[key] = value[:5000] + "...[TRUNCATED]"
|
|
181
|
+
else:
|
|
182
|
+
sanitized[key] = value
|
|
183
|
+
|
|
184
|
+
return sanitized
|
|
185
|
+
|
|
186
|
+
def enqueue(self, action, payload={}, user=None, suppress_job_activity=False):
|
|
187
|
+
"""
|
|
188
|
+
Enqueue jobs to SQS with independent job activity tracking.
|
|
189
|
+
|
|
190
|
+
This method uses its own transaction for aws_job_activity updates to ensure
|
|
191
|
+
job tracking is never rolled back with other operations.
|
|
192
|
+
"""
|
|
193
|
+
batch_id = str(uuid.uuid4())
|
|
194
|
+
results = {"batch_id": batch_id}
|
|
195
|
+
queue = boto3.resource("sqs").get_queue_by_name(
|
|
196
|
+
QueueName=os.environ["SqsWorkQueue"]
|
|
197
|
+
)
|
|
198
|
+
if isinstance(payload, dict):
|
|
199
|
+
payload = [payload]
|
|
200
|
+
messages = []
|
|
201
|
+
if user is None:
|
|
202
|
+
user = self.session().get("email_address") or "EnqueueTasks"
|
|
203
|
+
for item in payload:
|
|
204
|
+
message = {"action": action, "payload": item}
|
|
205
|
+
id = str(uuid.uuid4()).split("-")[0]
|
|
206
|
+
if suppress_job_activity:
|
|
207
|
+
messages.append({"Id": id, "MessageBody": to_json(message)})
|
|
208
|
+
else:
|
|
209
|
+
message["job_id"] = id
|
|
210
|
+
# Use separate transaction for job activity - this should never be rolled back
|
|
211
|
+
self.create_job(
|
|
212
|
+
{
|
|
213
|
+
"action": action,
|
|
214
|
+
"initial_timestamp": datetime.now(),
|
|
215
|
+
"created_by": user,
|
|
216
|
+
"sys_modified_by": user,
|
|
217
|
+
"payload": to_json(message),
|
|
218
|
+
"batch_id": str(batch_id),
|
|
219
|
+
"job_id": id,
|
|
220
|
+
"status": "Initialized",
|
|
221
|
+
"message": "Job Initialized",
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
messages.append({"Id": id, "MessageBody": to_json(message)})
|
|
225
|
+
|
|
226
|
+
if len(messages) == 10:
|
|
227
|
+
result = queue.send_messages(Entries=messages)
|
|
228
|
+
results = deep_merge(results, result)
|
|
229
|
+
messages.clear()
|
|
230
|
+
|
|
231
|
+
if messages:
|
|
232
|
+
result = queue.send_messages(Entries=messages)
|
|
233
|
+
results = deep_merge(results, result)
|
|
234
|
+
|
|
235
|
+
return results
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class AppError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AccessError(AppError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AlertError(AppError):
|
|
10
|
+
def get_payload(self):
|
|
11
|
+
data = self.args[0]
|
|
12
|
+
if isinstance(data, str):
|
|
13
|
+
return {"message": data}
|
|
14
|
+
elif isinstance(data, dict):
|
|
15
|
+
return data
|
|
16
|
+
return {"message": "No message was supplied to AlertError"}
|
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
from velocity.misc.format import to_json
|
|
2
1
|
import json
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import traceback
|
|
6
|
-
from support.app import DEBUG
|
|
7
|
-
from support.app import helpers, AlertError, enqueue
|
|
8
|
-
|
|
9
|
-
from velocity.aws.handlers import Response
|
|
10
|
-
|
|
2
|
+
from velocity.aws.handlers.base_handler import BaseHandler
|
|
3
|
+
from velocity.aws.handlers.response import Response
|
|
11
4
|
from . import context
|
|
12
5
|
|
|
6
|
+
# TODO: helpers import needs to be resolved - may need to pass table name instead
|
|
7
|
+
# from some_app import helpers
|
|
8
|
+
|
|
13
9
|
|
|
14
|
-
class LambdaHandler:
|
|
10
|
+
class LambdaHandler(BaseHandler):
|
|
15
11
|
def __init__(self, aws_event, aws_context, context_class=context.Context):
|
|
16
|
-
|
|
17
|
-
self.aws_context = aws_context
|
|
18
|
-
self.serve_action_default = True # Set to False to disable OnActionDefault
|
|
19
|
-
self.skip_action = False # Set to True to skip all actions
|
|
12
|
+
super().__init__(aws_event, aws_context, context_class)
|
|
20
13
|
|
|
14
|
+
# LambdaHandler-specific initialization
|
|
21
15
|
requestContext = aws_event.get("requestContext") or {}
|
|
22
16
|
identity = requestContext.get("identity") or {}
|
|
23
17
|
headers = aws_event.get("headers") or {}
|
|
@@ -48,36 +42,6 @@ class LambdaHandler:
|
|
|
48
42
|
else:
|
|
49
43
|
self.session["device_type"] = "unknown"
|
|
50
44
|
|
|
51
|
-
self.ContextClass = context_class
|
|
52
|
-
|
|
53
|
-
def log(self, tx, message, function=None):
|
|
54
|
-
if not function:
|
|
55
|
-
function = "<Unknown>"
|
|
56
|
-
idx = 0
|
|
57
|
-
while True:
|
|
58
|
-
try:
|
|
59
|
-
temp = sys._getframe(idx).f_code.co_name
|
|
60
|
-
except ValueError as e:
|
|
61
|
-
break
|
|
62
|
-
if temp in ["x", "log", "_transaction"]:
|
|
63
|
-
idx += 1
|
|
64
|
-
continue
|
|
65
|
-
function = temp
|
|
66
|
-
break
|
|
67
|
-
|
|
68
|
-
data = {
|
|
69
|
-
"app_name": os.environ["ProjectName"],
|
|
70
|
-
"source_ip": self.session["source_ip"],
|
|
71
|
-
"referer": self.session["referer"],
|
|
72
|
-
"user_agent": self.session["user_agent"],
|
|
73
|
-
"device_type": self.session["device_type"],
|
|
74
|
-
"function": function,
|
|
75
|
-
"message": message,
|
|
76
|
-
}
|
|
77
|
-
if "email_address" in self.session:
|
|
78
|
-
data["sys_modified_by"] = self.session["email_address"]
|
|
79
|
-
tx.table("sys_log").insert(data)
|
|
80
|
-
|
|
81
45
|
def serve(self, tx):
|
|
82
46
|
response = Response()
|
|
83
47
|
body = self.aws_event.get("body")
|
|
@@ -85,7 +49,7 @@ class LambdaHandler:
|
|
|
85
49
|
if isinstance(body, str) and len(body) > 0:
|
|
86
50
|
try:
|
|
87
51
|
postdata = json.loads(body)
|
|
88
|
-
except:
|
|
52
|
+
except (json.JSONDecodeError, TypeError):
|
|
89
53
|
postdata = {"raw_body": body}
|
|
90
54
|
elif isinstance(body, dict):
|
|
91
55
|
postdata = body
|
|
@@ -93,7 +57,7 @@ class LambdaHandler:
|
|
|
93
57
|
try:
|
|
94
58
|
new = "\n".join(body)
|
|
95
59
|
postdata = json.loads(new)
|
|
96
|
-
except:
|
|
60
|
+
except (json.JSONDecodeError, TypeError):
|
|
97
61
|
postdata = {"raw_body": body}
|
|
98
62
|
|
|
99
63
|
req_params = self.aws_event.get("queryStringParameters") or {}
|
|
@@ -106,43 +70,20 @@ class LambdaHandler:
|
|
|
106
70
|
session=self.session,
|
|
107
71
|
log=lambda message, function=None: self.log(message, function),
|
|
108
72
|
)
|
|
73
|
+
|
|
74
|
+
# Determine action from postdata or query parameters
|
|
75
|
+
action = postdata.get("action") or req_params.get("action")
|
|
76
|
+
|
|
77
|
+
# Get the list of actions to execute
|
|
78
|
+
actions = self.get_actions_to_execute(action)
|
|
79
|
+
|
|
80
|
+
# Use BaseHandler's execute_actions method
|
|
109
81
|
try:
|
|
110
|
-
|
|
111
|
-
self.beforeAction(local_context)
|
|
112
|
-
actions = []
|
|
113
|
-
action = postdata.get("action", req_params.get("action"))
|
|
114
|
-
if action:
|
|
115
|
-
actions.append(
|
|
116
|
-
f"on action {action.replace('-', ' ').replace('_', ' ')}".title().replace(
|
|
117
|
-
" ", ""
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
if self.serve_action_default:
|
|
121
|
-
actions.append("OnActionDefault")
|
|
122
|
-
for action in actions:
|
|
123
|
-
if self.skip_action:
|
|
124
|
-
break
|
|
125
|
-
if hasattr(self, action):
|
|
126
|
-
result = getattr(self, action)(local_context)
|
|
127
|
-
if result is not None:
|
|
128
|
-
raise Exception(
|
|
129
|
-
f"Deprecated Feature Error: Action {action} returned a response but this is not allowed. Use Repsonse object instead: {type(result)}"
|
|
130
|
-
)
|
|
131
|
-
break
|
|
132
|
-
if hasattr(self, "afterAction"):
|
|
133
|
-
self.afterAction(local_context)
|
|
134
|
-
except AlertError as e:
|
|
135
|
-
response.alert(e.get_payload())
|
|
82
|
+
self.execute_actions(tx, local_context, actions)
|
|
136
83
|
except Exception as e:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
local_context,
|
|
141
|
-
exc=e.__class__.__name__,
|
|
142
|
-
tb=traceback.format_exc(),
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
return response.render()
|
|
84
|
+
self.handle_error(tx, local_context, e)
|
|
85
|
+
|
|
86
|
+
return local_context.response().render()
|
|
146
87
|
|
|
147
88
|
def track(self, tx, data={}, user=None):
|
|
148
89
|
data = data.copy()
|
|
@@ -155,7 +96,8 @@ class LambdaHandler:
|
|
|
155
96
|
"sys_modified_by": self.session["email_address"],
|
|
156
97
|
}
|
|
157
98
|
)
|
|
158
|
-
|
|
99
|
+
# TODO: Fix undefined helpers reference
|
|
100
|
+
# tx.table(helpers.get_tracking_table(user or self.session)).insert(data)
|
|
159
101
|
|
|
160
102
|
def OnActionDefault(self, tx, context):
|
|
161
103
|
context.response().set_body(
|
|
@@ -164,6 +106,3 @@ class LambdaHandler:
|
|
|
164
106
|
|
|
165
107
|
def OnActionTracking(self, tx, context):
|
|
166
108
|
self.track(tx, context.payload().get("data", {}))
|
|
167
|
-
|
|
168
|
-
def enqueue(self, tx, action, payload={}):
|
|
169
|
-
enqueue(tx, action, payload, self.session["email_address"])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mixins for AWS Lambda handlers.
|
|
3
|
+
|
|
4
|
+
This package provides reusable mixins for common Lambda handler functionality:
|
|
5
|
+
- ActivityTracker: Standardized activity logging and tracking
|
|
6
|
+
- ErrorHandler: Standardized error handling and notifications
|
|
7
|
+
- StandardMixin: Combined mixin for most common use cases
|
|
8
|
+
- LegacyMixin: Backward-compatible enhanced tracking for existing handlers
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .activity_tracker import ActivityTracker
|
|
12
|
+
from .error_handler import ErrorHandler
|
|
13
|
+
from .standard_mixin import StandardMixin
|
|
14
|
+
from .legacy_mixin import LegacyMixin
|
|
15
|
+
|
|
16
|
+
__all__ = ['ActivityTracker', 'ErrorHandler', 'StandardMixin', 'LegacyMixin']
|