velocity-python 0.1.101__tar.gz → 0.1.103__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.101 → velocity_python-0.1.103}/PKG-INFO +1 -1
  2. {velocity_python-0.1.101 → velocity_python-0.1.103}/pyproject.toml +1 -1
  3. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/mixins/data_service.py +257 -4
  5. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/response.py +27 -1
  6. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/ssm_config.py +9 -0
  7. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/braintree_adapter.py +29 -0
  8. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity_python.egg-info/PKG-INFO +1 -1
  9. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity_python.egg-info/SOURCES.txt +4 -0
  10. velocity_python-0.1.103/tests/test_data_service_query_cap.py +115 -0
  11. velocity_python-0.1.103/tests/test_data_service_renumber.py +140 -0
  12. velocity_python-0.1.103/tests/test_data_service_save_as.py +190 -0
  13. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_payment_braintree_adapter.py +38 -0
  14. velocity_python-0.1.103/tests/test_response.py +32 -0
  15. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_ssm_config.py +13 -0
  16. {velocity_python-0.1.101 → velocity_python-0.1.103}/LICENSE +0 -0
  17. {velocity_python-0.1.101 → velocity_python-0.1.103}/README.md +0 -0
  18. {velocity_python-0.1.101 → velocity_python-0.1.103}/setup.cfg +0 -0
  19. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/__init__.py +0 -0
  20. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/amplify.py +0 -0
  21. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/amplify_build.py +0 -0
  22. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/assets/__init__.py +0 -0
  23. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/assets/backfill.py +0 -0
  24. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/assets/indexing.py +0 -0
  25. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/assets/references.py +0 -0
  26. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/assets/service.py +0 -0
  27. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/assets/usage_index.py +0 -0
  28. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/blobs.py +0 -0
  29. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/deploy/__init__.py +0 -0
  30. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/deploy/config.py +0 -0
  31. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/deploy/operations.py +0 -0
  32. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/deploy/policies.py +0 -0
  33. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/deploy/reconcile.py +0 -0
  34. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/deploy/targets.py +0 -0
  35. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/dirty_pipeline.py +0 -0
  36. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/email.py +0 -0
  37. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/__init__.py +0 -0
  38. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/base_handler.py +0 -0
  39. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/context.py +0 -0
  40. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/context_factory.py +0 -0
  41. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/exceptions.py +0 -0
  42. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  43. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/masquerade.py +0 -0
  44. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  45. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  46. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  47. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/perf.py +0 -0
  48. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  49. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/job_results.py +0 -0
  50. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/s3.py +0 -0
  51. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/sms.py +0 -0
  52. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/__init__.py +0 -0
  53. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  54. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  55. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  56. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  57. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  58. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_email.py +0 -0
  59. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  60. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_response.py +0 -0
  61. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/aws/tests/test_sms.py +0 -0
  62. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/__init__.py +0 -0
  63. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/__init__.py +0 -0
  64. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/async_support.py +0 -0
  65. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/column.py +0 -0
  66. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/database.py +0 -0
  67. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/decorators.py +0 -0
  68. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/engine.py +0 -0
  69. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/jsonproxy.py +0 -0
  70. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/result.py +0 -0
  71. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/row.py +0 -0
  72. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/sequence.py +0 -0
  73. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/table.py +0 -0
  74. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/transaction.py +0 -0
  75. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/core/view.py +0 -0
  76. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/exceptions.py +0 -0
  77. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/maintenance.py +0 -0
  78. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/migrations.py +0 -0
  79. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/__init__.py +0 -0
  80. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/base/__init__.py +0 -0
  81. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/base/initializer.py +0 -0
  82. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/base/operators.py +0 -0
  83. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/base/sql.py +0 -0
  84. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/base/types.py +0 -0
  85. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/mysql/__init__.py +0 -0
  86. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/mysql/operators.py +0 -0
  87. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/mysql/reserved.py +0 -0
  88. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/mysql/sql.py +0 -0
  89. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/mysql/types.py +0 -0
  90. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/postgres/__init__.py +0 -0
  91. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/postgres/operators.py +0 -0
  92. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/postgres/reserved.py +0 -0
  93. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/postgres/sql.py +0 -0
  94. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/postgres/types.py +0 -0
  95. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  96. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlite/operators.py +0 -0
  97. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  98. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlite/sql.py +0 -0
  99. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlite/types.py +0 -0
  100. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  101. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  102. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  103. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  104. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/sqlserver/types.py +0 -0
  105. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/servers/tablehelper.py +0 -0
  106. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/__init__.py +0 -0
  107. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/common_db_test.py +0 -0
  108. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/__init__.py +0 -0
  109. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/common.py +0 -0
  110. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/conftest.py +0 -0
  111. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_column.py +0 -0
  112. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  113. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_database.py +0 -0
  114. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  115. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  116. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  117. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_result.py +0 -0
  118. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_row.py +0 -0
  119. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  120. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  121. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  122. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  123. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  124. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_table.py +0 -0
  125. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  126. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  127. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/sql/__init__.py +0 -0
  128. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/sql/common.py +0 -0
  129. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  130. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  131. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  132. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_db_utils.py +0 -0
  133. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_postgres.py +0 -0
  134. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  135. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  136. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_result_caching.py +0 -0
  137. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  138. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  139. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  140. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  141. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_sql_builder.py +0 -0
  142. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_tablehelper.py +0 -0
  143. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/tests/test_view_helper.py +0 -0
  144. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/db/utils.py +0 -0
  145. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/logging.py +0 -0
  146. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/__init__.py +0 -0
  147. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/conv/__init__.py +0 -0
  148. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/conv/iconv.py +0 -0
  149. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/conv/oconv.py +0 -0
  150. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/db.py +0 -0
  151. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/export.py +0 -0
  152. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/format.py +0 -0
  153. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/mail.py +0 -0
  154. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/merge.py +0 -0
  155. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/pdf.py +0 -0
  156. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/__init__.py +0 -0
  157. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_db.py +0 -0
  158. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_fix.py +0 -0
  159. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_format.py +0 -0
  160. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_iconv.py +0 -0
  161. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_mail_send.py +0 -0
  162. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_merge.py +0 -0
  163. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_oconv.py +0 -0
  164. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_original_error.py +0 -0
  165. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tests/test_timer.py +0 -0
  166. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/timer.py +0 -0
  167. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/misc/tools.py +0 -0
  168. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/__init__.py +0 -0
  169. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/core/__init__.py +0 -0
  170. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/core/capabilities.py +0 -0
  171. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/core/store.py +0 -0
  172. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/core/where.py +0 -0
  173. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/exceptions.py +0 -0
  174. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/__init__.py +0 -0
  175. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/base/__init__.py +0 -0
  176. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  177. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  178. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  179. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  180. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  181. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  182. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  183. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  184. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  185. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  186. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  187. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  188. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/__init__.py +0 -0
  189. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/authorizenet_adapter.py +0 -0
  190. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/authorizenet_mirror.py +0 -0
  191. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/base_adapter.py +0 -0
  192. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/braintree_mirror.py +0 -0
  193. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/charge_rules.py +0 -0
  194. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/stripe_adapter.py +0 -0
  195. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity/payment/stripe_mirror.py +0 -0
  196. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  197. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity_python.egg-info/entry_points.txt +0 -0
  198. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity_python.egg-info/requires.txt +0 -0
  199. {velocity_python-0.1.101 → velocity_python-0.1.103}/src/velocity_python.egg-info/top_level.txt +0 -0
  200. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_activity_tracking_aborted_tx.py +0 -0
  201. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_amplify_build.py +0 -0
  202. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_asset_indexing.py +0 -0
  203. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_asset_references.py +0 -0
  204. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_assets_service.py +0 -0
  205. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_async_support.py +0 -0
  206. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_batch_operations.py +0 -0
  207. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_blob_store.py +0 -0
  208. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_column_tx_arg.py +0 -0
  209. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_concurrency_safety.py +0 -0
  210. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_connection_pool.py +0 -0
  211. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_connection_resilience.py +0 -0
  212. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_context_job_descriptions.py +0 -0
  213. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_db_credentials_ssm_cascade.py +0 -0
  214. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_db_maintenance.py +0 -0
  215. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_decorators.py +0 -0
  216. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_delete_hook_fk_blocked.py +0 -0
  217. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_dirty_pipeline_fast_path.py +0 -0
  218. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_email_processing.py +0 -0
  219. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_enqueue_send_failures.py +0 -0
  220. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_get_cognito_user_provider.py +0 -0
  221. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_http_handler_rollback.py +0 -0
  222. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_iconv_money_to_cents.py +0 -0
  223. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_identifier_injection_guard.py +0 -0
  224. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_job_handler.py +0 -0
  225. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_job_results.py +0 -0
  226. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_json_columns.py +0 -0
  227. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_jsonb_dict_adapter.py +0 -0
  228. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_lambda_handler.py +0 -0
  229. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_lambda_handler_auth.py +0 -0
  230. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_lambda_handler_masquerade.py +0 -0
  231. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_masquerade_grant.py +0 -0
  232. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_mixins_import.py +0 -0
  233. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_n_plus_one.py +0 -0
  234. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_nosql_dynamodb.py +0 -0
  235. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_nosql_memory.py +0 -0
  236. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_nosql_mongodb.py +0 -0
  237. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_nosql_redis.py +0 -0
  238. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_nosql_translation.py +0 -0
  239. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_nosql_where.py +0 -0
  240. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_observability.py +0 -0
  241. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_on_commit.py +0 -0
  242. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_payment_authorizenet_adapter.py +0 -0
  243. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_payment_braintree_mirror.py +0 -0
  244. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_payment_mirror_empty_table.py +0 -0
  245. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_payment_profile_sorting.py +0 -0
  246. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_payment_stripe_adapter.py +0 -0
  247. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_pdf.py +0 -0
  248. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_prepared_statements.py +0 -0
  249. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_psycopg3_upgrade.py +0 -0
  250. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_query_cache.py +0 -0
  251. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_query_missing_table_unlocked.py +0 -0
  252. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_restricted_direct_tables.py +0 -0
  253. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_retry_side_effect_guard.py +0 -0
  254. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_return_default_safety.py +0 -0
  255. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_row_batch_update.py +0 -0
  256. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_row_cache_staleness.py +0 -0
  257. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_schema_migrations.py +0 -0
  258. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_security_hardening.py +0 -0
  259. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_server_cursor.py +0 -0
  260. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_single_autocommit_safety.py +0 -0
  261. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_spreadsheet_functions.py +0 -0
  262. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_sqlite_backend.py +0 -0
  263. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_sqs_per_record_transactions.py +0 -0
  264. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_store_user_data.py +0 -0
  265. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  266. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_table_alter.py +0 -0
  267. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_transaction_class_wrapping.py +0 -0
  268. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_transaction_commit_and_ownership.py +0 -0
  269. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_transaction_edge_cases.py +0 -0
  270. {velocity_python-0.1.101 → velocity_python-0.1.103}/tests/test_where_clause_validation.py +0 -0
  271. {velocity_python-0.1.101 → velocity_python-0.1.103}/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.101
3
+ Version: 0.1.103
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.101"
7
+ version = "0.1.103"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.101"
1
+ __version__ = version = "0.1.103"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -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,129 @@ 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 _sys_id_fk_references(tx, table):
69
+ """Foreign keys pointing AT this table's sys_id, as
70
+ (referencing_table, referencing_column) pairs. A multi-column FK yields
71
+ referencing_column None (callers treat it conservatively)."""
72
+ return tx.execute(
73
+ """select con.conrelid::regclass::text as referencing_table,
74
+ case when array_length(con.conkey, 1) = 1
75
+ then (select att.attname from pg_attribute att
76
+ where att.attrelid = con.conrelid
77
+ and att.attnum = con.conkey[1])
78
+ end as referencing_column
79
+ from pg_constraint con
80
+ join pg_class rel on rel.oid = con.confrelid
81
+ join pg_namespace nsp on nsp.oid = rel.relnamespace
82
+ where con.contype = 'f' and nsp.nspname = 'public'
83
+ and rel.relname = %s
84
+ and (select att.attname from pg_attribute att
85
+ where att.attrelid = con.confrelid
86
+ and att.attnum = con.confkey[1]) = 'sys_id'""",
87
+ (table,),
88
+ ).all()
89
+
90
+
91
+ def _count_fk_rows(tx, references, sys_id):
92
+ """Rows that actually reference ``sys_id`` through the given FK pairs.
93
+ Returns human-readable "table (n row(s))" descriptions, or raises for an
94
+ FK shape too odd to check safely (multi-column, unquotable names)."""
95
+ blocked = []
96
+ for ref in references:
97
+ ref_table = ref.get("referencing_table") or ""
98
+ ref_column = ref.get("referencing_column") or ""
99
+ if not (_IDENTIFIER_RE.match(ref_table) and _IDENTIFIER_RE.match(ref_column)):
100
+ raise AlertError(
101
+ f"Renumbering is not allowed: {ref_table or 'a table'} references "
102
+ f"this table through a foreign key this tool cannot inspect."
103
+ )
104
+ count = tx.execute(
105
+ f'select count(*) as n from "{ref_table}" where "{ref_column}" = %s',
106
+ (sys_id,),
107
+ ).scalar()
108
+ if count:
109
+ blocked.append(f"{ref_table} ({count} row(s))")
110
+ return blocked
111
+
112
+
113
+ def renumber_row(tx, table, old_sys_id, new_sys_id, overwrite=False):
114
+ """Assign a CHOSEN sys_id to an existing row (admin surgery).
115
+
116
+ Rules (deliberately simple):
117
+ - a foreign-key CONSTRAINT on this table's sys_id is fine; what refuses
118
+ the change is actual referencing ROWS — rows pointing at the old id
119
+ (reassigning references is not implemented), or, on overwrite, rows
120
+ pointing at the occupant that would be deleted;
121
+ - when the target sys_id already exists: refused unless ``overwrite`` —
122
+ which DELETES the occupying row first;
123
+ - the table's sys_id sequence is bumped past MAX(sys_id) afterwards so
124
+ future inserts can never collide with the chosen id.
125
+
126
+ Returns "renumbered" on success, or "exists" when the target id is
127
+ occupied and overwrite was not requested (the caller asks the user).
128
+ Raises AlertError for every refusal.
129
+ """
130
+ if not _IDENTIFIER_RE.match(table or ""):
131
+ raise AlertError(f"'{table}' is not a valid table name.")
132
+ try:
133
+ old_id = int(old_sys_id)
134
+ new_id = int(new_sys_id)
135
+ except (TypeError, ValueError):
136
+ raise AlertError("sys_id values must be integers.")
137
+ if new_id <= 0:
138
+ raise AlertError("The new sys_id must be a positive integer.")
139
+ if new_id == old_id:
140
+ raise AlertError("That is already this record's sys_id.")
141
+
142
+ if not tx.table(table).find(old_id):
143
+ raise AlertError(f"Record {old_id} was not found in {table}.")
144
+
145
+ references = _sys_id_fk_references(tx, table)
146
+ blocked = _count_fk_rows(tx, references, old_id)
147
+ if blocked:
148
+ raise AlertError(
149
+ f"Renumbering is not allowed: {', '.join(sorted(blocked))} still "
150
+ f"reference {table} {old_id}. (Reassigning references is not "
151
+ "implemented — repoint or remove those rows first.)"
152
+ )
153
+
154
+ if tx.table(table).find(new_id):
155
+ if not overwrite:
156
+ return "exists"
157
+ occupied = _count_fk_rows(tx, references, new_id)
158
+ if occupied:
159
+ raise AlertError(
160
+ f"Cannot overwrite {table} {new_id}: {', '.join(sorted(occupied))} "
161
+ "still reference it."
162
+ )
163
+ tx.execute(f'DELETE FROM "{table}" WHERE sys_id = %s', (new_id,))
164
+
165
+ tx.execute(
166
+ f'UPDATE "{table}" SET sys_id = %s WHERE sys_id = %s',
167
+ (new_id, old_id),
168
+ )
169
+
170
+ _bump_sys_id_sequence(tx, table)
171
+ return "renumbered"
172
+
173
+
51
174
  class DataServiceMixin:
52
175
  """
53
176
  Mixin providing generic CRUD operations for Lambda handlers.
@@ -77,6 +200,12 @@ class DataServiceMixin:
77
200
  # which DO run hooks — remain the supported path for them.
78
201
  restricted_direct_tables = frozenset()
79
202
 
203
+ # Ceiling on rows a single un-paginated `query` may materialize (None =
204
+ # no cap). Apps set the class attribute; the `QueryMaxRows` config key
205
+ # overrides it per environment. Promoted 2026-09-04 from BackOffice's
206
+ # full query_hook override, which existed largely to carry this.
207
+ query_max_rows = None
208
+
80
209
  # PostgreSQL type mappings for frontend display
81
210
  _pg_types = {
82
211
  "bool": "string",
@@ -94,6 +223,22 @@ class DataServiceMixin:
94
223
  "timestamp": "string",
95
224
  }
96
225
 
226
+ def _resolve_query_max_rows(self):
227
+ """Effective query row ceiling: `QueryMaxRows` config key first, then
228
+ the class attribute. Returns a positive int or None (no cap)."""
229
+ raw = None
230
+ try:
231
+ raw = ssm_config.getenv("QueryMaxRows", None)
232
+ except Exception:
233
+ raw = None
234
+ if raw in (None, ""):
235
+ raw = self.query_max_rows
236
+ try:
237
+ value = int(raw) if raw not in (None, "") else None
238
+ except (TypeError, ValueError):
239
+ return None
240
+ return value if value and value > 0 else None
241
+
97
242
  def _assert_direct_access_allowed(self, table):
98
243
  """Reject hook-bypassing direct access to a restricted table."""
99
244
  if table in self.restricted_direct_tables:
@@ -185,7 +330,7 @@ class DataServiceMixin:
185
330
  row = row.to_dict()
186
331
  return row
187
332
 
188
- def write_hook(self, tx, table, sys_id, incoming, context):
333
+ def write_hook(self, tx, table, sys_id, incoming, context, save_as=False):
189
334
  row = {}
190
335
  incoming.pop("sys_id", None)
191
336
  if sys_id == "@new":
@@ -223,9 +368,23 @@ class DataServiceMixin:
223
368
  # error rather than a phantom row inserted with a client-chosen PK.
224
369
  row = tx.table(table).find(sys_id)
225
370
  if not row:
226
- raise AlertError(
227
- f"Cannot update {table}: record {sys_id} no longer exists."
371
+ if not save_as:
372
+ raise AlertError(
373
+ f"Cannot update {table}: record {sys_id} no longer exists."
374
+ )
375
+ # Save-As: the user explicitly chose this id — claim it, then
376
+ # fall through to the shared update tail. before_new is skipped
377
+ # (the data comes from a populated form, not a blank create);
378
+ # after_new still fires so create-time defaults apply. The
379
+ # sequence is bumped past MAX(sys_id) so future auto-ids can
380
+ # never collide with the chosen one.
381
+ tx.table(table).insert({"sys_id": sys_id})
382
+ _bump_sys_id_sequence(tx, table)
383
+ row = tx.table(table).find(sys_id)
384
+ self._call_rwx_hook(
385
+ "after_new", "common", tx, table, sys_id, row, context
228
386
  )
387
+ self._call_rwx_hook("after_new", table, tx, table, sys_id, row, context)
229
388
  else:
230
389
  raise Exception("Object sys_id was not supplied on write operation.")
231
390
  row.update(incoming)
@@ -436,6 +595,23 @@ class DataServiceMixin:
436
595
  },
437
596
  }
438
597
 
598
+ # Optional ceiling on un-paginated queries: fetch one row past the cap
599
+ # so truncation is detectable and reportable rather than silent.
600
+ max_rows = self._resolve_query_max_rows()
601
+ requested_qty = params.get("qty")
602
+ try:
603
+ requested_qty = (
604
+ int(requested_qty) if requested_qty not in (None, "") else None
605
+ )
606
+ except (TypeError, ValueError):
607
+ requested_qty = None
608
+ cap_applied = bool(max_rows) and (
609
+ requested_qty is None or requested_qty > max_rows
610
+ )
611
+ if cap_applied:
612
+ params = dict(params)
613
+ params["qty"] = max_rows + 1
614
+
439
615
  result = tx.table(obj).select(**params)
440
616
 
441
617
  swallowed_error = getattr(tx, "_last_return_default_error", None)
@@ -514,6 +690,23 @@ class DataServiceMixin:
514
690
  },
515
691
  }
516
692
  )
693
+ if (
694
+ cap_applied
695
+ and isinstance(data.get("rows"), list)
696
+ and len(data["rows"]) > max_rows
697
+ ):
698
+ data["rows"] = data["rows"][:max_rows]
699
+ truncation = {"truncated": True, "maxRows": max_rows}
700
+ if isinstance(data.get("config"), dict):
701
+ data["config"]["truncated"] = truncation
702
+ context.response().toast(
703
+ f"Showing the first {max_rows:,} rows — refine the search "
704
+ "or paginate to see the rest.",
705
+ "warning",
706
+ )
707
+ else:
708
+ data["truncated"] = truncation
709
+
517
710
  if payload.get("count"):
518
711
  data["count"] = tx.table(payload["obj"]).count(
519
712
  where=params.get("where", None)
@@ -640,6 +833,29 @@ class DataServiceMixin:
640
833
  if not any(value is not None for value in incoming.values()):
641
834
  raise ValueError("Parameter 'object' cannot contain only None values")
642
835
 
836
+ # Save-As: the client chose the target sys_id. When that id is already
837
+ # taken, answer "exists" so the UI can confirm the overwrite instead of
838
+ # silently clobbering; with saveAsOverwrite the normal update path
839
+ # writes the full object onto the existing row. A free id is created
840
+ # by write_hook (save_as=True).
841
+ save_as = bool(payload.get("saveAs"))
842
+ if save_as:
843
+ target_sys_id = incoming.get("sys_id")
844
+ if target_sys_id not in (None, "", "@new"):
845
+ try:
846
+ chosen = int(target_sys_id)
847
+ except (TypeError, ValueError):
848
+ raise AlertError("Save-As ID must be a positive integer.")
849
+ if chosen <= 0:
850
+ raise AlertError("Save-As ID must be a positive integer.")
851
+ if not payload.get("saveAsOverwrite") and tx.table(table_name).find(
852
+ chosen
853
+ ):
854
+ context.response().set_body(
855
+ {"saveAsStatus": "exists", "sys_id": chosen}
856
+ )
857
+ return
858
+
643
859
  logger.debug(
644
860
  "Writing to table",
645
861
  extra={
@@ -656,6 +872,7 @@ class DataServiceMixin:
656
872
  incoming.get("sys_id"),
657
873
  incoming,
658
874
  context,
875
+ save_as=save_as,
659
876
  )
660
877
 
661
878
  if not row:
@@ -964,6 +1181,42 @@ class DataServiceMixin:
964
1181
  if not deleteList:
965
1182
  context.response().toast("No items were selected.", "warning")
966
1183
 
1184
+ def OnActionRenumberObject(self, tx, context):
1185
+ """Assign a chosen sys_id to one row.
1186
+
1187
+ Payload: tableName, sys_id (current), new_sys_id, overwrite (bool).
1188
+ Responds {status: "exists"} when the target id is occupied and
1189
+ overwrite was not requested; on success reloads the renumbered row
1190
+ into the form (load-object) and reports the change.
1191
+ """
1192
+ payload = context.payload()
1193
+ for field in ("tableName", "sys_id", "new_sys_id"):
1194
+ if field not in payload:
1195
+ raise ValueError(f"Missing required parameter '{field}' in payload")
1196
+ table = payload["tableName"]
1197
+ self._assert_direct_access_allowed(table)
1198
+
1199
+ result = renumber_row(
1200
+ tx,
1201
+ table,
1202
+ payload["sys_id"],
1203
+ payload["new_sys_id"],
1204
+ overwrite=bool(payload.get("overwrite")),
1205
+ )
1206
+ if result == "exists":
1207
+ context.response().set_body(
1208
+ {"status": "exists", "new_sys_id": payload["new_sys_id"]}
1209
+ )
1210
+ return
1211
+
1212
+ row = tx.table(table).find(int(payload["new_sys_id"]))
1213
+ context.response().set_body({"status": "renumbered", "object": dict(row)})
1214
+ context.response().load_object(dict(row))
1215
+ context.response().toast(
1216
+ f"Record renumbered: {payload['sys_id']} → {payload['new_sys_id']}.",
1217
+ "success",
1218
+ )
1219
+
967
1220
  def OnActionGetTables(self, tx, context):
968
1221
  """Get list of all tables in the database."""
969
1222
  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
  # ========================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.101
3
+ Version: 0.1.103
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
@@ -202,6 +202,9 @@ tests/test_concurrency_safety.py
202
202
  tests/test_connection_pool.py
203
203
  tests/test_connection_resilience.py
204
204
  tests/test_context_job_descriptions.py
205
+ tests/test_data_service_query_cap.py
206
+ tests/test_data_service_renumber.py
207
+ tests/test_data_service_save_as.py
205
208
  tests/test_db_credentials_ssm_cascade.py
206
209
  tests/test_db_maintenance.py
207
210
  tests/test_decorators.py
@@ -242,6 +245,7 @@ tests/test_prepared_statements.py
242
245
  tests/test_psycopg3_upgrade.py
243
246
  tests/test_query_cache.py
244
247
  tests/test_query_missing_table_unlocked.py
248
+ tests/test_response.py
245
249
  tests/test_restricted_direct_tables.py
246
250
  tests/test_retry_side_effect_guard.py
247
251
  tests/test_return_default_safety.py
@@ -0,0 +1,115 @@
1
+ """query_hook row ceiling: QueryMaxRows config / query_max_rows attribute,
2
+ fetch-one-extra truncation detection, and the no-cap default."""
3
+
4
+ import pytest
5
+
6
+ from velocity.aws.handlers.mixins.data_service import DataServiceMixin
7
+
8
+
9
+ class FakeResult:
10
+ def __init__(self, rows):
11
+ self._rows = rows
12
+ self.sql = "select 1"
13
+ self.headers = ["sys_id"]
14
+
15
+ def all(self):
16
+ return list(self._rows)
17
+
18
+ def as_list(self):
19
+ return self
20
+
21
+
22
+ class FakeTable:
23
+ def __init__(self, tx):
24
+ self.tx = tx
25
+
26
+ def select(self, **params):
27
+ self.tx.select_params = params
28
+ qty = params.get("qty")
29
+ rows = [{"sys_id": i} for i in range(1, self.tx.row_count + 1)]
30
+ if qty:
31
+ rows = rows[: int(qty)]
32
+ return FakeResult(rows)
33
+
34
+
35
+ class FakeTx:
36
+ def __init__(self, row_count):
37
+ self.row_count = row_count
38
+ self.select_params = None
39
+
40
+ def table(self, name):
41
+ return FakeTable(self)
42
+
43
+
44
+ class FakeResponse:
45
+ def __init__(self):
46
+ self.toasts = []
47
+
48
+ def toast(self, message, level=None):
49
+ self.toasts.append((message, level))
50
+
51
+
52
+ class FakeContext:
53
+ def __init__(self):
54
+ self._response = FakeResponse()
55
+
56
+ def response(self):
57
+ return self._response
58
+
59
+
60
+ class Handler(DataServiceMixin):
61
+ def _call_rwx_hook(self, *args, **kwargs):
62
+ pass
63
+
64
+
65
+ class CappedHandler(Handler):
66
+ query_max_rows = 3
67
+
68
+
69
+ @pytest.fixture(autouse=True)
70
+ def _no_env_cap(monkeypatch):
71
+ monkeypatch.delenv("QueryMaxRows", raising=False)
72
+
73
+
74
+ def test_resolver_uses_attribute_then_env(monkeypatch):
75
+ assert Handler()._resolve_query_max_rows() is None
76
+ assert CappedHandler()._resolve_query_max_rows() == 3
77
+ monkeypatch.setenv("QueryMaxRows", "7")
78
+ assert CappedHandler()._resolve_query_max_rows() == 7
79
+ monkeypatch.setenv("QueryMaxRows", "not-a-number")
80
+ assert CappedHandler()._resolve_query_max_rows() is None
81
+ monkeypatch.setenv("QueryMaxRows", "0")
82
+ assert CappedHandler()._resolve_query_max_rows() is None
83
+
84
+
85
+ def test_unpaginated_query_is_capped_and_reported():
86
+ tx = FakeTx(row_count=10)
87
+ context = FakeContext()
88
+ data = CappedHandler().query_hook(
89
+ tx, "studio", {"obj": "studio", "params": {}}, context
90
+ )
91
+ assert tx.select_params["qty"] == 4 # cap + 1 to detect truncation
92
+ assert len(data["rows"]) == 3
93
+ assert data["config"]["truncated"] == {"truncated": True, "maxRows": 3}
94
+ assert context.response().toasts and "first 3" in context.response().toasts[0][0]
95
+
96
+
97
+ def test_paginated_query_below_cap_is_untouched():
98
+ tx = FakeTx(row_count=10)
99
+ context = FakeContext()
100
+ data = CappedHandler().query_hook(
101
+ tx, "studio", {"obj": "studio", "params": {"qty": 2}}, context
102
+ )
103
+ assert tx.select_params["qty"] == 2
104
+ assert len(data["rows"]) == 2
105
+ assert "truncated" not in data["config"]
106
+ assert not context.response().toasts
107
+
108
+
109
+ def test_no_cap_by_default():
110
+ tx = FakeTx(row_count=10)
111
+ context = FakeContext()
112
+ data = Handler().query_hook(tx, "studio", {"obj": "studio", "params": {}}, context)
113
+ assert tx.select_params.get("qty") is None
114
+ assert len(data["rows"]) == 10
115
+ assert "truncated" not in data["config"]