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.
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 +251 -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 +48 -13
  27. velocity/db/core/engine.py +187 -840
  28. velocity/db/core/result.py +33 -25
  29. velocity/db/core/row.py +15 -3
  30. velocity/db/core/table.py +493 -50
  31. velocity/db/core/transaction.py +28 -15
  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 +270 -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 +129 -51
  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.161.dist-info}/METADATA +2 -2
  110. velocity_python-0.0.161.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.161.dist-info}/WHEEL +0 -0
  119. {velocity_python-0.0.109.dist-info → velocity_python-0.0.161.dist-info}/licenses/LICENSE +0 -0
  120. {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 utility functions to handle common database operations
5
- safely, including sorting with None values and other edge cases.
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
- from typing import Any, Callable, List, Optional, Union
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(field_name: str, default_value: Any = "") -> Callable[[dict], Any]:
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(rows: List[dict], field_name: str,
83
- none_handling: str = "last",
84
- default_value: Any = "",
85
- reverse: bool = False) -> List[dict]:
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(f"Invalid none_handling option: {none_handling}. "
127
- "Must be 'first', 'last', or 'default'")
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(grouped_rows: dict, field_name: str,
176
- none_handling: str = "last",
177
- default_value: Any = "",
178
- reverse: bool = False) -> dict:
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(rows, field_name, none_handling, default_value, reverse)
208
-
283
+ result[key] = safe_sort_rows(
284
+ rows, field_name, none_handling, default_value, reverse
285
+ )
286
+
209
287
  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()