velocity-python 0.1.100__tar.gz → 0.1.102__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 (271) hide show
  1. {velocity_python-0.1.100 → velocity_python-0.1.102}/PKG-INFO +1 -1
  2. {velocity_python-0.1.100 → velocity_python-0.1.102}/pyproject.toml +1 -1
  3. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.1.102/src/velocity/aws/blobs.py +91 -0
  5. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/data_service.py +213 -4
  6. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/response.py +27 -1
  7. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/ssm_config.py +9 -0
  8. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/braintree_adapter.py +29 -0
  9. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/stripe_adapter.py +149 -0
  10. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/PKG-INFO +1 -1
  11. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/SOURCES.txt +6 -0
  12. velocity_python-0.1.102/tests/test_blob_store.py +64 -0
  13. velocity_python-0.1.102/tests/test_data_service_query_cap.py +115 -0
  14. velocity_python-0.1.102/tests/test_data_service_renumber.py +93 -0
  15. velocity_python-0.1.102/tests/test_data_service_save_as.py +190 -0
  16. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_braintree_adapter.py +38 -0
  17. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_stripe_adapter.py +139 -0
  18. velocity_python-0.1.102/tests/test_response.py +32 -0
  19. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_ssm_config.py +13 -0
  20. {velocity_python-0.1.100 → velocity_python-0.1.102}/LICENSE +0 -0
  21. {velocity_python-0.1.100 → velocity_python-0.1.102}/README.md +0 -0
  22. {velocity_python-0.1.100 → velocity_python-0.1.102}/setup.cfg +0 -0
  23. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/__init__.py +0 -0
  24. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/amplify.py +0 -0
  25. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/amplify_build.py +0 -0
  26. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/__init__.py +0 -0
  27. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/backfill.py +0 -0
  28. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/indexing.py +0 -0
  29. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/references.py +0 -0
  30. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/service.py +0 -0
  31. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/usage_index.py +0 -0
  32. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/__init__.py +0 -0
  33. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/config.py +0 -0
  34. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/operations.py +0 -0
  35. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/policies.py +0 -0
  36. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/reconcile.py +0 -0
  37. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/targets.py +0 -0
  38. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/dirty_pipeline.py +0 -0
  39. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/email.py +0 -0
  40. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/__init__.py +0 -0
  41. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/base_handler.py +0 -0
  42. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/context.py +0 -0
  43. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/context_factory.py +0 -0
  44. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/exceptions.py +0 -0
  45. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  46. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/masquerade.py +0 -0
  47. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  48. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  49. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  50. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/perf.py +0 -0
  51. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  52. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/job_results.py +0 -0
  53. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/s3.py +0 -0
  54. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/sms.py +0 -0
  55. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/__init__.py +0 -0
  56. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  57. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  58. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  59. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  60. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  61. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_email.py +0 -0
  62. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  63. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_response.py +0 -0
  64. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_sms.py +0 -0
  65. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/__init__.py +0 -0
  66. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/__init__.py +0 -0
  67. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/async_support.py +0 -0
  68. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/column.py +0 -0
  69. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/database.py +0 -0
  70. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/decorators.py +0 -0
  71. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/engine.py +0 -0
  72. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/jsonproxy.py +0 -0
  73. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/result.py +0 -0
  74. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/row.py +0 -0
  75. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/sequence.py +0 -0
  76. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/table.py +0 -0
  77. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/transaction.py +0 -0
  78. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/view.py +0 -0
  79. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/exceptions.py +0 -0
  80. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/maintenance.py +0 -0
  81. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/migrations.py +0 -0
  82. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/__init__.py +0 -0
  83. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/__init__.py +0 -0
  84. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/initializer.py +0 -0
  85. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/operators.py +0 -0
  86. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/sql.py +0 -0
  87. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/types.py +0 -0
  88. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/__init__.py +0 -0
  89. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/operators.py +0 -0
  90. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/reserved.py +0 -0
  91. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/sql.py +0 -0
  92. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/types.py +0 -0
  93. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/__init__.py +0 -0
  94. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/operators.py +0 -0
  95. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/reserved.py +0 -0
  96. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/sql.py +0 -0
  97. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/types.py +0 -0
  98. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  99. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/operators.py +0 -0
  100. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  101. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/sql.py +0 -0
  102. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/types.py +0 -0
  103. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  104. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  105. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  106. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  107. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/types.py +0 -0
  108. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/tablehelper.py +0 -0
  109. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/__init__.py +0 -0
  110. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/common_db_test.py +0 -0
  111. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/__init__.py +0 -0
  112. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/common.py +0 -0
  113. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/conftest.py +0 -0
  114. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_column.py +0 -0
  115. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  116. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_database.py +0 -0
  117. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  118. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  119. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  120. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_result.py +0 -0
  121. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_row.py +0 -0
  122. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  123. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  124. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  125. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  126. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  127. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_table.py +0 -0
  128. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  129. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  130. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/__init__.py +0 -0
  131. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/common.py +0 -0
  132. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  133. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  134. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  135. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_db_utils.py +0 -0
  136. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_postgres.py +0 -0
  137. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  138. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  139. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_result_caching.py +0 -0
  140. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  141. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  142. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  143. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  144. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_sql_builder.py +0 -0
  145. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_tablehelper.py +0 -0
  146. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_view_helper.py +0 -0
  147. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/utils.py +0 -0
  148. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/logging.py +0 -0
  149. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/__init__.py +0 -0
  150. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/conv/__init__.py +0 -0
  151. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/conv/iconv.py +0 -0
  152. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/conv/oconv.py +0 -0
  153. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/db.py +0 -0
  154. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/export.py +0 -0
  155. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/format.py +0 -0
  156. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/mail.py +0 -0
  157. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/merge.py +0 -0
  158. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/pdf.py +0 -0
  159. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/__init__.py +0 -0
  160. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_db.py +0 -0
  161. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_fix.py +0 -0
  162. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_format.py +0 -0
  163. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_iconv.py +0 -0
  164. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_mail_send.py +0 -0
  165. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_merge.py +0 -0
  166. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_oconv.py +0 -0
  167. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_original_error.py +0 -0
  168. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_timer.py +0 -0
  169. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/timer.py +0 -0
  170. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tools.py +0 -0
  171. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/__init__.py +0 -0
  172. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/__init__.py +0 -0
  173. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/capabilities.py +0 -0
  174. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/store.py +0 -0
  175. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/where.py +0 -0
  176. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/exceptions.py +0 -0
  177. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/__init__.py +0 -0
  178. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/base/__init__.py +0 -0
  179. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  180. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  181. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  182. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  183. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  184. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  185. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  186. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  187. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  188. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  189. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  190. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  191. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/__init__.py +0 -0
  192. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/authorizenet_adapter.py +0 -0
  193. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/authorizenet_mirror.py +0 -0
  194. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/base_adapter.py +0 -0
  195. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/braintree_mirror.py +0 -0
  196. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/charge_rules.py +0 -0
  197. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/stripe_mirror.py +0 -0
  198. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  199. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/entry_points.txt +0 -0
  200. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/requires.txt +0 -0
  201. {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/top_level.txt +0 -0
  202. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_activity_tracking_aborted_tx.py +0 -0
  203. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_amplify_build.py +0 -0
  204. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_asset_indexing.py +0 -0
  205. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_asset_references.py +0 -0
  206. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_assets_service.py +0 -0
  207. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_async_support.py +0 -0
  208. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_batch_operations.py +0 -0
  209. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_column_tx_arg.py +0 -0
  210. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_concurrency_safety.py +0 -0
  211. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_connection_pool.py +0 -0
  212. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_connection_resilience.py +0 -0
  213. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_context_job_descriptions.py +0 -0
  214. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_db_credentials_ssm_cascade.py +0 -0
  215. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_db_maintenance.py +0 -0
  216. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_decorators.py +0 -0
  217. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_delete_hook_fk_blocked.py +0 -0
  218. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_dirty_pipeline_fast_path.py +0 -0
  219. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_email_processing.py +0 -0
  220. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_enqueue_send_failures.py +0 -0
  221. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_get_cognito_user_provider.py +0 -0
  222. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_http_handler_rollback.py +0 -0
  223. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_iconv_money_to_cents.py +0 -0
  224. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_identifier_injection_guard.py +0 -0
  225. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_job_handler.py +0 -0
  226. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_job_results.py +0 -0
  227. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_json_columns.py +0 -0
  228. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_jsonb_dict_adapter.py +0 -0
  229. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_lambda_handler.py +0 -0
  230. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_lambda_handler_auth.py +0 -0
  231. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_lambda_handler_masquerade.py +0 -0
  232. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_masquerade_grant.py +0 -0
  233. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_mixins_import.py +0 -0
  234. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_n_plus_one.py +0 -0
  235. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_dynamodb.py +0 -0
  236. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_memory.py +0 -0
  237. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_mongodb.py +0 -0
  238. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_redis.py +0 -0
  239. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_translation.py +0 -0
  240. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_where.py +0 -0
  241. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_observability.py +0 -0
  242. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_on_commit.py +0 -0
  243. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_authorizenet_adapter.py +0 -0
  244. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_braintree_mirror.py +0 -0
  245. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_mirror_empty_table.py +0 -0
  246. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_profile_sorting.py +0 -0
  247. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_pdf.py +0 -0
  248. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_prepared_statements.py +0 -0
  249. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_psycopg3_upgrade.py +0 -0
  250. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_query_cache.py +0 -0
  251. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_query_missing_table_unlocked.py +0 -0
  252. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_restricted_direct_tables.py +0 -0
  253. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_retry_side_effect_guard.py +0 -0
  254. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_return_default_safety.py +0 -0
  255. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_row_batch_update.py +0 -0
  256. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_row_cache_staleness.py +0 -0
  257. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_schema_migrations.py +0 -0
  258. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_security_hardening.py +0 -0
  259. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_server_cursor.py +0 -0
  260. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_single_autocommit_safety.py +0 -0
  261. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_spreadsheet_functions.py +0 -0
  262. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_sqlite_backend.py +0 -0
  263. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_sqs_per_record_transactions.py +0 -0
  264. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_store_user_data.py +0 -0
  265. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  266. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_table_alter.py +0 -0
  267. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_transaction_class_wrapping.py +0 -0
  268. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_transaction_commit_and_ownership.py +0 -0
  269. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_transaction_edge_cases.py +0 -0
  270. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_where_clause_validation.py +0 -0
  271. {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_write_hook_create_flow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.100
3
+ Version: 0.1.102
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.100"
7
+ version = "0.1.102"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.100"
1
+ __version__ = version = "0.1.102"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -0,0 +1,91 @@
1
+ """Content-addressed S3 blob store.
2
+
3
+ For offloading large row payloads (raw emails, documents, exports) out of
4
+ Postgres while the row keeps only the content hash. Keys are derived from
5
+ the sha256 of the content, so storage is idempotent and deduplicating by
6
+ construction: the same bytes always land on the same key, and a re-upload
7
+ of an existing object is skipped.
8
+
9
+ store = BlobStore("my-bucket", prefix="demo/gmail-raw", suffix=".eml")
10
+ sha, key = store.put_bytes(raw) # no-op if already stored
11
+ raw = store.get_bytes(sha) # or get_bytes(key)
12
+ url = store.presigned_get(sha) # hand-off to a browser
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ from typing import Optional, Tuple
19
+
20
+
21
+ class BlobStore:
22
+ def __init__(
23
+ self,
24
+ bucket: str,
25
+ prefix: str = "",
26
+ suffix: str = "",
27
+ region: Optional[str] = None,
28
+ client=None,
29
+ ):
30
+ if not bucket:
31
+ raise ValueError("BlobStore requires a bucket name")
32
+ self.bucket = bucket
33
+ self.prefix = prefix.strip("/")
34
+ self.suffix = suffix
35
+ self.region = region
36
+ self._client = client
37
+
38
+ @property
39
+ def client(self):
40
+ if self._client is None:
41
+ import boto3
42
+
43
+ kwargs = {"region_name": self.region} if self.region else {}
44
+ self._client = boto3.client("s3", **kwargs)
45
+ return self._client
46
+
47
+ def key_for(self, sha256_hex: str) -> str:
48
+ name = f"{sha256_hex}{self.suffix}"
49
+ return f"{self.prefix}/{name}" if self.prefix else name
50
+
51
+ def _resolve_key(self, sha_or_key: str) -> str:
52
+ # A bare 64-hex-char sha resolves through key_for; anything else is
53
+ # treated as a full key (lets callers store either form).
54
+ value = str(sha_or_key)
55
+ if len(value) == 64 and all(c in "0123456789abcdef" for c in value.lower()):
56
+ return self.key_for(value.lower())
57
+ return value
58
+
59
+ def exists(self, sha_or_key: str) -> bool:
60
+ try:
61
+ self.client.head_object(
62
+ Bucket=self.bucket, Key=self._resolve_key(sha_or_key)
63
+ )
64
+ return True
65
+ except Exception:
66
+ return False
67
+
68
+ def put_bytes(self, data: bytes, content_type: Optional[str] = None) -> Tuple[str, str]:
69
+ """Store ``data`` (idempotent); returns (sha256_hex, key)."""
70
+ payload = bytes(data or b"")
71
+ sha = hashlib.sha256(payload).hexdigest()
72
+ key = self.key_for(sha)
73
+ if not self.exists(key):
74
+ params = {"Bucket": self.bucket, "Key": key, "Body": payload}
75
+ if content_type:
76
+ params["ContentType"] = content_type
77
+ self.client.put_object(**params)
78
+ return sha, key
79
+
80
+ def get_bytes(self, sha_or_key: str) -> bytes:
81
+ response = self.client.get_object(
82
+ Bucket=self.bucket, Key=self._resolve_key(sha_or_key)
83
+ )
84
+ return response["Body"].read()
85
+
86
+ def presigned_get(self, sha_or_key: str, expires_seconds: int = 900) -> str:
87
+ return self.client.generate_presigned_url(
88
+ "get_object",
89
+ Params={"Bucket": self.bucket, "Key": self._resolve_key(sha_or_key)},
90
+ ExpiresIn=expires_seconds,
91
+ )
@@ -15,7 +15,7 @@ import re
15
15
  import xml.etree.ElementTree as ET
16
16
  from io import BytesIO, StringIO
17
17
 
18
- from velocity.aws import dirty_pipeline
18
+ from velocity.aws import dirty_pipeline, ssm_config
19
19
  from velocity.aws.handlers.exceptions import AlertError
20
20
  from velocity.db.exceptions import DbDataIntegrityError
21
21
  from velocity.misc import export
@@ -48,6 +48,85 @@ def _fk_blocked_delete_message(table, exc):
48
48
  )
49
49
 
50
50
 
51
+ _IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
52
+
53
+
54
+ def _bump_sys_id_sequence(tx, table):
55
+ """Keep the table's serial/identity sequence ahead of MAX(sys_id) so a
56
+ future auto-generated id can never collide with an explicitly chosen one
57
+ (renumber, Save-As)."""
58
+ if not _IDENTIFIER_RE.match(table or ""):
59
+ raise AlertError(f"'{table}' is not a valid table name.")
60
+ tx.execute(
61
+ f"""select setval(s.seq, greatest((select coalesce(max(sys_id), 1) from "{table}"), 1))
62
+ from (select pg_get_serial_sequence(%s, 'sys_id') as seq) s
63
+ where s.seq is not null""",
64
+ (table,),
65
+ )
66
+
67
+
68
+ def renumber_row(tx, table, old_sys_id, new_sys_id, overwrite=False):
69
+ """Assign a CHOSEN sys_id to an existing row (admin surgery).
70
+
71
+ Rules (deliberately simple):
72
+ - refused when any real foreign-key constraint references this table's
73
+ sys_id (renumbering under FKs is a cascade problem for another day);
74
+ - when the target sys_id already exists: refused unless ``overwrite`` —
75
+ which DELETES the occupying row first;
76
+ - the table's sys_id sequence is bumped past MAX(sys_id) afterwards so
77
+ future inserts can never collide with the chosen id.
78
+
79
+ Returns "renumbered" on success, or "exists" when the target id is
80
+ occupied and overwrite was not requested (the caller asks the user).
81
+ Raises AlertError for every refusal.
82
+ """
83
+ if not _IDENTIFIER_RE.match(table or ""):
84
+ raise AlertError(f"'{table}' is not a valid table name.")
85
+ try:
86
+ old_id = int(old_sys_id)
87
+ new_id = int(new_sys_id)
88
+ except (TypeError, ValueError):
89
+ raise AlertError("sys_id values must be integers.")
90
+ if new_id <= 0:
91
+ raise AlertError("The new sys_id must be a positive integer.")
92
+ if new_id == old_id:
93
+ raise AlertError("That is already this record's sys_id.")
94
+
95
+ if not tx.table(table).find(old_id):
96
+ raise AlertError(f"Record {old_id} was not found in {table}.")
97
+
98
+ referencing = tx.execute(
99
+ """select con.conrelid::regclass::text as referencing_table
100
+ from pg_constraint con
101
+ join pg_class rel on rel.oid = con.confrelid
102
+ join pg_namespace nsp on nsp.oid = rel.relnamespace
103
+ where con.contype = 'f' and nsp.nspname = 'public'
104
+ and rel.relname = %s
105
+ limit 5""",
106
+ (table,),
107
+ ).all()
108
+ if referencing:
109
+ tables = ", ".join(sorted({r["referencing_table"] for r in referencing}))
110
+ raise AlertError(
111
+ f"Renumbering is not allowed: {tables} reference(s) {table} "
112
+ "through a foreign key. (Denied for now — reassigning "
113
+ "references is not implemented.)"
114
+ )
115
+
116
+ if tx.table(table).find(new_id):
117
+ if not overwrite:
118
+ return "exists"
119
+ tx.execute(f'DELETE FROM "{table}" WHERE sys_id = %s', (new_id,))
120
+
121
+ tx.execute(
122
+ f'UPDATE "{table}" SET sys_id = %s WHERE sys_id = %s',
123
+ (new_id, old_id),
124
+ )
125
+
126
+ _bump_sys_id_sequence(tx, table)
127
+ return "renumbered"
128
+
129
+
51
130
  class DataServiceMixin:
52
131
  """
53
132
  Mixin providing generic CRUD operations for Lambda handlers.
@@ -77,6 +156,12 @@ class DataServiceMixin:
77
156
  # which DO run hooks — remain the supported path for them.
78
157
  restricted_direct_tables = frozenset()
79
158
 
159
+ # Ceiling on rows a single un-paginated `query` may materialize (None =
160
+ # no cap). Apps set the class attribute; the `QueryMaxRows` config key
161
+ # overrides it per environment. Promoted 2026-09-04 from BackOffice's
162
+ # full query_hook override, which existed largely to carry this.
163
+ query_max_rows = None
164
+
80
165
  # PostgreSQL type mappings for frontend display
81
166
  _pg_types = {
82
167
  "bool": "string",
@@ -94,6 +179,22 @@ class DataServiceMixin:
94
179
  "timestamp": "string",
95
180
  }
96
181
 
182
+ def _resolve_query_max_rows(self):
183
+ """Effective query row ceiling: `QueryMaxRows` config key first, then
184
+ the class attribute. Returns a positive int or None (no cap)."""
185
+ raw = None
186
+ try:
187
+ raw = ssm_config.getenv("QueryMaxRows", None)
188
+ except Exception:
189
+ raw = None
190
+ if raw in (None, ""):
191
+ raw = self.query_max_rows
192
+ try:
193
+ value = int(raw) if raw not in (None, "") else None
194
+ except (TypeError, ValueError):
195
+ return None
196
+ return value if value and value > 0 else None
197
+
97
198
  def _assert_direct_access_allowed(self, table):
98
199
  """Reject hook-bypassing direct access to a restricted table."""
99
200
  if table in self.restricted_direct_tables:
@@ -185,7 +286,7 @@ class DataServiceMixin:
185
286
  row = row.to_dict()
186
287
  return row
187
288
 
188
- def write_hook(self, tx, table, sys_id, incoming, context):
289
+ def write_hook(self, tx, table, sys_id, incoming, context, save_as=False):
189
290
  row = {}
190
291
  incoming.pop("sys_id", None)
191
292
  if sys_id == "@new":
@@ -223,9 +324,23 @@ class DataServiceMixin:
223
324
  # error rather than a phantom row inserted with a client-chosen PK.
224
325
  row = tx.table(table).find(sys_id)
225
326
  if not row:
226
- raise AlertError(
227
- f"Cannot update {table}: record {sys_id} no longer exists."
327
+ if not save_as:
328
+ raise AlertError(
329
+ f"Cannot update {table}: record {sys_id} no longer exists."
330
+ )
331
+ # Save-As: the user explicitly chose this id — claim it, then
332
+ # fall through to the shared update tail. before_new is skipped
333
+ # (the data comes from a populated form, not a blank create);
334
+ # after_new still fires so create-time defaults apply. The
335
+ # sequence is bumped past MAX(sys_id) so future auto-ids can
336
+ # never collide with the chosen one.
337
+ tx.table(table).insert({"sys_id": sys_id})
338
+ _bump_sys_id_sequence(tx, table)
339
+ row = tx.table(table).find(sys_id)
340
+ self._call_rwx_hook(
341
+ "after_new", "common", tx, table, sys_id, row, context
228
342
  )
343
+ self._call_rwx_hook("after_new", table, tx, table, sys_id, row, context)
229
344
  else:
230
345
  raise Exception("Object sys_id was not supplied on write operation.")
231
346
  row.update(incoming)
@@ -436,6 +551,23 @@ class DataServiceMixin:
436
551
  },
437
552
  }
438
553
 
554
+ # Optional ceiling on un-paginated queries: fetch one row past the cap
555
+ # so truncation is detectable and reportable rather than silent.
556
+ max_rows = self._resolve_query_max_rows()
557
+ requested_qty = params.get("qty")
558
+ try:
559
+ requested_qty = (
560
+ int(requested_qty) if requested_qty not in (None, "") else None
561
+ )
562
+ except (TypeError, ValueError):
563
+ requested_qty = None
564
+ cap_applied = bool(max_rows) and (
565
+ requested_qty is None or requested_qty > max_rows
566
+ )
567
+ if cap_applied:
568
+ params = dict(params)
569
+ params["qty"] = max_rows + 1
570
+
439
571
  result = tx.table(obj).select(**params)
440
572
 
441
573
  swallowed_error = getattr(tx, "_last_return_default_error", None)
@@ -514,6 +646,23 @@ class DataServiceMixin:
514
646
  },
515
647
  }
516
648
  )
649
+ if (
650
+ cap_applied
651
+ and isinstance(data.get("rows"), list)
652
+ and len(data["rows"]) > max_rows
653
+ ):
654
+ data["rows"] = data["rows"][:max_rows]
655
+ truncation = {"truncated": True, "maxRows": max_rows}
656
+ if isinstance(data.get("config"), dict):
657
+ data["config"]["truncated"] = truncation
658
+ context.response().toast(
659
+ f"Showing the first {max_rows:,} rows — refine the search "
660
+ "or paginate to see the rest.",
661
+ "warning",
662
+ )
663
+ else:
664
+ data["truncated"] = truncation
665
+
517
666
  if payload.get("count"):
518
667
  data["count"] = tx.table(payload["obj"]).count(
519
668
  where=params.get("where", None)
@@ -640,6 +789,29 @@ class DataServiceMixin:
640
789
  if not any(value is not None for value in incoming.values()):
641
790
  raise ValueError("Parameter 'object' cannot contain only None values")
642
791
 
792
+ # Save-As: the client chose the target sys_id. When that id is already
793
+ # taken, answer "exists" so the UI can confirm the overwrite instead of
794
+ # silently clobbering; with saveAsOverwrite the normal update path
795
+ # writes the full object onto the existing row. A free id is created
796
+ # by write_hook (save_as=True).
797
+ save_as = bool(payload.get("saveAs"))
798
+ if save_as:
799
+ target_sys_id = incoming.get("sys_id")
800
+ if target_sys_id not in (None, "", "@new"):
801
+ try:
802
+ chosen = int(target_sys_id)
803
+ except (TypeError, ValueError):
804
+ raise AlertError("Save-As ID must be a positive integer.")
805
+ if chosen <= 0:
806
+ raise AlertError("Save-As ID must be a positive integer.")
807
+ if not payload.get("saveAsOverwrite") and tx.table(table_name).find(
808
+ chosen
809
+ ):
810
+ context.response().set_body(
811
+ {"saveAsStatus": "exists", "sys_id": chosen}
812
+ )
813
+ return
814
+
643
815
  logger.debug(
644
816
  "Writing to table",
645
817
  extra={
@@ -656,6 +828,7 @@ class DataServiceMixin:
656
828
  incoming.get("sys_id"),
657
829
  incoming,
658
830
  context,
831
+ save_as=save_as,
659
832
  )
660
833
 
661
834
  if not row:
@@ -964,6 +1137,42 @@ class DataServiceMixin:
964
1137
  if not deleteList:
965
1138
  context.response().toast("No items were selected.", "warning")
966
1139
 
1140
+ def OnActionRenumberObject(self, tx, context):
1141
+ """Assign a chosen sys_id to one row.
1142
+
1143
+ Payload: tableName, sys_id (current), new_sys_id, overwrite (bool).
1144
+ Responds {status: "exists"} when the target id is occupied and
1145
+ overwrite was not requested; on success reloads the renumbered row
1146
+ into the form (load-object) and reports the change.
1147
+ """
1148
+ payload = context.payload()
1149
+ for field in ("tableName", "sys_id", "new_sys_id"):
1150
+ if field not in payload:
1151
+ raise ValueError(f"Missing required parameter '{field}' in payload")
1152
+ table = payload["tableName"]
1153
+ self._assert_direct_access_allowed(table)
1154
+
1155
+ result = renumber_row(
1156
+ tx,
1157
+ table,
1158
+ payload["sys_id"],
1159
+ payload["new_sys_id"],
1160
+ overwrite=bool(payload.get("overwrite")),
1161
+ )
1162
+ if result == "exists":
1163
+ context.response().set_body(
1164
+ {"status": "exists", "new_sys_id": payload["new_sys_id"]}
1165
+ )
1166
+ return
1167
+
1168
+ row = tx.table(table).find(int(payload["new_sys_id"]))
1169
+ context.response().set_body({"status": "renumbered", "object": dict(row)})
1170
+ context.response().load_object(dict(row))
1171
+ context.response().toast(
1172
+ f"Record renumbered: {payload['sys_id']} → {payload['new_sys_id']}.",
1173
+ "success",
1174
+ )
1175
+
967
1176
  def OnActionGetTables(self, tx, context):
968
1177
  """Get list of all tables in the database."""
969
1178
  context.response().set_body({"tables": tx.tables()})
@@ -36,9 +36,16 @@ class Response:
36
36
  """
37
37
  Finalize the response body as JSON and return the complete response dictionary.
38
38
 
39
+ When a raw body has been set via :meth:`set_raw_body`, it is returned
40
+ verbatim (no JSON encoding, no actions envelope) — the escape hatch
41
+ for non-JSON responses like TwiML or plain text.
42
+
39
43
  Returns:
40
- Dict[str, Any]: The complete HTTP response with headers, status code, and JSON body.
44
+ Dict[str, Any]: The complete HTTP response with headers, status code, and body.
41
45
  """
46
+ if getattr(self, "_raw_body", None) is not None:
47
+ self.raw["body"] = self._raw_body
48
+ return self.raw
42
49
  if self.actions:
43
50
  self.body["actions"] = self.actions
44
51
  if self.jobs.events:
@@ -46,6 +53,25 @@ class Response:
46
53
  self.raw["body"] = to_json(self.body)
47
54
  return self.raw
48
55
 
56
+ def set_raw_body(self, body: str, content_type: str = "text/plain") -> "Response":
57
+ """
58
+ Send a non-JSON body verbatim (e.g. TwiML, plain text, XML).
59
+
60
+ Bypasses the JSON envelope entirely: render() returns this body
61
+ unencoded and skips actions/jobs. Added 2026-09-04 — apps previously
62
+ had to override serve() to do this.
63
+
64
+ Args:
65
+ body (str): The exact body string to return.
66
+ content_type (str): Content-Type header value. Defaults to "text/plain".
67
+
68
+ Returns:
69
+ Response: The current Response object, allowing method chaining.
70
+ """
71
+ self._raw_body = body
72
+ self.raw["headers"]["Content-Type"] = content_type
73
+ return self
74
+
49
75
  def alert(self, message: str, title: str = "Notification") -> "Response":
50
76
  """
51
77
  Add an alert action to the response.
@@ -122,6 +122,15 @@ def get_stage(default: Optional[str] = None) -> Optional[str]:
122
122
  return _first_nonempty_env(*_STAGE_ENV_KEYS) or default
123
123
 
124
124
 
125
+ def is_production() -> bool:
126
+ """True when the resolved stage is the production branch.
127
+
128
+ The missing primitive behind six per-app ``runtime_config`` copies: apps
129
+ kept re-wrapping :func:`get_stage` just to ask this one question.
130
+ """
131
+ return (get_stage() or "").strip().lower() == "production"
132
+
133
+
125
134
  def get_region(default: Optional[str] = "us-east-1") -> Optional[str]:
126
135
  return _first_nonempty_env("AWS_REGION", "REGION", "USER_REGION") or default
127
136
 
@@ -14,6 +14,8 @@ Key Features:
14
14
  - Authorization with later capture support
15
15
  """
16
16
 
17
+ import time
18
+
17
19
  import braintree
18
20
  from typing import Dict, Any, Optional
19
21
  from datetime import datetime, timezone, timedelta
@@ -66,6 +68,33 @@ class BraintreeAdapter(PaymentProcessorAdapter):
66
68
  self.environment = environment_name
67
69
  self.platform = config.get("platform")
68
70
 
71
+ def generate_client_token(
72
+ self,
73
+ customer_id: Optional[str] = None,
74
+ attempts: int = 3,
75
+ backoff_seconds: float = 0.25,
76
+ ) -> str:
77
+ """Generate a Braintree client token, retrying transient failures.
78
+
79
+ The retry policy lives here, once — apps were hand-rolling identical
80
+ backoff loops around raw ``client_token.generate()`` calls (promoted
81
+ 2026-09-04 from DonateMain / BackOffice copies).
82
+ """
83
+ params: Dict[str, Any] = {"customer_id": customer_id} if customer_id else {}
84
+ last_exc: Optional[Exception] = None
85
+ for attempt in range(max(1, attempts)):
86
+ try:
87
+ return self.gateway.client_token.generate(params)
88
+ except Exception as exc:
89
+ last_exc = exc
90
+ if attempt < attempts - 1:
91
+ time.sleep(backoff_seconds * (attempt + 1))
92
+ raise ProcessorError(
93
+ "braintree",
94
+ "client_token_unavailable",
95
+ f"Client token unavailable after {attempts} attempts: {last_exc}",
96
+ ) from last_exc
97
+
69
98
  # ========================================================================
70
99
  # ACCOUNT MANAGEMENT
71
100
  # ========================================================================
@@ -853,6 +853,155 @@ class StripeAdapter(PaymentProcessorAdapter):
853
853
  metadata={"charge_id": processor_charge_id},
854
854
  )
855
855
 
856
+ # ========================================================================
857
+ # CHECKOUT SESSIONS (hosted payment page)
858
+ # ========================================================================
859
+
860
+ def create_checkout_session(
861
+ self, tx, session_data: Dict[str, Any]
862
+ ) -> Dict[str, Any]:
863
+ """
864
+ Create a hosted Stripe Checkout Session for a one-off payment.
865
+
866
+ The buyer is redirected to Stripe's hosted page, so the application
867
+ never touches card data (minimal PCI scope). Works on a direct
868
+ account by default; pass ``processor_account_id`` (and optionally
869
+ ``application_fee_amount``) for a Connect destination charge.
870
+
871
+ Args:
872
+ tx: Database transaction (reserved for mirror hooks; may be None)
873
+ session_data: Dictionary containing:
874
+ - amount: Amount in cents (required)
875
+ - name: Line-item name shown on the Stripe page (required)
876
+ - success_url: Redirect after payment (required; include
877
+ ``{CHECKOUT_SESSION_ID}`` to receive the session id back)
878
+ - cancel_url: Redirect when the buyer backs out (required)
879
+ - currency: default 'usd'
880
+ - customer_email: prefills the Stripe page (optional)
881
+ - metadata: dict stamped on session AND payment intent
882
+ - processor_account_id / application_fee_amount: Connect only
883
+ - idempotency_key: collapses double-submits (optional)
884
+
885
+ Returns:
886
+ {"success": True, "session_id", "url", "status", "payment_status"}
887
+ """
888
+ del tx # no mirror writes at create time; nothing has settled yet
889
+ try:
890
+ metadata = dict(session_data.get("metadata") or {})
891
+ metadata.setdefault("environment", self.environment)
892
+ if self.platform:
893
+ metadata.setdefault("platform", str(self.platform))
894
+
895
+ params: Dict[str, Any] = {
896
+ "mode": "payment",
897
+ "line_items": [
898
+ {
899
+ "quantity": 1,
900
+ "price_data": {
901
+ "currency": session_data.get("currency", "usd"),
902
+ "unit_amount": int(session_data["amount"]),
903
+ "product_data": {"name": session_data["name"]},
904
+ },
905
+ }
906
+ ],
907
+ "success_url": session_data["success_url"],
908
+ "cancel_url": session_data["cancel_url"],
909
+ "metadata": metadata,
910
+ "payment_intent_data": {"metadata": metadata},
911
+ "api_key": self._api_key,
912
+ }
913
+ if session_data.get("customer_email"):
914
+ params["customer_email"] = session_data["customer_email"]
915
+ processor_account_id = session_data.get("processor_account_id")
916
+ if processor_account_id:
917
+ transfer: Dict[str, Any] = {
918
+ "transfer_data": {"destination": processor_account_id}
919
+ }
920
+ fee = session_data.get("application_fee_amount")
921
+ if fee is not None:
922
+ transfer["application_fee_amount"] = int(fee)
923
+ params["payment_intent_data"] = {
924
+ **params["payment_intent_data"],
925
+ **transfer,
926
+ }
927
+ idempotency_key = session_data.get("idempotency_key")
928
+ if idempotency_key:
929
+ params["idempotency_key"] = str(idempotency_key)
930
+
931
+ session = stripe.checkout.Session.create(**params)
932
+ return {
933
+ "success": True,
934
+ "session_id": session.id,
935
+ "url": session.url,
936
+ "status": session.status,
937
+ "payment_status": session.payment_status,
938
+ }
939
+ except KeyError as e:
940
+ raise ProcessorError(
941
+ processor="stripe",
942
+ error_code="missing_field",
943
+ error_message=f"checkout session requires {e}",
944
+ metadata={},
945
+ )
946
+ except stripe.error.StripeError as e:
947
+ raise ProcessorError(
948
+ processor="stripe",
949
+ error_code=getattr(e, "code", None) or "unknown",
950
+ error_message=str(e),
951
+ metadata={},
952
+ )
953
+
954
+ def retrieve_checkout_session(self, tx, session_id: str) -> Dict[str, Any]:
955
+ """
956
+ Retrieve a Checkout Session and report whether it is paid.
957
+
958
+ Server-side verification for the success redirect: the client hands
959
+ back only the session id; the truth (``payment_status``) comes from
960
+ Stripe. A settled session's PaymentIntent artifacts are mirrored
961
+ when a transaction is supplied.
962
+
963
+ Returns:
964
+ {"success": True, "session_id", "status", "payment_status",
965
+ "paid": bool, "amount_total", "currency",
966
+ "payment_intent_id", "customer_email", "metadata"}
967
+ """
968
+ try:
969
+ # latest_charge stays an ID on purpose: the mirror helper
970
+ # re-retrieves it with balance_transaction expanded (real fees).
971
+ session = stripe.checkout.Session.retrieve(
972
+ session_id,
973
+ expand=["payment_intent"],
974
+ api_key=self._api_key,
975
+ )
976
+ payment_intent = session.payment_intent
977
+ paid = session.payment_status == "paid"
978
+ if paid and tx is not None and payment_intent is not None:
979
+ self._mirror_payment_intent_artifacts(
980
+ tx,
981
+ payment_intent,
982
+ source="api:stripe_adapter.retrieve_checkout_session",
983
+ )
984
+ details = session.get("customer_details") or {}
985
+ return {
986
+ "success": True,
987
+ "session_id": session.id,
988
+ "status": session.status,
989
+ "payment_status": session.payment_status,
990
+ "paid": paid,
991
+ "amount_total": session.amount_total,
992
+ "currency": session.currency,
993
+ "payment_intent_id": getattr(payment_intent, "id", payment_intent),
994
+ "customer_email": session.get("customer_email") or details.get("email"),
995
+ "metadata": dict(session.metadata or {}),
996
+ }
997
+ except stripe.error.StripeError as e:
998
+ raise ProcessorError(
999
+ processor="stripe",
1000
+ error_code=getattr(e, "code", None) or "unknown",
1001
+ error_message=str(e),
1002
+ metadata={"session_id": session_id},
1003
+ )
1004
+
856
1005
  # ========================================================================
857
1006
  # UTILITY METHODS
858
1007
  # ========================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.100
3
+ Version: 0.1.102
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