velocity-python 0.0.211__tar.gz → 0.0.213__tar.gz

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 (150) hide show
  1. {velocity_python-0.0.211 → velocity_python-0.0.213}/PKG-INFO +1 -1
  2. {velocity_python-0.0.211 → velocity_python-0.0.213}/pyproject.toml +1 -1
  3. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/tests/test_spreadsheet_functions.py +32 -2
  5. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/base_handler.py +41 -4
  6. velocity_python-0.0.213/src/velocity/aws/tests/test_base_handler_error_response.py +62 -0
  7. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/export.py +117 -15
  8. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity_python.egg-info/PKG-INFO +1 -1
  9. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity_python.egg-info/SOURCES.txt +1 -0
  10. {velocity_python-0.0.211 → velocity_python-0.0.213}/LICENSE +0 -0
  11. {velocity_python-0.0.211 → velocity_python-0.0.213}/README.md +0 -0
  12. {velocity_python-0.0.211 → velocity_python-0.0.213}/setup.cfg +0 -0
  13. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/__init__.py +0 -0
  14. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/invoices.py +0 -0
  15. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/orders.py +0 -0
  16. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/payments.py +0 -0
  17. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/purchase_orders.py +0 -0
  18. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/tests/__init__.py +0 -0
  19. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/tests/test_email_processing.py +0 -0
  20. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  21. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/__init__.py +0 -0
  22. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/amplify.py +0 -0
  23. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/__init__.py +0 -0
  24. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/context.py +0 -0
  25. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/context_factory.py +0 -0
  26. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/exceptions.py +0 -0
  27. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  28. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  29. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  30. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  31. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/perf.py +0 -0
  32. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/response.py +0 -0
  33. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  34. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/tests/__init__.py +0 -0
  35. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  36. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/aws/tests/test_response.py +0 -0
  37. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/__init__.py +0 -0
  38. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/__init__.py +0 -0
  39. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/column.py +0 -0
  40. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/database.py +0 -0
  41. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/decorators.py +0 -0
  42. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/engine.py +0 -0
  43. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/result.py +0 -0
  44. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/row.py +0 -0
  45. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/sequence.py +0 -0
  46. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/table.py +0 -0
  47. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/transaction.py +0 -0
  48. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/core/view.py +0 -0
  49. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/exceptions.py +0 -0
  50. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/__init__.py +0 -0
  51. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/base/__init__.py +0 -0
  52. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/base/initializer.py +0 -0
  53. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/base/operators.py +0 -0
  54. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/base/sql.py +0 -0
  55. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/base/types.py +0 -0
  56. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/mysql/__init__.py +0 -0
  57. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/mysql/operators.py +0 -0
  58. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/mysql/reserved.py +0 -0
  59. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/mysql/sql.py +0 -0
  60. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/mysql/types.py +0 -0
  61. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/postgres/__init__.py +0 -0
  62. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/postgres/operators.py +0 -0
  63. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/postgres/reserved.py +0 -0
  64. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/postgres/sql.py +0 -0
  65. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/postgres/types.py +0 -0
  66. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  67. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlite/operators.py +0 -0
  68. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  69. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlite/sql.py +0 -0
  70. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlite/types.py +0 -0
  71. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  72. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  73. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  74. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  75. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/sqlserver/types.py +0 -0
  76. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/servers/tablehelper.py +0 -0
  77. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/__init__.py +0 -0
  78. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/common_db_test.py +0 -0
  79. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/__init__.py +0 -0
  80. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/common.py +0 -0
  81. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_column.py +0 -0
  82. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  83. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_database.py +0 -0
  84. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  85. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  86. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  87. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_result.py +0 -0
  88. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_row.py +0 -0
  89. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  90. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  91. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  92. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  93. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  94. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_table.py +0 -0
  95. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  96. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  97. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/sql/__init__.py +0 -0
  98. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/sql/common.py +0 -0
  99. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  100. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  101. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  102. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_db_utils.py +0 -0
  103. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_postgres.py +0 -0
  104. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  105. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  106. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_result_caching.py +0 -0
  107. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  108. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  109. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  110. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  111. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_sql_builder.py +0 -0
  112. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_tablehelper.py +0 -0
  113. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/tests/test_view_helper.py +0 -0
  114. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/db/utils.py +0 -0
  115. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/logging.py +0 -0
  116. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/__init__.py +0 -0
  117. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/conv/__init__.py +0 -0
  118. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/conv/iconv.py +0 -0
  119. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/conv/oconv.py +0 -0
  120. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/db.py +0 -0
  121. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/format.py +0 -0
  122. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/mail.py +0 -0
  123. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/merge.py +0 -0
  124. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/__init__.py +0 -0
  125. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_db.py +0 -0
  126. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_fix.py +0 -0
  127. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_format.py +0 -0
  128. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_iconv.py +0 -0
  129. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_merge.py +0 -0
  130. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_oconv.py +0 -0
  131. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_original_error.py +0 -0
  132. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tests/test_timer.py +0 -0
  133. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/timer.py +0 -0
  134. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/misc/tools.py +0 -0
  135. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/payment/__init__.py +0 -0
  136. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/payment/base_adapter.py +0 -0
  137. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/payment/braintree_adapter.py +0 -0
  138. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/payment/router.py +0 -0
  139. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity/payment/stripe_adapter.py +0 -0
  140. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  141. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity_python.egg-info/requires.txt +0 -0
  142. {velocity_python-0.0.211 → velocity_python-0.0.213}/src/velocity_python.egg-info/top_level.txt +0 -0
  143. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_decorators.py +0 -0
  144. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_iconv_money_to_cents.py +0 -0
  145. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_lambda_handler.py +0 -0
  146. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_lambda_handler_auth.py +0 -0
  147. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_mixins_import.py +0 -0
  148. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  149. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_table_alter.py +0 -0
  150. {velocity_python-0.0.211 → velocity_python-0.0.213}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.211
3
+ Version: 0.0.213
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.211"
7
+ version = "0.0.213"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.211"
1
+ __version__ = version = "0.0.213"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -2,6 +2,7 @@ import unittest
2
2
  import base64
3
3
  from io import BytesIO
4
4
  from openpyxl import load_workbook
5
+ from openpyxl.cell.cell import MergedCell
5
6
  from velocity.misc.export import (
6
7
  extract,
7
8
  autosize_columns,
@@ -73,9 +74,9 @@ class TestSpreadsheetFunctions(unittest.TestCase):
73
74
 
74
75
  # Verify merged cells and styles
75
76
  self.assertTrue(worksheet.merged_cells.ranges)
76
- self.assertEqual(str(worksheet.merged_cells.ranges[0]), "A1:B1")
77
+ self.assertIn("A1:B1", {str(cell_range) for cell_range in worksheet.merged_cells.ranges})
77
78
  self.assertEqual(worksheet["A1"].style, "col_header")
78
- self.assertEqual(worksheet["B1"].style, "col_header")
79
+ self.assertIsInstance(worksheet["B1"], MergedCell)
79
80
 
80
81
  def test_create_spreadsheet_with_freeze_panes_and_dimensions(self):
81
82
  """Test creating a spreadsheet with freeze panes and custom row/column dimensions."""
@@ -97,6 +98,35 @@ class TestSpreadsheetFunctions(unittest.TestCase):
97
98
  self.assertEqual(worksheet.column_dimensions["A"].width, 20)
98
99
  self.assertEqual(worksheet.column_dimensions["B"].width, 30)
99
100
 
101
+ def test_create_spreadsheet_autosizes_boolean_values(self):
102
+ """Test that autosizing accounts for displayed boolean values."""
103
+ buffer = BytesIO()
104
+
105
+ create_spreadsheet([""], [[False]], buffer)
106
+ buffer.seek(0)
107
+ workbook = load_workbook(buffer)
108
+ worksheet = workbook.active
109
+
110
+ self.assertFalse(worksheet["A2"].value)
111
+ self.assertAlmostEqual(worksheet.column_dimensions["A"].width, 8.4, places=1)
112
+
113
+ def test_create_spreadsheet_autosizes_formatted_numbers(self):
114
+ """Test that autosizing uses formatted number display width."""
115
+ buffer = BytesIO()
116
+
117
+ create_spreadsheet(
118
+ ["Amt"],
119
+ [[1234]],
120
+ buffer,
121
+ formats={"A2": "$#,##0.00"},
122
+ )
123
+ buffer.seek(0)
124
+ workbook = load_workbook(buffer)
125
+ worksheet = workbook.active
126
+
127
+ self.assertEqual(worksheet["A2"].number_format, "$#,##0.00")
128
+ self.assertAlmostEqual(worksheet.column_dimensions["A"].width, 13.2, places=1)
129
+
100
130
  def test_get_downloadable_spreadsheet(self):
101
131
  """Test generating a downloadable spreadsheet encoded in base64."""
102
132
  headers = ["Header1", "Header2"]
@@ -288,12 +288,49 @@ class BaseHandler:
288
288
  "onerror_exc": on_error_exc.__class__.__name__,
289
289
  },
290
290
  )
291
- return
292
- raise
293
- else:
294
- # Re-raise if no error handler is defined
291
+ else:
292
+ raise
293
+
294
+ if self._set_unhandled_error_response(local_context):
295
+ return
296
+
297
+ if not hasattr(self, "onError"):
298
+ # Non-HTTP contexts without a custom error hook should still fail fast.
295
299
  raise exception
296
300
 
301
+ def _set_unhandled_error_response(self, local_context) -> bool:
302
+ response_getter = getattr(local_context, "response", None)
303
+ if not callable(response_getter):
304
+ return False
305
+
306
+ response = response_getter()
307
+ if response is None:
308
+ return False
309
+
310
+ reference_id = getattr(self.aws_context, "aws_request_id", None)
311
+ title = "Request Failed"
312
+ message = "Something went wrong while processing your request."
313
+ detail = "More information has been logged on the backend."
314
+
315
+ body = {
316
+ "message": message,
317
+ "detail": detail,
318
+ "logged": True,
319
+ }
320
+
321
+ user_message = f"{message} {detail}"
322
+ console_message = detail
323
+ if reference_id:
324
+ body["reference_id"] = str(reference_id)
325
+ user_message = f"{user_message} Reference: {reference_id}."
326
+ console_message = f"{console_message} Reference: {reference_id}."
327
+
328
+ response.set_status(500)
329
+ response.set_body(body)
330
+ response.console(console_message, title=title)
331
+ response.alert(user_message, title=title)
332
+ return True
333
+
297
334
  def _is_transient_db_disconnect(self, exc: Exception = None, *args, **kwargs) -> bool:
298
335
  """Return True if an exception looks like a transient DB disconnect.
299
336
 
@@ -0,0 +1,62 @@
1
+ import json
2
+ import unittest
3
+ from types import SimpleNamespace
4
+
5
+ from velocity.aws.handlers.base_handler import BaseHandler
6
+ from velocity.aws.handlers.response import Response
7
+
8
+
9
+ class DummyContext:
10
+ def __init__(self, response):
11
+ self._response = response
12
+
13
+ def response(self):
14
+ return self._response
15
+
16
+ def action(self):
17
+ return "button-click"
18
+
19
+
20
+ class DummyHandler(BaseHandler):
21
+ def __init__(self, aws_context=None):
22
+ super().__init__({}, aws_context or SimpleNamespace(aws_request_id="req-123"))
23
+ self.on_error_calls = []
24
+
25
+ def onError(self, tx, context, exc, tb):
26
+ self.on_error_calls.append({"exc": exc, "tb": tb})
27
+
28
+
29
+ class TestBaseHandlerErrorResponse(unittest.TestCase):
30
+ def test_unhandled_http_error_sets_generic_response(self):
31
+ handler = DummyHandler()
32
+ response = Response()
33
+ context = DummyContext(response)
34
+
35
+ handler.handle_error(tx=None, local_context=context, exception=RuntimeError("secret details"))
36
+
37
+ self.assertEqual(response.status(), 500)
38
+ self.assertEqual(response.body["message"], "Something went wrong while processing your request.")
39
+ self.assertEqual(response.body["detail"], "More information has been logged on the backend.")
40
+ self.assertTrue(response.body["logged"])
41
+ self.assertEqual(response.body["reference_id"], "req-123")
42
+
43
+ rendered = response.render()
44
+ body = json.loads(rendered["body"])
45
+ self.assertEqual(rendered["statusCode"], 500)
46
+ self.assertEqual(len(body["actions"]), 2)
47
+ self.assertEqual(body["actions"][0]["action"], "console")
48
+ self.assertEqual(body["actions"][1]["action"], "alert")
49
+ self.assertIn("Reference: req-123.", body["actions"][1]["payload"]["message"])
50
+ self.assertNotIn("secret details", body["actions"][1]["payload"]["message"])
51
+ self.assertEqual(len(handler.on_error_calls), 1)
52
+
53
+ def test_unhandled_non_http_error_without_onerror_reraises(self):
54
+ handler = BaseHandler({}, SimpleNamespace(aws_request_id="req-456"))
55
+ context = DummyContext(None)
56
+
57
+ with self.assertRaises(RuntimeError):
58
+ handler.handle_error(tx=None, local_context=context, exception=RuntimeError("boom"))
59
+
60
+
61
+ if __name__ == "__main__":
62
+ unittest.main()
@@ -1,24 +1,120 @@
1
- from typing import List, Dict
1
+ from datetime import date, datetime, time, timedelta
2
+ from decimal import Decimal
2
3
  from io import BytesIO
4
+ from typing import Dict, List
3
5
  import base64
6
+ import re
7
+
4
8
  import openpyxl
5
- from openpyxl.styles import NamedStyle, Font, Border, Side, Alignment
9
+ from openpyxl.styles import Alignment, Border, Font, NamedStyle, Side
6
10
  from openpyxl.utils import get_column_letter
7
11
 
8
12
 
13
+ NUMBER_FORMAT_RE = re.compile(r"[#0?][#0?,]*(?:\.[#0?]+)?")
14
+
15
+
9
16
  def extract(d: dict, keys: List[str]) -> List:
10
17
  """Extract values from a dictionary based on a list of keys."""
11
18
  return [d.get(key) for key in keys]
12
19
 
13
20
 
14
- def autosize_columns(ws, fixed: Dict[str, float] = {}):
21
+ def _stringify_cell_value(value) -> str:
22
+ if value is None:
23
+ return ""
24
+ if isinstance(value, bool):
25
+ return "TRUE" if value else "FALSE"
26
+ return str(value)
27
+
28
+
29
+ def _select_number_format_section(format_code: str, value) -> str:
30
+ sections = format_code.split(";")
31
+ if not sections:
32
+ return format_code
33
+ if value < 0 and len(sections) > 1:
34
+ return sections[1]
35
+ if value == 0 and len(sections) > 2:
36
+ return sections[2]
37
+ return sections[0]
38
+
39
+
40
+ def _clean_number_format(format_code: str) -> str:
41
+ cleaned = []
42
+ in_quotes = False
43
+ idx = 0
44
+ while idx < len(format_code):
45
+ char = format_code[idx]
46
+ if char == '"':
47
+ in_quotes = not in_quotes
48
+ elif in_quotes:
49
+ cleaned.append(char)
50
+ elif char == "\\":
51
+ idx += 1
52
+ if idx < len(format_code):
53
+ cleaned.append(format_code[idx])
54
+ elif char in {"_", "*"}:
55
+ idx += 1
56
+ elif char == "[":
57
+ end = format_code.find("]", idx + 1)
58
+ if end == -1:
59
+ break
60
+ idx = end
61
+ else:
62
+ cleaned.append(char)
63
+ idx += 1
64
+ return "".join(cleaned)
65
+
66
+
67
+ def _format_numeric_value(value, format_code: str) -> str:
68
+ section = _clean_number_format(_select_number_format_section(format_code, value))
69
+ if not section or section.lower() == "general":
70
+ return str(value)
71
+
72
+ match = NUMBER_FORMAT_RE.search(section)
73
+ if not match:
74
+ return str(value)
75
+
76
+ pattern = match.group(0)
77
+ prefix = section[: match.start()]
78
+ suffix = section[match.end() :]
79
+ percent_multiplier = section.count("%")
80
+ scaled_value = abs(value) * (100**percent_multiplier)
81
+ decimal_places = len(pattern.split(".", 1)[1]) if "." in pattern else 0
82
+ use_grouping = "," in pattern.split(".", 1)[0]
83
+
84
+ if use_grouping:
85
+ number = f"{scaled_value:,.{decimal_places}f}"
86
+ else:
87
+ number = f"{scaled_value:.{decimal_places}f}"
88
+
89
+ if value < 0 and len(format_code.split(";")) == 1 and "-" not in prefix and "(" not in prefix:
90
+ prefix = f"-{prefix}"
91
+
92
+ return f"{prefix}{number}{suffix}"
93
+
94
+
95
+ def _display_value(cell) -> str:
96
+ value = cell.value
97
+ if value is None:
98
+ return ""
99
+ if isinstance(value, bool):
100
+ return "TRUE" if value else "FALSE"
101
+ if isinstance(value, (datetime, date, time, timedelta)):
102
+ return str(value)
103
+ if isinstance(value, (int, float, Decimal)):
104
+ return _format_numeric_value(value, cell.number_format)
105
+ return str(value)
106
+
107
+
108
+ def autosize_columns(ws, fixed: Dict[str, float] | None = None):
15
109
  """Autosize columns in the worksheet based on content length."""
110
+ fixed = fixed or {}
16
111
  for col in ws.columns:
17
112
  max_length = 0
18
113
  for cell in col:
19
114
  try:
20
- if cell.value and len(str(cell.value)) > max_length:
21
- max_length = len(str(cell.value))
115
+ display_value = _display_value(cell)
116
+ if len(display_value) > max_length:
117
+ max_length = len(display_value)
22
118
  except Exception:
23
119
  continue
24
120
  adjusted_width = (max_length + 2) * 1.2
@@ -30,15 +126,20 @@ def create_spreadsheet(
30
126
  headers: List[str],
31
127
  rows: List[List],
32
128
  fileorbuffer,
33
- styles: Dict[str, str] = {},
34
- merge: List[str] = [],
35
- formats: Dict[str, str] = {},
36
- named_styles: List[NamedStyle] = [],
129
+ styles: Dict[str, str] | None = None,
130
+ merge: List[str] | None = None,
131
+ formats: Dict[str, str] | None = None,
132
+ named_styles: List[NamedStyle] | None = None,
37
133
  freeze_panes: str = "A2",
38
134
  dimensions: dict = None,
39
135
  auto_size: bool = True,
40
136
  ):
41
137
  """Create an Excel spreadsheet with specified headers, rows, and styles."""
138
+ styles = styles or {}
139
+ merge = merge or []
140
+ formats = formats or {}
141
+ named_styles = named_styles or []
142
+
42
143
  wb = openpyxl.Workbook()
43
144
  ws = wb.active
44
145
 
@@ -102,6 +203,9 @@ def create_spreadsheet(
102
203
  # Set freeze panes
103
204
  ws.freeze_panes = freeze_panes
104
205
 
206
+ for cell, format_code in formats.items():
207
+ ws[cell].number_format = format_code
208
+
105
209
  # Auto-size columns if enabled
106
210
  if auto_size:
107
211
  autosize_columns(ws, fixed={})
@@ -119,8 +223,6 @@ def create_spreadsheet(
119
223
  ws[cell].style = local_styles[style_name]
120
224
  for cell_range in merge:
121
225
  ws.merge_cells(cell_range)
122
- for cell, format_code in formats.items():
123
- ws[cell].number_format = format_code
124
226
 
125
227
  # Save workbook to the provided file or buffer
126
228
  wb.save(fileorbuffer)
@@ -129,10 +231,10 @@ def create_spreadsheet(
129
231
  def get_downloadable_spreadsheet(
130
232
  headers: List[str],
131
233
  rows: List[List],
132
- styles: Dict[str, str] = {},
133
- merge: List[str] = [],
134
- formats: Dict[str, str] = {},
135
- named_styles: List[NamedStyle] = [],
234
+ styles: Dict[str, str] | None = None,
235
+ merge: List[str] | None = None,
236
+ formats: Dict[str, str] | None = None,
237
+ named_styles: List[NamedStyle] | None = None,
136
238
  freeze_panes: str = "A2",
137
239
  dimensions: dict = None,
138
240
  auto_size: bool = True,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.211
3
+ Version: 0.0.213
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -27,6 +27,7 @@ src/velocity/aws/handlers/mixins/__init__.py
27
27
  src/velocity/aws/handlers/mixins/data_service.py
28
28
  src/velocity/aws/handlers/mixins/web_handler.py
29
29
  src/velocity/aws/tests/__init__.py
30
+ src/velocity/aws/tests/test_base_handler_error_response.py
30
31
  src/velocity/aws/tests/test_lambda_handler_json_serialization.py
31
32
  src/velocity/aws/tests/test_response.py
32
33
  src/velocity/db/__init__.py