velocity-python 0.0.109__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 -839
- velocity/db/core/result.py +30 -24
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +279 -40
- 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.109.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.109.dist-info/RECORD +0 -56
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
|
@@ -6,109 +6,59 @@ It includes logging capabilities, action routing, and error handling.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
-
import
|
|
10
|
-
import sys
|
|
11
|
-
import traceback
|
|
12
|
-
from typing import Any, Dict, Optional
|
|
9
|
+
from typing import Any, Dict
|
|
13
10
|
|
|
14
|
-
from velocity.aws import DEBUG
|
|
15
11
|
from velocity.aws.handlers import context as VelocityContext
|
|
16
|
-
from velocity.
|
|
12
|
+
from velocity.aws.handlers.base_handler import BaseHandler
|
|
17
13
|
|
|
18
14
|
|
|
19
|
-
class SqsHandler:
|
|
15
|
+
class SqsHandler(BaseHandler):
|
|
20
16
|
"""
|
|
21
17
|
Base class for handling SQS events in AWS Lambda functions.
|
|
22
|
-
|
|
18
|
+
|
|
23
19
|
Provides structured processing of SQS records with automatic action routing,
|
|
24
20
|
logging capabilities, and error handling hooks.
|
|
25
21
|
"""
|
|
26
22
|
|
|
27
|
-
def __init__(
|
|
28
|
-
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
aws_event: Dict[str, Any],
|
|
26
|
+
aws_context: Any,
|
|
27
|
+
context_class=VelocityContext.Context,
|
|
28
|
+
):
|
|
29
29
|
"""
|
|
30
30
|
Initialize the SQS handler.
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
Args:
|
|
33
33
|
aws_event: The AWS Lambda event containing SQS records
|
|
34
34
|
aws_context: The AWS Lambda context object
|
|
35
35
|
context_class: The context class to use for processing
|
|
36
36
|
"""
|
|
37
|
-
|
|
38
|
-
self.aws_context = aws_context
|
|
39
|
-
self.serve_action_default = True
|
|
40
|
-
self.skip_action = False
|
|
41
|
-
self.ContextClass = context_class
|
|
42
|
-
|
|
43
|
-
def log(self, tx, message: str, function: Optional[str] = None):
|
|
44
|
-
"""
|
|
45
|
-
Log a message to the system log table.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
tx: Database transaction object
|
|
49
|
-
message: The message to log
|
|
50
|
-
function: Optional function name, auto-detected if not provided
|
|
51
|
-
"""
|
|
52
|
-
if not function:
|
|
53
|
-
function = self._get_calling_function()
|
|
54
|
-
|
|
55
|
-
data = {
|
|
56
|
-
"app_name": os.environ.get("ProjectName", "Unknown"),
|
|
57
|
-
"referer": "SQS",
|
|
58
|
-
"user_agent": "QueueHandler",
|
|
59
|
-
"device_type": "Lambda",
|
|
60
|
-
"function": function,
|
|
61
|
-
"message": message,
|
|
62
|
-
"sys_modified_by": "Lambda:BackOfficeQueueHandler",
|
|
63
|
-
}
|
|
64
|
-
tx.table("sys_log").insert(data)
|
|
65
|
-
|
|
66
|
-
def _get_calling_function(self) -> str:
|
|
67
|
-
"""
|
|
68
|
-
Get the name of the calling function by inspecting the call stack.
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
The name of the calling function or "<Unknown>" if not found
|
|
72
|
-
"""
|
|
73
|
-
skip_functions = {"x", "log", "_transaction", "_get_calling_function"}
|
|
74
|
-
|
|
75
|
-
for idx in range(10): # Limit search to prevent infinite loops
|
|
76
|
-
try:
|
|
77
|
-
frame = sys._getframe(idx)
|
|
78
|
-
function_name = frame.f_code.co_name
|
|
79
|
-
|
|
80
|
-
if function_name not in skip_functions:
|
|
81
|
-
return function_name
|
|
82
|
-
|
|
83
|
-
except ValueError:
|
|
84
|
-
# No more frames in the stack
|
|
85
|
-
break
|
|
86
|
-
|
|
87
|
-
return "<Unknown>"
|
|
37
|
+
super().__init__(aws_event, aws_context, context_class)
|
|
88
38
|
|
|
89
39
|
def serve(self, tx):
|
|
90
40
|
"""
|
|
91
41
|
Process all SQS records in the event.
|
|
92
|
-
|
|
42
|
+
|
|
93
43
|
Args:
|
|
94
44
|
tx: Database transaction object
|
|
95
45
|
"""
|
|
96
46
|
records = self.aws_event.get("Records", [])
|
|
97
|
-
|
|
47
|
+
|
|
98
48
|
for record in records:
|
|
99
49
|
self._process_record(tx, record)
|
|
100
|
-
|
|
50
|
+
|
|
101
51
|
def _process_record(self, tx, record: Dict[str, Any]):
|
|
102
52
|
"""
|
|
103
53
|
Process a single SQS record.
|
|
104
|
-
|
|
54
|
+
|
|
105
55
|
Args:
|
|
106
56
|
tx: Database transaction object
|
|
107
57
|
record: Individual SQS record to process
|
|
108
58
|
"""
|
|
109
59
|
attrs = record.get("attributes", {})
|
|
110
60
|
postdata = {}
|
|
111
|
-
|
|
61
|
+
|
|
112
62
|
# Parse message body if present
|
|
113
63
|
body = record.get("body")
|
|
114
64
|
if body:
|
|
@@ -127,93 +77,28 @@ class SqsHandler:
|
|
|
127
77
|
response=None,
|
|
128
78
|
session=None,
|
|
129
79
|
)
|
|
80
|
+
|
|
81
|
+
# Determine action from postdata
|
|
82
|
+
action = postdata.get("action") if isinstance(postdata, dict) else None
|
|
130
83
|
|
|
84
|
+
# Get the list of actions to execute
|
|
85
|
+
actions = self.get_actions_to_execute(action)
|
|
86
|
+
|
|
87
|
+
# Use BaseHandler's execute_actions method
|
|
131
88
|
try:
|
|
132
|
-
self.
|
|
89
|
+
self.execute_actions(tx, local_context, actions)
|
|
133
90
|
except Exception as e:
|
|
134
|
-
|
|
135
|
-
self.onError(
|
|
136
|
-
local_context,
|
|
137
|
-
exc=e.__class__.__name__,
|
|
138
|
-
tb=traceback.format_exc(),
|
|
139
|
-
)
|
|
140
|
-
else:
|
|
141
|
-
# Re-raise if no error handler is defined
|
|
142
|
-
raise
|
|
143
|
-
|
|
144
|
-
def _execute_actions(self, local_context):
|
|
145
|
-
"""
|
|
146
|
-
Execute the appropriate actions for the given context.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
local_context: The context object for this record
|
|
150
|
-
"""
|
|
151
|
-
# Execute beforeAction hook if available
|
|
152
|
-
if hasattr(self, "beforeAction"):
|
|
153
|
-
self.beforeAction(local_context)
|
|
154
|
-
|
|
155
|
-
# Determine which actions to execute
|
|
156
|
-
actions = self._get_actions_to_execute(local_context)
|
|
157
|
-
|
|
158
|
-
# Execute the first matching action
|
|
159
|
-
for action in actions:
|
|
160
|
-
if self.skip_action:
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
if hasattr(self, action):
|
|
164
|
-
getattr(self, action)(local_context)
|
|
165
|
-
break
|
|
166
|
-
|
|
167
|
-
# Execute afterAction hook if available
|
|
168
|
-
if hasattr(self, "afterAction"):
|
|
169
|
-
self.afterAction(local_context)
|
|
170
|
-
|
|
171
|
-
def _get_actions_to_execute(self, local_context) -> list:
|
|
172
|
-
"""
|
|
173
|
-
Get the list of actions to execute for the given context.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
local_context: The context object for this record
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
List of action method names to try executing
|
|
180
|
-
"""
|
|
181
|
-
actions = []
|
|
182
|
-
|
|
183
|
-
# Add specific action if available
|
|
184
|
-
action = local_context.action()
|
|
185
|
-
if action:
|
|
186
|
-
action_method = self._format_action_name(action)
|
|
187
|
-
actions.append(action_method)
|
|
188
|
-
|
|
189
|
-
# Add default action if enabled
|
|
190
|
-
if self.serve_action_default:
|
|
191
|
-
actions.append("OnActionDefault")
|
|
192
|
-
|
|
193
|
-
return actions
|
|
194
|
-
|
|
195
|
-
def _format_action_name(self, action: str) -> str:
|
|
196
|
-
"""
|
|
197
|
-
Format an action string into a method name.
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
action: The raw action string
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
Formatted method name
|
|
204
|
-
"""
|
|
205
|
-
formatted = action.replace('-', ' ').replace('_', ' ')
|
|
206
|
-
return f"on action {formatted}".title().replace(" ", "")
|
|
91
|
+
self.handle_error(tx, local_context, e)
|
|
207
92
|
|
|
208
93
|
def OnActionDefault(self, tx, context):
|
|
209
94
|
"""
|
|
210
95
|
Default action handler when no specific action is found.
|
|
211
|
-
|
|
96
|
+
|
|
212
97
|
Args:
|
|
213
98
|
tx: Database transaction object
|
|
214
99
|
context: The context object for this record
|
|
215
100
|
"""
|
|
216
|
-
action = context.action() if hasattr(context,
|
|
101
|
+
action = context.action() if hasattr(context, "action") else "unknown"
|
|
217
102
|
warning_message = (
|
|
218
103
|
f"[Warn] Action handler not found. Calling default action "
|
|
219
104
|
f"`SqsHandler.OnActionDefault` with the following parameters:\n"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# AWS module tests
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test Lambda Handler JSON Serialization Fix
|
|
3
|
+
|
|
4
|
+
This test verifies that LambdaHandler.serve() returns a JSON-serializable
|
|
5
|
+
dictionary instead of a Response object, preventing the
|
|
6
|
+
"Object of type method is not JSON serializable" error.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import unittest
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
# Add src to path for testing
|
|
16
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
17
|
+
|
|
18
|
+
from velocity.aws.handlers.lambda_handler import LambdaHandler
|
|
19
|
+
from velocity.aws.handlers.response import Response
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestLambdaHandlerJSONSerialization(unittest.TestCase):
|
|
23
|
+
"""Test cases for Lambda Handler JSON serialization."""
|
|
24
|
+
|
|
25
|
+
def setUp(self):
|
|
26
|
+
"""Set up test fixtures."""
|
|
27
|
+
self.test_event = {
|
|
28
|
+
"body": '{"action": "test", "payload": {}}',
|
|
29
|
+
"httpMethod": "POST",
|
|
30
|
+
"headers": {"Content-Type": "application/json"},
|
|
31
|
+
"requestContext": {
|
|
32
|
+
"identity": {
|
|
33
|
+
"sourceIp": "127.0.0.1", # localhost test IP for unit testing
|
|
34
|
+
"userAgent": "test-agent"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"queryStringParameters": {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
self.test_context = MagicMock()
|
|
41
|
+
self.test_context.function_name = "test-function"
|
|
42
|
+
|
|
43
|
+
def test_serve_returns_json_serializable_dict(self):
|
|
44
|
+
"""Test that serve() returns a JSON-serializable dictionary."""
|
|
45
|
+
|
|
46
|
+
# Create handler
|
|
47
|
+
handler = LambdaHandler(self.test_event, self.test_context)
|
|
48
|
+
|
|
49
|
+
# Mock the transaction decorator to pass through tx
|
|
50
|
+
with patch('velocity.aws.handlers.lambda_handler.engine') as mock_engine:
|
|
51
|
+
def mock_transaction(func):
|
|
52
|
+
def wrapper(*args, **kwargs):
|
|
53
|
+
mock_tx = MagicMock()
|
|
54
|
+
return func(mock_tx, *args, **kwargs)
|
|
55
|
+
return wrapper
|
|
56
|
+
|
|
57
|
+
mock_engine.transaction = mock_transaction
|
|
58
|
+
|
|
59
|
+
# Call serve method
|
|
60
|
+
result = handler.serve(MagicMock())
|
|
61
|
+
|
|
62
|
+
# Verify result is a dictionary (JSON-serializable)
|
|
63
|
+
self.assertIsInstance(result, dict)
|
|
64
|
+
|
|
65
|
+
# Verify it has the expected Lambda response structure
|
|
66
|
+
self.assertIn("statusCode", result)
|
|
67
|
+
self.assertIn("headers", result)
|
|
68
|
+
self.assertIn("body", result)
|
|
69
|
+
|
|
70
|
+
# Verify the body is a JSON string
|
|
71
|
+
self.assertIsInstance(result["body"], str)
|
|
72
|
+
|
|
73
|
+
# Verify the entire result can be JSON serialized
|
|
74
|
+
try:
|
|
75
|
+
json.dumps(result)
|
|
76
|
+
except (TypeError, ValueError) as e:
|
|
77
|
+
self.fail(f"Result is not JSON serializable: {e}")
|
|
78
|
+
|
|
79
|
+
def test_response_object_has_render_method(self):
|
|
80
|
+
"""Test that Response object has a proper render method."""
|
|
81
|
+
response = Response()
|
|
82
|
+
|
|
83
|
+
# Verify render method exists
|
|
84
|
+
self.assertTrue(hasattr(response, 'render'))
|
|
85
|
+
self.assertTrue(callable(response.render))
|
|
86
|
+
|
|
87
|
+
# Verify render returns a dictionary
|
|
88
|
+
rendered = response.render()
|
|
89
|
+
self.assertIsInstance(rendered, dict)
|
|
90
|
+
|
|
91
|
+
# Verify structure
|
|
92
|
+
self.assertIn("statusCode", rendered)
|
|
93
|
+
self.assertIn("headers", rendered)
|
|
94
|
+
self.assertIn("body", rendered)
|
|
95
|
+
|
|
96
|
+
# Verify JSON serializable
|
|
97
|
+
try:
|
|
98
|
+
json.dumps(rendered)
|
|
99
|
+
except (TypeError, ValueError) as e:
|
|
100
|
+
self.fail(f"Rendered response is not JSON serializable: {e}")
|
|
101
|
+
|
|
102
|
+
def test_response_render_vs_raw_object(self):
|
|
103
|
+
"""Test the difference between Response object and rendered response."""
|
|
104
|
+
response = Response()
|
|
105
|
+
|
|
106
|
+
# Raw response object should not be directly JSON serializable
|
|
107
|
+
# (it contains method references)
|
|
108
|
+
with self.assertRaises((TypeError, ValueError)):
|
|
109
|
+
json.dumps(response)
|
|
110
|
+
|
|
111
|
+
# But rendered response should be JSON serializable
|
|
112
|
+
rendered = response.render()
|
|
113
|
+
try:
|
|
114
|
+
json.dumps(rendered)
|
|
115
|
+
except (TypeError, ValueError) as e:
|
|
116
|
+
self.fail(f"Rendered response should be JSON serializable: {e}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == '__main__':
|
|
120
|
+
unittest.main()
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import sys
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
|
|
5
|
+
# Mock the support module before importing the module that depends on it
|
|
6
|
+
sys.modules["support"] = MagicMock()
|
|
7
|
+
sys.modules["support.app"] = MagicMock()
|
|
8
|
+
sys.modules["support.app"].DEBUG = True
|
|
9
|
+
|
|
10
|
+
from velocity.aws.handlers.response import Response # Replace with actual module path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestResponse(unittest.TestCase):
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.response = Response()
|
|
17
|
+
|
|
18
|
+
def test_initial_status_code(self):
|
|
19
|
+
self.assertEqual(self.response.status(), 200)
|
|
20
|
+
|
|
21
|
+
def test_set_status_code(self):
|
|
22
|
+
self.response.set_status(404)
|
|
23
|
+
self.assertEqual(self.response.status(), 404)
|
|
24
|
+
|
|
25
|
+
def test_initial_headers(self):
|
|
26
|
+
headers = self.response.headers()
|
|
27
|
+
self.assertIn("Content-Type", headers)
|
|
28
|
+
self.assertEqual(headers["Content-Type"], "application/json")
|
|
29
|
+
|
|
30
|
+
def test_set_headers(self):
|
|
31
|
+
custom_headers = {"x-custom-header": "value"}
|
|
32
|
+
self.response.set_headers(custom_headers)
|
|
33
|
+
headers = self.response.headers()
|
|
34
|
+
self.assertEqual(headers["X-Custom-Header"], "value") # Ensures capitalization
|
|
35
|
+
self.assertIn("Content-Type", headers)
|
|
36
|
+
|
|
37
|
+
def test_alert_action(self):
|
|
38
|
+
self.response.alert("Test message", "Alert Title")
|
|
39
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
40
|
+
self.assertEqual(self.response.actions[0]["action"], "alert")
|
|
41
|
+
self.assertEqual(self.response.actions[0]["payload"]["title"], "Alert Title")
|
|
42
|
+
self.assertEqual(self.response.actions[0]["payload"]["message"], "Test message")
|
|
43
|
+
|
|
44
|
+
def test_toast_action_valid_variant(self):
|
|
45
|
+
self.response.toast("Toast message", "warning")
|
|
46
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
47
|
+
self.assertEqual(self.response.actions[0]["action"], "toast")
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
self.response.actions[0]["payload"]["options"]["variant"], "warning"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def test_toast_action_invalid_variant(self):
|
|
53
|
+
with self.assertRaises(ValueError) as context:
|
|
54
|
+
self.response.toast("Invalid toast", "invalid_variant")
|
|
55
|
+
self.assertIn(
|
|
56
|
+
"Notistack variant 'invalid_variant' not in", str(context.exception)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def test_load_object_action(self):
|
|
60
|
+
payload = {"key": "value"}
|
|
61
|
+
self.response.load_object(payload)
|
|
62
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
63
|
+
self.assertEqual(self.response.actions[0]["action"], "load-object")
|
|
64
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
65
|
+
|
|
66
|
+
def test_update_store_action(self):
|
|
67
|
+
payload = {"key": "value"}
|
|
68
|
+
self.response.update_store(payload)
|
|
69
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
70
|
+
self.assertEqual(self.response.actions[0]["action"], "update-store")
|
|
71
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
72
|
+
|
|
73
|
+
def test_file_download_action(self):
|
|
74
|
+
payload = {"file": "file.txt"}
|
|
75
|
+
self.response.file_download(payload)
|
|
76
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
77
|
+
self.assertEqual(self.response.actions[0]["action"], "file-download")
|
|
78
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
79
|
+
|
|
80
|
+
def test_redirect_action(self):
|
|
81
|
+
self.response.redirect("https://example.com")
|
|
82
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
83
|
+
self.assertEqual(self.response.actions[0]["action"], "redirect")
|
|
84
|
+
self.assertEqual(
|
|
85
|
+
self.response.actions[0]["payload"]["location"], "https://example.com"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def test_signout_action(self):
|
|
89
|
+
self.response.signout()
|
|
90
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
91
|
+
self.assertEqual(self.response.actions[0]["action"], "signout")
|
|
92
|
+
|
|
93
|
+
def test_set_table_action(self):
|
|
94
|
+
payload = {"table": "data"}
|
|
95
|
+
self.response.set_table(payload)
|
|
96
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
97
|
+
self.assertEqual(self.response.actions[0]["action"], "set-table")
|
|
98
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
99
|
+
|
|
100
|
+
def test_set_repo_action(self):
|
|
101
|
+
payload = {"repo": "data"}
|
|
102
|
+
self.response.set_repo(payload)
|
|
103
|
+
self.assertEqual(len(self.response.actions), 1)
|
|
104
|
+
self.assertEqual(self.response.actions[0]["action"], "set-repo")
|
|
105
|
+
self.assertEqual(self.response.actions[0]["payload"], payload)
|
|
106
|
+
|
|
107
|
+
def test_exception_handling_debug_on(self):
|
|
108
|
+
with patch("your_module.DEBUG", True), patch(
|
|
109
|
+
"traceback.format_exc", return_value="formatted traceback"
|
|
110
|
+
):
|
|
111
|
+
try:
|
|
112
|
+
raise ValueError("Test exception")
|
|
113
|
+
except ValueError:
|
|
114
|
+
self.response.exception()
|
|
115
|
+
|
|
116
|
+
self.assertEqual(self.response.status(), 500)
|
|
117
|
+
exception_info = self.response.body["python_exception"]
|
|
118
|
+
self.assertEqual(exception_info["value"], "Test exception")
|
|
119
|
+
self.assertEqual(exception_info["traceback"], "formatted traceback")
|
|
120
|
+
|
|
121
|
+
def test_exception_handling_debug_off(self):
|
|
122
|
+
with patch("your_module.DEBUG", False), patch(
|
|
123
|
+
"traceback.format_exc", return_value="formatted traceback"
|
|
124
|
+
):
|
|
125
|
+
try:
|
|
126
|
+
raise ValueError("Test exception")
|
|
127
|
+
except ValueError:
|
|
128
|
+
self.response.exception()
|
|
129
|
+
|
|
130
|
+
self.assertEqual(self.response.status(), 500)
|
|
131
|
+
exception_info = self.response.body["python_exception"]
|
|
132
|
+
self.assertEqual(exception_info["value"], "Test exception")
|
|
133
|
+
self.assertIsNone(exception_info["traceback"])
|
|
134
|
+
|
|
135
|
+
def test_chaining_methods(self):
|
|
136
|
+
response = (
|
|
137
|
+
self.response.set_status(201)
|
|
138
|
+
.alert("Chain Alert")
|
|
139
|
+
.toast("Chain Toast", "info")
|
|
140
|
+
.redirect("https://chained-example.com")
|
|
141
|
+
)
|
|
142
|
+
self.assertEqual(response.status(), 201)
|
|
143
|
+
self.assertEqual(len(response.actions), 3)
|
|
144
|
+
self.assertEqual(
|
|
145
|
+
response.actions[2]["payload"]["location"], "https://chained-example.com"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def test_render(self):
|
|
149
|
+
self.response.set_body({"key": "value"})
|
|
150
|
+
rendered_response = self.response.render()
|
|
151
|
+
self.assertEqual(rendered_response["statusCode"], 200)
|
|
152
|
+
self.assertEqual(
|
|
153
|
+
rendered_response["headers"]["Content-Type"], "application/json"
|
|
154
|
+
)
|
|
155
|
+
self.assertIn('"key": "value"', rendered_response["body"])
|
|
156
|
+
|
|
157
|
+
def test_format_header_key(self):
|
|
158
|
+
result = Response._format_header_key("x-custom-header")
|
|
159
|
+
self.assertEqual(result, "X-Custom-Header")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
unittest.main()
|
velocity/db/__init__.py
CHANGED
|
@@ -5,9 +5,6 @@ from velocity.db.servers import sqlite
|
|
|
5
5
|
from velocity.db.servers import sqlserver
|
|
6
6
|
from velocity.db import utils
|
|
7
7
|
|
|
8
|
-
# Export exceptions at the package level for backward compatibility
|
|
9
|
-
from velocity.db.exceptions import *
|
|
10
|
-
|
|
11
8
|
# Export commonly used utility functions
|
|
12
9
|
from velocity.db.utils import (
|
|
13
10
|
safe_sort_rows,
|
|
@@ -15,5 +12,20 @@ from velocity.db.utils import (
|
|
|
15
12
|
safe_sort_key_none_first,
|
|
16
13
|
safe_sort_key_with_default,
|
|
17
14
|
group_by_fields,
|
|
18
|
-
safe_sort_grouped_rows
|
|
15
|
+
safe_sort_grouped_rows,
|
|
19
16
|
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"exceptions",
|
|
20
|
+
"postgres",
|
|
21
|
+
"mysql",
|
|
22
|
+
"sqlite",
|
|
23
|
+
"sqlserver",
|
|
24
|
+
"utils",
|
|
25
|
+
"safe_sort_rows",
|
|
26
|
+
"safe_sort_key_none_last",
|
|
27
|
+
"safe_sort_key_none_first",
|
|
28
|
+
"safe_sort_key_with_default",
|
|
29
|
+
"group_by_fields",
|
|
30
|
+
"safe_sort_grouped_rows",
|
|
31
|
+
]
|
velocity/db/core/decorators.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
2
|
import random
|
|
3
|
-
import traceback
|
|
4
3
|
from functools import wraps
|
|
5
4
|
from velocity.db import exceptions
|
|
6
5
|
|
|
@@ -100,7 +99,8 @@ def return_default(
|
|
|
100
99
|
|
|
101
100
|
def create_missing(func):
|
|
102
101
|
"""
|
|
103
|
-
If the function call fails with DbColumnMissingError or DbTableMissingError,
|
|
102
|
+
If the function call fails with DbColumnMissingError or DbTableMissingError,
|
|
103
|
+
tries to create them and re-run (only if schema is not locked).
|
|
104
104
|
"""
|
|
105
105
|
|
|
106
106
|
@wraps(func)
|
|
@@ -110,8 +110,16 @@ def create_missing(func):
|
|
|
110
110
|
result = func(self, *args, **kwds)
|
|
111
111
|
self.tx.release_savepoint(sp, cursor=self.cursor())
|
|
112
112
|
return result
|
|
113
|
-
except exceptions.DbColumnMissingError:
|
|
113
|
+
except exceptions.DbColumnMissingError as e:
|
|
114
114
|
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
115
|
+
|
|
116
|
+
# Check if schema is locked
|
|
117
|
+
if self.tx.engine.schema_locked:
|
|
118
|
+
raise exceptions.DbSchemaLockedError(
|
|
119
|
+
f"Cannot create missing column: schema is locked. Original error: {e}"
|
|
120
|
+
) from e
|
|
121
|
+
|
|
122
|
+
# Existing logic for automatic creation
|
|
115
123
|
data = {}
|
|
116
124
|
if "pk" in kwds:
|
|
117
125
|
data.update(kwds["pk"])
|
|
@@ -122,8 +130,16 @@ def create_missing(func):
|
|
|
122
130
|
data.update(arg)
|
|
123
131
|
self.alter(data)
|
|
124
132
|
return func(self, *args, **kwds)
|
|
125
|
-
except exceptions.DbTableMissingError:
|
|
133
|
+
except exceptions.DbTableMissingError as e:
|
|
126
134
|
self.tx.rollback_savepoint(sp, cursor=self.cursor())
|
|
135
|
+
|
|
136
|
+
# Check if schema is locked
|
|
137
|
+
if self.tx.engine.schema_locked:
|
|
138
|
+
raise exceptions.DbSchemaLockedError(
|
|
139
|
+
f"Cannot create missing table: schema is locked. Original error: {e}"
|
|
140
|
+
) from e
|
|
141
|
+
|
|
142
|
+
# Existing logic for automatic creation
|
|
127
143
|
data = {}
|
|
128
144
|
if "pk" in kwds:
|
|
129
145
|
data.update(kwds["pk"])
|