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.
- 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 +251 -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 +48 -13
- velocity/db/core/engine.py +187 -840
- velocity/db/core/result.py +33 -25
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +493 -50
- velocity/db/core/transaction.py +28 -15
- 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 +270 -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 +129 -51
- 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.161.dist-info}/METADATA +2 -2
- velocity_python-0.0.161.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.161.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for database utility functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
from src.velocity.db.utils import (
|
|
8
|
+
safe_sort_key_none_last,
|
|
9
|
+
safe_sort_key_none_first,
|
|
10
|
+
safe_sort_key_with_default,
|
|
11
|
+
safe_sort_rows,
|
|
12
|
+
group_by_fields,
|
|
13
|
+
safe_sort_grouped_rows,
|
|
14
|
+
mask_config_for_display,
|
|
15
|
+
mask_sensitive_in_string,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestDatabaseUtils(unittest.TestCase):
|
|
20
|
+
"""Test database utility functions."""
|
|
21
|
+
|
|
22
|
+
def setUp(self):
|
|
23
|
+
"""Set up test data."""
|
|
24
|
+
self.sample_data = [
|
|
25
|
+
{"id": 1, "name": "Alice", "date": "2024-03", "amount": 100},
|
|
26
|
+
{"id": 2, "name": "Bob", "date": None, "amount": 200},
|
|
27
|
+
{"id": 3, "name": "Charlie", "date": "2024-01", "amount": None},
|
|
28
|
+
{"id": 4, "name": "David", "date": "2024-02", "amount": 150},
|
|
29
|
+
{"id": 5, "name": "Eve", "date": None, "amount": 300},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
def test_safe_sort_key_none_last(self):
|
|
33
|
+
"""Test sorting with None values at the end."""
|
|
34
|
+
sort_key = safe_sort_key_none_last("date")
|
|
35
|
+
sorted_data = sorted(self.sample_data, key=sort_key)
|
|
36
|
+
|
|
37
|
+
dates = [row["date"] for row in sorted_data]
|
|
38
|
+
expected = ["2024-01", "2024-02", "2024-03", None, None]
|
|
39
|
+
self.assertEqual(dates, expected)
|
|
40
|
+
|
|
41
|
+
def test_safe_sort_key_none_first(self):
|
|
42
|
+
"""Test sorting with None values at the beginning."""
|
|
43
|
+
sort_key = safe_sort_key_none_first("date")
|
|
44
|
+
sorted_data = sorted(self.sample_data, key=sort_key)
|
|
45
|
+
|
|
46
|
+
dates = [row["date"] for row in sorted_data]
|
|
47
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
48
|
+
self.assertEqual(dates, expected)
|
|
49
|
+
|
|
50
|
+
def test_safe_sort_key_with_default(self):
|
|
51
|
+
"""Test sorting with None values replaced by default."""
|
|
52
|
+
sort_key = safe_sort_key_with_default("date", "1900-01")
|
|
53
|
+
sorted_data = sorted(self.sample_data, key=sort_key)
|
|
54
|
+
|
|
55
|
+
dates = [row["date"] for row in sorted_data]
|
|
56
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
57
|
+
self.assertEqual(dates, expected)
|
|
58
|
+
|
|
59
|
+
def test_safe_sort_rows_none_last(self):
|
|
60
|
+
"""Test safe_sort_rows with none_handling='last'."""
|
|
61
|
+
sorted_data = safe_sort_rows(self.sample_data, "date", none_handling="last")
|
|
62
|
+
|
|
63
|
+
dates = [row["date"] for row in sorted_data]
|
|
64
|
+
expected = ["2024-01", "2024-02", "2024-03", None, None]
|
|
65
|
+
self.assertEqual(dates, expected)
|
|
66
|
+
|
|
67
|
+
def test_safe_sort_rows_none_first(self):
|
|
68
|
+
"""Test safe_sort_rows with none_handling='first'."""
|
|
69
|
+
sorted_data = safe_sort_rows(self.sample_data, "date", none_handling="first")
|
|
70
|
+
|
|
71
|
+
dates = [row["date"] for row in sorted_data]
|
|
72
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
73
|
+
self.assertEqual(dates, expected)
|
|
74
|
+
|
|
75
|
+
def test_safe_sort_rows_with_default(self):
|
|
76
|
+
"""Test safe_sort_rows with none_handling='default'."""
|
|
77
|
+
sorted_data = safe_sort_rows(
|
|
78
|
+
self.sample_data, "date", none_handling="default", default_value="1900-01"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
dates = [row["date"] for row in sorted_data]
|
|
82
|
+
expected = [None, None, "2024-01", "2024-02", "2024-03"]
|
|
83
|
+
self.assertEqual(dates, expected)
|
|
84
|
+
|
|
85
|
+
def test_safe_sort_rows_reverse(self):
|
|
86
|
+
"""Test safe_sort_rows with reverse=True."""
|
|
87
|
+
sorted_data = safe_sort_rows(self.sample_data, "date", reverse=True)
|
|
88
|
+
|
|
89
|
+
dates = [row["date"] for row in sorted_data]
|
|
90
|
+
expected = [None, None, "2024-03", "2024-02", "2024-01"]
|
|
91
|
+
self.assertEqual(dates, expected)
|
|
92
|
+
|
|
93
|
+
def test_safe_sort_rows_invalid_none_handling(self):
|
|
94
|
+
"""Test safe_sort_rows with invalid none_handling option."""
|
|
95
|
+
with self.assertRaises(ValueError) as context:
|
|
96
|
+
safe_sort_rows(self.sample_data, "date", none_handling="invalid")
|
|
97
|
+
|
|
98
|
+
self.assertIn("Invalid none_handling option", str(context.exception))
|
|
99
|
+
|
|
100
|
+
def test_group_by_fields_single_field(self):
|
|
101
|
+
"""Test grouping by a single field."""
|
|
102
|
+
# Add data with same names for grouping
|
|
103
|
+
test_data = [
|
|
104
|
+
{"name": "Alice", "type": "A", "value": 1},
|
|
105
|
+
{"name": "Bob", "type": "B", "value": 2},
|
|
106
|
+
{"name": "Alice", "type": "C", "value": 3},
|
|
107
|
+
{"name": "Bob", "type": "A", "value": 4},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
groups = group_by_fields(test_data, "name")
|
|
111
|
+
|
|
112
|
+
self.assertEqual(len(groups), 2)
|
|
113
|
+
self.assertIn(("Alice",), groups)
|
|
114
|
+
self.assertIn(("Bob",), groups)
|
|
115
|
+
self.assertEqual(len(groups[("Alice",)]), 2)
|
|
116
|
+
self.assertEqual(len(groups[("Bob",)]), 2)
|
|
117
|
+
|
|
118
|
+
def test_group_by_fields_multiple_fields(self):
|
|
119
|
+
"""Test grouping by multiple fields."""
|
|
120
|
+
test_data = [
|
|
121
|
+
{"name": "Alice", "type": "A", "value": 1},
|
|
122
|
+
{"name": "Bob", "type": "B", "value": 2},
|
|
123
|
+
{"name": "Alice", "type": "A", "value": 3},
|
|
124
|
+
{"name": "Alice", "type": "B", "value": 4},
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
groups = group_by_fields(test_data, "name", "type")
|
|
128
|
+
|
|
129
|
+
self.assertEqual(len(groups), 3)
|
|
130
|
+
self.assertIn(("Alice", "A"), groups)
|
|
131
|
+
self.assertIn(("Bob", "B"), groups)
|
|
132
|
+
self.assertIn(("Alice", "B"), groups)
|
|
133
|
+
self.assertEqual(len(groups[("Alice", "A")]), 2)
|
|
134
|
+
self.assertEqual(len(groups[("Bob", "B")]), 1)
|
|
135
|
+
self.assertEqual(len(groups[("Alice", "B")]), 1)
|
|
136
|
+
|
|
137
|
+
def test_safe_sort_grouped_rows(self):
|
|
138
|
+
"""Test sorting rows within groups."""
|
|
139
|
+
# Create grouped data
|
|
140
|
+
test_data = [
|
|
141
|
+
{"group": "A", "date": "2024-03", "value": 1},
|
|
142
|
+
{"group": "A", "date": None, "value": 2},
|
|
143
|
+
{"group": "A", "date": "2024-01", "value": 3},
|
|
144
|
+
{"group": "B", "date": "2024-02", "value": 4},
|
|
145
|
+
{"group": "B", "date": None, "value": 5},
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
groups = group_by_fields(test_data, "group")
|
|
149
|
+
sorted_groups = safe_sort_grouped_rows(groups, "date")
|
|
150
|
+
|
|
151
|
+
# Check group A is sorted correctly
|
|
152
|
+
group_a_dates = [row["date"] for row in sorted_groups[("A",)]]
|
|
153
|
+
expected_a = ["2024-01", "2024-03", None]
|
|
154
|
+
self.assertEqual(group_a_dates, expected_a)
|
|
155
|
+
|
|
156
|
+
# Check group B is sorted correctly
|
|
157
|
+
group_b_dates = [row["date"] for row in sorted_groups[("B",)]]
|
|
158
|
+
expected_b = ["2024-02", None]
|
|
159
|
+
self.assertEqual(group_b_dates, expected_b)
|
|
160
|
+
|
|
161
|
+
def test_payment_profile_scenario(self):
|
|
162
|
+
"""Test the specific payment profile scenario that was failing."""
|
|
163
|
+
payment_profiles = [
|
|
164
|
+
{
|
|
165
|
+
"sys_id": 1,
|
|
166
|
+
"email_address": "test@example.com",
|
|
167
|
+
"card_number": "1234",
|
|
168
|
+
"expiration_date": "2024-12",
|
|
169
|
+
"status": "active",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"sys_id": 2,
|
|
173
|
+
"email_address": "test@example.com",
|
|
174
|
+
"card_number": "1234",
|
|
175
|
+
"expiration_date": "2024-06",
|
|
176
|
+
"status": "active",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"sys_id": 3,
|
|
180
|
+
"email_address": "test@example.com",
|
|
181
|
+
"card_number": "1234",
|
|
182
|
+
"expiration_date": None,
|
|
183
|
+
"status": "active",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"sys_id": 4,
|
|
187
|
+
"email_address": "other@example.com",
|
|
188
|
+
"card_number": "5678",
|
|
189
|
+
"expiration_date": "2025-01",
|
|
190
|
+
"status": "active",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"sys_id": 5,
|
|
194
|
+
"email_address": "other@example.com",
|
|
195
|
+
"card_number": "5678",
|
|
196
|
+
"expiration_date": None,
|
|
197
|
+
"status": "active",
|
|
198
|
+
},
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
# Group by email and card number
|
|
202
|
+
groups = group_by_fields(payment_profiles, "email_address", "card_number")
|
|
203
|
+
|
|
204
|
+
# Sort each group by expiration date
|
|
205
|
+
sorted_groups = safe_sort_grouped_rows(groups, "expiration_date")
|
|
206
|
+
|
|
207
|
+
# Verify we can safely enumerate through each group
|
|
208
|
+
for group_key, group in sorted_groups.items():
|
|
209
|
+
for idx, row in enumerate(group):
|
|
210
|
+
# This should not raise any errors
|
|
211
|
+
self.assertIsInstance(idx, int)
|
|
212
|
+
self.assertIn("sys_id", row)
|
|
213
|
+
self.assertIn("expiration_date", row)
|
|
214
|
+
|
|
215
|
+
# Check specific group sorting
|
|
216
|
+
test_group = sorted_groups[("test@example.com", "1234")]
|
|
217
|
+
exp_dates = [row["expiration_date"] for row in test_group]
|
|
218
|
+
expected = ["2024-06", "2024-12", None]
|
|
219
|
+
self.assertEqual(exp_dates, expected)
|
|
220
|
+
|
|
221
|
+
def test_mask_config_for_display_redacts_direct_passwords(self):
|
|
222
|
+
"""Ensure direct password/token fields are masked."""
|
|
223
|
+
config = {
|
|
224
|
+
"host": "db.local",
|
|
225
|
+
"password": "supersecret",
|
|
226
|
+
"token": "abc123",
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
masked = mask_config_for_display(config)
|
|
230
|
+
|
|
231
|
+
self.assertEqual(masked["host"], "db.local")
|
|
232
|
+
self.assertEqual(masked["password"], "*****")
|
|
233
|
+
self.assertEqual(masked["token"], "*****")
|
|
234
|
+
|
|
235
|
+
def test_mask_config_for_display_handles_nested_structures(self):
|
|
236
|
+
"""Verify masking applies to nested dicts, lists, tuples, and DSN strings."""
|
|
237
|
+
config = {
|
|
238
|
+
"options": {
|
|
239
|
+
"passwd": "innersecret",
|
|
240
|
+
"hosts": [
|
|
241
|
+
{"url": "postgresql://user:pwd@localhost/db"},
|
|
242
|
+
("token=xyz",),
|
|
243
|
+
],
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
masked = mask_config_for_display(config)
|
|
248
|
+
|
|
249
|
+
self.assertEqual(masked["options"]["passwd"], "*****")
|
|
250
|
+
self.assertEqual(
|
|
251
|
+
masked["options"]["hosts"][0]["url"],
|
|
252
|
+
"postgresql://user:*****@localhost/db",
|
|
253
|
+
)
|
|
254
|
+
self.assertEqual(masked["options"]["hosts"][1][0], "token=*****")
|
|
255
|
+
|
|
256
|
+
def test_mask_sensitive_in_string_redacts_key_value_pairs(self):
|
|
257
|
+
"""Key/value DSN parameters should be redacted."""
|
|
258
|
+
dsn = "host=db password=abc123;user=test"
|
|
259
|
+
masked = mask_sensitive_in_string(dsn)
|
|
260
|
+
self.assertEqual(masked, "host=db password=*****;user=test")
|
|
261
|
+
|
|
262
|
+
def test_mask_sensitive_in_string_redacts_url_credentials(self):
|
|
263
|
+
"""URL style credentials should hide the password portion."""
|
|
264
|
+
url = "postgresql://user:secret@host/db"
|
|
265
|
+
masked = mask_sensitive_in_string(url)
|
|
266
|
+
self.assertEqual(masked, "postgresql://user:*****@host/db")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
unittest.main()
|