velocity-python 0.1.58__tar.gz → 0.1.62__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 (201) hide show
  1. {velocity_python-0.1.58 → velocity_python-0.1.62}/PKG-INFO +1 -1
  2. {velocity_python-0.1.58 → velocity_python-0.1.62}/pyproject.toml +1 -1
  3. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/lambda_handler.py +10 -0
  5. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/mixins/data_service.py +49 -6
  6. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/tablehelper.py +56 -1
  7. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/stripe_adapter.py +15 -0
  8. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity_python.egg-info/PKG-INFO +1 -1
  9. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity_python.egg-info/SOURCES.txt +5 -1
  10. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_dirty_pipeline_fast_path.py +8 -0
  11. velocity_python-0.1.62/tests/test_http_handler_rollback.py +82 -0
  12. velocity_python-0.1.62/tests/test_identifier_injection_guard.py +116 -0
  13. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_payment_stripe_adapter.py +85 -0
  14. velocity_python-0.1.62/tests/test_restricted_direct_tables.py +59 -0
  15. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_security_hardening.py +6 -5
  16. velocity_python-0.1.62/tests/test_write_hook_create_flow.py +138 -0
  17. {velocity_python-0.1.58 → velocity_python-0.1.62}/LICENSE +0 -0
  18. {velocity_python-0.1.58 → velocity_python-0.1.62}/README.md +0 -0
  19. {velocity_python-0.1.58 → velocity_python-0.1.62}/setup.cfg +0 -0
  20. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/__init__.py +0 -0
  21. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/amplify.py +0 -0
  22. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/amplify_build.py +0 -0
  23. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/assets/__init__.py +0 -0
  24. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/assets/backfill.py +0 -0
  25. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/assets/indexing.py +0 -0
  26. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/assets/references.py +0 -0
  27. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/assets/service.py +0 -0
  28. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/assets/usage_index.py +0 -0
  29. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/dirty_pipeline.py +0 -0
  30. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/__init__.py +0 -0
  31. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/base_handler.py +0 -0
  32. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/context.py +0 -0
  33. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/context_factory.py +0 -0
  34. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/exceptions.py +0 -0
  35. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  36. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  37. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/perf.py +0 -0
  38. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/response.py +0 -0
  39. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  40. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/s3.py +0 -0
  41. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/ssm_config.py +0 -0
  42. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/tests/__init__.py +0 -0
  43. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  44. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  45. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/aws/tests/test_response.py +0 -0
  46. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/__init__.py +0 -0
  47. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/__init__.py +0 -0
  48. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/async_support.py +0 -0
  49. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/column.py +0 -0
  50. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/database.py +0 -0
  51. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/decorators.py +0 -0
  52. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/engine.py +0 -0
  53. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/result.py +0 -0
  54. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/row.py +0 -0
  55. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/sequence.py +0 -0
  56. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/table.py +0 -0
  57. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/transaction.py +0 -0
  58. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/core/view.py +0 -0
  59. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/exceptions.py +0 -0
  60. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/migrations.py +0 -0
  61. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/__init__.py +0 -0
  62. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/base/__init__.py +0 -0
  63. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/base/initializer.py +0 -0
  64. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/base/operators.py +0 -0
  65. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/base/sql.py +0 -0
  66. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/base/types.py +0 -0
  67. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/mysql/__init__.py +0 -0
  68. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/mysql/operators.py +0 -0
  69. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/mysql/reserved.py +0 -0
  70. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/mysql/sql.py +0 -0
  71. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/mysql/types.py +0 -0
  72. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/postgres/__init__.py +0 -0
  73. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/postgres/operators.py +0 -0
  74. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/postgres/reserved.py +0 -0
  75. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/postgres/sql.py +0 -0
  76. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/postgres/types.py +0 -0
  77. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  78. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlite/operators.py +0 -0
  79. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  80. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlite/sql.py +0 -0
  81. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlite/types.py +0 -0
  82. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  83. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  84. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  85. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  86. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/servers/sqlserver/types.py +0 -0
  87. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/__init__.py +0 -0
  88. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/common_db_test.py +0 -0
  89. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/__init__.py +0 -0
  90. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/common.py +0 -0
  91. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/conftest.py +0 -0
  92. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_column.py +0 -0
  93. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  94. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_database.py +0 -0
  95. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  96. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  97. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  98. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_result.py +0 -0
  99. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_row.py +0 -0
  100. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  101. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  102. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  103. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  104. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  105. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_table.py +0 -0
  106. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  107. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  108. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/sql/__init__.py +0 -0
  109. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/sql/common.py +0 -0
  110. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  111. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  112. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  113. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_db_utils.py +0 -0
  114. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_postgres.py +0 -0
  115. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  116. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  117. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_result_caching.py +0 -0
  118. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  119. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  120. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  121. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  122. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_sql_builder.py +0 -0
  123. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_tablehelper.py +0 -0
  124. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/tests/test_view_helper.py +0 -0
  125. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/db/utils.py +0 -0
  126. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/logging.py +0 -0
  127. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/__init__.py +0 -0
  128. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/conv/__init__.py +0 -0
  129. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/conv/iconv.py +0 -0
  130. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/conv/oconv.py +0 -0
  131. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/db.py +0 -0
  132. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/export.py +0 -0
  133. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/format.py +0 -0
  134. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/mail.py +0 -0
  135. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/merge.py +0 -0
  136. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/pdf.py +0 -0
  137. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/__init__.py +0 -0
  138. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_db.py +0 -0
  139. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_fix.py +0 -0
  140. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_format.py +0 -0
  141. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_iconv.py +0 -0
  142. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_merge.py +0 -0
  143. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_oconv.py +0 -0
  144. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_original_error.py +0 -0
  145. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tests/test_timer.py +0 -0
  146. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/timer.py +0 -0
  147. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/misc/tools.py +0 -0
  148. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/__init__.py +0 -0
  149. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/authorizenet_adapter.py +0 -0
  150. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/authorizenet_mirror.py +0 -0
  151. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/base_adapter.py +0 -0
  152. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/braintree_adapter.py +0 -0
  153. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/braintree_mirror.py +0 -0
  154. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/charge_rules.py +0 -0
  155. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity/payment/stripe_mirror.py +0 -0
  156. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  157. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity_python.egg-info/entry_points.txt +0 -0
  158. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity_python.egg-info/requires.txt +0 -0
  159. {velocity_python-0.1.58 → velocity_python-0.1.62}/src/velocity_python.egg-info/top_level.txt +0 -0
  160. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_amplify_build.py +0 -0
  161. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_asset_indexing.py +0 -0
  162. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_asset_references.py +0 -0
  163. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_assets_service.py +0 -0
  164. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_async_support.py +0 -0
  165. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_batch_operations.py +0 -0
  166. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_concurrency_safety.py +0 -0
  167. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_connection_pool.py +0 -0
  168. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_connection_resilience.py +0 -0
  169. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_context_job_descriptions.py +0 -0
  170. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_db_credentials_ssm_cascade.py +0 -0
  171. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_decorators.py +0 -0
  172. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_email_processing.py +0 -0
  173. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_iconv_money_to_cents.py +0 -0
  174. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_jsonb_dict_adapter.py +0 -0
  175. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_lambda_handler.py +0 -0
  176. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_lambda_handler_auth.py +0 -0
  177. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_mixins_import.py +0 -0
  178. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_n_plus_one.py +0 -0
  179. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_observability.py +0 -0
  180. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_payment_authorizenet_adapter.py +0 -0
  181. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_payment_braintree_adapter.py +0 -0
  182. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_payment_braintree_mirror.py +0 -0
  183. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_payment_profile_sorting.py +0 -0
  184. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_pdf.py +0 -0
  185. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_prepared_statements.py +0 -0
  186. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_psycopg3_upgrade.py +0 -0
  187. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_query_cache.py +0 -0
  188. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_retry_side_effect_guard.py +0 -0
  189. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_return_default_safety.py +0 -0
  190. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_row_batch_update.py +0 -0
  191. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_row_cache_staleness.py +0 -0
  192. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_row_dirty_tracking.py +0 -0
  193. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_schema_migrations.py +0 -0
  194. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_server_cursor.py +0 -0
  195. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_single_autocommit_safety.py +0 -0
  196. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_spreadsheet_functions.py +0 -0
  197. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_sqs_per_record_transactions.py +0 -0
  198. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_ssm_config.py +0 -0
  199. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  200. {velocity_python-0.1.58 → velocity_python-0.1.62}/tests/test_table_alter.py +0 -0
  201. {velocity_python-0.1.58 → velocity_python-0.1.62}/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.1.58
3
+ Version: 0.1.62
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.1.58"
7
+ version = "0.1.62"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.58"
1
+ __version__ = version = "0.1.62"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -209,6 +209,16 @@ class LambdaHandler(BaseHandler):
209
209
  try:
210
210
  self.execute_actions(tx, local_context, actions)
211
211
  except Exception as e:
212
+ # A single HTTP action is atomic: any exception (including a handled
213
+ # AlertError validation rejection) must discard partial writes so the
214
+ # surrounding @engine.transaction does not commit them on the normal
215
+ # return below. Without this, e.g. a write_hook that inserts a @new
216
+ # row and then raises a validation AlertError would leak a committed
217
+ # orphan row. Mirrors SqsHandler._process_record.
218
+ try:
219
+ tx.rollback()
220
+ except Exception:
221
+ pass
212
222
  self.handle_error(tx, local_context, e)
213
223
  local_context.perf.log("execute_actions total (serve)")
214
224
 
@@ -16,6 +16,7 @@ import xml.etree.ElementTree as ET
16
16
  from io import BytesIO, StringIO
17
17
 
18
18
  from velocity.aws import dirty_pipeline
19
+ from velocity.aws.handlers.exceptions import AlertError
19
20
  from velocity.misc import export
20
21
 
21
22
  logger = logging.getLogger(__name__)
@@ -41,6 +42,15 @@ class DataServiceMixin:
41
42
  Override read_hook, write_hook, etc. methods to add custom business logic.
42
43
  """
43
44
 
45
+ # Tables that must NOT be reachable through the hook-bypassing generic
46
+ # actions (query-direct, update-rows, get-table-schema). The generic actions
47
+ # do not run rwx hooks, so any per-table row-scoping or secret-redaction a
48
+ # table relies on would be bypassed. Apps populate this set (business policy
49
+ # lives in the app, not this business-agnostic mixin) with the names of such
50
+ # tables; the normal read-object/find-object/query/write-object actions —
51
+ # which DO run hooks — remain the supported path for them.
52
+ restricted_direct_tables = frozenset()
53
+
44
54
  # PostgreSQL type mappings for frontend display
45
55
  _pg_types = {
46
56
  "bool": "string",
@@ -58,6 +68,14 @@ class DataServiceMixin:
58
68
  "timestamp": "string",
59
69
  }
60
70
 
71
+ def _assert_direct_access_allowed(self, table):
72
+ """Reject hook-bypassing direct access to a restricted table."""
73
+ if table in self.restricted_direct_tables:
74
+ raise AlertError(
75
+ f"Direct access to '{table}' is not permitted; "
76
+ "use the standard data actions."
77
+ )
78
+
61
79
  def _get_field_type(self, column_info):
62
80
  """Convert database column type to frontend display type"""
63
81
  return (
@@ -149,20 +167,39 @@ class DataServiceMixin:
149
167
  self._call_rwx_hook(
150
168
  "before_new", table, tx, table, sys_id, incoming, context
151
169
  )
170
+ # Run before_write BEFORE inserting the row, passing the "@new"
171
+ # sentinel so table hooks can detect a create, validate required
172
+ # fields, and reject it without leaving an orphan row. (The serve()
173
+ # error path rolls the transaction back, but validating pre-insert
174
+ # avoids ever allocating the row in the first place.)
175
+ self._call_rwx_hook(
176
+ "before_write", "common", tx, table, "@new", incoming, context
177
+ )
178
+ self._call_rwx_hook(
179
+ "before_write", table, tx, table, "@new", incoming, context
180
+ )
152
181
  row = tx.table(table).new()
153
182
  sys_id = row["sys_id"]
154
183
  self._call_rwx_hook("after_new", "common", tx, table, sys_id, row, context)
155
184
  self._call_rwx_hook("after_new", table, tx, table, sys_id, row, context)
156
185
  elif sys_id:
157
186
  sys_id = int(sys_id)
187
+ self._call_rwx_hook(
188
+ "before_write", "common", tx, table, sys_id, incoming, context
189
+ )
190
+ self._call_rwx_hook(
191
+ "before_write", table, tx, table, sys_id, incoming, context
192
+ )
193
+ # Update path: the row must already exist. Use find (not get, which
194
+ # is get-or-create) so a write to a stale/unknown sys_id is a clear
195
+ # error rather than a phantom row inserted with a client-chosen PK.
196
+ row = tx.table(table).find(sys_id)
197
+ if not row:
198
+ raise AlertError(
199
+ f"Cannot update {table}: record {sys_id} no longer exists."
200
+ )
158
201
  else:
159
202
  raise Exception("Object sys_id was not supplied on write operation.")
160
- self._call_rwx_hook(
161
- "before_write", "common", tx, table, sys_id, incoming, context
162
- )
163
- self._call_rwx_hook("before_write", table, tx, table, sys_id, incoming, context)
164
- if not row:
165
- row = tx.table(table).get(sys_id)
166
203
  row.update(incoming)
167
204
  self._call_rwx_hook("after_write", "common", tx, table, sys_id, row, context)
168
205
  self._call_rwx_hook("after_write", table, tx, table, sys_id, row, context)
@@ -842,6 +879,8 @@ class DataServiceMixin:
842
879
  if not rows:
843
880
  raise ValueError("Parameter 'updateRows' cannot be empty")
844
881
 
882
+ self._assert_direct_access_allowed(table)
883
+
845
884
  t = tx.table(table)
846
885
  count = t.update(data, {"sys_id": rows})
847
886
  context.response().toast(f"Updated {count} item(s).", "success")
@@ -867,6 +906,8 @@ class DataServiceMixin:
867
906
  if not table_name:
868
907
  raise ValueError("Parameter 'obj' cannot be empty")
869
908
 
909
+ self._assert_direct_access_allowed(table_name)
910
+
870
911
  params = payload.get("params", {})
871
912
 
872
913
  if payload.get("result_format") == "excel":
@@ -932,6 +973,8 @@ class DataServiceMixin:
932
973
  if not table_name:
933
974
  raise ValueError("Parameter 'tableName' cannot be empty")
934
975
 
976
+ self._assert_direct_access_allowed(table_name)
977
+
935
978
  try:
936
979
  # Query information_schema to get table schema
937
980
  schema_query = """
@@ -158,6 +158,54 @@ class TableHelper:
158
158
  self.foreign_keys[key] = data
159
159
  return data
160
160
 
161
+ # Substrings that never legitimately appear inside a column/WHERE-key
162
+ # reference and are the signatures of identifier-level SQL injection. The
163
+ # values side of every predicate is parameterized; only the identifier side
164
+ # flows through here, so a placeholder, statement terminator, or comment in
165
+ # this position is always an attack, never a real column or expression.
166
+ _INJECTION_SIGNATURES: ClassVar[Tuple[str, ...]] = (
167
+ ";", # statement terminator / stacked query
168
+ "--", # line comment
169
+ "/*", # block comment open
170
+ "*/", # block comment close
171
+ "%s", # positional bind-parameter smuggling
172
+ "%(", # named bind-parameter smuggling
173
+ )
174
+
175
+ def _assert_safe_reference(self, key: str) -> None:
176
+ """
177
+ Reject identifier/expression references carrying SQL-injection signatures.
178
+
179
+ This is a defense-in-depth guard for the *structured* query path (WHERE
180
+ dict keys, ORDER BY / GROUP BY entries, projected columns). Values are
181
+ always parameterized; this guard protects the identifier side, which is
182
+ interpolated. It deliberately does not reject legitimate SQL expressions
183
+ (aggregates, CASE/CAST, window functions, correlated subqueries) — only
184
+ the unambiguous injection markers and unbalanced parentheses. Raw string
185
+ WHERE clauses are intentionally not routed through here and remain
186
+ caller-beware per the documented contract.
187
+
188
+ Raises:
189
+ ValueError: unconditionally (ignores bypass_on_error) when an
190
+ injection signature is present, because such input is never a
191
+ valid reference.
192
+ """
193
+ if not isinstance(key, str):
194
+ return
195
+ # Strip a leading operator prefix (e.g. '%', '%%', '>=') so that a LIKE
196
+ # key such as '%status' is not misread as the '%s' placeholder marker.
197
+ body = self.remove_operator(key)
198
+ lowered = body.lower()
199
+ for marker in self._INJECTION_SIGNATURES:
200
+ if marker in lowered:
201
+ raise ValueError(
202
+ f"Unsafe SQL reference detected (contains {marker!r}): {key!r}"
203
+ )
204
+ if not self.are_parentheses_balanced(body):
205
+ raise ValueError(
206
+ f"Unsafe SQL reference detected (unbalanced parentheses): {key!r}"
207
+ )
208
+
161
209
  def resolve_references(
162
210
  self, key: str, options: Optional[Dict[str, Any]] = None
163
211
  ) -> str:
@@ -176,13 +224,20 @@ class TableHelper:
176
224
  Resolved column reference with appropriate aliasing
177
225
 
178
226
  Raises:
179
- ValueError: If key is invalid and bypass_on_error is False
227
+ ValueError: If key is invalid and bypass_on_error is False, or if the
228
+ key carries an SQL-injection signature (always, regardless of
229
+ bypass_on_error)
180
230
  """
181
231
  if not key or not isinstance(key, str):
182
232
  if options and options.get("bypass_on_error"):
183
233
  return key or ""
184
234
  raise ValueError(f"Invalid key: {key}")
185
235
 
236
+ # Identifier-level injection guard runs before bypass_on_error so that a
237
+ # poisoned reference is a hard error on every path, not silently passed
238
+ # through.
239
+ self._assert_safe_reference(key)
240
+
186
241
  if options is None:
187
242
  options = {"alias_column": True, "alias_table": False, "alias_only": False}
188
243
 
@@ -469,6 +469,14 @@ class StripeAdapter(PaymentProcessorAdapter):
469
469
  if self.platform:
470
470
  payment_metadata["platform"] = str(self.platform)
471
471
 
472
+ # Optional idempotency key: when the caller supplies one, Stripe
473
+ # collapses retries of the *same* request (engine auto-retry, donor
474
+ # double-submit) into a single charge instead of charging again.
475
+ request_options = {}
476
+ idempotency_key = payment_data.get("idempotency_key")
477
+ if idempotency_key:
478
+ request_options["idempotency_key"] = str(idempotency_key)
479
+
472
480
  payment_intent = stripe.PaymentIntent.create(
473
481
  amount=amount_cents,
474
482
  currency=currency,
@@ -482,6 +490,7 @@ class StripeAdapter(PaymentProcessorAdapter):
482
490
  },
483
491
  metadata=payment_metadata,
484
492
  api_key=self._api_key,
493
+ **request_options,
485
494
  )
486
495
  charge = self._mirror_payment_intent_artifacts(
487
496
  tx,
@@ -575,6 +584,11 @@ class StripeAdapter(PaymentProcessorAdapter):
575
584
  if self.platform:
576
585
  payment_metadata["platform"] = str(self.platform)
577
586
 
587
+ request_options = {}
588
+ idempotency_key = payment_data.get("idempotency_key")
589
+ if idempotency_key:
590
+ request_options["idempotency_key"] = str(idempotency_key)
591
+
578
592
  payment_intent = stripe.PaymentIntent.create(
579
593
  amount=amount_cents,
580
594
  currency=currency,
@@ -587,6 +601,7 @@ class StripeAdapter(PaymentProcessorAdapter):
587
601
  description=payment_data.get("description"),
588
602
  metadata=payment_metadata,
589
603
  api_key=self._api_key,
604
+ **request_options,
590
605
  )
591
606
  charge = self._mirror_payment_intent_artifacts(
592
607
  tx,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.58
3
+ Version: 0.1.62
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
@@ -162,7 +162,9 @@ tests/test_db_credentials_ssm_cascade.py
162
162
  tests/test_decorators.py
163
163
  tests/test_dirty_pipeline_fast_path.py
164
164
  tests/test_email_processing.py
165
+ tests/test_http_handler_rollback.py
165
166
  tests/test_iconv_money_to_cents.py
167
+ tests/test_identifier_injection_guard.py
166
168
  tests/test_jsonb_dict_adapter.py
167
169
  tests/test_lambda_handler.py
168
170
  tests/test_lambda_handler_auth.py
@@ -178,6 +180,7 @@ tests/test_pdf.py
178
180
  tests/test_prepared_statements.py
179
181
  tests/test_psycopg3_upgrade.py
180
182
  tests/test_query_cache.py
183
+ tests/test_restricted_direct_tables.py
181
184
  tests/test_retry_side_effect_guard.py
182
185
  tests/test_return_default_safety.py
183
186
  tests/test_row_batch_update.py
@@ -192,4 +195,5 @@ tests/test_sqs_per_record_transactions.py
192
195
  tests/test_ssm_config.py
193
196
  tests/test_sys_modified_count_postgres_demo.py
194
197
  tests/test_table_alter.py
195
- tests/test_where_clause_validation.py
198
+ tests/test_where_clause_validation.py
199
+ tests/test_write_hook_create_flow.py
@@ -39,6 +39,14 @@ class FakeTable:
39
39
  def get(self, sys_id):
40
40
  return self.row
41
41
 
42
+ def find(self, sys_id):
43
+ # Update path now uses find() (not get, which is get-or-create) so a
44
+ # write to a stale sys_id is an error rather than a phantom row.
45
+ return self.row
46
+
47
+ def new(self):
48
+ return self.row
49
+
42
50
 
43
51
  class FakeTx:
44
52
  def __init__(self, row):
@@ -0,0 +1,82 @@
1
+ """V2 — the HTTP handler must roll back partial writes before handle_error.
2
+
3
+ A single HTTP action is atomic. Because the handler class is wrapped in
4
+ @engine.transaction, a normal return from serve() commits. Without an explicit
5
+ rollback on the error path, an action that writes and then raises — including a
6
+ write_hook that inserts a @new row and then raises a validation AlertError
7
+ (D2) — would leave a committed orphan row. These tests pin the rollback.
8
+ """
9
+
10
+ import unittest
11
+ from unittest.mock import MagicMock
12
+
13
+ from velocity.aws.handlers.exceptions import AlertError
14
+ from velocity.aws.handlers.lambda_handler import LambdaHandler
15
+ from velocity.aws.handlers.context_factory import ContextFactory
16
+
17
+
18
+ class _StubContext:
19
+ def __init__(self, response):
20
+ self._response = response
21
+ self.perf = MagicMock()
22
+
23
+ def parse_postdata(self):
24
+ return {"action": "do-thing", "payload": {}}
25
+
26
+ def update_postdata(self, postdata):
27
+ pass
28
+
29
+ def configure_perf(self, **kw):
30
+ pass
31
+
32
+ def action(self):
33
+ return "do-thing"
34
+
35
+ def postdata(self, keys=-1, default=None):
36
+ return {"action": "do-thing", "payload": {}}
37
+
38
+ def response(self):
39
+ return self._response
40
+
41
+
42
+ class _StubContextFactory(ContextFactory):
43
+ def create(self, *, aws_event, aws_context, args, postdata, response, session):
44
+ return _StubContext(response)
45
+
46
+
47
+ def _make_handler(raise_exc):
48
+ handler = LambdaHandler(
49
+ {"queryStringParameters": {}},
50
+ MagicMock(aws_request_id="req-123"),
51
+ context_factory=_StubContextFactory(),
52
+ )
53
+
54
+ def _boom(tx, local_context, actions):
55
+ raise raise_exc
56
+
57
+ handler.execute_actions = _boom
58
+ # Keep onError off the DB during the generic-exception path.
59
+ handler.onError = lambda tx, context, exc, tb: None
60
+ return handler
61
+
62
+
63
+ class HttpHandlerRollbackTest(unittest.TestCase):
64
+ def test_alert_error_rolls_back_before_handle_error(self):
65
+ # D2: a validation AlertError raised after a @new insert must not commit
66
+ # the orphan row.
67
+ handler = _make_handler(AlertError("required field missing"))
68
+ tx = MagicMock()
69
+ handler.serve(tx)
70
+ tx.rollback.assert_called_once()
71
+
72
+ def test_unhandled_exception_rolls_back_and_returns_500(self):
73
+ handler = _make_handler(RuntimeError("kaboom"))
74
+ tx = MagicMock()
75
+ rendered = handler.serve(tx)
76
+ tx.rollback.assert_called_once()
77
+ # _set_unhandled_error_response sets a 500 on the response.
78
+ self.assertIn("500", str(rendered.get("statusCode", "")) + str(rendered))
79
+
80
+
81
+ if __name__ == "__main__":
82
+ unittest.main()
@@ -0,0 +1,116 @@
1
+ """V1 — identifier-level SQL-injection guard for the structured query path.
2
+
3
+ The value side of every predicate is parameterized; the identifier side (WHERE
4
+ dict keys, ORDER BY / GROUP BY entries, projected columns) is interpolated and
5
+ flows through TableHelper.resolve_references / make_predicate. These tests pin
6
+ down that injection signatures on the identifier side are rejected while
7
+ legitimate column expressions (aggregates, CASE/CAST, window functions, pointer
8
+ syntax, operator prefixes including LIKE/ILIKE) still resolve.
9
+ """
10
+
11
+ import unittest
12
+
13
+ # Importing the postgres SQL module configures TableHelper.operators / .reserved
14
+ # as a side effect, which make_predicate / resolve_references rely on.
15
+ from velocity.db.servers.postgres.sql import SQL # noqa: F401
16
+ from velocity.db.servers.postgres.operators import OPERATORS
17
+ from velocity.db.servers.postgres.reserved import reserved_words
18
+ from velocity.db.servers.tablehelper import TableHelper
19
+
20
+
21
+ class IdentifierInjectionGuardTest(unittest.TestCase):
22
+ def setUp(self):
23
+ # Pin the postgres operator/reserved tables on the shared class attrs so
24
+ # this suite is independent of any other test that mutates them.
25
+ TableHelper.operators = OPERATORS
26
+ TableHelper.reserved = reserved_words
27
+ # tx is only consulted for pointer (foreign-key) resolution; the cases
28
+ # here are all local columns/expressions, so None is sufficient.
29
+ self.helper = TableHelper(tx=None, table="customers")
30
+
31
+ # --- the confirmed proof-of-concept vectors from the code review ---------
32
+
33
+ def test_where_key_placeholder_injection_rejected(self):
34
+ with self.assertRaises(ValueError):
35
+ self.helper.make_predicate("email = %s) OR (1=1", "x")
36
+
37
+ def test_orderby_style_stacked_query_rejected(self):
38
+ with self.assertRaises(ValueError):
39
+ self.helper.resolve_references(
40
+ "name; DROP TABLE users",
41
+ options={"alias_only": True, "bypass_on_error": True},
42
+ )
43
+
44
+ def test_column_comment_and_stacking_rejected(self):
45
+ with self.assertRaises(ValueError):
46
+ self.helper.resolve_references(
47
+ "1 FROM users; DELETE FROM users WHERE 1=1 --",
48
+ options={
49
+ "alias_column": True,
50
+ "alias_table": True,
51
+ "bypass_on_error": True,
52
+ },
53
+ )
54
+
55
+ # --- bypass_on_error must NOT swallow an injection ------------------------
56
+
57
+ def test_bypass_on_error_does_not_swallow_injection(self):
58
+ for poisoned in (
59
+ "col; SELECT 1",
60
+ "col /* x */",
61
+ "col -- comment",
62
+ "col = %s",
63
+ "col = %(name)s",
64
+ "col) OR (1=1",
65
+ ):
66
+ with self.subTest(poisoned=poisoned):
67
+ with self.assertRaises(ValueError):
68
+ self.helper.resolve_references(
69
+ poisoned, options={"bypass_on_error": True}
70
+ )
71
+
72
+ # --- legitimate references still resolve ---------------------------------
73
+
74
+ def test_plain_column_resolves(self):
75
+ sql, val = self.helper.make_predicate("email_address", "a@example.com")
76
+ self.assertEqual(sql, "email_address = %s")
77
+ self.assertEqual(val, "a@example.com")
78
+
79
+ def test_like_operator_prefix_not_flagged_as_placeholder(self):
80
+ # '%status' is a LIKE on the `status` column; the leading '%' is an
81
+ # operator prefix, not a '%s' bind placeholder.
82
+ sql, val = self.helper.make_predicate("%status", "active%")
83
+ self.assertIn("status", sql)
84
+ self.assertIn("LIKE", sql.upper())
85
+ self.assertEqual(val, "active%")
86
+
87
+ def test_ilike_double_percent_prefix_ok(self):
88
+ sql, _ = self.helper.make_predicate("%%name", "smith")
89
+ self.assertIn("ILIKE", sql.upper())
90
+
91
+ def test_aggregate_expression_column_ok(self):
92
+ result = self.helper.resolve_references(
93
+ "SUM(amount)",
94
+ options={"alias_column": True, "bypass_on_error": True},
95
+ )
96
+ self.assertIn("amount", result)
97
+
98
+ def test_balanced_function_expression_ok(self):
99
+ result = self.helper.resolve_references(
100
+ "COALESCE(amount, 0)",
101
+ options={"alias_column": True, "bypass_on_error": True},
102
+ )
103
+ self.assertIn("amount", result)
104
+
105
+ def test_in_list_predicate_ok(self):
106
+ sql, val = self.helper.make_predicate("sys_id", [1, 2, 3])
107
+ self.assertIn("IN", sql.upper())
108
+ self.assertEqual(tuple(val), (1, 2, 3))
109
+
110
+ def test_not_equal_operator_prefix_ok(self):
111
+ sql, _ = self.helper.make_predicate("!status", "deleted")
112
+ self.assertIn("status", sql)
113
+
114
+
115
+ if __name__ == "__main__":
116
+ unittest.main()
@@ -290,6 +290,91 @@ def test_authorize_payment_uses_explicit_application_fee(adapter, monkeypatch):
290
290
  assert result["metadata"]["client_amount"] == 9200 # $92.00 to client
291
291
 
292
292
 
293
+ def test_authorize_payment_passes_idempotency_key(adapter, monkeypatch):
294
+ captured = {}
295
+
296
+ def fake_create(**kwargs):
297
+ captured.update(kwargs)
298
+ return SimpleNamespace(
299
+ id="pi_idem",
300
+ latest_charge=None,
301
+ status="succeeded",
302
+ amount=5000,
303
+ currency="usd",
304
+ metadata={},
305
+ charges=SimpleNamespace(data=[]),
306
+ )
307
+
308
+ monkeypatch.setattr(
309
+ "velocity.payment.stripe_adapter.stripe.PaymentIntent.create", fake_create
310
+ )
311
+ monkeypatch.setattr(
312
+ "velocity.payment.stripe_adapter.upsert_stripe_payment_intent_record",
313
+ lambda tx, payment_intent, source=None: None,
314
+ )
315
+ monkeypatch.setattr(
316
+ "velocity.payment.stripe_adapter.upsert_stripe_charge_record",
317
+ lambda tx, charge, source=None: None,
318
+ )
319
+
320
+ adapter.authorize_payment(
321
+ None,
322
+ {
323
+ "amount": 5000,
324
+ "payment_method": "pm_1",
325
+ "processor_account_id": "acct_1",
326
+ "revenue_split_percentage": 8,
327
+ "capture_method": "automatic",
328
+ "client_id": "client_1",
329
+ "idempotency_key": "donatemain-req-abc-123",
330
+ },
331
+ )
332
+
333
+ assert captured["idempotency_key"] == "donatemain-req-abc-123"
334
+
335
+
336
+ def test_authorize_payment_omits_idempotency_key_when_absent(adapter, monkeypatch):
337
+ captured = {}
338
+
339
+ def fake_create(**kwargs):
340
+ captured.update(kwargs)
341
+ return SimpleNamespace(
342
+ id="pi_noidem",
343
+ latest_charge=None,
344
+ status="succeeded",
345
+ amount=5000,
346
+ currency="usd",
347
+ metadata={},
348
+ charges=SimpleNamespace(data=[]),
349
+ )
350
+
351
+ monkeypatch.setattr(
352
+ "velocity.payment.stripe_adapter.stripe.PaymentIntent.create", fake_create
353
+ )
354
+ monkeypatch.setattr(
355
+ "velocity.payment.stripe_adapter.upsert_stripe_payment_intent_record",
356
+ lambda tx, payment_intent, source=None: None,
357
+ )
358
+ monkeypatch.setattr(
359
+ "velocity.payment.stripe_adapter.upsert_stripe_charge_record",
360
+ lambda tx, charge, source=None: None,
361
+ )
362
+
363
+ adapter.authorize_payment(
364
+ None,
365
+ {
366
+ "amount": 5000,
367
+ "payment_method": "pm_1",
368
+ "processor_account_id": "acct_1",
369
+ "revenue_split_percentage": 8,
370
+ "capture_method": "automatic",
371
+ "client_id": "client_1",
372
+ },
373
+ )
374
+
375
+ assert "idempotency_key" not in captured
376
+
377
+
293
378
  def test_attach_payment_method_upserts_stripe_payment_method_mirror(adapter, monkeypatch):
294
379
  mirrored = []
295
380
  retrieved = SimpleNamespace(
@@ -0,0 +1,59 @@
1
+ """D6 — hook-bypassing generic actions must refuse restricted tables.
2
+
3
+ query-direct / update-rows / get-table-schema run no rwx hooks, so a table
4
+ that relies on per-row scoping or secret redaction in its hooks must not be
5
+ reachable through them. Apps declare such tables in restricted_direct_tables.
6
+ """
7
+
8
+ import unittest
9
+ from unittest.mock import MagicMock
10
+
11
+ from velocity.aws.handlers.exceptions import AlertError
12
+ from velocity.aws.handlers.mixins.data_service import DataServiceMixin
13
+
14
+
15
+ class _Service(DataServiceMixin):
16
+ restricted_direct_tables = frozenset({"mail_accounts"})
17
+
18
+
19
+ def _ctx(payload):
20
+ ctx = MagicMock()
21
+ ctx.payload.return_value = payload
22
+ return ctx
23
+
24
+
25
+ class RestrictedDirectTablesTest(unittest.TestCase):
26
+ def setUp(self):
27
+ self.service = _Service()
28
+ self.tx = MagicMock()
29
+
30
+ def test_query_direct_blocks_restricted(self):
31
+ ctx = _ctx({"obj": "mail_accounts", "params": {}})
32
+ with self.assertRaises(AlertError):
33
+ self.service.OnActionQueryDirect(self.tx, ctx)
34
+
35
+ def test_update_rows_blocks_restricted(self):
36
+ ctx = _ctx(
37
+ {"table": "mail_accounts", "updateData": {"x": 1}, "updateRows": [1, 2]}
38
+ )
39
+ with self.assertRaises(AlertError):
40
+ self.service.OnActionUpdateRows(self.tx, ctx)
41
+
42
+ def test_get_table_schema_blocks_restricted(self):
43
+ ctx = _ctx({"tableName": "mail_accounts"})
44
+ with self.assertRaises(AlertError):
45
+ self.service.OnActionGetTableSchema(self.tx, ctx)
46
+
47
+ def test_unrestricted_table_not_blocked_by_guard(self):
48
+ # A non-restricted table passes the guard (it then proceeds to touch the
49
+ # mocked tx, which is fine for this assertion — no AlertError from the
50
+ # guard itself).
51
+ ctx = _ctx({"table": "customers", "updateData": {"x": 1}, "updateRows": [1]})
52
+ try:
53
+ self.service.OnActionUpdateRows(self.tx, ctx)
54
+ except AlertError:
55
+ self.fail("guard should not block an unrestricted table")
56
+
57
+
58
+ if __name__ == "__main__":
59
+ unittest.main()