velocity-python 0.1.9__tar.gz → 0.1.10__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 (185) hide show
  1. {velocity_python-0.1.9/src/velocity_python.egg-info → velocity_python-0.1.10}/PKG-INFO +1 -1
  2. {velocity_python-0.1.9 → velocity_python-0.1.10}/pyproject.toml +4 -1
  3. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.1.10/src/velocity/db/migrations.py +579 -0
  5. {velocity_python-0.1.9 → velocity_python-0.1.10/src/velocity_python.egg-info}/PKG-INFO +1 -1
  6. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity_python.egg-info/SOURCES.txt +3 -0
  7. velocity_python-0.1.10/src/velocity_python.egg-info/entry_points.txt +2 -0
  8. velocity_python-0.1.10/tests/test_schema_migrations.py +922 -0
  9. {velocity_python-0.1.9 → velocity_python-0.1.10}/LICENSE +0 -0
  10. {velocity_python-0.1.9 → velocity_python-0.1.10}/README.md +0 -0
  11. {velocity_python-0.1.9 → velocity_python-0.1.10}/setup.cfg +0 -0
  12. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/__init__.py +0 -0
  13. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/formbuilder/__init__.py +0 -0
  14. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/formbuilder/reshaper.py +0 -0
  15. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/invoices.py +0 -0
  16. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/orders.py +0 -0
  17. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/payments.py +0 -0
  18. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/purchase_orders.py +0 -0
  19. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/tests/__init__.py +0 -0
  20. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/tests/test_email_processing.py +0 -0
  21. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  22. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  23. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/validators/__init__.py +0 -0
  24. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/app/validators/formbuilder_template.py +0 -0
  25. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/__init__.py +0 -0
  26. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/amplify.py +0 -0
  27. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/amplify_build.py +0 -0
  28. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/__init__.py +0 -0
  29. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/base_handler.py +0 -0
  30. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/context.py +0 -0
  31. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/context_factory.py +0 -0
  32. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/exceptions.py +0 -0
  33. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  34. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  35. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  36. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  37. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/perf.py +0 -0
  38. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/response.py +0 -0
  39. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  40. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/tests/__init__.py +0 -0
  41. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  42. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  43. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/aws/tests/test_response.py +0 -0
  44. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/__init__.py +0 -0
  45. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/__init__.py +0 -0
  46. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/async_support.py +0 -0
  47. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/column.py +0 -0
  48. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/database.py +0 -0
  49. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/decorators.py +0 -0
  50. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/engine.py +0 -0
  51. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/result.py +0 -0
  52. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/row.py +0 -0
  53. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/sequence.py +0 -0
  54. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/table.py +0 -0
  55. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/transaction.py +0 -0
  56. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/core/view.py +0 -0
  57. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/exceptions.py +0 -0
  58. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/__init__.py +0 -0
  59. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/base/__init__.py +0 -0
  60. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/base/initializer.py +0 -0
  61. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/base/operators.py +0 -0
  62. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/base/sql.py +0 -0
  63. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/base/types.py +0 -0
  64. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/mysql/__init__.py +0 -0
  65. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/mysql/operators.py +0 -0
  66. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/mysql/reserved.py +0 -0
  67. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/mysql/sql.py +0 -0
  68. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/mysql/types.py +0 -0
  69. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/postgres/__init__.py +0 -0
  70. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/postgres/operators.py +0 -0
  71. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/postgres/reserved.py +0 -0
  72. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/postgres/sql.py +0 -0
  73. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/postgres/types.py +0 -0
  74. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  75. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlite/operators.py +0 -0
  76. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  77. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlite/sql.py +0 -0
  78. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlite/types.py +0 -0
  79. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  80. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  81. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  82. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  83. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/sqlserver/types.py +0 -0
  84. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/servers/tablehelper.py +0 -0
  85. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/__init__.py +0 -0
  86. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/common_db_test.py +0 -0
  87. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/__init__.py +0 -0
  88. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/common.py +0 -0
  89. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_column.py +0 -0
  90. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  91. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_database.py +0 -0
  92. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  93. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  94. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  95. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_result.py +0 -0
  96. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_row.py +0 -0
  97. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  98. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  99. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  100. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  101. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  102. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_table.py +0 -0
  103. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  104. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  105. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/sql/__init__.py +0 -0
  106. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/sql/common.py +0 -0
  107. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  108. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  109. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  110. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_db_utils.py +0 -0
  111. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_postgres.py +0 -0
  112. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  113. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  114. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_result_caching.py +0 -0
  115. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  116. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  117. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  118. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  119. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_sql_builder.py +0 -0
  120. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_tablehelper.py +0 -0
  121. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/tests/test_view_helper.py +0 -0
  122. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/db/utils.py +0 -0
  123. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/logging.py +0 -0
  124. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/__init__.py +0 -0
  125. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/conv/__init__.py +0 -0
  126. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/conv/iconv.py +0 -0
  127. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/conv/oconv.py +0 -0
  128. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/db.py +0 -0
  129. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/export.py +0 -0
  130. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/format.py +0 -0
  131. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/mail.py +0 -0
  132. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/merge.py +0 -0
  133. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/__init__.py +0 -0
  134. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_db.py +0 -0
  135. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_fix.py +0 -0
  136. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_format.py +0 -0
  137. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_iconv.py +0 -0
  138. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_merge.py +0 -0
  139. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_oconv.py +0 -0
  140. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_original_error.py +0 -0
  141. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tests/test_timer.py +0 -0
  142. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/timer.py +0 -0
  143. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/misc/tools.py +0 -0
  144. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/__init__.py +0 -0
  145. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/authorizenet_adapter.py +0 -0
  146. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/base_adapter.py +0 -0
  147. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/braintree_adapter.py +0 -0
  148. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/charge_rules.py +0 -0
  149. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/demo_profiles.py +0 -0
  150. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/profiles.py +0 -0
  151. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/router.py +0 -0
  152. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity/payment/stripe_adapter.py +0 -0
  153. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  154. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity_python.egg-info/requires.txt +0 -0
  155. {velocity_python-0.1.9 → velocity_python-0.1.10}/src/velocity_python.egg-info/top_level.txt +0 -0
  156. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_amplify_build.py +0 -0
  157. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_async_support.py +0 -0
  158. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_batch_operations.py +0 -0
  159. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_concurrency_safety.py +0 -0
  160. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_connection_pool.py +0 -0
  161. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_connection_resilience.py +0 -0
  162. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_decorators.py +0 -0
  163. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_formbuilder_reshaper.py +0 -0
  164. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_formbuilder_template_validator.py +0 -0
  165. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_iconv_money_to_cents.py +0 -0
  166. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_lambda_handler.py +0 -0
  167. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_lambda_handler_auth.py +0 -0
  168. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_mixins_import.py +0 -0
  169. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_n_plus_one.py +0 -0
  170. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_observability.py +0 -0
  171. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_payment_braintree_adapter.py +0 -0
  172. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_payment_demo_profiles.py +0 -0
  173. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_payment_profiles.py +0 -0
  174. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_payment_router.py +0 -0
  175. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_payment_stripe_adapter.py +0 -0
  176. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_prepared_statements.py +0 -0
  177. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_psycopg3_upgrade.py +0 -0
  178. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_query_cache.py +0 -0
  179. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_row_batch_update.py +0 -0
  180. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_row_cache_staleness.py +0 -0
  181. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_security_hardening.py +0 -0
  182. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_sqs_per_record_transactions.py +0 -0
  183. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  184. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_table_alter.py +0 -0
  185. {velocity_python-0.1.9 → velocity_python-0.1.10}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.9
3
+ Version: 0.1.10
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.9"
7
+ version = "0.1.10"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -29,6 +29,9 @@ dependencies = [
29
29
  "sqlparse>=0.5.0"
30
30
  ]
31
31
 
32
+ [project.scripts]
33
+ velocity = "velocity.db.migrations:cli"
34
+
32
35
  [project.urls]
33
36
  Homepage = "https://codeclubs.org/projects/velocity"
34
37
  Documentation = "https://codeclubs.org/projects/velocity"
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.9"
1
+ __version__ = version = "0.1.10"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -0,0 +1,579 @@
1
+ """
2
+ Schema migration framework for velocity-python.
3
+
4
+ Provides versioned, transactional schema migrations with up/down support,
5
+ a tracking table (``velocity_migrations``), and a CLI interface.
6
+
7
+ Usage::
8
+
9
+ from velocity.db.migrations import migration, MigrationRunner
10
+
11
+ @migration(version=1, description="Create users table")
12
+ def migrate_001(tx):
13
+ tx.table("users").create({"name": "TEXT", "email": "TEXT"})
14
+
15
+ @migration(version=1, down=True, description="Drop users table")
16
+ def rollback_001(tx):
17
+ tx.table("users").drop()
18
+
19
+ # Apply all pending migrations
20
+ runner = MigrationRunner(engine)
21
+ runner.migrate()
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ import inspect
28
+ import logging
29
+ import os
30
+ import importlib
31
+ import importlib.util
32
+ import time
33
+ from datetime import datetime, timezone
34
+ from pathlib import Path
35
+ from typing import Any, Callable, Dict, List, Optional, Tuple
36
+
37
+ logger = logging.getLogger("velocity.db.migrations")
38
+
39
+ # ── Global migration registry ──────────────────────────────────────
40
+
41
+ _registry: Dict[int, Dict[str, Any]] = {}
42
+
43
+ TRACKING_TABLE = "velocity_migrations"
44
+
45
+
46
+ def migration(version: int, description: str = "", down: bool = False):
47
+ """
48
+ Decorator that registers a function as a migration step.
49
+
50
+ Args:
51
+ version: Integer migration version (must be unique per direction).
52
+ description: Human-readable description of what this migration does.
53
+ down: If True, registers as the rollback (down) function for this version.
54
+
55
+ Example::
56
+
57
+ @migration(version=1, description="Add users table")
58
+ def migrate_001(tx):
59
+ tx.table("users").create({"name": "TEXT", "email": "TEXT"})
60
+
61
+ @migration(version=1, down=True)
62
+ def rollback_001(tx):
63
+ tx.table("users").drop()
64
+ """
65
+
66
+ def decorator(func: Callable) -> Callable:
67
+ if version not in _registry:
68
+ _registry[version] = {"up": None, "down": None, "description": ""}
69
+
70
+ direction = "down" if down else "up"
71
+ if _registry[version][direction] is not None:
72
+ raise ValueError(
73
+ f"Migration version {version} already has a '{direction}' function registered: "
74
+ f"{_registry[version][direction].__name__}"
75
+ )
76
+
77
+ _registry[version][direction] = func
78
+ if description and not down:
79
+ _registry[version]["description"] = description
80
+ elif description and down and not _registry[version]["description"]:
81
+ _registry[version]["description"] = description
82
+
83
+ return func
84
+
85
+ return decorator
86
+
87
+
88
+ def clear_registry():
89
+ """Clear all registered migrations. Useful for testing."""
90
+ _registry.clear()
91
+
92
+
93
+ def get_registry() -> Dict[int, Dict[str, Any]]:
94
+ """Return a copy of the current migration registry."""
95
+ return dict(_registry)
96
+
97
+
98
+ # ── Migration Runner ────────────────────────────────────────────────
99
+
100
+
101
+ class MigrationRunner:
102
+ """
103
+ Discovers, applies, and rolls back schema migrations.
104
+
105
+ Migrations are tracked in a ``velocity_migrations`` table with columns:
106
+
107
+ - ``version`` (INT, PRIMARY KEY) — migration version number
108
+ - ``description`` (TEXT) — human-readable description
109
+ - ``applied_at`` (TIMESTAMPTZ) — when the migration was applied
110
+ - ``checksum`` (TEXT) — SHA-256 of the migration function source
111
+ - ``execution_ms`` (INT) — how long the migration took in milliseconds
112
+
113
+ Args:
114
+ engine: A velocity Engine instance.
115
+ migrations_dir: Optional path to a directory containing migration .py files.
116
+ If provided, all .py files in that directory are imported to register
117
+ their ``@migration`` decorated functions.
118
+ """
119
+
120
+ def __init__(self, engine, migrations_dir: Optional[str] = None):
121
+ self.engine = engine
122
+ self._migrations_dir = migrations_dir
123
+
124
+ if migrations_dir:
125
+ self._load_migrations_from_dir(migrations_dir)
126
+
127
+ # ── Discovery ───────────────────────────────────────────────
128
+
129
+ def _load_migrations_from_dir(self, dir_path: str) -> None:
130
+ """Import all .py files from a directory to register @migration decorators."""
131
+ p = Path(dir_path)
132
+ if not p.is_dir():
133
+ raise FileNotFoundError(f"Migrations directory not found: {dir_path}")
134
+
135
+ for py_file in sorted(p.glob("*.py")):
136
+ if py_file.name.startswith("_"):
137
+ continue
138
+ module_name = f"velocity_migrations.{py_file.stem}"
139
+ spec = importlib.util.spec_from_file_location(module_name, py_file)
140
+ if spec and spec.loader:
141
+ mod = importlib.util.module_from_spec(spec)
142
+ spec.loader.exec_module(mod)
143
+
144
+ # ── Tracking table ──────────────────────────────────────────
145
+
146
+ def _ensure_tracking_table(self, tx) -> None:
147
+ """Create the velocity_migrations tracking table if it doesn't exist."""
148
+ table = tx.table(TRACKING_TABLE)
149
+ if not table.exists():
150
+ # Use raw SQL to avoid velocity system columns — we manage our own schema
151
+ tx.execute(
152
+ f"""
153
+ CREATE TABLE IF NOT EXISTS {TRACKING_TABLE} (
154
+ version INTEGER PRIMARY KEY,
155
+ description TEXT NOT NULL DEFAULT '',
156
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
157
+ checksum TEXT NOT NULL DEFAULT '',
158
+ execution_ms INTEGER NOT NULL DEFAULT 0
159
+ )
160
+ """,
161
+ cursor=tx.cursor(),
162
+ )
163
+ logger.info("Created tracking table: %s", TRACKING_TABLE)
164
+
165
+ def _get_applied_versions(self, tx) -> Dict[int, Dict[str, Any]]:
166
+ """Return dict of {version: {description, applied_at, checksum, execution_ms}}."""
167
+ self._ensure_tracking_table(tx)
168
+ result = tx.execute(
169
+ f"SELECT version, description, applied_at, checksum, execution_ms "
170
+ f"FROM {TRACKING_TABLE} ORDER BY version",
171
+ cursor=tx.cursor(),
172
+ )
173
+ applied = {}
174
+ for row in result.all():
175
+ applied[row["version"]] = {
176
+ "description": row["description"],
177
+ "applied_at": row["applied_at"],
178
+ "checksum": row["checksum"],
179
+ "execution_ms": row["execution_ms"],
180
+ }
181
+ return applied
182
+
183
+ def _record_migration(self, tx, version: int, description: str, checksum: str, execution_ms: int) -> None:
184
+ """Insert a record into the tracking table."""
185
+ tx.execute(
186
+ f"INSERT INTO {TRACKING_TABLE} (version, description, checksum, execution_ms) "
187
+ f"VALUES (%s, %s, %s, %s)",
188
+ (version, description, checksum, execution_ms),
189
+ cursor=tx.cursor(),
190
+ )
191
+
192
+ def _remove_migration_record(self, tx, version: int) -> None:
193
+ """Delete a record from the tracking table."""
194
+ tx.execute(
195
+ f"DELETE FROM {TRACKING_TABLE} WHERE version = %s",
196
+ (version,),
197
+ cursor=tx.cursor(),
198
+ )
199
+
200
+ # ── Checksum ────────────────────────────────────────────────
201
+
202
+ @staticmethod
203
+ def _checksum(func: Callable) -> str:
204
+ """SHA-256 hash of a migration function's source code."""
205
+ try:
206
+ source = inspect.getsource(func)
207
+ except (OSError, TypeError):
208
+ source = func.__name__
209
+ return hashlib.sha256(source.encode()).hexdigest()[:16]
210
+
211
+ # ── Core operations ─────────────────────────────────────────
212
+
213
+ def pending(self) -> List[int]:
214
+ """
215
+ Return list of pending (unapplied) migration versions, sorted ascending.
216
+ """
217
+ tx = self.engine.transaction()
218
+ try:
219
+ applied = self._get_applied_versions(tx)
220
+ pending = sorted(v for v in _registry if v not in applied and _registry[v]["up"] is not None)
221
+ return pending
222
+ finally:
223
+ tx.close()
224
+
225
+ def status(self) -> List[Dict[str, Any]]:
226
+ """
227
+ Return migration status for all registered versions.
228
+
229
+ Returns a list of dicts with keys:
230
+ - version, description, status ("applied" | "pending"), applied_at, checksum_match
231
+ """
232
+ tx = self.engine.transaction()
233
+ try:
234
+ applied = self._get_applied_versions(tx)
235
+ result = []
236
+ all_versions = sorted(set(list(_registry.keys()) + list(applied.keys())))
237
+
238
+ for v in all_versions:
239
+ entry: Dict[str, Any] = {"version": v}
240
+ reg = _registry.get(v, {})
241
+ app = applied.get(v)
242
+
243
+ entry["description"] = reg.get("description", "") or (app["description"] if app else "")
244
+
245
+ if app:
246
+ entry["status"] = "applied"
247
+ entry["applied_at"] = app["applied_at"]
248
+ entry["execution_ms"] = app["execution_ms"]
249
+ # Check if source has changed since applied
250
+ up_func = reg.get("up")
251
+ if up_func:
252
+ entry["checksum_match"] = self._checksum(up_func) == app["checksum"]
253
+ else:
254
+ entry["checksum_match"] = None # Not in registry
255
+ else:
256
+ entry["status"] = "pending"
257
+ entry["applied_at"] = None
258
+ entry["execution_ms"] = None
259
+ entry["checksum_match"] = None
260
+
261
+ has_down = reg.get("down") is not None if reg else False
262
+ entry["reversible"] = has_down
263
+
264
+ result.append(entry)
265
+
266
+ return result
267
+ finally:
268
+ tx.close()
269
+
270
+ def migrate(self, target: Optional[int] = None, dry_run: bool = False) -> List[int]:
271
+ """
272
+ Apply pending migrations up to ``target`` version (inclusive).
273
+
274
+ Each migration runs in its own transaction — if a migration fails,
275
+ all previously applied migrations in this run remain committed.
276
+
277
+ Args:
278
+ target: Apply up to this version. None = apply all pending.
279
+ dry_run: If True, log what would be done without applying.
280
+
281
+ Returns:
282
+ List of versions that were applied.
283
+ """
284
+ pending = self.pending()
285
+ if target is not None:
286
+ pending = [v for v in pending if v <= target]
287
+
288
+ if not pending:
289
+ logger.info("No pending migrations.")
290
+ return []
291
+
292
+ if dry_run:
293
+ for v in pending:
294
+ desc = _registry[v].get("description", "")
295
+ logger.info("[DRY RUN] Would apply migration %d: %s", v, desc)
296
+ return pending
297
+
298
+ applied = []
299
+ for v in pending:
300
+ reg = _registry[v]
301
+ up_func = reg["up"]
302
+ desc = reg.get("description", "")
303
+ checksum = self._checksum(up_func)
304
+
305
+ logger.info("Applying migration %d: %s", v, desc)
306
+ start = time.perf_counter()
307
+
308
+ # Each migration gets its own transaction
309
+ tx = self.engine.transaction()
310
+ try:
311
+ # Temporarily unlock schema for migration DDL
312
+ was_locked = self.engine.schema_locked
313
+ if was_locked:
314
+ self.engine.unlock_schema()
315
+ try:
316
+ self._ensure_tracking_table(tx)
317
+ up_func(tx)
318
+ elapsed_ms = int((time.perf_counter() - start) * 1000)
319
+ self._record_migration(tx, v, desc, checksum, elapsed_ms)
320
+ tx.commit()
321
+ applied.append(v)
322
+ logger.info("Applied migration %d in %dms", v, elapsed_ms)
323
+ finally:
324
+ if was_locked:
325
+ self.engine.lock_schema()
326
+ except Exception:
327
+ tx.rollback()
328
+ logger.error("Migration %d failed — rolled back", v, exc_info=True)
329
+ raise
330
+ finally:
331
+ tx.close()
332
+
333
+ return applied
334
+
335
+ def rollback(self, target: Optional[int] = None, dry_run: bool = False) -> List[int]:
336
+ """
337
+ Roll back applied migrations down to ``target`` version (exclusive).
338
+
339
+ Migrations are rolled back in reverse order. Each rollback runs in its
340
+ own transaction.
341
+
342
+ Args:
343
+ target: Roll back to this version (this version stays applied).
344
+ None = roll back the most recent migration only.
345
+ dry_run: If True, log what would be done without applying.
346
+
347
+ Returns:
348
+ List of versions that were rolled back (in rollback order).
349
+
350
+ Raises:
351
+ ValueError: If a migration has no ``down`` function registered.
352
+ """
353
+ tx = self.engine.transaction()
354
+ try:
355
+ applied = self._get_applied_versions(tx)
356
+ finally:
357
+ tx.close()
358
+
359
+ applied_versions = sorted(applied.keys(), reverse=True)
360
+
361
+ if not applied_versions:
362
+ logger.info("No migrations to roll back.")
363
+ return []
364
+
365
+ if target is None:
366
+ # Roll back only the latest
367
+ to_rollback = [applied_versions[0]]
368
+ else:
369
+ to_rollback = [v for v in applied_versions if v > target]
370
+
371
+ if not to_rollback:
372
+ logger.info("No migrations to roll back (already at or below target %d).", target)
373
+ return []
374
+
375
+ # Check all have down functions before starting
376
+ for v in to_rollback:
377
+ reg = _registry.get(v, {})
378
+ if not reg.get("down"):
379
+ raise ValueError(
380
+ f"Migration {v} has no 'down' function registered — cannot roll back."
381
+ )
382
+
383
+ if dry_run:
384
+ for v in to_rollback:
385
+ desc = _registry.get(v, {}).get("description", "")
386
+ logger.info("[DRY RUN] Would roll back migration %d: %s", v, desc)
387
+ return to_rollback
388
+
389
+ rolled_back = []
390
+ for v in to_rollback:
391
+ reg = _registry[v]
392
+ down_func = reg["down"]
393
+ desc = reg.get("description", "")
394
+
395
+ logger.info("Rolling back migration %d: %s", v, desc)
396
+ start = time.perf_counter()
397
+
398
+ tx = self.engine.transaction()
399
+ try:
400
+ was_locked = self.engine.schema_locked
401
+ if was_locked:
402
+ self.engine.unlock_schema()
403
+ try:
404
+ down_func(tx)
405
+ self._remove_migration_record(tx, v)
406
+ tx.commit()
407
+ elapsed_ms = int((time.perf_counter() - start) * 1000)
408
+ rolled_back.append(v)
409
+ logger.info("Rolled back migration %d in %dms", v, elapsed_ms)
410
+ finally:
411
+ if was_locked:
412
+ self.engine.lock_schema()
413
+ except Exception:
414
+ tx.rollback()
415
+ logger.error("Rollback of migration %d failed", v, exc_info=True)
416
+ raise
417
+ finally:
418
+ tx.close()
419
+
420
+ return rolled_back
421
+
422
+ def diff(self) -> Dict[str, Any]:
423
+ """
424
+ Compare current database schema with the expected state from migrations.
425
+
426
+ Returns a dict with:
427
+ - ``pending_count``: Number of unapplied migrations.
428
+ - ``pending_versions``: List of unapplied version numbers.
429
+ - ``applied_count``: Number of applied migrations.
430
+ - ``modified``: List of versions where the source checksum differs
431
+ from what was applied (migration was edited after being applied).
432
+ - ``orphaned``: List of versions applied in DB but not in the registry
433
+ (migration file was deleted or not loaded).
434
+ """
435
+ tx = self.engine.transaction()
436
+ try:
437
+ applied = self._get_applied_versions(tx)
438
+ finally:
439
+ tx.close()
440
+
441
+ pending_versions = sorted(v for v in _registry if v not in applied and _registry[v].get("up"))
442
+ orphaned = sorted(v for v in applied if v not in _registry)
443
+ modified = []
444
+ for v, app in applied.items():
445
+ reg = _registry.get(v, {})
446
+ up_func = reg.get("up")
447
+ if up_func and self._checksum(up_func) != app["checksum"]:
448
+ modified.append(v)
449
+
450
+ return {
451
+ "pending_count": len(pending_versions),
452
+ "pending_versions": pending_versions,
453
+ "applied_count": len(applied),
454
+ "modified": sorted(modified),
455
+ "orphaned": orphaned,
456
+ }
457
+
458
+
459
+ # ── CLI ─────────────────────────────────────────────────────────────
460
+
461
+
462
+ def _cli_main(args: Optional[List[str]] = None) -> int:
463
+ """
464
+ CLI entry point for ``velocity migrate``.
465
+
466
+ Commands:
467
+ velocity migrate Apply all pending migrations
468
+ velocity migrate --to 5 Apply up to version 5
469
+ velocity migrate --dry-run Show what would be applied
470
+ velocity rollback Roll back the latest migration
471
+ velocity rollback --to 3 Roll back to version 3
472
+ velocity status Show migration status
473
+ velocity diff Compare DB schema with migration registry
474
+ """
475
+ import argparse
476
+
477
+ parser = argparse.ArgumentParser(
478
+ prog="velocity",
479
+ description="Velocity-Python schema migration tool",
480
+ )
481
+ sub = parser.add_subparsers(dest="command")
482
+
483
+ # migrate
484
+ mig = sub.add_parser("migrate", help="Apply pending migrations")
485
+ mig.add_argument("--to", type=int, default=None, help="Apply up to this version (inclusive)")
486
+ mig.add_argument("--dry-run", action="store_true", help="Show what would be applied")
487
+ mig.add_argument("--dir", type=str, default=None, help="Directory containing migration .py files")
488
+
489
+ # rollback
490
+ rb = sub.add_parser("rollback", help="Roll back migrations")
491
+ rb.add_argument("--to", type=int, default=None, help="Roll back to this version (exclusive, version stays)")
492
+ rb.add_argument("--dry-run", action="store_true", help="Show what would be rolled back")
493
+ rb.add_argument("--dir", type=str, default=None, help="Directory containing migration .py files")
494
+
495
+ # status
496
+ st = sub.add_parser("status", help="Show migration status")
497
+ st.add_argument("--dir", type=str, default=None, help="Directory containing migration .py files")
498
+
499
+ # diff
500
+ df = sub.add_parser("diff", help="Compare DB schema with migration registry")
501
+ df.add_argument("--dir", type=str, default=None, help="Directory containing migration .py files")
502
+
503
+ parsed = parser.parse_args(args)
504
+
505
+ if not parsed.command:
506
+ parser.print_help()
507
+ return 1
508
+
509
+ # Configure logging for CLI
510
+ logging.basicConfig(
511
+ level=logging.INFO,
512
+ format="%(levelname)s %(message)s",
513
+ )
514
+
515
+ # Initialize engine from env vars (standard velocity pattern)
516
+ try:
517
+ from velocity.db.servers.postgres import initialize
518
+ engine = initialize()
519
+ except Exception as e:
520
+ logger.error("Failed to initialize database engine: %s", e)
521
+ logger.error("Set DBHost, DBDatabase, DBUser, DBPassword environment variables.")
522
+ return 1
523
+
524
+ migrations_dir = getattr(parsed, "dir", None)
525
+ runner = MigrationRunner(engine, migrations_dir=migrations_dir)
526
+
527
+ try:
528
+ if parsed.command == "migrate":
529
+ applied = runner.migrate(target=parsed.to, dry_run=parsed.dry_run)
530
+ if not parsed.dry_run:
531
+ print(f"{len(applied)} migration(s) applied.")
532
+ return 0
533
+
534
+ elif parsed.command == "rollback":
535
+ rolled = runner.rollback(target=parsed.to, dry_run=parsed.dry_run)
536
+ if not parsed.dry_run:
537
+ print(f"{len(rolled)} migration(s) rolled back.")
538
+ return 0
539
+
540
+ elif parsed.command == "status":
541
+ entries = runner.status()
542
+ if not entries:
543
+ print("No migrations registered or applied.")
544
+ return 0
545
+ print(f"{'Ver':>4} {'Status':<9} {'Rev':>3} {'Time':>7} Description")
546
+ print("-" * 60)
547
+ for e in entries:
548
+ status_str = e["status"]
549
+ if e.get("checksum_match") is False:
550
+ status_str = "MODIFIED"
551
+ rev = "yes" if e.get("reversible") else "no"
552
+ ms = f"{e['execution_ms']}ms" if e.get("execution_ms") is not None else "-"
553
+ print(f"{e['version']:>4} {status_str:<9} {rev:>3} {ms:>7} {e['description']}")
554
+ return 0
555
+
556
+ elif parsed.command == "diff":
557
+ d = runner.diff()
558
+ print(f"Applied: {d['applied_count']}")
559
+ print(f"Pending: {d['pending_count']} {d['pending_versions'] or ''}")
560
+ if d["modified"]:
561
+ print(f"Modified since applied: {d['modified']}")
562
+ if d["orphaned"]:
563
+ print(f"Orphaned (in DB, not in code): {d['orphaned']}")
564
+ if not d["pending_versions"] and not d["modified"] and not d["orphaned"]:
565
+ print("Schema is up to date.")
566
+ return 0
567
+
568
+ except Exception as e:
569
+ logger.error("%s", e)
570
+ return 1
571
+
572
+ return 0
573
+
574
+
575
+ def cli():
576
+ """Setuptools console_scripts entry point."""
577
+ import sys
578
+
579
+ sys.exit(_cli_main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.9
3
+ Version: 0.1.10
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
@@ -37,6 +37,7 @@ src/velocity/aws/tests/test_lambda_handler_json_serialization.py
37
37
  src/velocity/aws/tests/test_response.py
38
38
  src/velocity/db/__init__.py
39
39
  src/velocity/db/exceptions.py
40
+ src/velocity/db/migrations.py
40
41
  src/velocity/db/utils.py
41
42
  src/velocity/db/core/__init__.py
42
43
  src/velocity/db/core/async_support.py
@@ -146,6 +147,7 @@ src/velocity/payment/stripe_adapter.py
146
147
  src/velocity_python.egg-info/PKG-INFO
147
148
  src/velocity_python.egg-info/SOURCES.txt
148
149
  src/velocity_python.egg-info/dependency_links.txt
150
+ src/velocity_python.egg-info/entry_points.txt
149
151
  src/velocity_python.egg-info/requires.txt
150
152
  src/velocity_python.egg-info/top_level.txt
151
153
  tests/test_amplify_build.py
@@ -173,6 +175,7 @@ tests/test_psycopg3_upgrade.py
173
175
  tests/test_query_cache.py
174
176
  tests/test_row_batch_update.py
175
177
  tests/test_row_cache_staleness.py
178
+ tests/test_schema_migrations.py
176
179
  tests/test_security_hardening.py
177
180
  tests/test_sqs_per_record_transactions.py
178
181
  tests/test_sys_modified_count_postgres_demo.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ velocity = velocity.db.migrations:cli