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
velocity/db/utils.py
CHANGED
|
@@ -5,115 +5,123 @@ This module provides utility functions to handle common database operations
|
|
|
5
5
|
safely, including sorting with None values and other edge cases.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any, Callable, List
|
|
8
|
+
from typing import Any, Callable, List
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def safe_sort_key_none_last(field_name: str) -> Callable[[dict], tuple]:
|
|
12
12
|
"""
|
|
13
13
|
Create a sort key function that places None values at the end.
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
Args:
|
|
16
16
|
field_name: Name of the field to sort by
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Returns:
|
|
19
19
|
A function suitable for use as a sort key that handles None values
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
Example:
|
|
22
22
|
rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
|
|
23
23
|
sorted_rows = sorted(rows, key=safe_sort_key_none_last("date"))
|
|
24
24
|
# Result: [{"date": "2023-12"}, {"date": "2024-01"}, {"date": None}]
|
|
25
25
|
"""
|
|
26
|
+
|
|
26
27
|
def sort_key(row: dict) -> tuple:
|
|
27
28
|
value = row.get(field_name)
|
|
28
29
|
if value is None:
|
|
29
30
|
return (1, "") # None values sort last
|
|
30
31
|
return (0, value)
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
return sort_key
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def safe_sort_key_none_first(field_name: str) -> Callable[[dict], tuple]:
|
|
36
37
|
"""
|
|
37
38
|
Create a sort key function that places None values at the beginning.
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
Args:
|
|
40
41
|
field_name: Name of the field to sort by
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
Returns:
|
|
43
44
|
A function suitable for use as a sort key that handles None values
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
Example:
|
|
46
47
|
rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
|
|
47
48
|
sorted_rows = sorted(rows, key=safe_sort_key_none_first("date"))
|
|
48
49
|
# Result: [{"date": None}, {"date": "2023-12"}, {"date": "2024-01"}]
|
|
49
50
|
"""
|
|
51
|
+
|
|
50
52
|
def sort_key(row: dict) -> tuple:
|
|
51
53
|
value = row.get(field_name)
|
|
52
54
|
if value is None:
|
|
53
55
|
return (0, "") # None values sort first
|
|
54
56
|
return (1, value)
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
return sort_key
|
|
57
59
|
|
|
58
60
|
|
|
59
|
-
def safe_sort_key_with_default(
|
|
61
|
+
def safe_sort_key_with_default(
|
|
62
|
+
field_name: str, default_value: Any = ""
|
|
63
|
+
) -> Callable[[dict], Any]:
|
|
60
64
|
"""
|
|
61
65
|
Create a sort key function that replaces None values with a default.
|
|
62
|
-
|
|
66
|
+
|
|
63
67
|
Args:
|
|
64
68
|
field_name: Name of the field to sort by
|
|
65
69
|
default_value: Value to use for None entries
|
|
66
|
-
|
|
70
|
+
|
|
67
71
|
Returns:
|
|
68
72
|
A function suitable for use as a sort key that handles None values
|
|
69
|
-
|
|
73
|
+
|
|
70
74
|
Example:
|
|
71
75
|
rows = [{"date": "2024-01"}, {"date": None}, {"date": "2023-12"}]
|
|
72
76
|
sorted_rows = sorted(rows, key=safe_sort_key_with_default("date", "1900-01"))
|
|
73
77
|
# Result: [{"date": None}, {"date": "2023-12"}, {"date": "2024-01"}]
|
|
74
78
|
"""
|
|
79
|
+
|
|
75
80
|
def sort_key(row: dict) -> Any:
|
|
76
81
|
value = row.get(field_name)
|
|
77
82
|
return default_value if value is None else value
|
|
78
|
-
|
|
83
|
+
|
|
79
84
|
return sort_key
|
|
80
85
|
|
|
81
86
|
|
|
82
|
-
def safe_sort_rows(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
def safe_sort_rows(
|
|
88
|
+
rows: List[dict],
|
|
89
|
+
field_name: str,
|
|
90
|
+
none_handling: str = "last",
|
|
91
|
+
default_value: Any = "",
|
|
92
|
+
reverse: bool = False,
|
|
93
|
+
) -> List[dict]:
|
|
86
94
|
"""
|
|
87
95
|
Safely sort a list of dictionaries by a field that may contain None values.
|
|
88
|
-
|
|
96
|
+
|
|
89
97
|
Args:
|
|
90
98
|
rows: List of dictionaries to sort
|
|
91
99
|
field_name: Name of the field to sort by
|
|
92
100
|
none_handling: How to handle None values - "first", "last", or "default"
|
|
93
101
|
default_value: Default value to use when none_handling is "default"
|
|
94
102
|
reverse: Whether to reverse the sort order
|
|
95
|
-
|
|
103
|
+
|
|
96
104
|
Returns:
|
|
97
105
|
New list of dictionaries sorted by the specified field
|
|
98
|
-
|
|
106
|
+
|
|
99
107
|
Raises:
|
|
100
108
|
ValueError: If none_handling is not a valid option
|
|
101
|
-
|
|
109
|
+
|
|
102
110
|
Example:
|
|
103
111
|
rows = [
|
|
104
112
|
{"name": "Alice", "date": "2024-01"},
|
|
105
113
|
{"name": "Bob", "date": None},
|
|
106
114
|
{"name": "Charlie", "date": "2023-12"}
|
|
107
115
|
]
|
|
108
|
-
|
|
116
|
+
|
|
109
117
|
# None values last
|
|
110
118
|
sorted_rows = safe_sort_rows(rows, "date")
|
|
111
|
-
|
|
112
|
-
# None values first
|
|
119
|
+
|
|
120
|
+
# None values first
|
|
113
121
|
sorted_rows = safe_sort_rows(rows, "date", none_handling="first")
|
|
114
|
-
|
|
122
|
+
|
|
115
123
|
# Replace None with default
|
|
116
|
-
sorted_rows = safe_sort_rows(rows, "date", none_handling="default",
|
|
124
|
+
sorted_rows = safe_sort_rows(rows, "date", none_handling="default",
|
|
117
125
|
default_value="1900-01")
|
|
118
126
|
"""
|
|
119
127
|
if none_handling == "last":
|
|
@@ -123,37 +131,39 @@ def safe_sort_rows(rows: List[dict], field_name: str,
|
|
|
123
131
|
elif none_handling == "default":
|
|
124
132
|
key_func = safe_sort_key_with_default(field_name, default_value)
|
|
125
133
|
else:
|
|
126
|
-
raise ValueError(
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Invalid none_handling option: {none_handling}. "
|
|
136
|
+
"Must be 'first', 'last', or 'default'"
|
|
137
|
+
)
|
|
138
|
+
|
|
129
139
|
return sorted(rows, key=key_func, reverse=reverse)
|
|
130
140
|
|
|
131
141
|
|
|
132
142
|
def group_by_fields(rows: List[dict], *field_names: str) -> dict:
|
|
133
143
|
"""
|
|
134
144
|
Group rows by one or more field values.
|
|
135
|
-
|
|
145
|
+
|
|
136
146
|
Args:
|
|
137
147
|
rows: List of dictionaries to group
|
|
138
148
|
*field_names: Names of fields to group by
|
|
139
|
-
|
|
149
|
+
|
|
140
150
|
Returns:
|
|
141
151
|
Dictionary where keys are tuples of field values and values are lists of rows
|
|
142
|
-
|
|
152
|
+
|
|
143
153
|
Example:
|
|
144
154
|
rows = [
|
|
145
155
|
{"email": "alice@example.com", "type": "premium", "amount": 100},
|
|
146
156
|
{"email": "alice@example.com", "type": "basic", "amount": 50},
|
|
147
157
|
{"email": "bob@example.com", "type": "premium", "amount": 100},
|
|
148
158
|
]
|
|
149
|
-
|
|
159
|
+
|
|
150
160
|
# Group by email only
|
|
151
161
|
groups = group_by_fields(rows, "email")
|
|
152
162
|
# Result: {
|
|
153
163
|
# ("alice@example.com",): [row1, row2],
|
|
154
164
|
# ("bob@example.com",): [row3]
|
|
155
165
|
# }
|
|
156
|
-
|
|
166
|
+
|
|
157
167
|
# Group by email and type
|
|
158
168
|
groups = group_by_fields(rows, "email", "type")
|
|
159
169
|
# Result: {
|
|
@@ -168,34 +178,37 @@ def group_by_fields(rows: List[dict], *field_names: str) -> dict:
|
|
|
168
178
|
if key not in groups:
|
|
169
179
|
groups[key] = []
|
|
170
180
|
groups[key].append(row)
|
|
171
|
-
|
|
181
|
+
|
|
172
182
|
return groups
|
|
173
183
|
|
|
174
184
|
|
|
175
|
-
def safe_sort_grouped_rows(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
def safe_sort_grouped_rows(
|
|
186
|
+
grouped_rows: dict,
|
|
187
|
+
field_name: str,
|
|
188
|
+
none_handling: str = "last",
|
|
189
|
+
default_value: Any = "",
|
|
190
|
+
reverse: bool = False,
|
|
191
|
+
) -> dict:
|
|
179
192
|
"""
|
|
180
193
|
Safely sort rows within each group of a grouped result.
|
|
181
|
-
|
|
194
|
+
|
|
182
195
|
Args:
|
|
183
196
|
grouped_rows: Dictionary of grouped rows (from group_by_fields)
|
|
184
197
|
field_name: Name of the field to sort by within each group
|
|
185
198
|
none_handling: How to handle None values - "first", "last", or "default"
|
|
186
199
|
default_value: Default value to use when none_handling is "default"
|
|
187
200
|
reverse: Whether to reverse the sort order
|
|
188
|
-
|
|
201
|
+
|
|
189
202
|
Returns:
|
|
190
203
|
Dictionary with the same keys but sorted lists as values
|
|
191
|
-
|
|
204
|
+
|
|
192
205
|
Example:
|
|
193
206
|
# After grouping payment profiles by email and card number
|
|
194
207
|
groups = group_by_fields(payment_profiles, "email_address", "card_number")
|
|
195
|
-
|
|
208
|
+
|
|
196
209
|
# Sort each group by expiration date, with None values last
|
|
197
210
|
sorted_groups = safe_sort_grouped_rows(groups, "expiration_date")
|
|
198
|
-
|
|
211
|
+
|
|
199
212
|
# Now process each sorted group
|
|
200
213
|
for group_key, sorted_group in sorted_groups.items():
|
|
201
214
|
for idx, row in enumerate(sorted_group):
|
|
@@ -204,6 +217,8 @@ def safe_sort_grouped_rows(grouped_rows: dict, field_name: str,
|
|
|
204
217
|
"""
|
|
205
218
|
result = {}
|
|
206
219
|
for key, rows in grouped_rows.items():
|
|
207
|
-
result[key] = safe_sort_rows(
|
|
208
|
-
|
|
220
|
+
result[key] = safe_sort_rows(
|
|
221
|
+
rows, field_name, none_handling, default_value, reverse
|
|
222
|
+
)
|
|
223
|
+
|
|
209
224
|
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()
|