velocity-python 0.1.107__tar.gz → 0.1.109__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 (295) hide show
  1. {velocity_python-0.1.107 → velocity_python-0.1.109}/PKG-INFO +2 -1
  2. {velocity_python-0.1.107 → velocity_python-0.1.109}/README.md +1 -0
  3. {velocity_python-0.1.107 → velocity_python-0.1.109}/pyproject.toml +1 -1
  4. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/__init__.py +1 -1
  5. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/__init__.py +2 -0
  6. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/base.py +48 -0
  7. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/security.py +6 -1
  8. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/feature_flags.py +88 -55
  9. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/mixins/session_control.py +4 -3
  10. velocity_python-0.1.109/src/velocity/aws/mirror.py +267 -0
  11. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity_python.egg-info/PKG-INFO +2 -1
  12. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  13. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_capability_catalog.py +32 -0
  14. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_feature_flags.py +85 -0
  15. velocity_python-0.1.109/tests/test_mirror.py +232 -0
  16. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_single_session_capability.py +4 -0
  17. {velocity_python-0.1.107 → velocity_python-0.1.109}/LICENSE +0 -0
  18. {velocity_python-0.1.107 → velocity_python-0.1.109}/setup.cfg +0 -0
  19. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/__init__.py +0 -0
  20. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/amplify.py +0 -0
  21. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/amplify_build.py +0 -0
  22. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/assets/__init__.py +0 -0
  23. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/assets/backfill.py +0 -0
  24. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/assets/indexing.py +0 -0
  25. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/assets/references.py +0 -0
  26. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/assets/service.py +0 -0
  27. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/assets/usage_index.py +0 -0
  28. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/blobs.py +0 -0
  29. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/dispatch.py +0 -0
  30. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/infra.py +0 -0
  31. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/jobs.py +0 -0
  32. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/provisioning.py +0 -0
  33. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/retention.py +0 -0
  34. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/capabilities/single_session.py +0 -0
  35. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/deploy/__init__.py +0 -0
  36. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/deploy/config.py +0 -0
  37. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/deploy/operations.py +0 -0
  38. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/deploy/policies.py +0 -0
  39. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/deploy/reconcile.py +0 -0
  40. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/deploy/targets.py +0 -0
  41. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/dirty_pipeline.py +0 -0
  42. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/email.py +0 -0
  43. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/__init__.py +0 -0
  44. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/base_handler.py +0 -0
  45. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/context.py +0 -0
  46. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/context_factory.py +0 -0
  47. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/exceptions.py +0 -0
  48. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  49. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/masquerade.py +0 -0
  50. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  51. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  52. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  53. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  54. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/perf.py +0 -0
  55. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/response.py +0 -0
  56. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  57. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/job_lifecycle.py +0 -0
  58. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/job_results.py +0 -0
  59. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/s3.py +0 -0
  60. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/sms.py +0 -0
  61. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/ssm_config.py +0 -0
  62. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/__init__.py +0 -0
  63. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  64. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  65. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  66. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  67. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  68. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_email.py +0 -0
  69. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  70. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_response.py +0 -0
  71. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/aws/tests/test_sms.py +0 -0
  72. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/__init__.py +0 -0
  73. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/__init__.py +0 -0
  74. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/async_support.py +0 -0
  75. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/column.py +0 -0
  76. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/database.py +0 -0
  77. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/decorators.py +0 -0
  78. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/engine.py +0 -0
  79. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/jsonproxy.py +0 -0
  80. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/result.py +0 -0
  81. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/row.py +0 -0
  82. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/sequence.py +0 -0
  83. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/table.py +0 -0
  84. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/transaction.py +0 -0
  85. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/core/view.py +0 -0
  86. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/exceptions.py +0 -0
  87. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/maintenance.py +0 -0
  88. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/migrations.py +0 -0
  89. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/__init__.py +0 -0
  90. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/base/__init__.py +0 -0
  91. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/base/initializer.py +0 -0
  92. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/base/operators.py +0 -0
  93. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/base/sql.py +0 -0
  94. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/base/types.py +0 -0
  95. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/mysql/__init__.py +0 -0
  96. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/mysql/operators.py +0 -0
  97. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/mysql/reserved.py +0 -0
  98. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/mysql/sql.py +0 -0
  99. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/mysql/types.py +0 -0
  100. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/postgres/__init__.py +0 -0
  101. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/postgres/operators.py +0 -0
  102. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/postgres/reserved.py +0 -0
  103. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/postgres/sql.py +0 -0
  104. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/postgres/types.py +0 -0
  105. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  106. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlite/operators.py +0 -0
  107. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  108. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlite/sql.py +0 -0
  109. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlite/types.py +0 -0
  110. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  111. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  112. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  113. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  114. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/sqlserver/types.py +0 -0
  115. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/servers/tablehelper.py +0 -0
  116. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/__init__.py +0 -0
  117. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/common_db_test.py +0 -0
  118. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/__init__.py +0 -0
  119. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/common.py +0 -0
  120. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/conftest.py +0 -0
  121. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_column.py +0 -0
  122. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  123. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_database.py +0 -0
  124. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  125. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  126. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  127. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_result.py +0 -0
  128. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_row.py +0 -0
  129. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  130. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  131. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  132. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  133. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  134. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_table.py +0 -0
  135. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  136. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  137. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/sql/__init__.py +0 -0
  138. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/sql/common.py +0 -0
  139. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  140. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  141. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  142. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_db_utils.py +0 -0
  143. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_postgres.py +0 -0
  144. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  145. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  146. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_result_caching.py +0 -0
  147. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  148. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  149. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  150. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  151. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_sql_builder.py +0 -0
  152. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_tablehelper.py +0 -0
  153. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/tests/test_view_helper.py +0 -0
  154. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/db/utils.py +0 -0
  155. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/logging.py +0 -0
  156. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/__init__.py +0 -0
  157. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/conv/__init__.py +0 -0
  158. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/conv/iconv.py +0 -0
  159. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/conv/oconv.py +0 -0
  160. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/db.py +0 -0
  161. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/export.py +0 -0
  162. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/format.py +0 -0
  163. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/mail.py +0 -0
  164. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/merge.py +0 -0
  165. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/pdf.py +0 -0
  166. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/__init__.py +0 -0
  167. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_db.py +0 -0
  168. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_fix.py +0 -0
  169. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_format.py +0 -0
  170. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_iconv.py +0 -0
  171. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_mail_send.py +0 -0
  172. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_merge.py +0 -0
  173. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_oconv.py +0 -0
  174. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_original_error.py +0 -0
  175. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tests/test_timer.py +0 -0
  176. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/timer.py +0 -0
  177. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/misc/tools.py +0 -0
  178. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/__init__.py +0 -0
  179. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/core/__init__.py +0 -0
  180. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/core/capabilities.py +0 -0
  181. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/core/store.py +0 -0
  182. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/core/where.py +0 -0
  183. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/exceptions.py +0 -0
  184. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/__init__.py +0 -0
  185. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/base/__init__.py +0 -0
  186. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  187. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  188. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  189. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  190. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  191. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  192. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  193. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  194. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  195. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  196. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  197. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  198. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/__init__.py +0 -0
  199. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/authorizenet_adapter.py +0 -0
  200. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/authorizenet_mirror.py +0 -0
  201. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/base_adapter.py +0 -0
  202. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/braintree_adapter.py +0 -0
  203. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/braintree_mirror.py +0 -0
  204. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/charge_rules.py +0 -0
  205. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/stripe_adapter.py +0 -0
  206. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity/payment/stripe_mirror.py +0 -0
  207. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  208. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity_python.egg-info/entry_points.txt +0 -0
  209. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity_python.egg-info/requires.txt +0 -0
  210. {velocity_python-0.1.107 → velocity_python-0.1.109}/src/velocity_python.egg-info/top_level.txt +0 -0
  211. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_access_error_403.py +0 -0
  212. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_activity_tracking_aborted_tx.py +0 -0
  213. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_amplify_build.py +0 -0
  214. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_asset_indexing.py +0 -0
  215. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_asset_references.py +0 -0
  216. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_assets_service.py +0 -0
  217. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_async_support.py +0 -0
  218. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_batch_operations.py +0 -0
  219. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_blob_store.py +0 -0
  220. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_capabilities.py +0 -0
  221. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_column_tx_arg.py +0 -0
  222. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_concurrency_safety.py +0 -0
  223. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_connection_pool.py +0 -0
  224. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_connection_resilience.py +0 -0
  225. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_context_flags_loader.py +0 -0
  226. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_context_job_descriptions.py +0 -0
  227. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_data_service_query_cap.py +0 -0
  228. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_data_service_renumber.py +0 -0
  229. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_data_service_save_as.py +0 -0
  230. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_db_credentials_ssm_cascade.py +0 -0
  231. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_db_maintenance.py +0 -0
  232. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_decorators.py +0 -0
  233. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_delete_hook_fk_blocked.py +0 -0
  234. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_dirty_pipeline_fast_path.py +0 -0
  235. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_email_processing.py +0 -0
  236. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_enqueue_send_failures.py +0 -0
  237. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_get_cognito_user_provider.py +0 -0
  238. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_http_handler_rollback.py +0 -0
  239. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_iconv_money_to_cents.py +0 -0
  240. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_identifier_injection_guard.py +0 -0
  241. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_job_handler.py +0 -0
  242. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_job_lifecycle.py +0 -0
  243. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_job_results.py +0 -0
  244. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_json_columns.py +0 -0
  245. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_jsonb_dict_adapter.py +0 -0
  246. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_lambda_handler.py +0 -0
  247. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_lambda_handler_auth.py +0 -0
  248. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_lambda_handler_masquerade.py +0 -0
  249. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_masquerade_grant.py +0 -0
  250. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_mixins_import.py +0 -0
  251. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_n_plus_one.py +0 -0
  252. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_nosql_dynamodb.py +0 -0
  253. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_nosql_memory.py +0 -0
  254. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_nosql_mongodb.py +0 -0
  255. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_nosql_redis.py +0 -0
  256. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_nosql_translation.py +0 -0
  257. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_nosql_where.py +0 -0
  258. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_observability.py +0 -0
  259. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_on_commit.py +0 -0
  260. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_payment_authorizenet_adapter.py +0 -0
  261. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_payment_braintree_adapter.py +0 -0
  262. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_payment_braintree_mirror.py +0 -0
  263. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_payment_mirror_empty_table.py +0 -0
  264. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_payment_profile_sorting.py +0 -0
  265. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_payment_stripe_adapter.py +0 -0
  266. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_pdf.py +0 -0
  267. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_prepared_statements.py +0 -0
  268. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_psycopg3_upgrade.py +0 -0
  269. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_query_cache.py +0 -0
  270. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_query_missing_table_unlocked.py +0 -0
  271. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_response.py +0 -0
  272. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_restricted_direct_tables.py +0 -0
  273. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_retry_side_effect_guard.py +0 -0
  274. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_return_default_safety.py +0 -0
  275. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_row_batch_update.py +0 -0
  276. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_row_cache_staleness.py +0 -0
  277. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_schema_migrations.py +0 -0
  278. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_security_capability.py +0 -0
  279. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_security_hardening.py +0 -0
  280. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_server_cursor.py +0 -0
  281. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_session_control.py +0 -0
  282. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_single_autocommit_safety.py +0 -0
  283. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_spreadsheet_functions.py +0 -0
  284. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_sqlite_backend.py +0 -0
  285. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_sqs_per_record_transactions.py +0 -0
  286. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_ssm_config.py +0 -0
  287. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_store_user_data.py +0 -0
  288. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  289. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_table_alter.py +0 -0
  290. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_table_purge.py +0 -0
  291. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_transaction_class_wrapping.py +0 -0
  292. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_transaction_commit_and_ownership.py +0 -0
  293. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_transaction_edge_cases.py +0 -0
  294. {velocity_python-0.1.107 → velocity_python-0.1.109}/tests/test_where_clause_validation.py +0 -0
  295. {velocity_python-0.1.107 → velocity_python-0.1.109}/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.107
3
+ Version: 0.1.109
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
@@ -251,6 +251,7 @@ Full documentation is included in the `docs/` directory of the source distributi
251
251
  | Async Support | `docs/async.md` | AsyncTransaction, AsyncTable, AsyncResult, parallel queries with gather() |
252
252
  | Configuration Reference | `docs/configuration.md` | All environment variables, engine options, and connection settings |
253
253
  | AWS Lambda Handlers | `docs/aws-handlers.md` | LambdaHandler, SqsHandler, auth modes, per-record transactions, async jobs (JobHandler + job result stores) |
254
+ | Cross-database Mirroring | `docs/mirroring.md` | Keep a table identical in two databases (demo ↔ production) by natural key; rwx hooks, FK translation, reconcile |
254
255
  | NoSQL Adapters | `docs/nosql.md` | Redis, DynamoDB, MongoDB, Elasticsearch, Cassandra, Couchbase behind one capability-honest contract |
255
256
  | Payment Processing | `docs/payment.md` | Stripe and Braintree adapters, payment lifecycle |
256
257
  | Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing and SMTP sending |
@@ -157,6 +157,7 @@ Full documentation is included in the `docs/` directory of the source distributi
157
157
  | Async Support | `docs/async.md` | AsyncTransaction, AsyncTable, AsyncResult, parallel queries with gather() |
158
158
  | Configuration Reference | `docs/configuration.md` | All environment variables, engine options, and connection settings |
159
159
  | AWS Lambda Handlers | `docs/aws-handlers.md` | LambdaHandler, SqsHandler, auth modes, per-record transactions, async jobs (JobHandler + job result stores) |
160
+ | Cross-database Mirroring | `docs/mirroring.md` | Keep a table identical in two databases (demo ↔ production) by natural key; rwx hooks, FK translation, reconcile |
160
161
  | NoSQL Adapters | `docs/nosql.md` | Redis, DynamoDB, MongoDB, Elasticsearch, Cassandra, Couchbase behind one capability-honest contract |
161
162
  | Payment Processing | `docs/payment.md` | Stripe and Braintree adapters, payment lifecycle |
162
163
  | Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing and SMTP sending |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.1.107"
7
+ version = "0.1.109"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.107"
1
+ __version__ = version = "0.1.109"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -20,6 +20,7 @@ from .base import ( # noqa: F401
20
20
  load_config,
21
21
  policy_field,
22
22
  resolve_config,
23
+ summarize_policy,
23
24
  validate_policy,
24
25
  )
25
26
  from .dispatch import ( # noqa: F401
@@ -52,6 +53,7 @@ __all__ = [
52
53
  "provision",
53
54
  "resolve_config",
54
55
  "run_capability",
56
+ "summarize_policy",
55
57
  "trigger_key",
56
58
  "validate_policy",
57
59
  "NIGHTLY",
@@ -161,6 +161,54 @@ def load_config(raw) -> dict:
161
161
  return value
162
162
 
163
163
 
164
+ def summarize_policy(name, config, *, max_items=3) -> str:
165
+ """One line a list can show for a row's policy — "Tables: aws_api_activity
166
+ 7d, filebuffer 14d · IP deny list: 2" — built from the declared fields so
167
+ it reads the same for every capability. Empty string for an empty policy;
168
+ undeclared keys are counted, not shown."""
169
+ cap = REGISTRY.get(name)
170
+ try:
171
+ config = load_config(config)
172
+ except ValueError:
173
+ return "invalid JSON"
174
+ if cap is None or not config:
175
+ return ""
176
+ parts = []
177
+ declared = set()
178
+ for field in cap.policy:
179
+ declared.add(field["name"])
180
+ value = config.get(field["name"])
181
+ if value in (None, "", [], {}):
182
+ continue
183
+ parts.append(f"{field['label']}: {_brief(field, value, max_items)}")
184
+ extra = len([key for key in config if key not in declared])
185
+ if extra:
186
+ parts.append(f"+{extra} other key{'s' if extra > 1 else ''}")
187
+ return " · ".join(parts)
188
+
189
+
190
+ def _brief(field, value, max_items):
191
+ kind = field["kind"]
192
+ if kind == TABLE_DAYS and isinstance(value, dict):
193
+ items = [
194
+ f"{table} {days}d" if days not in (None, "", 0) else f"{table} (default)"
195
+ for table, days in value.items()
196
+ ]
197
+ return _clip(items, max_items)
198
+ if kind in (CIDR_LIST, COUNTRY_LIST, LIST) and isinstance(value, (list, tuple)):
199
+ return _clip([str(v) for v in value], max_items)
200
+ if kind == BOOLEAN:
201
+ return "on" if str(value).lower() in ("true", "1", "yes", "on") else "off"
202
+ text = str(value)
203
+ return text if len(text) <= 40 else text[:37] + "..."
204
+
205
+
206
+ def _clip(items, max_items):
207
+ if len(items) <= max_items:
208
+ return ", ".join(items)
209
+ return ", ".join(items[:max_items]) + f" (+{len(items) - max_items} more)"
210
+
211
+
164
212
  def validate_policy(name, config) -> list:
165
213
  """Check a site's ``config`` against the capability's declared policy.
166
214
 
@@ -66,7 +66,9 @@ _cache: dict = {}
66
66
 
67
67
 
68
68
  def clear_cache():
69
+ """Forget the cached site row AND the shared flags row (tests)."""
69
70
  _cache.clear()
71
+ feature_flags.clear_cache()
70
72
 
71
73
 
72
74
  # ── request facts ───────────────────────────────────────────────────────
@@ -201,7 +203,10 @@ def _site_row(tx):
201
203
 
202
204
 
203
205
  def _flags(tx):
204
- return _cached(("flags",), lambda: feature_flags.get(tx))
206
+ """The stage's flags row from velocity's container cache — shared with
207
+ the handler context and session control, so the guards add no read of
208
+ their own to a request."""
209
+ return feature_flags.get_cached(tx)
205
210
 
206
211
 
207
212
  def _row_config(row) -> dict:
@@ -23,19 +23,31 @@ carries one row per stage and each environment reads its own; the alternate
23
23
  database name comes from config (:data:`ALTERNATE_DB_CONFIG_KEY`). (An app that
24
24
  one day consolidates several systems into one database can widen the key then;
25
25
  today the stage is the whole identity.)
26
+
27
+ The mirroring itself is the generic :mod:`velocity.aws.mirror` (any table,
28
+ any natural key, optional foreign-key translation); this module is that
29
+ mechanism applied to the flags table, keyed by ``applied_system``.
26
30
  """
27
31
 
28
32
  from __future__ import annotations
29
33
 
30
34
  import logging
35
+ import time
36
+
37
+ from velocity.aws import mirror as _mirror
31
38
 
32
39
  log = logging.getLogger(__name__)
33
40
 
34
41
  SYSTEM_COLUMN = "applied_system"
35
42
  DEFAULT_TABLE = "sys_feature_flags"
36
- ALTERNATE_DB_CONFIG_KEY = "DBAlternate"
43
+ ALTERNATE_DB_CONFIG_KEY = _mirror.ALTERNATE_DB_CONFIG_KEY
44
+ # how long a warm container may reuse the flags row before re-reading it
45
+ CACHE_SECONDS_CONFIG_KEY = "FeatureFlagCacheSeconds"
46
+ DEFAULT_CACHE_SECONDS = 60
37
47
 
38
- _alternate_engines: dict = {}
48
+ _alternate_engines = _mirror._alternate_engines
49
+ _cache: dict = {} # (system, table) -> (expires_at, row)
50
+ _cache_seconds: float | None = None
39
51
 
40
52
 
41
53
  def current_system(default: str | None = None) -> str | None:
@@ -68,20 +80,76 @@ def get(
68
80
  return dict(row)
69
81
 
70
82
 
83
+ def cache_seconds() -> float:
84
+ """Seconds a warm container may reuse the flags row: the
85
+ ``FeatureFlagCacheSeconds`` config key, default 60; ``0`` reads the row
86
+ on every call. Resolved once per container."""
87
+ global _cache_seconds
88
+ if _cache_seconds is None:
89
+ raw = None
90
+ try:
91
+ from velocity.aws import ssm_config
92
+
93
+ raw = ssm_config.getenv(CACHE_SECONDS_CONFIG_KEY, None)
94
+ except Exception:
95
+ raw = None
96
+ try:
97
+ _cache_seconds = (
98
+ float(raw) if raw not in (None, "") else float(DEFAULT_CACHE_SECONDS)
99
+ )
100
+ except (TypeError, ValueError):
101
+ _cache_seconds = float(DEFAULT_CACHE_SECONDS)
102
+ return _cache_seconds
103
+
104
+
105
+ def clear_cache() -> None:
106
+ """Forget cached rows (after a local flags write; tests)."""
107
+ global _cache_seconds
108
+ _cache.clear()
109
+ _cache_seconds = None
110
+
111
+
112
+ def get_cached(
113
+ tx, system: str | None = None, *, table: str = DEFAULT_TABLE, now: float | None = None
114
+ ) -> dict:
115
+ """:func:`get` behind a per-container TTL cache — the hot-path reader.
116
+
117
+ The flags row is ONE row per stage, and every reader in a request
118
+ (the handler context, the security guards, session control, any
119
+ capability knob) wants the same one: this makes it at most one read per
120
+ :func:`cache_seconds` per container, however many readers there are. A
121
+ flag saved through the mirror hook clears the local cache, so the
122
+ environment that made the change sees it at once; the other environment
123
+ sees it within the TTL.
124
+ """
125
+ system = system if system is not None else current_system()
126
+ ttl = cache_seconds()
127
+ if ttl <= 0:
128
+ return get(tx, system, table=table)
129
+ now = time.monotonic() if now is None else now
130
+ key = (system, table)
131
+ hit = _cache.get(key)
132
+ if hit and hit[0] > now:
133
+ return dict(hit[1])
134
+ row = get(tx, system, table=table)
135
+ _cache[key] = (now + ttl, dict(row))
136
+ return dict(row)
137
+
138
+
71
139
  def load(tx, loader=None) -> dict:
72
140
  """The request-time flag source used by handler contexts.
73
141
 
74
142
  An injected ``loader(tx)`` wins (apps that resolve flags their own way);
75
- otherwise :func:`get` — velocity's stage-scoped reader is the source,
76
- never raising into the request. The legacy CaringCent
77
- ``support.app.helpers`` loader remains only as a last resort when the
78
- velocity reader finds no row (it raises on a missing row, which those
79
- apps rely on); absent that package the answer is ``{}``.
143
+ otherwise :func:`get_cached` — velocity's stage-scoped reader behind the
144
+ container cache — is the source, never raising into the request. The
145
+ legacy CaringCent ``support.app.helpers`` loader remains only as a last
146
+ resort when the velocity reader finds no row (it raises on a missing
147
+ row, which those apps rely on); absent that package the answer is ``{}``.
80
148
  """
81
149
  if callable(loader):
82
150
  return loader(tx)
83
151
  try:
84
- flags = get(tx)
152
+ flags = get_cached(tx)
85
153
  except Exception:
86
154
  flags = {}
87
155
  if flags:
@@ -101,28 +169,13 @@ def _legacy_support_loader(tx):
101
169
  return helpers.get_feature_flags(tx)
102
170
 
103
171
 
104
- def _without_sys(row) -> dict:
105
- """A row's business columns — the ``sys_*`` bookkeeping never crosses over."""
106
- return {k: v for k, v in dict(row).items() if not str(k).startswith("sys_")}
172
+ _without_sys = _mirror.business_columns
173
+ _toast = _mirror.toast
107
174
 
108
175
 
109
176
  def alternate_engine(alternate_db: str | None = None):
110
- """Engine for the alternate database — same instance, other ``database``.
111
-
112
- Host/port/user/password are inherited from the running config, so no
113
- separate credentials are needed; only the database name changes. Cached per
114
- database name so repeated mirrors in one process reuse the connection.
115
- """
116
- from velocity.aws import ssm_config
117
- from velocity.db.servers import postgres
118
-
119
- if alternate_db is None:
120
- alternate_db = ssm_config.getenv(ALTERNATE_DB_CONFIG_KEY, raise_on_missing=True)
121
- if alternate_db not in _alternate_engines:
122
- _alternate_engines[alternate_db] = postgres.initialize(
123
- {"database": alternate_db}
124
- )
125
- return _alternate_engines[alternate_db]
177
+ """Engine for the alternate database — see :func:`velocity.aws.mirror.alternate_engine`."""
178
+ return _mirror.alternate_engine(alternate_db)
126
179
 
127
180
 
128
181
  def mirror_row(
@@ -133,14 +186,9 @@ def mirror_row(
133
186
  Returns the mirrored key, or ``None`` when the row has no system key (a
134
187
  keyless row is skipped, never mirrored to an arbitrary target).
135
188
  """
136
- data = _without_sys(row)
137
- key = data.get(SYSTEM_COLUMN)
138
- if not key:
139
- return None
140
189
  engine = engine or alternate_engine(alternate_db)
141
- with engine.transaction() as alt:
142
- alt.table(table).merge(data, pk={SYSTEM_COLUMN: key})
143
- return key
190
+ pk = _mirror.mirror_row(row, table=table, key=(SYSTEM_COLUMN,), engine=engine)
191
+ return pk[SYSTEM_COLUMN] if pk else None
144
192
 
145
193
 
146
194
  def reconcile(
@@ -149,27 +197,11 @@ def reconcile(
149
197
  """Mirror every flags row into the alternate database. Catch-up companion of
150
198
  :func:`mirror_row`. Returns ``{"synced": [...], "skipped": [...]}``."""
151
199
  engine = engine or alternate_engine(alternate_db)
152
- synced: list = []
153
- skipped: list = []
154
- rows = list(tx.table(table).select())
155
- with engine.transaction() as alt:
156
- for row in rows:
157
- data = _without_sys(row)
158
- key = data.get(SYSTEM_COLUMN)
159
- if not key:
160
- skipped.append(dict(row).get("sys_id"))
161
- continue
162
- alt.table(table).merge(data, pk={SYSTEM_COLUMN: key})
163
- synced.append(key)
164
- return {"synced": synced, "skipped": skipped}
165
-
166
-
167
- def _toast(context, message: str, level: str) -> None:
168
- """Best-effort operator toast; a context without one is fine (a queue run)."""
169
- response = getattr(context, "response", None)
170
- toast = getattr(response() if callable(response) else None, "toast", None)
171
- if callable(toast):
172
- toast(message, level)
200
+ result = _mirror.reconcile(tx, table=table, key=(SYSTEM_COLUMN,), engine=engine)
201
+ return {
202
+ "synced": [pk[SYSTEM_COLUMN] for pk in result["synced"]],
203
+ "skipped": result["skipped"],
204
+ }
173
205
 
174
206
 
175
207
  def make_mirror_hook(
@@ -187,6 +219,7 @@ def make_mirror_hook(
187
219
 
188
220
  def hook(tx, table_name, sys_id, row, context):
189
221
  del tx, table_name, sys_id
222
+ clear_cache() # the row just changed here: this container re-reads it
190
223
  try:
191
224
  key = mirror_row(row, alternate_db=alternate_db, table=table)
192
225
  if notify and key:
@@ -120,14 +120,15 @@ class SessionControlMixin:
120
120
  # ── switch resolution ────────────────────────────────────────────────
121
121
  def _session_flags(self, tx):
122
122
  """The stage's feature flags (the `single_session` capability's
123
- knobs) {} when there is no tx or no flags table, so sessions never
124
- depend on the flag row existing."""
123
+ knobs) from velocity's container cache no read of their own per
124
+ heartbeat; {} when there is no tx or no flags table, so sessions
125
+ never depend on the flag row existing."""
125
126
  if tx is None:
126
127
  return {}
127
128
  try:
128
129
  from velocity.aws import feature_flags
129
130
 
130
- return feature_flags.get(tx) or {}
131
+ return feature_flags.get_cached(tx) or {}
131
132
  except Exception:
132
133
  return {}
133
134
 
@@ -0,0 +1,267 @@
1
+ """Cross-database row mirroring: one table, two databases on the same
2
+ instance (demo <-> production), rows kept identical no matter where the
3
+ edit happened.
4
+
5
+ Generalised from the feature-flag mirror (:mod:`velocity.aws.feature_flags`
6
+ now delegates here). A row crosses over by its **natural key** — the columns
7
+ that identify it in either database (``applied_system`` for flags;
8
+ ``site_sys_id, env_name, capability`` for site capabilities) — never by
9
+ ``sys_id``, which differs per database; ``sys_*`` bookkeeping never crosses.
10
+
11
+ Foreign keys whose ids differ between databases are the caller's to
12
+ translate: ``translate(tx, alt, data)`` rewrites the outgoing row for the
13
+ alternate database (map a site by its slug, say) or raises
14
+ :class:`MirrorSkip` with the reason the row cannot cross — surfaced to the
15
+ operator as a warning, never an error: the local save always stands.
16
+
17
+ Writes into the alternate database go straight through the table API (no
18
+ handler, no rwx hooks), so a mirror never triggers a mirror back. The hooks
19
+ are warn-and-continue; :func:`reconcile` is the catch-up once an unreachable
20
+ alternate is back.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ ALTERNATE_DB_CONFIG_KEY = "DBAlternate"
30
+ SYS_PREFIX = "sys_"
31
+
32
+ _alternate_engines: dict = {}
33
+
34
+
35
+ class MirrorSkip(Exception):
36
+ """Raised by a ``translate`` to say this row cannot cross, and why."""
37
+
38
+
39
+ def alternate_engine(alternate_db: str | None = None):
40
+ """Engine for the alternate database — same instance, other ``database``.
41
+
42
+ Host/port/user/password are inherited from the running config, so no
43
+ separate credentials are needed; only the database name changes (the
44
+ ``DBAlternate`` config key by default). Cached per database name so
45
+ repeated mirrors in one process reuse the connection.
46
+ """
47
+ from velocity.aws import ssm_config
48
+ from velocity.db.servers import postgres
49
+
50
+ if alternate_db is None:
51
+ alternate_db = ssm_config.getenv(ALTERNATE_DB_CONFIG_KEY, raise_on_missing=True)
52
+ if alternate_db not in _alternate_engines:
53
+ _alternate_engines[alternate_db] = postgres.initialize(
54
+ {"database": alternate_db}
55
+ )
56
+ return _alternate_engines[alternate_db]
57
+
58
+
59
+ def business_columns(row) -> dict:
60
+ """A row's business columns — the ``sys_*`` bookkeeping never crosses over."""
61
+ return {k: v for k, v in dict(row).items() if not str(k).startswith(SYS_PREFIX)}
62
+
63
+
64
+ def natural_key(data, key) -> dict | None:
65
+ """``{column: value}`` for the key columns, or None when any is unset — a
66
+ keyless row is skipped, never mirrored onto an arbitrary target."""
67
+ pk = {}
68
+ for column in key:
69
+ value = data.get(column)
70
+ if value in (None, ""):
71
+ return None
72
+ pk[column] = value
73
+ return pk
74
+
75
+
76
+ def _outgoing(tx, alt, row, key, translate):
77
+ data = business_columns(row)
78
+ if translate is not None:
79
+ data = translate(tx, alt, data)
80
+ if data is None:
81
+ return None, None
82
+ return data, natural_key(data, key)
83
+
84
+
85
+ def mirror_row(
86
+ row,
87
+ *,
88
+ table: str,
89
+ key,
90
+ alternate_db: str | None = None,
91
+ engine=None,
92
+ translate=None,
93
+ tx=None,
94
+ ) -> dict | None:
95
+ """Upsert one row into the alternate database by its natural key.
96
+
97
+ Returns the key used, or None when the row was skipped (keyless, or
98
+ ``translate`` returned None). A ``MirrorSkip`` from ``translate``
99
+ propagates — the hooks turn it into a warning for the operator.
100
+ """
101
+ engine = engine or alternate_engine(alternate_db)
102
+ with engine.transaction() as alt:
103
+ data, pk = _outgoing(tx, alt, row, key, translate)
104
+ if pk is None:
105
+ return None
106
+ alt.table(table).merge(data, pk=pk)
107
+ return pk
108
+
109
+
110
+ def delete_mirror(
111
+ row,
112
+ *,
113
+ table: str,
114
+ key,
115
+ alternate_db: str | None = None,
116
+ engine=None,
117
+ translate=None,
118
+ tx=None,
119
+ ) -> dict | None:
120
+ """Delete the row's counterpart in the alternate database (by natural
121
+ key). Same skip/return contract as :func:`mirror_row`."""
122
+ engine = engine or alternate_engine(alternate_db)
123
+ with engine.transaction() as alt:
124
+ data, pk = _outgoing(tx, alt, row, key, translate)
125
+ if pk is None:
126
+ return None
127
+ alt.table(table).delete(pk)
128
+ return pk
129
+
130
+
131
+ def reconcile(
132
+ tx,
133
+ *,
134
+ table: str,
135
+ key,
136
+ alternate_db: str | None = None,
137
+ engine=None,
138
+ translate=None,
139
+ ) -> dict:
140
+ """Mirror every row of ``table`` into the alternate database — the
141
+ catch-up companion of :func:`mirror_row` (after the alternate was
142
+ unreachable, or for a fresh environment). One-way: rows that exist only
143
+ in the alternate are left alone. Returns ``{"synced": [keys],
144
+ "skipped": [sys_ids]}``."""
145
+ engine = engine or alternate_engine(alternate_db)
146
+ synced: list = []
147
+ skipped: list = []
148
+ rows = list(tx.table(table).select())
149
+ with engine.transaction() as alt:
150
+ for row in rows:
151
+ try:
152
+ data, pk = _outgoing(tx, alt, row, key, translate)
153
+ except MirrorSkip:
154
+ pk = None
155
+ if pk is None:
156
+ skipped.append(dict(row).get("sys_id"))
157
+ continue
158
+ alt.table(table).merge(data, pk=pk)
159
+ synced.append(pk)
160
+ return {"synced": synced, "skipped": skipped}
161
+
162
+
163
+ def toast(context, message: str, level: str) -> None:
164
+ """Best-effort operator toast; a context without one is fine (a queue run)."""
165
+ response = getattr(context, "response", None)
166
+ toaster = getattr(response() if callable(response) else None, "toast", None)
167
+ if callable(toaster):
168
+ toaster(message, level)
169
+
170
+
171
+ def _run(operation, context, what, verbs, notify):
172
+ """Run one mirror operation warn-and-continue; tell the operator."""
173
+ done, undone = verbs
174
+ try:
175
+ pk = operation()
176
+ except MirrorSkip as skip:
177
+ log.warning("%s not %s the alternate database: %s", what, done, skip)
178
+ if notify:
179
+ toast(context, f"{what} {undone} the alternate database: {skip}.", "warning")
180
+ return None
181
+ except Exception as exc: # never block the local write
182
+ log.warning("%s: mirror to the alternate database failed", what, exc_info=True)
183
+ if notify:
184
+ toast(
185
+ context,
186
+ f"{what} {undone} the alternate database ({exc}). Run the "
187
+ "reconcile once it is reachable.",
188
+ "warning",
189
+ )
190
+ return None
191
+ if pk is None:
192
+ return None # keyless (an after_new stub, say): nothing to mirror
193
+ if notify:
194
+ toast(context, f"{what} {done} the alternate database.", "success")
195
+ return pk
196
+
197
+
198
+ def make_mirror_hook(
199
+ *,
200
+ table: str,
201
+ key,
202
+ alternate_db: str | None = None,
203
+ translate=None,
204
+ notify: bool = True,
205
+ label: str | None = None,
206
+ ):
207
+ """An RWX ``after_write`` (or ``after_new``) callable that mirrors the
208
+ saved row. The app's ``rwx/<table>.py`` is a registration line; the
209
+ logic stays here."""
210
+ what = label or table
211
+
212
+ def hook(tx, table_name, sys_id, row, context):
213
+ del table_name, sys_id
214
+ return _run(
215
+ lambda: mirror_row(
216
+ row,
217
+ table=table,
218
+ key=key,
219
+ alternate_db=alternate_db,
220
+ translate=translate,
221
+ tx=tx,
222
+ ),
223
+ context,
224
+ what,
225
+ ("mirrored to", "saved here but not mirrored to"),
226
+ notify,
227
+ )
228
+
229
+ return hook
230
+
231
+
232
+ def make_delete_hook(
233
+ *,
234
+ table: str,
235
+ key,
236
+ alternate_db: str | None = None,
237
+ translate=None,
238
+ notify: bool = True,
239
+ label: str | None = None,
240
+ ):
241
+ """An RWX ``before_delete`` callable that removes the row's counterpart.
242
+ ``before`` because the row must still exist here to read its natural
243
+ key; the local delete proceeds whatever happens on the other side."""
244
+ what = label or table
245
+
246
+ def hook(tx, table_name, sys_id, context):
247
+ if tx is None:
248
+ return None
249
+ row = tx.table(table_name).find({"sys_id": sys_id})
250
+ if not row:
251
+ return None
252
+ return _run(
253
+ lambda: delete_mirror(
254
+ row,
255
+ table=table,
256
+ key=key,
257
+ alternate_db=alternate_db,
258
+ translate=translate,
259
+ tx=tx,
260
+ ),
261
+ context,
262
+ what,
263
+ ("removed from", "deleted here but not removed from"),
264
+ notify,
265
+ )
266
+
267
+ return hook
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.107
3
+ Version: 0.1.109
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
@@ -251,6 +251,7 @@ Full documentation is included in the `docs/` directory of the source distributi
251
251
  | Async Support | `docs/async.md` | AsyncTransaction, AsyncTable, AsyncResult, parallel queries with gather() |
252
252
  | Configuration Reference | `docs/configuration.md` | All environment variables, engine options, and connection settings |
253
253
  | AWS Lambda Handlers | `docs/aws-handlers.md` | LambdaHandler, SqsHandler, auth modes, per-record transactions, async jobs (JobHandler + job result stores) |
254
+ | Cross-database Mirroring | `docs/mirroring.md` | Keep a table identical in two databases (demo ↔ production) by natural key; rwx hooks, FK translation, reconcile |
254
255
  | NoSQL Adapters | `docs/nosql.md` | Redis, DynamoDB, MongoDB, Elasticsearch, Cassandra, Couchbase behind one capability-honest contract |
255
256
  | Payment Processing | `docs/payment.md` | Stripe and Braintree adapters, payment lifecycle |
256
257
  | Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing and SMTP sending |
@@ -12,6 +12,7 @@ src/velocity/aws/email.py
12
12
  src/velocity/aws/feature_flags.py
13
13
  src/velocity/aws/job_lifecycle.py
14
14
  src/velocity/aws/job_results.py
15
+ src/velocity/aws/mirror.py
15
16
  src/velocity/aws/s3.py
16
17
  src/velocity/aws/sms.py
17
18
  src/velocity/aws/ssm_config.py
@@ -242,6 +243,7 @@ tests/test_lambda_handler.py
242
243
  tests/test_lambda_handler_auth.py
243
244
  tests/test_lambda_handler_masquerade.py
244
245
  tests/test_masquerade_grant.py
246
+ tests/test_mirror.py
245
247
  tests/test_mixins_import.py
246
248
  tests/test_n_plus_one.py
247
249
  tests/test_nosql_dynamodb.py