velocity-python 0.0.237__tar.gz → 0.0.238__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 (167) hide show
  1. {velocity_python-0.0.237/src/velocity_python.egg-info → velocity_python-0.0.238}/PKG-INFO +1 -1
  2. {velocity_python-0.0.237 → velocity_python-0.0.238}/pyproject.toml +1 -1
  3. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.0.238/src/velocity/app/formbuilder/__init__.py +1 -0
  5. velocity_python-0.0.238/src/velocity/app/formbuilder/reshaper.py +174 -0
  6. {velocity_python-0.0.237 → velocity_python-0.0.238/src/velocity_python.egg-info}/PKG-INFO +1 -1
  7. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity_python.egg-info/SOURCES.txt +3 -0
  8. velocity_python-0.0.238/tests/test_formbuilder_reshaper.py +262 -0
  9. {velocity_python-0.0.237 → velocity_python-0.0.238}/LICENSE +0 -0
  10. {velocity_python-0.0.237 → velocity_python-0.0.238}/README.md +0 -0
  11. {velocity_python-0.0.237 → velocity_python-0.0.238}/setup.cfg +0 -0
  12. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/__init__.py +0 -0
  13. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/invoices.py +0 -0
  14. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/orders.py +0 -0
  15. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/payments.py +0 -0
  16. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/purchase_orders.py +0 -0
  17. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/tests/__init__.py +0 -0
  18. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/tests/test_email_processing.py +0 -0
  19. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  20. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  21. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/validators/__init__.py +0 -0
  22. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/app/validators/formbuilder_template.py +0 -0
  23. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/__init__.py +0 -0
  24. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/amplify.py +0 -0
  25. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/amplify_build.py +0 -0
  26. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/__init__.py +0 -0
  27. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/base_handler.py +0 -0
  28. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/context.py +0 -0
  29. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/context_factory.py +0 -0
  30. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/exceptions.py +0 -0
  31. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  32. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  33. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  34. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  35. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/perf.py +0 -0
  36. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/response.py +0 -0
  37. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  38. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/tests/__init__.py +0 -0
  39. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  40. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  41. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/aws/tests/test_response.py +0 -0
  42. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/__init__.py +0 -0
  43. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/__init__.py +0 -0
  44. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/column.py +0 -0
  45. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/database.py +0 -0
  46. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/decorators.py +0 -0
  47. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/engine.py +0 -0
  48. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/result.py +0 -0
  49. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/row.py +0 -0
  50. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/sequence.py +0 -0
  51. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/table.py +0 -0
  52. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/transaction.py +0 -0
  53. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/core/view.py +0 -0
  54. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/exceptions.py +0 -0
  55. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/__init__.py +0 -0
  56. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/base/__init__.py +0 -0
  57. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/base/initializer.py +0 -0
  58. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/base/operators.py +0 -0
  59. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/base/sql.py +0 -0
  60. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/base/types.py +0 -0
  61. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/mysql/__init__.py +0 -0
  62. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/mysql/operators.py +0 -0
  63. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/mysql/reserved.py +0 -0
  64. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/mysql/sql.py +0 -0
  65. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/mysql/types.py +0 -0
  66. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/postgres/__init__.py +0 -0
  67. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/postgres/operators.py +0 -0
  68. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/postgres/reserved.py +0 -0
  69. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/postgres/sql.py +0 -0
  70. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/postgres/types.py +0 -0
  71. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  72. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlite/operators.py +0 -0
  73. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  74. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlite/sql.py +0 -0
  75. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlite/types.py +0 -0
  76. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  77. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  78. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  79. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  80. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/sqlserver/types.py +0 -0
  81. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/servers/tablehelper.py +0 -0
  82. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/__init__.py +0 -0
  83. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/common_db_test.py +0 -0
  84. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/__init__.py +0 -0
  85. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/common.py +0 -0
  86. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_column.py +0 -0
  87. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  88. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_database.py +0 -0
  89. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  90. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  91. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  92. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_result.py +0 -0
  93. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_row.py +0 -0
  94. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  95. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  96. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  97. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  98. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  99. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_table.py +0 -0
  100. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  101. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  102. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/sql/__init__.py +0 -0
  103. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/sql/common.py +0 -0
  104. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  105. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  106. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  107. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_db_utils.py +0 -0
  108. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_postgres.py +0 -0
  109. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  110. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  111. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_result_caching.py +0 -0
  112. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  113. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  114. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  115. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  116. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_sql_builder.py +0 -0
  117. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_tablehelper.py +0 -0
  118. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/tests/test_view_helper.py +0 -0
  119. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/db/utils.py +0 -0
  120. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/logging.py +0 -0
  121. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/__init__.py +0 -0
  122. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/conv/__init__.py +0 -0
  123. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/conv/iconv.py +0 -0
  124. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/conv/oconv.py +0 -0
  125. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/db.py +0 -0
  126. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/export.py +0 -0
  127. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/format.py +0 -0
  128. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/mail.py +0 -0
  129. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/merge.py +0 -0
  130. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/__init__.py +0 -0
  131. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_db.py +0 -0
  132. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_fix.py +0 -0
  133. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_format.py +0 -0
  134. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_iconv.py +0 -0
  135. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_merge.py +0 -0
  136. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_oconv.py +0 -0
  137. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_original_error.py +0 -0
  138. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tests/test_timer.py +0 -0
  139. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/timer.py +0 -0
  140. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/misc/tools.py +0 -0
  141. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/__init__.py +0 -0
  142. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/authorizenet_adapter.py +0 -0
  143. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/base_adapter.py +0 -0
  144. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/braintree_adapter.py +0 -0
  145. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/charge_rules.py +0 -0
  146. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/demo_profiles.py +0 -0
  147. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/profiles.py +0 -0
  148. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/router.py +0 -0
  149. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity/payment/stripe_adapter.py +0 -0
  150. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  151. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity_python.egg-info/requires.txt +0 -0
  152. {velocity_python-0.0.237 → velocity_python-0.0.238}/src/velocity_python.egg-info/top_level.txt +0 -0
  153. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_amplify_build.py +0 -0
  154. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_decorators.py +0 -0
  155. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_formbuilder_template_validator.py +0 -0
  156. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_iconv_money_to_cents.py +0 -0
  157. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_lambda_handler.py +0 -0
  158. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_lambda_handler_auth.py +0 -0
  159. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_mixins_import.py +0 -0
  160. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_payment_braintree_adapter.py +0 -0
  161. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_payment_demo_profiles.py +0 -0
  162. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_payment_profiles.py +0 -0
  163. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_payment_router.py +0 -0
  164. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_payment_stripe_adapter.py +0 -0
  165. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  166. {velocity_python-0.0.237 → velocity_python-0.0.238}/tests/test_table_alter.py +0 -0
  167. {velocity_python-0.0.237 → velocity_python-0.0.238}/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.237
3
+ Version: 0.0.238
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.237"
7
+ version = "0.0.238"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.237"
1
+ __version__ = version = "0.0.238"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -0,0 +1 @@
1
+ """velocity.app.formbuilder — shared logic for form_builder_template records."""
@@ -0,0 +1,174 @@
1
+ """velocity.app.formbuilder.reshaper
2
+
3
+ Reshape a raw ``form_builder_template`` database row into the public ``fb``
4
+ payload structure as produced by ``DonateMainEvents.OnActionGetFormBuilder``.
5
+
6
+ The single public entry-point is :func:`reshape_formbuilder_template`. It is
7
+ a **pure function** — the input is deep-copied and never mutated.
8
+
9
+ Payment tokens, reCAPTCHA keys, and live thermometer totals are **not**
10
+ included; those are runtime secrets and database query results that the caller
11
+ must add separately (as DonateMain does in
12
+ ``OnActionGetFormBuilder``). The returned dict is safe to serialise and
13
+ return as a preview payload.
14
+
15
+ Usage::
16
+
17
+ from velocity.app.formbuilder.reshaper import reshape_formbuilder_template
18
+
19
+ fb = reshape_formbuilder_template(template_row.to_dict())
20
+ """
21
+
22
+ import ast
23
+ import copy
24
+ from typing import Any, Dict, Optional
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Internal per-metric / per-donation helpers
29
+ # ---------------------------------------------------------------------------
30
+
31
+ def _parse_key_value_string(raw: Optional[str]) -> Dict[str, Any]:
32
+ """Parse a ``"key1: val1, key2: val2"`` string into a dict.
33
+
34
+ Wraps the string in braces and calls ``ast.literal_eval``, exactly as the
35
+ original DonateMain ``getMetric`` / ``getDonation`` functions do.
36
+ Returns an empty dict on failure so a bad value never crashes the reshaper.
37
+ """
38
+ if not raw:
39
+ return {}
40
+ try:
41
+ return ast.literal_eval("{" + raw + "}")
42
+ except Exception:
43
+ return {}
44
+
45
+
46
+ def _extract_metric(count: int, data: Dict[str, Any]) -> Dict[str, Any]:
47
+ """Extract metric_{count}_* fields from *data* (mutates *data*).
48
+
49
+ Returns the nested metric dict when the metric has a label and is not
50
+ hidden, otherwise returns ``{}``. Either way all flat metric_N_* keys
51
+ are removed from *data*.
52
+ """
53
+ prefix = f"metric_{count}_"
54
+ flat_keys = [
55
+ "header", "image", "label", "secondarytext", "helpertext",
56
+ "values", "defaultvalue", "show_other", "other_minimum_value",
57
+ "orientation",
58
+ ]
59
+
60
+ is_hidden = data.get(f"{prefix}hide_donation_from_ui")
61
+ has_label = data.get(f"{prefix}label")
62
+
63
+ if not is_hidden and has_label:
64
+ result = {
65
+ "header": data.pop(f"{prefix}header", None),
66
+ "image": data.pop(f"{prefix}image", None),
67
+ "label": data.pop(f"{prefix}label", None),
68
+ "secondarytext": data.pop(f"{prefix}secondarytext", None),
69
+ "helpertext": data.pop(f"{prefix}helpertext", None),
70
+ "values": _parse_key_value_string(data.pop(f"{prefix}values", None)),
71
+ "defaultvalue": data.pop(f"{prefix}defaultvalue", None),
72
+ "show_other": data.pop(f"{prefix}show_other", ""),
73
+ "other_minimum_value": data.pop(f"{prefix}other_minimum_value", ""),
74
+ "orientation": data.pop(f"{prefix}orientation", ""),
75
+ }
76
+ data.pop(f"{prefix}hide_donation_from_ui", None)
77
+ return result
78
+
79
+ # Strip all flat keys even when hidden / empty
80
+ for key in flat_keys:
81
+ data.pop(f"{prefix}{key}", None)
82
+ data.pop(f"{prefix}hide_donation_from_ui", None)
83
+ return {}
84
+
85
+
86
+ def _extract_donation(count: int, data: Dict[str, Any]) -> Dict[str, Any]:
87
+ """Extract donation_{count}_* fields from *data* (mutates *data*).
88
+
89
+ Returns the nested donation dict when the donation has a label and is not
90
+ hidden, otherwise returns ``{}``. Either way all flat donation_N_* keys
91
+ are removed from *data*.
92
+ """
93
+ prefix = f"donation_{count}_"
94
+ flat_keys = [
95
+ "header", "image", "html", "label", "secondarytext", "helpertext",
96
+ "values", "defaultvalue", "show_other", "other_minimum_value",
97
+ "orientation", "dropdown", "dropdown_label",
98
+ ]
99
+
100
+ is_hidden = data.get(f"{prefix}hide_donation_from_ui")
101
+ has_label = data.get(f"{prefix}label")
102
+
103
+ if not is_hidden and has_label:
104
+ result = {
105
+ "header": data.pop(f"{prefix}header", None),
106
+ "image": data.pop(f"{prefix}image", None),
107
+ "html": data.pop(f"{prefix}html", None),
108
+ "label": data.pop(f"{prefix}label", None),
109
+ "secondarytext": data.pop(f"{prefix}secondarytext", None),
110
+ "helpertext": data.pop(f"{prefix}helpertext", None),
111
+ "values": _parse_key_value_string(data.pop(f"{prefix}values", None)),
112
+ "defaultvalue": data.pop(f"{prefix}defaultvalue", None),
113
+ "show_other": data.pop(f"{prefix}show_other", ""),
114
+ "other_minimum_value": data.pop(f"{prefix}other_minimum_value", ""),
115
+ "orientation": data.pop(f"{prefix}orientation", ""),
116
+ "dropdown": data.pop(f"{prefix}dropdown", None),
117
+ "dropdown_label": data.pop(f"{prefix}dropdown_label", None),
118
+ }
119
+ data.pop(f"{prefix}hide_donation_from_ui", None)
120
+ return result
121
+
122
+ for key in flat_keys:
123
+ data.pop(f"{prefix}{key}", None)
124
+ data.pop(f"{prefix}hide_donation_from_ui", None)
125
+ return {}
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Public API
130
+ # ---------------------------------------------------------------------------
131
+
132
+ def reshape_formbuilder_template(raw_row: Dict[str, Any]) -> Dict[str, Any]:
133
+ """Reshape a raw ``form_builder_template`` DB row into the public ``fb`` dict.
134
+
135
+ This replicates exactly what ``DonateMainEvents.OnActionGetFormBuilder``
136
+ does to reshape the flat database row before returning it to the browser,
137
+ without the live DB queries (thermometer totals) or runtime secrets
138
+ (payment tokens, reCAPTCHA keys).
139
+
140
+ The caller is responsible for:
141
+
142
+ - Passing in a plain Python dict (call ``.to_dict()`` on a velocity DB
143
+ row first so that Decimal and datetime values are already serialisable).
144
+ - Optionally adding ``fb["thermometer"]`` / ``fb["rival_thermometer"]``
145
+ after the call if live totals are desired.
146
+
147
+ Returns a new dict; the input is never mutated.
148
+
149
+ Example::
150
+
151
+ row = tx.table("form_builder_template").one({"sys_id": sys_id})
152
+ fb = reshape_formbuilder_template(row.to_dict())
153
+ """
154
+ fb = copy.deepcopy(raw_row)
155
+
156
+ fb["metrics"] = {
157
+ f"metric_{i}": _extract_metric(i, fb) for i in range(1, 10)
158
+ }
159
+ fb["donations"] = {
160
+ f"donation_{i}": _extract_donation(i, fb) for i in range(1, 10)
161
+ }
162
+
163
+ # Expose the primary key under the public name used by DonateMain
164
+ fb["_num_entries"] = fb.get("sys_id")
165
+
166
+ # Strip internal / private fields
167
+ for key in list(fb.keys()):
168
+ if key.startswith("sys_"):
169
+ del fb[key]
170
+
171
+ for key in ("url_slug", "url_slug_alternate"):
172
+ fb.pop(key, None)
173
+
174
+ return fb
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.237
3
+ Version: 0.0.238
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
@@ -8,6 +8,8 @@ src/velocity/app/invoices.py
8
8
  src/velocity/app/orders.py
9
9
  src/velocity/app/payments.py
10
10
  src/velocity/app/purchase_orders.py
11
+ src/velocity/app/formbuilder/__init__.py
12
+ src/velocity/app/formbuilder/reshaper.py
11
13
  src/velocity/app/tests/__init__.py
12
14
  src/velocity/app/tests/test_email_processing.py
13
15
  src/velocity/app/tests/test_payment_profile_sorting.py
@@ -147,6 +149,7 @@ src/velocity_python.egg-info/requires.txt
147
149
  src/velocity_python.egg-info/top_level.txt
148
150
  tests/test_amplify_build.py
149
151
  tests/test_decorators.py
152
+ tests/test_formbuilder_reshaper.py
150
153
  tests/test_formbuilder_template_validator.py
151
154
  tests/test_iconv_money_to_cents.py
152
155
  tests/test_lambda_handler.py
@@ -0,0 +1,262 @@
1
+ """Tests for velocity.app.formbuilder.reshaper."""
2
+ import copy
3
+ import pytest
4
+ from velocity.app.formbuilder.reshaper import (
5
+ reshape_formbuilder_template,
6
+ _extract_metric,
7
+ _extract_donation,
8
+ _parse_key_value_string,
9
+ )
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Helpers
14
+ # ---------------------------------------------------------------------------
15
+
16
+ def _make_row(**overrides):
17
+ """Return a minimal form_builder_template dict with all expected fields."""
18
+ base = {
19
+ "sys_id": 42,
20
+ "sys_created": "2024-01-01",
21
+ "sys_modified": "2024-01-02",
22
+ "client": "TIGERS",
23
+ "campaign": "2024",
24
+ "sub_campaign": "",
25
+ "is_published": True,
26
+ "url_slug": "tigers-2024",
27
+ "url_slug_alternate": "",
28
+ "primary_color": "#003087",
29
+ "secondary_color": "#FF7900",
30
+ "header_font": "Roboto",
31
+ "text_font": "Open Sans",
32
+ "page_title": "Give to Tigers",
33
+ "descriptor": "TIGERS DONATION",
34
+ "campaign_end_date": None,
35
+ "show_rival_thermometer": False,
36
+ "rival_client": None,
37
+ "rival_campaign": None,
38
+ "campaign_start_dollars": None,
39
+ "campaign_start_donors": None,
40
+ "campaign_goal_dollars": None,
41
+ "campaign_goal_donors": None,
42
+ }
43
+ # Add 9 empty metric slots
44
+ for i in range(1, 10):
45
+ p = f"metric_{i}_"
46
+ base.update({
47
+ f"{p}hide_donation_from_ui": False,
48
+ f"{p}header": "",
49
+ f"{p}image": "",
50
+ f"{p}label": "",
51
+ f"{p}secondarytext": "",
52
+ f"{p}helpertext": "",
53
+ f"{p}values": "",
54
+ f"{p}defaultvalue": "",
55
+ f"{p}show_other": False,
56
+ f"{p}other_minimum_value": "",
57
+ f"{p}orientation": "horizontal",
58
+ })
59
+ # Add 9 empty donation slots
60
+ for i in range(1, 10):
61
+ p = f"donation_{i}_"
62
+ base.update({
63
+ f"{p}hide_donation_from_ui": False,
64
+ f"{p}header": "",
65
+ f"{p}html": "",
66
+ f"{p}image": "",
67
+ f"{p}label": "",
68
+ f"{p}secondarytext": "",
69
+ f"{p}helpertext": "",
70
+ f"{p}values": "",
71
+ f"{p}defaultvalue": "",
72
+ f"{p}show_other": False,
73
+ f"{p}other_minimum_value": "",
74
+ f"{p}orientation": "vertical",
75
+ f"{p}dropdown": "",
76
+ f"{p}dropdown_label": "",
77
+ })
78
+ base.update(overrides)
79
+ return base
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # _parse_key_value_string
84
+ # ---------------------------------------------------------------------------
85
+
86
+ class TestParseKeyValueString:
87
+ def test_empty_string_returns_empty_dict(self):
88
+ assert _parse_key_value_string("") == {}
89
+
90
+ def test_none_returns_empty_dict(self):
91
+ assert _parse_key_value_string(None) == {}
92
+
93
+ def test_simple_string_values(self):
94
+ result = _parse_key_value_string('"Entry A": "25", "Entry B": "50"')
95
+ assert result == {"Entry A": "25", "Entry B": "50"}
96
+
97
+ def test_numeric_values(self):
98
+ result = _parse_key_value_string('"$25": 25, "$50": 50')
99
+ assert result == {"$25": 25, "$50": 50}
100
+
101
+ def test_malformed_string_returns_empty_dict(self):
102
+ assert _parse_key_value_string("not valid python") == {}
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Input immutability
107
+ # ---------------------------------------------------------------------------
108
+
109
+ class TestImmutability:
110
+ def test_input_dict_not_mutated(self):
111
+ row = _make_row(metric_1_label="Seats", metric_1_values='"A": "1"')
112
+ original = copy.deepcopy(row)
113
+ reshape_formbuilder_template(row)
114
+ assert row == original
115
+
116
+ def test_returns_new_object(self):
117
+ row = _make_row()
118
+ result = reshape_formbuilder_template(row)
119
+ assert result is not row
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # Field stripping
124
+ # ---------------------------------------------------------------------------
125
+
126
+ class TestFieldStripping:
127
+ def test_sys_fields_stripped(self):
128
+ row = _make_row()
129
+ result = reshape_formbuilder_template(row)
130
+ assert not any(k.startswith("sys_") for k in result)
131
+
132
+ def test_url_slug_stripped(self):
133
+ row = _make_row(url_slug="tigers", url_slug_alternate="t2024")
134
+ result = reshape_formbuilder_template(row)
135
+ assert "url_slug" not in result
136
+ assert "url_slug_alternate" not in result
137
+
138
+ def test_num_entries_added(self):
139
+ row = _make_row()
140
+ result = reshape_formbuilder_template(row)
141
+ assert result["_num_entries"] == 42
142
+
143
+ def test_flat_metric_keys_stripped(self):
144
+ row = _make_row(metric_1_label="Seats")
145
+ result = reshape_formbuilder_template(row)
146
+ flat_metric_keys = [k for k in result if k.startswith("metric_")]
147
+ assert flat_metric_keys == [], flat_metric_keys
148
+
149
+ def test_flat_donation_keys_stripped(self):
150
+ row = _make_row(donation_1_label="Game")
151
+ result = reshape_formbuilder_template(row)
152
+ flat_donation_keys = [k for k in result if k.startswith("donation_")]
153
+ assert flat_donation_keys == [], flat_donation_keys
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Metric reshaping
158
+ # ---------------------------------------------------------------------------
159
+
160
+ class TestMetricReshaping:
161
+ def test_metric_with_label_included(self):
162
+ row = _make_row(
163
+ metric_1_label="Seats",
164
+ metric_1_header="Choose seats",
165
+ metric_1_values='"1 Seat": "1", "2 Seats": "2"',
166
+ metric_1_defaultvalue="1",
167
+ metric_1_orientation="vertical",
168
+ )
169
+ result = reshape_formbuilder_template(row)
170
+ m = result["metrics"]["metric_1"]
171
+ assert m["label"] == "Seats"
172
+ assert m["header"] == "Choose seats"
173
+ assert m["values"] == {"1 Seat": "1", "2 Seats": "2"}
174
+ assert m["defaultvalue"] == "1"
175
+ assert m["orientation"] == "vertical"
176
+
177
+ def test_metric_without_label_is_empty_dict(self):
178
+ row = _make_row()
179
+ result = reshape_formbuilder_template(row)
180
+ for i in range(1, 10):
181
+ assert result["metrics"][f"metric_{i}"] == {}
182
+
183
+ def test_hidden_metric_is_empty_dict(self):
184
+ row = _make_row(metric_1_label="Seats", metric_1_hide_donation_from_ui=True)
185
+ result = reshape_formbuilder_template(row)
186
+ assert result["metrics"]["metric_1"] == {}
187
+
188
+ def test_all_9_metric_slots_present(self):
189
+ row = _make_row()
190
+ result = reshape_formbuilder_template(row)
191
+ assert set(result["metrics"].keys()) == {f"metric_{i}" for i in range(1, 10)}
192
+
193
+ def test_multiple_active_metrics(self):
194
+ row = _make_row(metric_1_label="A", metric_2_label="B", metric_3_label="C")
195
+ result = reshape_formbuilder_template(row)
196
+ for key in ("metric_1", "metric_2", "metric_3"):
197
+ assert result["metrics"][key] != {}
198
+ for key in (f"metric_{i}" for i in range(4, 10)):
199
+ assert result["metrics"][key] == {}
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Donation reshaping
204
+ # ---------------------------------------------------------------------------
205
+
206
+ class TestDonationReshaping:
207
+ def test_donation_with_label_included(self):
208
+ row = _make_row(
209
+ donation_1_label="General Gift",
210
+ donation_1_header="Support us",
211
+ donation_1_values='"$25": 25, "$50": 50',
212
+ donation_1_defaultvalue="25",
213
+ donation_1_html="<b>Thanks</b>",
214
+ donation_1_dropdown_label="Fund",
215
+ donation_1_dropdown="General,Athletics",
216
+ )
217
+ result = reshape_formbuilder_template(row)
218
+ d = result["donations"]["donation_1"]
219
+ assert d["label"] == "General Gift"
220
+ assert d["values"] == {"$25": 25, "$50": 50}
221
+ assert d["html"] == "<b>Thanks</b>"
222
+ assert d["dropdown_label"] == "Fund"
223
+
224
+ def test_donation_without_label_is_empty_dict(self):
225
+ row = _make_row()
226
+ result = reshape_formbuilder_template(row)
227
+ for i in range(1, 10):
228
+ assert result["donations"][f"donation_{i}"] == {}
229
+
230
+ def test_hidden_donation_is_empty_dict(self):
231
+ row = _make_row(donation_1_label="Gift", donation_1_hide_donation_from_ui=True)
232
+ result = reshape_formbuilder_template(row)
233
+ assert result["donations"]["donation_1"] == {}
234
+
235
+ def test_all_9_donation_slots_present(self):
236
+ row = _make_row()
237
+ result = reshape_formbuilder_template(row)
238
+ assert set(result["donations"].keys()) == {f"donation_{i}" for i in range(1, 10)}
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # Top-level field preservation
243
+ # ---------------------------------------------------------------------------
244
+
245
+ class TestTopLevelFields:
246
+ def test_branding_fields_preserved(self):
247
+ row = _make_row(primary_color="#003087", page_title="Give Now")
248
+ result = reshape_formbuilder_template(row)
249
+ assert result["primary_color"] == "#003087"
250
+ assert result["page_title"] == "Give Now"
251
+
252
+ def test_client_campaign_preserved(self):
253
+ row = _make_row(client="TIGERS", campaign="2024")
254
+ result = reshape_formbuilder_template(row)
255
+ assert result["client"] == "TIGERS"
256
+ assert result["campaign"] == "2024"
257
+
258
+ def test_unknown_custom_fields_preserved(self):
259
+ """Extra columns added to the DB table should pass through untouched."""
260
+ row = _make_row(custom_new_field="hello")
261
+ result = reshape_formbuilder_template(row)
262
+ assert result["custom_new_field"] == "hello"