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.
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 +167 -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 +20 -4
  27. velocity/db/core/engine.py +185 -792
  28. velocity/db/core/result.py +36 -22
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +283 -44
  31. velocity/db/core/transaction.py +19 -11
  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 +221 -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 +62 -47
  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.105.dist-info → velocity_python-0.0.155.dist-info}/METADATA +2 -2
  110. velocity_python-0.0.155.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.105.dist-info/RECORD +0 -56
  118. {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
  120. {velocity_python-0.0.105.dist-info → velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  import traceback
3
- from typing import Any, Dict, List, Optional, Union
3
+ from typing import Any, Dict, List, Optional
4
4
  from velocity.misc.format import to_json
5
5
  from support.app import DEBUG
6
6
 
@@ -6,109 +6,59 @@ It includes logging capabilities, action routing, and error handling.
6
6
  """
7
7
 
8
8
  import json
9
- import os
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.misc.format import to_json
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__(self, aws_event: Dict[str, Any], aws_context: Any,
28
- context_class=VelocityContext.Context):
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
- self.aws_event = aws_event
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._execute_actions(local_context)
89
+ self.execute_actions(tx, local_context, actions)
133
90
  except Exception as e:
134
- if hasattr(self, "onError"):
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, 'action') else 'unknown'
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
+ ]
@@ -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, tries to create them and re-run.
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"])