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
velocity/db/utils.py
CHANGED
|
@@ -1,119 +1,190 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Database utility functions for common operations.
|
|
1
|
+
"""Utility helpers for velocity.db modules.
|
|
3
2
|
|
|
4
|
-
This module provides
|
|
5
|
-
|
|
3
|
+
This module provides helpers for redacting sensitive configuration values along
|
|
4
|
+
with common collection utilities used across the velocity database codebase.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Callable, List
|
|
9
|
+
|
|
10
|
+
_SENSITIVE_KEYWORDS = {
|
|
11
|
+
"password",
|
|
12
|
+
"passwd",
|
|
13
|
+
"pwd",
|
|
14
|
+
"secret",
|
|
15
|
+
"token",
|
|
16
|
+
"apikey",
|
|
17
|
+
"api_key",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_SENSITIVE_PATTERNS = [
|
|
21
|
+
re.compile(r"(password\s*=\s*)([^\s;]+)", re.IGNORECASE),
|
|
22
|
+
re.compile(r"(passwd\s*=\s*)([^\s;]+)", re.IGNORECASE),
|
|
23
|
+
re.compile(r"(pwd\s*=\s*)([^\s;]+)", re.IGNORECASE),
|
|
24
|
+
re.compile(r"(secret\s*=\s*)([^\s;]+)", re.IGNORECASE),
|
|
25
|
+
re.compile(r"(token\s*=\s*)([^\s;]+)", re.IGNORECASE),
|
|
26
|
+
re.compile(r"(api[_-]?key\s*=\s*)([^\s;]+)", re.IGNORECASE),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
_URL_CREDENTIAL_PATTERN = re.compile(r"(://[^:\s]+:)([^@/\s]+)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def mask_sensitive_in_string(value: str) -> str:
|
|
33
|
+
"""Return ``value`` with credential-like substrings redacted."""
|
|
34
|
+
|
|
35
|
+
if not value:
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
masked = value
|
|
39
|
+
for pattern in _SENSITIVE_PATTERNS:
|
|
40
|
+
masked = pattern.sub(lambda match: match.group(1) + "*****", masked)
|
|
41
|
+
|
|
42
|
+
return _URL_CREDENTIAL_PATTERN.sub(r"\1*****", masked)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mask_config_for_display(config: Any) -> Any:
|
|
46
|
+
"""Return ``config`` with common secret fields masked for logging/str()."""
|
|
47
|
+
|
|
48
|
+
if isinstance(config, dict):
|
|
49
|
+
masked = {}
|
|
50
|
+
for key, value in config.items():
|
|
51
|
+
if isinstance(key, str) and _contains_sensitive_keyword(key):
|
|
52
|
+
masked[key] = "*****"
|
|
53
|
+
else:
|
|
54
|
+
masked[key] = mask_config_for_display(value)
|
|
55
|
+
return masked
|
|
56
|
+
|
|
57
|
+
if isinstance(config, tuple):
|
|
58
|
+
return tuple(mask_config_for_display(item) for item in config)
|
|
59
|
+
|
|
60
|
+
if isinstance(config, list):
|
|
61
|
+
return [mask_config_for_display(item) for item in config]
|
|
62
|
+
|
|
63
|
+
if isinstance(config, str):
|
|
64
|
+
return mask_sensitive_in_string(config)
|
|
65
|
+
|
|
66
|
+
return config
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _contains_sensitive_keyword(key: str) -> bool:
|
|
70
|
+
lowered = key.lower()
|
|
71
|
+
return any(token in lowered for token in _SENSITIVE_KEYWORDS)
|
|
9
72
|
|
|
10
73
|
|
|
11
74
|
def safe_sort_key_none_last(field_name: str) -> Callable[[dict], tuple]:
|
|
12
75
|
"""
|
|
13
76
|
Create a sort key function that places None values at the end.
|
|
14
|
-
|
|
77
|
+
|
|
15
78
|
Args:
|
|
16
79
|
field_name: Name of the field to sort by
|
|
17
|
-
|
|
80
|
+
|
|
18
81
|
Returns:
|
|
19
82
|
A function suitable for use as a sort key that handles None values
|
|
20
|
-
|
|
83
|
+
|
|
21
84
|
Example:
|
|
22
85
|
rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
|
|
23
86
|
sorted_rows = sorted(rows, key=safe_sort_key_none_last("date"))
|
|
24
87
|
# Result: [{"date": "2023-12"}, {"date": "2024-01"}, {"date": None}]
|
|
25
88
|
"""
|
|
89
|
+
|
|
26
90
|
def sort_key(row: dict) -> tuple:
|
|
27
91
|
value = row.get(field_name)
|
|
28
92
|
if value is None:
|
|
29
93
|
return (1, "") # None values sort last
|
|
30
94
|
return (0, value)
|
|
31
|
-
|
|
95
|
+
|
|
32
96
|
return sort_key
|
|
33
97
|
|
|
34
98
|
|
|
35
99
|
def safe_sort_key_none_first(field_name: str) -> Callable[[dict], tuple]:
|
|
36
100
|
"""
|
|
37
101
|
Create a sort key function that places None values at the beginning.
|
|
38
|
-
|
|
102
|
+
|
|
39
103
|
Args:
|
|
40
104
|
field_name: Name of the field to sort by
|
|
41
|
-
|
|
105
|
+
|
|
42
106
|
Returns:
|
|
43
107
|
A function suitable for use as a sort key that handles None values
|
|
44
|
-
|
|
108
|
+
|
|
45
109
|
Example:
|
|
46
110
|
rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
|
|
47
111
|
sorted_rows = sorted(rows, key=safe_sort_key_none_first("date"))
|
|
48
112
|
# Result: [{"date": None}, {"date": "2023-12"}, {"date": "2024-01"}]
|
|
49
113
|
"""
|
|
114
|
+
|
|
50
115
|
def sort_key(row: dict) -> tuple:
|
|
51
116
|
value = row.get(field_name)
|
|
52
117
|
if value is None:
|
|
53
118
|
return (0, "") # None values sort first
|
|
54
119
|
return (1, value)
|
|
55
|
-
|
|
120
|
+
|
|
56
121
|
return sort_key
|
|
57
122
|
|
|
58
123
|
|
|
59
|
-
def safe_sort_key_with_default(
|
|
124
|
+
def safe_sort_key_with_default(
|
|
125
|
+
field_name: str, default_value: Any = ""
|
|
126
|
+
) -> Callable[[dict], Any]:
|
|
60
127
|
"""
|
|
61
128
|
Create a sort key function that replaces None values with a default.
|
|
62
|
-
|
|
129
|
+
|
|
63
130
|
Args:
|
|
64
131
|
field_name: Name of the field to sort by
|
|
65
132
|
default_value: Value to use for None entries
|
|
66
|
-
|
|
133
|
+
|
|
67
134
|
Returns:
|
|
68
135
|
A function suitable for use as a sort key that handles None values
|
|
69
|
-
|
|
136
|
+
|
|
70
137
|
Example:
|
|
71
138
|
rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
|
|
72
139
|
sorted_rows = sorted(rows, key=safe_sort_key_with_default("date", "1900-01"))
|
|
73
140
|
# Result: [{"date": None}, {"date": "2023-12"}, {"date": "2024-01"}]
|
|
74
141
|
"""
|
|
142
|
+
|
|
75
143
|
def sort_key(row: dict) -> Any:
|
|
76
144
|
value = row.get(field_name)
|
|
77
145
|
return default_value if value is None else value
|
|
78
|
-
|
|
146
|
+
|
|
79
147
|
return sort_key
|
|
80
148
|
|
|
81
149
|
|
|
82
|
-
def safe_sort_rows(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
def safe_sort_rows(
|
|
151
|
+
rows: List[dict],
|
|
152
|
+
field_name: str,
|
|
153
|
+
none_handling: str = "last",
|
|
154
|
+
default_value: Any = "",
|
|
155
|
+
reverse: bool = False,
|
|
156
|
+
) -> List[dict]:
|
|
86
157
|
"""
|
|
87
158
|
Safely sort a list of dictionaries by a field that may contain None values.
|
|
88
|
-
|
|
159
|
+
|
|
89
160
|
Args:
|
|
90
161
|
rows: List of dictionaries to sort
|
|
91
162
|
field_name: Name of the field to sort by
|
|
92
163
|
none_handling: How to handle None values - "first", "last", or "default"
|
|
93
164
|
default_value: Default value to use when none_handling is "default"
|
|
94
165
|
reverse: Whether to reverse the sort order
|
|
95
|
-
|
|
166
|
+
|
|
96
167
|
Returns:
|
|
97
168
|
New list of dictionaries sorted by the specified field
|
|
98
|
-
|
|
169
|
+
|
|
99
170
|
Raises:
|
|
100
171
|
ValueError: If none_handling is not a valid option
|
|
101
|
-
|
|
172
|
+
|
|
102
173
|
Example:
|
|
103
174
|
rows = [
|
|
104
175
|
{"name": "Alice", "date": "2024-01"},
|
|
105
176
|
{"name": "Bob", "date": None},
|
|
106
177
|
{"name": "Charlie", "date": "2023-12"}
|
|
107
178
|
]
|
|
108
|
-
|
|
179
|
+
|
|
109
180
|
# None values last
|
|
110
181
|
sorted_rows = safe_sort_rows(rows, "date")
|
|
111
|
-
|
|
112
|
-
# None values first
|
|
182
|
+
|
|
183
|
+
# None values first
|
|
113
184
|
sorted_rows = safe_sort_rows(rows, "date", none_handling="first")
|
|
114
|
-
|
|
185
|
+
|
|
115
186
|
# Replace None with default
|
|
116
|
-
sorted_rows = safe_sort_rows(rows, "date", none_handling="default",
|
|
187
|
+
sorted_rows = safe_sort_rows(rows, "date", none_handling="default",
|
|
117
188
|
default_value="1900-01")
|
|
118
189
|
"""
|
|
119
190
|
if none_handling == "last":
|
|
@@ -123,37 +194,39 @@ def safe_sort_rows(rows: List[dict], field_name: str,
|
|
|
123
194
|
elif none_handling == "default":
|
|
124
195
|
key_func = safe_sort_key_with_default(field_name, default_value)
|
|
125
196
|
else:
|
|
126
|
-
raise ValueError(
|
|
127
|
-
|
|
128
|
-
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"Invalid none_handling option: {none_handling}. "
|
|
199
|
+
"Must be 'first', 'last', or 'default'"
|
|
200
|
+
)
|
|
201
|
+
|
|
129
202
|
return sorted(rows, key=key_func, reverse=reverse)
|
|
130
203
|
|
|
131
204
|
|
|
132
205
|
def group_by_fields(rows: List[dict], *field_names: str) -> dict:
|
|
133
206
|
"""
|
|
134
207
|
Group rows by one or more field values.
|
|
135
|
-
|
|
208
|
+
|
|
136
209
|
Args:
|
|
137
210
|
rows: List of dictionaries to group
|
|
138
211
|
*field_names: Names of fields to group by
|
|
139
|
-
|
|
212
|
+
|
|
140
213
|
Returns:
|
|
141
214
|
Dictionary where keys are tuples of field values and values are lists of rows
|
|
142
|
-
|
|
215
|
+
|
|
143
216
|
Example:
|
|
144
217
|
rows = [
|
|
145
218
|
{"email": "alice@example.com", "type": "premium", "amount": 100},
|
|
146
219
|
{"email": "alice@example.com", "type": "basic", "amount": 50},
|
|
147
220
|
{"email": "bob@example.com", "type": "premium", "amount": 100},
|
|
148
221
|
]
|
|
149
|
-
|
|
222
|
+
|
|
150
223
|
# Group by email only
|
|
151
224
|
groups = group_by_fields(rows, "email")
|
|
152
225
|
# Result: {
|
|
153
226
|
# ("alice@example.com",): [row1, row2],
|
|
154
227
|
# ("bob@example.com",): [row3]
|
|
155
228
|
# }
|
|
156
|
-
|
|
229
|
+
|
|
157
230
|
# Group by email and type
|
|
158
231
|
groups = group_by_fields(rows, "email", "type")
|
|
159
232
|
# Result: {
|
|
@@ -168,34 +241,37 @@ def group_by_fields(rows: List[dict], *field_names: str) -> dict:
|
|
|
168
241
|
if key not in groups:
|
|
169
242
|
groups[key] = []
|
|
170
243
|
groups[key].append(row)
|
|
171
|
-
|
|
244
|
+
|
|
172
245
|
return groups
|
|
173
246
|
|
|
174
247
|
|
|
175
|
-
def safe_sort_grouped_rows(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
248
|
+
def safe_sort_grouped_rows(
|
|
249
|
+
grouped_rows: dict,
|
|
250
|
+
field_name: str,
|
|
251
|
+
none_handling: str = "last",
|
|
252
|
+
default_value: Any = "",
|
|
253
|
+
reverse: bool = False,
|
|
254
|
+
) -> dict:
|
|
179
255
|
"""
|
|
180
256
|
Safely sort rows within each group of a grouped result.
|
|
181
|
-
|
|
257
|
+
|
|
182
258
|
Args:
|
|
183
259
|
grouped_rows: Dictionary of grouped rows (from group_by_fields)
|
|
184
260
|
field_name: Name of the field to sort by within each group
|
|
185
261
|
none_handling: How to handle None values - "first", "last", or "default"
|
|
186
262
|
default_value: Default value to use when none_handling is "default"
|
|
187
263
|
reverse: Whether to reverse the sort order
|
|
188
|
-
|
|
264
|
+
|
|
189
265
|
Returns:
|
|
190
266
|
Dictionary with the same keys but sorted lists as values
|
|
191
|
-
|
|
267
|
+
|
|
192
268
|
Example:
|
|
193
269
|
# After grouping payment profiles by email and card number
|
|
194
270
|
groups = group_by_fields(payment_profiles, "email_address", "card_number")
|
|
195
|
-
|
|
271
|
+
|
|
196
272
|
# Sort each group by expiration date, with None values last
|
|
197
273
|
sorted_groups = safe_sort_grouped_rows(groups, "expiration_date")
|
|
198
|
-
|
|
274
|
+
|
|
199
275
|
# Now process each sorted group
|
|
200
276
|
for group_key, sorted_group in sorted_groups.items():
|
|
201
277
|
for idx, row in enumerate(sorted_group):
|
|
@@ -204,6 +280,8 @@ def safe_sort_grouped_rows(grouped_rows: dict, field_name: str,
|
|
|
204
280
|
"""
|
|
205
281
|
result = {}
|
|
206
282
|
for key, rows in grouped_rows.items():
|
|
207
|
-
result[key] = safe_sort_rows(
|
|
208
|
-
|
|
283
|
+
result[key] = safe_sort_rows(
|
|
284
|
+
rows, field_name, none_handling, default_value, reverse
|
|
285
|
+
)
|
|
286
|
+
|
|
209
287
|
return result
|
velocity/misc/conv/__init__.py
CHANGED
velocity/misc/conv/iconv.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import ast
|
|
3
3
|
import codecs
|
|
4
|
+
import decimal
|
|
4
5
|
from decimal import Decimal, ROUND_HALF_UP
|
|
5
6
|
from email.utils import parseaddr
|
|
6
7
|
from datetime import datetime
|
|
@@ -224,7 +225,7 @@ def money(data: str) -> Optional[Decimal]:
|
|
|
224
225
|
return None
|
|
225
226
|
try:
|
|
226
227
|
return Decimal(cleaned)
|
|
227
|
-
except:
|
|
228
|
+
except (ValueError, TypeError, decimal.InvalidOperation):
|
|
228
229
|
return None
|
|
229
230
|
|
|
230
231
|
|
|
@@ -246,7 +247,7 @@ def round_to(
|
|
|
246
247
|
cleaned = re.sub(r"[^0-9\.\-+]", "", val_str)
|
|
247
248
|
try:
|
|
248
249
|
as_dec = Decimal(cleaned)
|
|
249
|
-
except:
|
|
250
|
+
except (ValueError, TypeError, decimal.InvalidOperation):
|
|
250
251
|
return None
|
|
251
252
|
return as_dec.quantize(Decimal(10) ** -precision, rounding=ROUND_HALF_UP)
|
|
252
253
|
|
|
@@ -269,7 +270,7 @@ def decimal_val(data: str) -> Optional[Decimal]:
|
|
|
269
270
|
return None
|
|
270
271
|
try:
|
|
271
272
|
return Decimal(cleaned)
|
|
272
|
-
except:
|
|
273
|
+
except (ValueError, TypeError, decimal.InvalidOperation):
|
|
273
274
|
return None
|
|
274
275
|
|
|
275
276
|
|
|
@@ -305,7 +306,7 @@ def to_list(data: Union[str, list, None]) -> Optional[list]:
|
|
|
305
306
|
if data_str.startswith("[") and data_str.endswith("]"):
|
|
306
307
|
try:
|
|
307
308
|
return ast.literal_eval(data_str)
|
|
308
|
-
except:
|
|
309
|
+
except (ValueError, SyntaxError):
|
|
309
310
|
return [data_str] # fallback: treat as a single string
|
|
310
311
|
return [data_str]
|
|
311
312
|
|
velocity/misc/export.py
CHANGED
velocity/misc/merge.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Misc module tests
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
import numbers
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from velocity.db import exceptions
|
|
6
|
+
from ..db import NOTNULL, join, randomword, return_default, NotSupported
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestNotSupported(unittest.TestCase):
|
|
10
|
+
def test_not_supported(self):
|
|
11
|
+
with self.assertRaises(Exception) as context:
|
|
12
|
+
NotSupported()
|
|
13
|
+
self.assertEqual(
|
|
14
|
+
str(context.exception),
|
|
15
|
+
"Sorry, the driver for this database is not installed",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestNOTNULL(unittest.TestCase):
|
|
20
|
+
def test_notnull(self):
|
|
21
|
+
self.assertTrue(NOTNULL(("key", "value")))
|
|
22
|
+
self.assertFalse(NOTNULL(("key", None)))
|
|
23
|
+
self.assertFalse(NOTNULL(("key",)))
|
|
24
|
+
self.assertFalse(NOTNULL(()))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestJoin(unittest.TestCase):
|
|
28
|
+
def test_or(self):
|
|
29
|
+
self.assertEqual(join._or("a=1", "b=2"), "(a=1 or b=2)")
|
|
30
|
+
|
|
31
|
+
def test_and(self):
|
|
32
|
+
self.assertEqual(join._and("a=1", "b=2"), "(a=1 and b=2)")
|
|
33
|
+
|
|
34
|
+
def test_list(self):
|
|
35
|
+
result = join._list("a=1", key1=123, key2="value")
|
|
36
|
+
self.assertIn("a=1", result)
|
|
37
|
+
self.assertIn("key1=123", result)
|
|
38
|
+
self.assertIn("key2='value'", result)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestRandomWord(unittest.TestCase):
|
|
42
|
+
def test_randomword_length_specified(self):
|
|
43
|
+
word = randomword(10)
|
|
44
|
+
self.assertEqual(len(word), 10)
|
|
45
|
+
self.assertTrue(word.islower())
|
|
46
|
+
|
|
47
|
+
def test_randomword_length_random(self):
|
|
48
|
+
word = randomword()
|
|
49
|
+
self.assertTrue(5 <= len(word) <= 15)
|
|
50
|
+
self.assertTrue(word.islower())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestReturnDefault(unittest.TestCase):
|
|
54
|
+
def setUp(self):
|
|
55
|
+
class MockTransaction:
|
|
56
|
+
def create_savepoint(self, cursor):
|
|
57
|
+
return "savepoint"
|
|
58
|
+
|
|
59
|
+
def rollback_savepoint(self, sp, cursor):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def release_savepoint(self, sp, cursor):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
class MockTable:
|
|
66
|
+
cursor = MagicMock()
|
|
67
|
+
|
|
68
|
+
class MockClass:
|
|
69
|
+
tx = MockTransaction()
|
|
70
|
+
table = MockTable()
|
|
71
|
+
|
|
72
|
+
@return_default(default="default_value")
|
|
73
|
+
def func(self, raise_exception=False):
|
|
74
|
+
if raise_exception:
|
|
75
|
+
raise exceptions.DbApplicationError("Test error")
|
|
76
|
+
return "result"
|
|
77
|
+
|
|
78
|
+
self.mock_obj = MockClass()
|
|
79
|
+
|
|
80
|
+
def test_return_default_no_exception(self):
|
|
81
|
+
result = self.mock_obj.func(raise_exception=False)
|
|
82
|
+
self.assertEqual(result, "result")
|
|
83
|
+
|
|
84
|
+
def test_return_default_with_exception(self):
|
|
85
|
+
result = self.mock_obj.func(raise_exception=True)
|
|
86
|
+
self.assertEqual(result, "default_value")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
unittest.main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Test script to verify the duplicate_rows fix
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_grouping_fix():
|
|
7
|
+
"""Test the fixed grouping logic"""
|
|
8
|
+
|
|
9
|
+
# Simulate duplicate rows that would come from duplicate_rows()
|
|
10
|
+
duplicate_rows = [
|
|
11
|
+
{
|
|
12
|
+
"sys_id": 1,
|
|
13
|
+
"email_address": "test1@example.com",
|
|
14
|
+
"card_number": "1234",
|
|
15
|
+
"expiration_date": "2024-01",
|
|
16
|
+
"status": None,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"sys_id": 2,
|
|
20
|
+
"email_address": "test1@example.com",
|
|
21
|
+
"card_number": "1234",
|
|
22
|
+
"expiration_date": "2024-02",
|
|
23
|
+
"status": None,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"sys_id": 3,
|
|
27
|
+
"email_address": "test2@example.com",
|
|
28
|
+
"card_number": "5678",
|
|
29
|
+
"expiration_date": "2024-03",
|
|
30
|
+
"status": None,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"sys_id": 4,
|
|
34
|
+
"email_address": "test2@example.com",
|
|
35
|
+
"card_number": "5678",
|
|
36
|
+
"expiration_date": "2024-01",
|
|
37
|
+
"status": None,
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Group rows by email_address and card_number (the fixed logic)
|
|
42
|
+
groups = {}
|
|
43
|
+
for row in duplicate_rows:
|
|
44
|
+
key = (row["email_address"], row["card_number"])
|
|
45
|
+
if key not in groups:
|
|
46
|
+
groups[key] = []
|
|
47
|
+
groups[key].append(row)
|
|
48
|
+
|
|
49
|
+
print("Groups found:")
|
|
50
|
+
for key, group in groups.items():
|
|
51
|
+
print(f" Key: {key}, Group size: {len(group)}")
|
|
52
|
+
|
|
53
|
+
# Test the sorting that was causing the original error
|
|
54
|
+
try:
|
|
55
|
+
sorted_group = sorted(group, key=lambda x: x["expiration_date"])
|
|
56
|
+
print(
|
|
57
|
+
f" Sorted by expiration_date: {[row['expiration_date'] for row in sorted_group]}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Test the enumeration that happens in the original code
|
|
61
|
+
for idx, row in enumerate(sorted_group):
|
|
62
|
+
print(
|
|
63
|
+
f" {idx}: {row['sys_id']}, {row['email_address']}, {row['card_number']}, {row['expiration_date']}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except TypeError as e:
|
|
67
|
+
print(f" ERROR: {e}")
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
success = test_grouping_fix()
|
|
75
|
+
if success:
|
|
76
|
+
print("\n✓ Fix appears to work correctly!")
|
|
77
|
+
else:
|
|
78
|
+
print("\n✗ Fix has issues")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import decimal
|
|
3
|
+
from datetime import datetime, date, time, timedelta
|
|
4
|
+
from ..format import gallons, gallons2liters, currency, human_delta, to_json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestYourModule(unittest.TestCase):
|
|
8
|
+
|
|
9
|
+
def test_gallons(self):
|
|
10
|
+
"""Tests the gallons function with various inputs."""
|
|
11
|
+
self.assertEqual(gallons("10.5"), "10.50")
|
|
12
|
+
self.assertEqual(gallons(10.5), "10.50")
|
|
13
|
+
self.assertEqual(gallons(decimal.Decimal("10.5")), "10.50")
|
|
14
|
+
self.assertEqual(gallons(None), "")
|
|
15
|
+
self.assertEqual(gallons("invalid"), "")
|
|
16
|
+
|
|
17
|
+
def test_gallons2liters(self):
|
|
18
|
+
"""Tests the gallons2liters function with various inputs."""
|
|
19
|
+
self.assertEqual(gallons2liters("10"), "37.85") # 10 gallons to liters
|
|
20
|
+
self.assertEqual(gallons2liters(1), "3.79") # 1 gallon to liters
|
|
21
|
+
self.assertEqual(gallons2liters(decimal.Decimal("1")), "3.79")
|
|
22
|
+
self.assertEqual(gallons2liters(None), "")
|
|
23
|
+
self.assertEqual(gallons2liters("invalid"), "")
|
|
24
|
+
|
|
25
|
+
def test_currency(self):
|
|
26
|
+
"""Tests the currency function with various inputs."""
|
|
27
|
+
self.assertEqual(currency("1000.5"), "1000.50")
|
|
28
|
+
self.assertEqual(currency(1000.5), "1000.50")
|
|
29
|
+
self.assertEqual(currency(decimal.Decimal("1000.5")), "1000.50")
|
|
30
|
+
self.assertEqual(currency(None), "")
|
|
31
|
+
self.assertEqual(currency("invalid"), "")
|
|
32
|
+
|
|
33
|
+
def test_human_delta(self):
|
|
34
|
+
"""Tests the human_delta function with various timedelta values."""
|
|
35
|
+
self.assertEqual(human_delta(timedelta(seconds=45)), "45 sec")
|
|
36
|
+
self.assertEqual(human_delta(timedelta(minutes=2, seconds=15)), "2 min 15 sec")
|
|
37
|
+
self.assertEqual(
|
|
38
|
+
human_delta(timedelta(hours=1, minutes=5)), "1 hr(s) 5 min 0 sec"
|
|
39
|
+
)
|
|
40
|
+
self.assertEqual(
|
|
41
|
+
human_delta(timedelta(days=2, hours=4, minutes=30)),
|
|
42
|
+
"2 day(s) 4 hr(s) 30 min 0 sec",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def test_to_json(self):
|
|
46
|
+
"""Tests the to_json function with various custom objects."""
|
|
47
|
+
obj = {
|
|
48
|
+
"name": "Test",
|
|
49
|
+
"price": decimal.Decimal("19.99"),
|
|
50
|
+
"date": date(2023, 1, 1),
|
|
51
|
+
"datetime": datetime(2023, 1, 1, 12, 30, 45),
|
|
52
|
+
"time": time(12, 30, 45),
|
|
53
|
+
"duration": timedelta(days=1, hours=2, minutes=30),
|
|
54
|
+
}
|
|
55
|
+
result = to_json(obj)
|
|
56
|
+
self.assertIn('"price": 19.99', result)
|
|
57
|
+
self.assertIn('"date": "2023-01-01"', result)
|
|
58
|
+
self.assertIn('"datetime": "2023-01-01 12:30:45"', result)
|
|
59
|
+
self.assertIn('"time": "12:30:45"', result)
|
|
60
|
+
self.assertIn('"duration": "1 day(s) 2 hr(s) 30 min 0 sec"', result)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|