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.
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 -839
  28. velocity/db/core/result.py +30 -24
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +279 -40
  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.109.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.109.dist-info/RECORD +0 -56
  118. {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.109.dist-info → velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
  120. {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, Optional, Union
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(field_name: str, default_value: Any = "") -> Callable[[dict], Any]:
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(rows: List[dict], field_name: str,
83
- none_handling: str = "last",
84
- default_value: Any = "",
85
- reverse: bool = False) -> List[dict]:
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(f"Invalid none_handling option: {none_handling}. "
127
- "Must be 'first', 'last', or 'default'")
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(grouped_rows: dict, field_name: str,
176
- none_handling: str = "last",
177
- default_value: Any = "",
178
- reverse: bool = False) -> dict:
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(rows, field_name, none_handling, default_value, reverse)
208
-
220
+ result[key] = safe_sort_rows(
221
+ rows, field_name, none_handling, default_value, reverse
222
+ )
223
+
209
224
  return result
@@ -1,2 +1,4 @@
1
1
  from . import iconv
2
2
  from . import oconv
3
+
4
+ __all__ = ["iconv", "oconv"]
@@ -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
@@ -1,7 +1,4 @@
1
- import decimal
2
- import json
3
- from datetime import datetime, date, time, timedelta
4
- from typing import Union, List, Dict
1
+ from typing import List, Dict
5
2
  from io import BytesIO
6
3
  import base64
7
4
  import openpyxl
velocity/misc/merge.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from copy import deepcopy
2
2
  from functools import reduce
3
- from typing import Dict, List, Any
3
+ from typing import Dict, Any
4
4
 
5
5
 
6
6
  def deep_merge(*dicts: Dict[str, Any], update: bool = False) -> Dict[str, Any]:
@@ -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()