velocity-python 0.0.109__py3-none-any.whl → 0.0.161__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.
Files changed (120) hide show
  1. velocity/__init__.py +3 -1
  2. velocity/app/orders.py +3 -4
  3. velocity/app/tests/__init__.py +1 -0
  4. velocity/app/tests/test_email_processing.py +112 -0
  5. velocity/app/tests/test_payment_profile_sorting.py +191 -0
  6. velocity/app/tests/test_spreadsheet_functions.py +124 -0
  7. velocity/aws/__init__.py +3 -0
  8. velocity/aws/amplify.py +10 -6
  9. velocity/aws/handlers/__init__.py +2 -0
  10. velocity/aws/handlers/base_handler.py +248 -0
  11. velocity/aws/handlers/context.py +251 -2
  12. velocity/aws/handlers/exceptions.py +16 -0
  13. velocity/aws/handlers/lambda_handler.py +24 -85
  14. velocity/aws/handlers/mixins/__init__.py +16 -0
  15. velocity/aws/handlers/mixins/activity_tracker.py +181 -0
  16. velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
  17. velocity/aws/handlers/mixins/error_handler.py +192 -0
  18. velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
  19. velocity/aws/handlers/mixins/standard_mixin.py +73 -0
  20. velocity/aws/handlers/response.py +1 -1
  21. velocity/aws/handlers/sqs_handler.py +28 -143
  22. velocity/aws/tests/__init__.py +1 -0
  23. velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
  24. velocity/aws/tests/test_response.py +163 -0
  25. velocity/db/__init__.py +16 -4
  26. velocity/db/core/decorators.py +48 -13
  27. velocity/db/core/engine.py +187 -840
  28. velocity/db/core/result.py +33 -25
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +493 -50
  31. velocity/db/core/transaction.py +28 -15
  32. velocity/db/exceptions.py +42 -18
  33. velocity/db/servers/base/__init__.py +9 -0
  34. velocity/db/servers/base/initializer.py +70 -0
  35. velocity/db/servers/base/operators.py +98 -0
  36. velocity/db/servers/base/sql.py +503 -0
  37. velocity/db/servers/base/types.py +135 -0
  38. velocity/db/servers/mysql/__init__.py +73 -0
  39. velocity/db/servers/mysql/operators.py +54 -0
  40. velocity/db/servers/{mysql_reserved.py → mysql/reserved.py} +2 -14
  41. velocity/db/servers/mysql/sql.py +718 -0
  42. velocity/db/servers/mysql/types.py +107 -0
  43. velocity/db/servers/postgres/__init__.py +59 -11
  44. velocity/db/servers/postgres/operators.py +34 -0
  45. velocity/db/servers/postgres/sql.py +474 -120
  46. velocity/db/servers/postgres/types.py +88 -2
  47. velocity/db/servers/sqlite/__init__.py +61 -0
  48. velocity/db/servers/sqlite/operators.py +52 -0
  49. velocity/db/servers/sqlite/reserved.py +20 -0
  50. velocity/db/servers/sqlite/sql.py +677 -0
  51. velocity/db/servers/sqlite/types.py +92 -0
  52. velocity/db/servers/sqlserver/__init__.py +73 -0
  53. velocity/db/servers/sqlserver/operators.py +47 -0
  54. velocity/db/servers/sqlserver/reserved.py +32 -0
  55. velocity/db/servers/sqlserver/sql.py +805 -0
  56. velocity/db/servers/sqlserver/types.py +114 -0
  57. velocity/db/servers/tablehelper.py +117 -91
  58. velocity/db/tests/__init__.py +1 -0
  59. velocity/db/tests/common_db_test.py +0 -0
  60. velocity/db/tests/postgres/__init__.py +1 -0
  61. velocity/db/tests/postgres/common.py +49 -0
  62. velocity/db/tests/postgres/test_column.py +29 -0
  63. velocity/db/tests/postgres/test_connections.py +25 -0
  64. velocity/db/tests/postgres/test_database.py +21 -0
  65. velocity/db/tests/postgres/test_engine.py +205 -0
  66. velocity/db/tests/postgres/test_general_usage.py +88 -0
  67. velocity/db/tests/postgres/test_imports.py +8 -0
  68. velocity/db/tests/postgres/test_result.py +19 -0
  69. velocity/db/tests/postgres/test_row.py +137 -0
  70. velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
  71. velocity/db/tests/postgres/test_schema_locking.py +335 -0
  72. velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
  73. velocity/db/tests/postgres/test_sequence.py +34 -0
  74. velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
  75. velocity/db/tests/postgres/test_table.py +101 -0
  76. velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
  77. velocity/db/tests/postgres/test_transaction.py +106 -0
  78. velocity/db/tests/sql/__init__.py +1 -0
  79. velocity/db/tests/sql/common.py +177 -0
  80. velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
  81. velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
  82. velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
  83. velocity/db/tests/test_db_utils.py +270 -0
  84. velocity/db/tests/test_postgres.py +448 -0
  85. velocity/db/tests/test_postgres_unchanged.py +81 -0
  86. velocity/db/tests/test_process_error_robustness.py +292 -0
  87. velocity/db/tests/test_result_caching.py +279 -0
  88. velocity/db/tests/test_result_sql_aware.py +117 -0
  89. velocity/db/tests/test_row_get_missing_column.py +72 -0
  90. velocity/db/tests/test_schema_locking_initializers.py +226 -0
  91. velocity/db/tests/test_schema_locking_simple.py +97 -0
  92. velocity/db/tests/test_sql_builder.py +165 -0
  93. velocity/db/tests/test_tablehelper.py +486 -0
  94. velocity/db/utils.py +129 -51
  95. velocity/misc/conv/__init__.py +2 -0
  96. velocity/misc/conv/iconv.py +5 -4
  97. velocity/misc/export.py +1 -4
  98. velocity/misc/merge.py +1 -1
  99. velocity/misc/tests/__init__.py +1 -0
  100. velocity/misc/tests/test_db.py +90 -0
  101. velocity/misc/tests/test_fix.py +78 -0
  102. velocity/misc/tests/test_format.py +64 -0
  103. velocity/misc/tests/test_iconv.py +203 -0
  104. velocity/misc/tests/test_merge.py +82 -0
  105. velocity/misc/tests/test_oconv.py +144 -0
  106. velocity/misc/tests/test_original_error.py +52 -0
  107. velocity/misc/tests/test_timer.py +74 -0
  108. velocity/misc/tools.py +0 -1
  109. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/METADATA +2 -2
  110. velocity_python-0.0.161.dist-info/RECORD +129 -0
  111. velocity/db/core/exceptions.py +0 -70
  112. velocity/db/servers/mysql.py +0 -641
  113. velocity/db/servers/sqlite.py +0 -968
  114. velocity/db/servers/sqlite_reserved.py +0 -208
  115. velocity/db/servers/sqlserver.py +0 -921
  116. velocity/db/servers/sqlserver_reserved.py +0 -314
  117. velocity_python-0.0.109.dist-info/RECORD +0 -56
  118. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/licenses/LICENSE +0 -0
  120. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.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
+ }
@@ -1,6 +1,15 @@
1
+ import json
2
+ import os
3
+ import boto3
4
+ import uuid
1
5
  import support.app
6
+ from velocity.misc.format import to_json
7
+ from velocity.misc.merge import deep_merge
8
+ from datetime import datetime
2
9
 
3
- engine = support.app.postgres()
10
+ import velocity.db
11
+
12
+ engine = velocity.db.postgres.initialize()
4
13
 
5
14
 
6
15
  @engine.transaction
@@ -12,10 +21,12 @@ class Context:
12
21
  self.__args = args
13
22
  self.__postdata = postdata
14
23
  self.__response = response
15
- self.__session = session
24
+ self.__session = {} if session is None else session
16
25
  self.__aws_event = aws_event
17
26
  self.__aws_context = aws_context
18
27
  self.__log = log
28
+ self._job_record_cache = {}
29
+ self._job_cancelled_flag = False
19
30
 
20
31
  def postdata(self, keys=-1, default=None):
21
32
  if keys == -1:
@@ -68,3 +79,241 @@ class Context:
68
79
  print(f"{function}: {message}")
69
80
  else:
70
81
  print(f"{message}")
82
+
83
+ def update_job(self, tx, data=None):
84
+ """Update job status and message in aws_job_activity table.
85
+
86
+ This method only UPDATES existing jobs. For creating new jobs, use create_job.
87
+ """
88
+ if not data:
89
+ return
90
+ if self.postdata("job_id"):
91
+ # Sanitize data before storing in database
92
+ sanitized_data = self._sanitize_job_data(data)
93
+ job_id = self.postdata("job_id")
94
+ tx.table("aws_job_activity").update(sanitized_data, {"job_id": job_id})
95
+ self._job_record_cache.pop(job_id, None)
96
+ tx.commit()
97
+
98
+ def create_job(self, tx, job_data=None):
99
+ """Create a new job record in aws_job_activity table using independent transaction."""
100
+ if not job_data:
101
+ return
102
+ sanitized_data = self._sanitize_job_data(job_data)
103
+ tx.table("aws_job_activity").insert(sanitized_data)
104
+ job_id = sanitized_data.get("job_id")
105
+ if job_id:
106
+ self._job_record_cache.pop(job_id, None)
107
+ tx.commit()
108
+
109
+ def _sanitize_job_data(self, data):
110
+ """Sanitize sensitive data before storing in aws_job_activity table."""
111
+ if not isinstance(data, dict):
112
+ return data
113
+
114
+ sanitized = {}
115
+
116
+ # List of sensitive field patterns to sanitize
117
+ sensitive_patterns = [
118
+ "password",
119
+ "token",
120
+ "secret",
121
+ "key",
122
+ "credential",
123
+ "auth",
124
+ "cognito_user",
125
+ "session",
126
+ "cookie",
127
+ "authorization",
128
+ ]
129
+
130
+ for key, value in data.items():
131
+ # Check if key contains sensitive patterns
132
+ if any(pattern in key.lower() for pattern in sensitive_patterns):
133
+ sanitized[key] = "[REDACTED]" if value else value
134
+ elif key == "error" and value:
135
+ # Sanitize error messages - keep first 500 chars and remove potential sensitive info
136
+ error_str = str(value)[:500]
137
+ for pattern in sensitive_patterns:
138
+ if pattern in error_str.lower():
139
+ # Replace potential sensitive values with placeholder
140
+ import re
141
+
142
+ # Remove patterns like password=value, token=value, etc.
143
+ error_str = re.sub(
144
+ rf"{pattern}[=:]\s*[^\s,\]}}]+",
145
+ f"{pattern}=[REDACTED]",
146
+ error_str,
147
+ flags=re.IGNORECASE,
148
+ )
149
+ sanitized[key] = error_str
150
+ elif key == "traceback" and value:
151
+ # Sanitize traceback - keep structure but remove sensitive values
152
+ tb_str = str(value)
153
+ for pattern in sensitive_patterns:
154
+ if pattern in tb_str.lower():
155
+ import re
156
+
157
+ # Remove patterns like password=value, token=value, etc.
158
+ tb_str = re.sub(
159
+ rf"{pattern}[=:]\s*[^\s,\]}}]+",
160
+ f"{pattern}=[REDACTED]",
161
+ tb_str,
162
+ flags=re.IGNORECASE,
163
+ )
164
+ # Limit traceback size to prevent DB bloat
165
+ sanitized[key] = tb_str[:2000]
166
+ elif key == "message" and value:
167
+ # Sanitize message field
168
+ message_str = str(value)
169
+ for pattern in sensitive_patterns:
170
+ if pattern in message_str.lower():
171
+ import re
172
+
173
+ message_str = re.sub(
174
+ rf"{pattern}[=:]\s*[^\s,\]}}]+",
175
+ f"{pattern}=[REDACTED]",
176
+ message_str,
177
+ flags=re.IGNORECASE,
178
+ )
179
+ sanitized[key] = message_str[:1000] # Limit message size
180
+ else:
181
+ # For other fields, copy as-is but check for nested dicts
182
+ if isinstance(value, dict):
183
+ sanitized[key] = self._sanitize_job_data(value)
184
+ elif isinstance(value, str) and len(value) > 5000:
185
+ # Limit very large string fields
186
+ sanitized[key] = value[:5000] + "...[TRUNCATED]"
187
+ else:
188
+ sanitized[key] = value
189
+
190
+ return sanitized
191
+
192
+ def _get_job_record(self, tx, job_id=None, refresh=False):
193
+ job_id = job_id or self.postdata("job_id")
194
+ if not job_id:
195
+ return None
196
+
197
+ if refresh or job_id not in self._job_record_cache:
198
+ record = tx.table("aws_job_activity").find({"job_id": job_id})
199
+ if record is not None:
200
+ self._job_record_cache[job_id] = record
201
+ elif job_id in self._job_record_cache:
202
+ del self._job_record_cache[job_id]
203
+
204
+ return self._job_record_cache.get(job_id)
205
+
206
+ def is_job_cancel_requested(self, tx, force_refresh=False):
207
+ job = self._get_job_record(tx, refresh=force_refresh)
208
+ if not job:
209
+ return False
210
+
211
+ status = (job.get("status") or "").lower()
212
+ if status in {"cancelrequested", "cancelled"}:
213
+ return True
214
+
215
+ message_raw = job.get("message")
216
+ if not message_raw:
217
+ return False
218
+
219
+ if isinstance(message_raw, dict):
220
+ message = message_raw
221
+ else:
222
+ try:
223
+ message = json.loads(message_raw)
224
+ except (TypeError, ValueError, json.JSONDecodeError):
225
+ return False
226
+
227
+ return bool(message.get("cancel_requested") or message.get("cancelled"))
228
+
229
+ def mark_job_cancelled(self, tx, detail=None, requested_by=None):
230
+ job_id = self.postdata("job_id")
231
+ if not job_id:
232
+ return
233
+
234
+ job = self._get_job_record(tx, refresh=True) or {}
235
+ message_raw = job.get("message")
236
+ if isinstance(message_raw, dict):
237
+ message = dict(message_raw)
238
+ else:
239
+ try:
240
+ message = json.loads(message_raw) if message_raw else {}
241
+ except (TypeError, ValueError, json.JSONDecodeError):
242
+ message = {}
243
+
244
+ message.update(
245
+ {
246
+ "detail": detail or "Job cancelled",
247
+ "cancelled": True,
248
+ }
249
+ )
250
+
251
+ tx.table("aws_job_activity").update(
252
+ {
253
+ "status": "Cancelled",
254
+ "message": to_json(message),
255
+ "handler_complete_timestamp": datetime.now(),
256
+ "sys_modified": datetime.now(),
257
+ "sys_modified_by": requested_by
258
+ or self.session().get("email_address")
259
+ or "system",
260
+ },
261
+ {"job_id": job_id},
262
+ )
263
+ tx.commit()
264
+ self._job_record_cache.pop(job_id, None)
265
+ self._job_cancelled_flag = True
266
+
267
+ def was_job_cancelled(self):
268
+ return self._job_cancelled_flag
269
+
270
+ def enqueue(self, action, payload={}, user=None, suppress_job_activity=False):
271
+ """
272
+ Enqueue jobs to SQS with independent job activity tracking.
273
+
274
+ This method uses its own transaction for aws_job_activity updates to ensure
275
+ job tracking is never rolled back with other operations.
276
+ """
277
+ batch_id = str(uuid.uuid4())
278
+ results = {"batch_id": batch_id}
279
+ queue = boto3.resource("sqs").get_queue_by_name(
280
+ QueueName=os.environ["SqsWorkQueue"]
281
+ )
282
+ if isinstance(payload, dict):
283
+ payload = [payload]
284
+ messages = []
285
+ if user is None:
286
+ user = self.session().get("email_address") or "EnqueueTasks"
287
+ for item in payload:
288
+ message = {"action": action, "payload": item}
289
+ id = str(uuid.uuid4()).split("-")[0]
290
+ if suppress_job_activity:
291
+ messages.append({"Id": id, "MessageBody": to_json(message)})
292
+ else:
293
+ message["job_id"] = id
294
+ # Use separate transaction for job activity - this should never be rolled back
295
+ self.create_job(
296
+ {
297
+ "action": action,
298
+ "initial_timestamp": datetime.now(),
299
+ "created_by": user,
300
+ "sys_modified_by": user,
301
+ "payload": to_json(message),
302
+ "batch_id": str(batch_id),
303
+ "job_id": id,
304
+ "status": "Initialized",
305
+ "message": "Job Initialized",
306
+ }
307
+ )
308
+ messages.append({"Id": id, "MessageBody": to_json(message)})
309
+
310
+ if len(messages) == 10:
311
+ result = queue.send_messages(Entries=messages)
312
+ results = deep_merge(results, result)
313
+ messages.clear()
314
+
315
+ if messages:
316
+ result = queue.send_messages(Entries=messages)
317
+ results = deep_merge(results, result)
318
+
319
+ 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 sys
4
- import os
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
- self.aws_event = aws_event
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
- 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())
82
+ self.execute_actions(tx, local_context, actions)
136
83
  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
-
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
- tx.table(helpers.get_tracking_table(user or self.session)).insert(data)
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"])