velocity-python 0.0.115__tar.gz → 0.0.117__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.115 → velocity_python-0.0.117}/PKG-INFO +1 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/pyproject.toml +1 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/__init__.py +1 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/aws/amplify.py +8 -3
- velocity_python-0.0.117/src/velocity/aws/handlers/base_handler.py +245 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/aws/handlers/context.py +66 -44
- velocity_python-0.0.117/src/velocity/aws/handlers/exceptions.py +16 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/aws/handlers/lambda_handler.py +12 -77
- velocity_python-0.0.117/src/velocity/aws/handlers/sqs_handler.py +108 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/__init__.py +1 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/engine.py +30 -26
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/exceptions.py +3 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/result.py +30 -22
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/row.py +3 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/table.py +2 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/exceptions.py +35 -18
- velocity_python-0.0.117/src/velocity/db/servers/postgres/__init__.py +17 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/postgres/sql.py +32 -13
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/tablehelper.py +117 -91
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/utils.py +61 -46
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity_python.egg-info/SOURCES.txt +2 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_cursor_rowcount_fix.py +33 -33
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_db_utils.py +72 -46
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_fix.py +43 -13
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_oconv.py +3 -1
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_original_error.py +26 -8
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_payment_profile_sorting.py +79 -32
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_postgres.py +14 -13
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_process_error_robustness.py +132 -76
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_response.py +3 -3
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_result_caching.py +77 -75
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_result_sql_aware.py +37 -35
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_row_get_missing_column.py +32 -28
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_tablehelper.py +248 -200
- velocity_python-0.0.115/src/velocity/aws/handlers/sqs_handler.py +0 -224
- velocity_python-0.0.115/src/velocity/db/servers/postgres/__init__.py +0 -19
- {velocity_python-0.0.115 → velocity_python-0.0.117}/LICENSE +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/README.md +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/setup.cfg +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/mysql.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/mysql_reserved.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/sqlite.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/sqlite_reserved.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/sqlserver.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/db/servers/sqlserver_reserved.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_db.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_format.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_iconv.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_merge.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.115 → velocity_python-0.0.117}/tests/test_timer.py +0 -0
|
@@ -320,12 +320,17 @@ class AmplifyProject:
|
|
|
320
320
|
"Effect": "Allow",
|
|
321
321
|
"Resource": "*",
|
|
322
322
|
"Action": [
|
|
323
|
-
|
|
323
|
+
"ec2:DescribeInstances",
|
|
324
324
|
"ec2:CreateNetworkInterface",
|
|
325
325
|
"ec2:AttachNetworkInterface",
|
|
326
|
-
"ec2:DescribeNetworkInterfaces",
|
|
326
|
+
"ec2:DescribeNetworkInterfaces",
|
|
327
327
|
"ec2:DeleteNetworkInterface",
|
|
328
|
-
"
|
|
328
|
+
"ec2:DescribeSecurityGroups",
|
|
329
|
+
"ec2:AuthorizeSecurityGroupIngress",
|
|
330
|
+
"ec2:AuthorizeSecurityGroupEgress",
|
|
331
|
+
"ec2:RevokeSecurityGroupIngress",
|
|
332
|
+
"ec2:RevokeSecurityGroupEgress",
|
|
333
|
+
"cognito-idp:*"
|
|
329
334
|
],
|
|
330
335
|
}
|
|
331
336
|
],
|
|
@@ -0,0 +1,245 @@
|
|
|
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, local_context, actions: List[str]) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Execute the appropriate actions for the given context.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
local_context: The context object
|
|
125
|
+
actions: List of action method names to try
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
True if an action was executed, False otherwise
|
|
129
|
+
"""
|
|
130
|
+
# Execute beforeAction hook if available
|
|
131
|
+
if hasattr(self, "beforeAction"):
|
|
132
|
+
self.beforeAction(local_context)
|
|
133
|
+
|
|
134
|
+
action_executed = False
|
|
135
|
+
|
|
136
|
+
# Execute the first matching action
|
|
137
|
+
for action in actions:
|
|
138
|
+
if self.skip_action:
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
if hasattr(self, action):
|
|
142
|
+
result = getattr(self, action)(local_context)
|
|
143
|
+
|
|
144
|
+
# Check for deprecated return values (LambdaHandler specific)
|
|
145
|
+
if result is not None and hasattr(self, "_check_deprecated_return"):
|
|
146
|
+
self._check_deprecated_return(action, result)
|
|
147
|
+
|
|
148
|
+
action_executed = True
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
# Execute afterAction hook if available
|
|
152
|
+
if hasattr(self, "afterAction"):
|
|
153
|
+
self.afterAction(local_context)
|
|
154
|
+
|
|
155
|
+
return action_executed
|
|
156
|
+
|
|
157
|
+
def handle_error(self, local_context, exception: Exception):
|
|
158
|
+
"""
|
|
159
|
+
Handle errors that occur during action execution.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
local_context: The context object
|
|
163
|
+
exception: The exception that occurred
|
|
164
|
+
"""
|
|
165
|
+
if hasattr(self, "onError"):
|
|
166
|
+
self.onError(
|
|
167
|
+
local_context,
|
|
168
|
+
exc=exception.__class__.__name__,
|
|
169
|
+
tb=traceback.format_exc(),
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
# Re-raise if no error handler is defined
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
def log(self, tx, message: str, function: Optional[str] = None):
|
|
176
|
+
"""
|
|
177
|
+
Log a message to the system log table.
|
|
178
|
+
This is a base implementation that should be overridden by subclasses
|
|
179
|
+
to provide handler-specific logging details.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
tx: Database transaction object
|
|
183
|
+
message: The message to log
|
|
184
|
+
function: Optional function name, auto-detected if not provided
|
|
185
|
+
"""
|
|
186
|
+
if not function:
|
|
187
|
+
function = self.get_calling_function()
|
|
188
|
+
|
|
189
|
+
# Base log data - subclasses should extend this
|
|
190
|
+
data = {
|
|
191
|
+
"app_name": os.environ.get("ProjectName", "Unknown"),
|
|
192
|
+
"function": function,
|
|
193
|
+
"message": message,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Let subclasses add their specific fields
|
|
197
|
+
self._extend_log_data(data)
|
|
198
|
+
|
|
199
|
+
tx.table("sys_log").insert(data)
|
|
200
|
+
|
|
201
|
+
def _extend_log_data(self, data: Dict[str, Any]):
|
|
202
|
+
"""
|
|
203
|
+
Extend log data with handler-specific fields.
|
|
204
|
+
To be overridden by subclasses.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
data: The base log data dictionary to extend
|
|
208
|
+
"""
|
|
209
|
+
# Default implementation adds basic Lambda info
|
|
210
|
+
data.update(
|
|
211
|
+
{
|
|
212
|
+
"user_agent": "AWS Lambda",
|
|
213
|
+
"device_type": "Lambda",
|
|
214
|
+
"sys_modified_by": "Lambda",
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def OnActionDefault(self, local_context):
|
|
219
|
+
"""
|
|
220
|
+
Default action handler when no specific action is found.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
local_context: The context object
|
|
224
|
+
"""
|
|
225
|
+
action = getattr(local_context, "action", lambda: "unknown")()
|
|
226
|
+
warning_message = (
|
|
227
|
+
f"[Warn] Action handler not found. Calling default action "
|
|
228
|
+
f"`{self.__class__.__name__}.OnActionDefault` with the following parameters:\n"
|
|
229
|
+
f" - action: {action}\n"
|
|
230
|
+
f" - handler: {self.__class__.__name__}"
|
|
231
|
+
)
|
|
232
|
+
print(warning_message)
|
|
233
|
+
|
|
234
|
+
def get_context_args(self) -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Get the arguments to pass to the context constructor.
|
|
237
|
+
To be implemented by subclasses based on their specific needs.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Dictionary of arguments for context initialization
|
|
241
|
+
"""
|
|
242
|
+
return {
|
|
243
|
+
"aws_event": self.aws_event,
|
|
244
|
+
"aws_context": self.aws_context,
|
|
245
|
+
}
|
|
@@ -75,74 +75,98 @@ class Context:
|
|
|
75
75
|
else:
|
|
76
76
|
print(f"{message}")
|
|
77
77
|
|
|
78
|
-
def
|
|
79
|
-
"""Update job status and message in aws_job_activity table.
|
|
78
|
+
def update_job(self, tx, data=None):
|
|
79
|
+
"""Update job status and message in aws_job_activity table.
|
|
80
|
+
|
|
81
|
+
This method only UPDATES existing jobs. For creating new jobs, use create_job.
|
|
82
|
+
"""
|
|
80
83
|
if not data:
|
|
81
84
|
return
|
|
82
85
|
if self.postdata("job_id"):
|
|
83
86
|
# Sanitize data before storing in database
|
|
84
87
|
sanitized_data = self._sanitize_job_data(data)
|
|
85
|
-
tx.table("aws_job_activity").update(
|
|
88
|
+
tx.table("aws_job_activity").update(
|
|
89
|
+
sanitized_data, {"job_id": self.postdata("job_id")}
|
|
90
|
+
)
|
|
86
91
|
tx.commit()
|
|
87
92
|
|
|
93
|
+
def create_job(self, tx, job_data=None):
|
|
94
|
+
"""Create a new job record in aws_job_activity table using independent transaction."""
|
|
95
|
+
if not job_data:
|
|
96
|
+
return
|
|
97
|
+
sanitized_data = self._sanitize_job_data(job_data)
|
|
98
|
+
tx.table("aws_job_activity").insert(sanitized_data)
|
|
99
|
+
tx.commit()
|
|
100
|
+
|
|
88
101
|
def _sanitize_job_data(self, data):
|
|
89
102
|
"""Sanitize sensitive data before storing in aws_job_activity table."""
|
|
90
103
|
if not isinstance(data, dict):
|
|
91
104
|
return data
|
|
92
|
-
|
|
105
|
+
|
|
93
106
|
sanitized = {}
|
|
94
|
-
|
|
107
|
+
|
|
95
108
|
# List of sensitive field patterns to sanitize
|
|
96
109
|
sensitive_patterns = [
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
"password",
|
|
111
|
+
"token",
|
|
112
|
+
"secret",
|
|
113
|
+
"key",
|
|
114
|
+
"credential",
|
|
115
|
+
"auth",
|
|
116
|
+
"cognito_user",
|
|
117
|
+
"session",
|
|
118
|
+
"cookie",
|
|
119
|
+
"authorization",
|
|
99
120
|
]
|
|
100
|
-
|
|
121
|
+
|
|
101
122
|
for key, value in data.items():
|
|
102
123
|
# Check if key contains sensitive patterns
|
|
103
124
|
if any(pattern in key.lower() for pattern in sensitive_patterns):
|
|
104
125
|
sanitized[key] = "[REDACTED]" if value else value
|
|
105
|
-
elif key ==
|
|
126
|
+
elif key == "error" and value:
|
|
106
127
|
# Sanitize error messages - keep first 500 chars and remove potential sensitive info
|
|
107
128
|
error_str = str(value)[:500]
|
|
108
129
|
for pattern in sensitive_patterns:
|
|
109
130
|
if pattern in error_str.lower():
|
|
110
131
|
# Replace potential sensitive values with placeholder
|
|
111
132
|
import re
|
|
133
|
+
|
|
112
134
|
# Remove patterns like password=value, token=value, etc.
|
|
113
135
|
error_str = re.sub(
|
|
114
|
-
rf
|
|
115
|
-
f
|
|
116
|
-
error_str,
|
|
117
|
-
flags=re.IGNORECASE
|
|
136
|
+
rf"{pattern}[=:]\s*[^\s,\]}}]+",
|
|
137
|
+
f"{pattern}=[REDACTED]",
|
|
138
|
+
error_str,
|
|
139
|
+
flags=re.IGNORECASE,
|
|
118
140
|
)
|
|
119
141
|
sanitized[key] = error_str
|
|
120
|
-
elif key ==
|
|
142
|
+
elif key == "traceback" and value:
|
|
121
143
|
# Sanitize traceback - keep structure but remove sensitive values
|
|
122
144
|
tb_str = str(value)
|
|
123
145
|
for pattern in sensitive_patterns:
|
|
124
146
|
if pattern in tb_str.lower():
|
|
125
147
|
import re
|
|
148
|
+
|
|
126
149
|
# Remove patterns like password=value, token=value, etc.
|
|
127
150
|
tb_str = re.sub(
|
|
128
|
-
rf
|
|
129
|
-
f
|
|
130
|
-
tb_str,
|
|
131
|
-
flags=re.IGNORECASE
|
|
151
|
+
rf"{pattern}[=:]\s*[^\s,\]}}]+",
|
|
152
|
+
f"{pattern}=[REDACTED]",
|
|
153
|
+
tb_str,
|
|
154
|
+
flags=re.IGNORECASE,
|
|
132
155
|
)
|
|
133
156
|
# Limit traceback size to prevent DB bloat
|
|
134
157
|
sanitized[key] = tb_str[:2000]
|
|
135
|
-
elif key ==
|
|
158
|
+
elif key == "message" and value:
|
|
136
159
|
# Sanitize message field
|
|
137
160
|
message_str = str(value)
|
|
138
161
|
for pattern in sensitive_patterns:
|
|
139
162
|
if pattern in message_str.lower():
|
|
140
163
|
import re
|
|
164
|
+
|
|
141
165
|
message_str = re.sub(
|
|
142
|
-
rf
|
|
143
|
-
f
|
|
144
|
-
message_str,
|
|
145
|
-
flags=re.IGNORECASE
|
|
166
|
+
rf"{pattern}[=:]\s*[^\s,\]}}]+",
|
|
167
|
+
f"{pattern}=[REDACTED]",
|
|
168
|
+
message_str,
|
|
169
|
+
flags=re.IGNORECASE,
|
|
146
170
|
)
|
|
147
171
|
sanitized[key] = message_str[:1000] # Limit message size
|
|
148
172
|
else:
|
|
@@ -154,13 +178,13 @@ class Context:
|
|
|
154
178
|
sanitized[key] = value[:5000] + "...[TRUNCATED]"
|
|
155
179
|
else:
|
|
156
180
|
sanitized[key] = value
|
|
157
|
-
|
|
181
|
+
|
|
158
182
|
return sanitized
|
|
159
183
|
|
|
160
184
|
def enqueue(self, action, payload={}, user=None, suppress_job_activity=False):
|
|
161
185
|
"""
|
|
162
186
|
Enqueue jobs to SQS with independent job activity tracking.
|
|
163
|
-
|
|
187
|
+
|
|
164
188
|
This method uses its own transaction for aws_job_activity updates to ensure
|
|
165
189
|
job tracking is never rolled back with other operations.
|
|
166
190
|
"""
|
|
@@ -172,7 +196,7 @@ class Context:
|
|
|
172
196
|
if isinstance(payload, dict):
|
|
173
197
|
payload = [payload]
|
|
174
198
|
messages = []
|
|
175
|
-
if user==None:
|
|
199
|
+
if user == None:
|
|
176
200
|
user = self.session.get("email_address") or "EnqueueTasks"
|
|
177
201
|
for item in payload:
|
|
178
202
|
message = {"action": action, "payload": item}
|
|
@@ -182,30 +206,28 @@ class Context:
|
|
|
182
206
|
else:
|
|
183
207
|
message["job_id"] = id
|
|
184
208
|
# Use separate transaction for job activity - this should never be rolled back
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
job_tx.commit()
|
|
209
|
+
self.create_job(
|
|
210
|
+
{
|
|
211
|
+
"action": action,
|
|
212
|
+
"initial_timestamp": datetime.now(),
|
|
213
|
+
"created_by": user,
|
|
214
|
+
"sys_modified_by": user,
|
|
215
|
+
"payload": to_json(message),
|
|
216
|
+
"batch_id": str(batch_id),
|
|
217
|
+
"job_id": id,
|
|
218
|
+
"status": "Initialized",
|
|
219
|
+
"message": f"Job Initialized",
|
|
220
|
+
}
|
|
221
|
+
)
|
|
200
222
|
messages.append({"Id": id, "MessageBody": to_json(message)})
|
|
201
|
-
|
|
223
|
+
|
|
202
224
|
if len(messages) == 10:
|
|
203
225
|
result = queue.send_messages(Entries=messages)
|
|
204
226
|
results = deep_merge(results, result)
|
|
205
227
|
messages.clear()
|
|
206
|
-
|
|
228
|
+
|
|
207
229
|
if messages:
|
|
208
230
|
result = queue.send_messages(Entries=messages)
|
|
209
231
|
results = deep_merge(results, result)
|
|
210
|
-
|
|
232
|
+
|
|
211
233
|
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"}
|
{velocity_python-0.0.115 → velocity_python-0.0.117}/src/velocity/aws/handlers/lambda_handler.py
RENAMED
|
@@ -3,21 +3,20 @@ import json
|
|
|
3
3
|
import sys
|
|
4
4
|
import os
|
|
5
5
|
import traceback
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
from velocity.aws.handlers import Response
|
|
10
|
-
|
|
6
|
+
from velocity.aws.handlers.base_handler import BaseHandler
|
|
7
|
+
from velocity.aws.handlers.response import Response
|
|
8
|
+
from velocity.aws.handlers.exceptions import AlertError
|
|
11
9
|
from . import context
|
|
12
10
|
|
|
11
|
+
# TODO: helpers import needs to be resolved - may need to pass table name instead
|
|
12
|
+
# from some_app import helpers
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
class LambdaHandler(BaseHandler):
|
|
15
16
|
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
|
|
17
|
+
super().__init__(aws_event, aws_context, context_class)
|
|
20
18
|
|
|
19
|
+
# LambdaHandler-specific initialization
|
|
21
20
|
requestContext = aws_event.get("requestContext") or {}
|
|
22
21
|
identity = requestContext.get("identity") or {}
|
|
23
22
|
headers = aws_event.get("headers") or {}
|
|
@@ -48,36 +47,6 @@ class LambdaHandler:
|
|
|
48
47
|
else:
|
|
49
48
|
self.session["device_type"] = "unknown"
|
|
50
49
|
|
|
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
50
|
def serve(self, tx):
|
|
82
51
|
response = Response()
|
|
83
52
|
body = self.aws_event.get("body")
|
|
@@ -104,45 +73,11 @@ class LambdaHandler:
|
|
|
104
73
|
postdata=postdata,
|
|
105
74
|
response=response,
|
|
106
75
|
session=self.session,
|
|
107
|
-
log=lambda message, function=None: self.log(message, function),
|
|
76
|
+
log=lambda message, function=None: self.log(tx, message, function),
|
|
108
77
|
)
|
|
109
|
-
try:
|
|
110
|
-
if hasattr(self, "beforeAction"):
|
|
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())
|
|
136
|
-
except Exception as e:
|
|
137
|
-
response.exception()
|
|
138
|
-
if hasattr(self, "onError"):
|
|
139
|
-
self.onError(
|
|
140
|
-
local_context,
|
|
141
|
-
exc=e.__class__.__name__,
|
|
142
|
-
tb=traceback.format_exc(),
|
|
143
|
-
)
|
|
144
78
|
|
|
145
|
-
|
|
79
|
+
# Use BaseHandler's execute_actions method
|
|
80
|
+
return self.execute_actions(tx, local_context, postdata, req_params)
|
|
146
81
|
|
|
147
82
|
def track(self, tx, data={}, user=None):
|
|
148
83
|
data = data.copy()
|