velocity-python 0.1.109__tar.gz → 0.1.111__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 (300) hide show
  1. {velocity_python-0.1.109/src/velocity_python.egg-info → velocity_python-0.1.111}/PKG-INFO +1 -1
  2. {velocity_python-0.1.109 → velocity_python-0.1.111}/pyproject.toml +1 -1
  3. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/base.py +14 -4
  5. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/security.py +11 -11
  6. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/feature_flags.py +21 -20
  7. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/context.py +94 -45
  8. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/lambda_handler.py +26 -5
  9. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/data_service.py +11 -2
  10. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/mirror.py +3 -1
  11. velocity_python-0.1.111/src/velocity/cache.py +188 -0
  12. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/table.py +130 -16
  13. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/transaction.py +7 -1
  14. {velocity_python-0.1.109 → velocity_python-0.1.111/src/velocity_python.egg-info}/PKG-INFO +1 -1
  15. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/SOURCES.txt +5 -0
  16. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_capability_catalog.py +6 -1
  17. velocity_python-0.1.111/tests/test_cognito_user_cache.py +93 -0
  18. velocity_python-0.1.111/tests/test_lambda_handler_user_cache.py +87 -0
  19. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_mirror.py +35 -10
  20. velocity_python-0.1.111/tests/test_read_cache.py +119 -0
  21. velocity_python-0.1.111/tests/test_table_find_cache.py +178 -0
  22. {velocity_python-0.1.109 → velocity_python-0.1.111}/LICENSE +0 -0
  23. {velocity_python-0.1.109 → velocity_python-0.1.111}/README.md +0 -0
  24. {velocity_python-0.1.109 → velocity_python-0.1.111}/setup.cfg +0 -0
  25. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/__init__.py +0 -0
  26. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/amplify.py +0 -0
  27. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/amplify_build.py +0 -0
  28. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/__init__.py +0 -0
  29. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/backfill.py +0 -0
  30. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/indexing.py +0 -0
  31. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/references.py +0 -0
  32. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/service.py +0 -0
  33. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/assets/usage_index.py +0 -0
  34. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/blobs.py +0 -0
  35. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/__init__.py +0 -0
  36. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/dispatch.py +0 -0
  37. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/infra.py +0 -0
  38. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/jobs.py +0 -0
  39. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/provisioning.py +0 -0
  40. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/retention.py +0 -0
  41. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/capabilities/single_session.py +0 -0
  42. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/__init__.py +0 -0
  43. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/config.py +0 -0
  44. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/operations.py +0 -0
  45. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/policies.py +0 -0
  46. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/reconcile.py +0 -0
  47. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/deploy/targets.py +0 -0
  48. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/dirty_pipeline.py +0 -0
  49. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/email.py +0 -0
  50. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/__init__.py +0 -0
  51. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/base_handler.py +0 -0
  52. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/context_factory.py +0 -0
  53. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/exceptions.py +0 -0
  54. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/masquerade.py +0 -0
  55. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  56. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  57. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
  58. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  59. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/perf.py +0 -0
  60. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/response.py +0 -0
  61. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  62. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/job_lifecycle.py +0 -0
  63. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/job_results.py +0 -0
  64. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/s3.py +0 -0
  65. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/sms.py +0 -0
  66. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/ssm_config.py +0 -0
  67. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/__init__.py +0 -0
  68. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  69. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  70. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  71. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  72. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  73. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_email.py +0 -0
  74. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  75. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_response.py +0 -0
  76. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/aws/tests/test_sms.py +0 -0
  77. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/__init__.py +0 -0
  78. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/__init__.py +0 -0
  79. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/async_support.py +0 -0
  80. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/column.py +0 -0
  81. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/database.py +0 -0
  82. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/decorators.py +0 -0
  83. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/engine.py +0 -0
  84. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/jsonproxy.py +0 -0
  85. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/result.py +0 -0
  86. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/row.py +0 -0
  87. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/sequence.py +0 -0
  88. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/core/view.py +0 -0
  89. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/exceptions.py +0 -0
  90. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/maintenance.py +0 -0
  91. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/migrations.py +0 -0
  92. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/__init__.py +0 -0
  93. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/__init__.py +0 -0
  94. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/initializer.py +0 -0
  95. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/operators.py +0 -0
  96. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/sql.py +0 -0
  97. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/base/types.py +0 -0
  98. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/__init__.py +0 -0
  99. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/operators.py +0 -0
  100. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/reserved.py +0 -0
  101. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/sql.py +0 -0
  102. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/mysql/types.py +0 -0
  103. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/__init__.py +0 -0
  104. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/operators.py +0 -0
  105. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/reserved.py +0 -0
  106. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/sql.py +0 -0
  107. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/postgres/types.py +0 -0
  108. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  109. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/operators.py +0 -0
  110. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  111. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/sql.py +0 -0
  112. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlite/types.py +0 -0
  113. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  114. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  115. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  116. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  117. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/sqlserver/types.py +0 -0
  118. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/servers/tablehelper.py +0 -0
  119. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/__init__.py +0 -0
  120. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/common_db_test.py +0 -0
  121. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/__init__.py +0 -0
  122. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/common.py +0 -0
  123. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/conftest.py +0 -0
  124. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_column.py +0 -0
  125. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  126. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_database.py +0 -0
  127. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  128. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  129. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  130. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_result.py +0 -0
  131. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_row.py +0 -0
  132. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  133. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  134. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  135. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  136. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  137. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_table.py +0 -0
  138. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  139. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  140. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/__init__.py +0 -0
  141. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/common.py +0 -0
  142. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  143. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  144. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  145. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_db_utils.py +0 -0
  146. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_postgres.py +0 -0
  147. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  148. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  149. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_result_caching.py +0 -0
  150. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  151. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  152. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  153. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  154. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_sql_builder.py +0 -0
  155. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_tablehelper.py +0 -0
  156. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/tests/test_view_helper.py +0 -0
  157. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/db/utils.py +0 -0
  158. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/logging.py +0 -0
  159. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/__init__.py +0 -0
  160. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/conv/__init__.py +0 -0
  161. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/conv/iconv.py +0 -0
  162. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/conv/oconv.py +0 -0
  163. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/db.py +0 -0
  164. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/export.py +0 -0
  165. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/format.py +0 -0
  166. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/mail.py +0 -0
  167. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/merge.py +0 -0
  168. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/pdf.py +0 -0
  169. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/__init__.py +0 -0
  170. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_db.py +0 -0
  171. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_fix.py +0 -0
  172. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_format.py +0 -0
  173. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_iconv.py +0 -0
  174. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_mail_send.py +0 -0
  175. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_merge.py +0 -0
  176. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_oconv.py +0 -0
  177. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_original_error.py +0 -0
  178. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tests/test_timer.py +0 -0
  179. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/timer.py +0 -0
  180. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/misc/tools.py +0 -0
  181. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/__init__.py +0 -0
  182. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/__init__.py +0 -0
  183. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/capabilities.py +0 -0
  184. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/store.py +0 -0
  185. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/core/where.py +0 -0
  186. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/exceptions.py +0 -0
  187. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/__init__.py +0 -0
  188. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/base/__init__.py +0 -0
  189. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  190. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  191. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  192. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  193. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  194. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  195. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  196. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  197. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  198. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  199. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  200. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  201. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/__init__.py +0 -0
  202. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/authorizenet_adapter.py +0 -0
  203. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/authorizenet_mirror.py +0 -0
  204. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/base_adapter.py +0 -0
  205. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/braintree_adapter.py +0 -0
  206. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/braintree_mirror.py +0 -0
  207. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/charge_rules.py +0 -0
  208. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/stripe_adapter.py +0 -0
  209. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity/payment/stripe_mirror.py +0 -0
  210. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  211. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/entry_points.txt +0 -0
  212. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/requires.txt +0 -0
  213. {velocity_python-0.1.109 → velocity_python-0.1.111}/src/velocity_python.egg-info/top_level.txt +0 -0
  214. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_access_error_403.py +0 -0
  215. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_activity_tracking_aborted_tx.py +0 -0
  216. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_amplify_build.py +0 -0
  217. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_asset_indexing.py +0 -0
  218. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_asset_references.py +0 -0
  219. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_assets_service.py +0 -0
  220. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_async_support.py +0 -0
  221. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_batch_operations.py +0 -0
  222. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_blob_store.py +0 -0
  223. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_capabilities.py +0 -0
  224. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_column_tx_arg.py +0 -0
  225. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_concurrency_safety.py +0 -0
  226. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_connection_pool.py +0 -0
  227. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_connection_resilience.py +0 -0
  228. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_context_flags_loader.py +0 -0
  229. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_context_job_descriptions.py +0 -0
  230. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_data_service_query_cap.py +0 -0
  231. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_data_service_renumber.py +0 -0
  232. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_data_service_save_as.py +0 -0
  233. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_db_credentials_ssm_cascade.py +0 -0
  234. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_db_maintenance.py +0 -0
  235. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_decorators.py +0 -0
  236. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_delete_hook_fk_blocked.py +0 -0
  237. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_dirty_pipeline_fast_path.py +0 -0
  238. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_email_processing.py +0 -0
  239. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_enqueue_send_failures.py +0 -0
  240. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_feature_flags.py +0 -0
  241. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_get_cognito_user_provider.py +0 -0
  242. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_http_handler_rollback.py +0 -0
  243. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_iconv_money_to_cents.py +0 -0
  244. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_identifier_injection_guard.py +0 -0
  245. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_job_handler.py +0 -0
  246. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_job_lifecycle.py +0 -0
  247. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_job_results.py +0 -0
  248. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_json_columns.py +0 -0
  249. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_jsonb_dict_adapter.py +0 -0
  250. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_lambda_handler.py +0 -0
  251. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_lambda_handler_auth.py +0 -0
  252. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_lambda_handler_masquerade.py +0 -0
  253. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_masquerade_grant.py +0 -0
  254. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_mixins_import.py +0 -0
  255. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_n_plus_one.py +0 -0
  256. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_dynamodb.py +0 -0
  257. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_memory.py +0 -0
  258. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_mongodb.py +0 -0
  259. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_redis.py +0 -0
  260. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_translation.py +0 -0
  261. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_nosql_where.py +0 -0
  262. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_observability.py +0 -0
  263. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_on_commit.py +0 -0
  264. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_authorizenet_adapter.py +0 -0
  265. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_braintree_adapter.py +0 -0
  266. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_braintree_mirror.py +0 -0
  267. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_mirror_empty_table.py +0 -0
  268. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_profile_sorting.py +0 -0
  269. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_payment_stripe_adapter.py +0 -0
  270. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_pdf.py +0 -0
  271. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_prepared_statements.py +0 -0
  272. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_psycopg3_upgrade.py +0 -0
  273. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_query_cache.py +0 -0
  274. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_query_missing_table_unlocked.py +0 -0
  275. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_response.py +0 -0
  276. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_restricted_direct_tables.py +0 -0
  277. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_retry_side_effect_guard.py +0 -0
  278. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_return_default_safety.py +0 -0
  279. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_row_batch_update.py +0 -0
  280. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_row_cache_staleness.py +0 -0
  281. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_schema_migrations.py +0 -0
  282. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_security_capability.py +0 -0
  283. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_security_hardening.py +0 -0
  284. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_server_cursor.py +0 -0
  285. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_session_control.py +0 -0
  286. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_single_autocommit_safety.py +0 -0
  287. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_single_session_capability.py +0 -0
  288. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_spreadsheet_functions.py +0 -0
  289. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_sqlite_backend.py +0 -0
  290. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_sqs_per_record_transactions.py +0 -0
  291. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_ssm_config.py +0 -0
  292. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_store_user_data.py +0 -0
  293. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  294. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_table_alter.py +0 -0
  295. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_table_purge.py +0 -0
  296. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_transaction_class_wrapping.py +0 -0
  297. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_transaction_commit_and_ownership.py +0 -0
  298. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_transaction_edge_cases.py +0 -0
  299. {velocity_python-0.1.109 → velocity_python-0.1.111}/tests/test_where_clause_validation.py +0 -0
  300. {velocity_python-0.1.109 → velocity_python-0.1.111}/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.109
3
+ Version: 0.1.111
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.109"
7
+ version = "0.1.111"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.109"
1
+ __version__ = version = "0.1.111"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -55,7 +55,16 @@ TEXT = "text"
55
55
  BOOLEAN = "boolean"
56
56
  CHOICE = "choice" # one of `options`
57
57
  LIST = "list" # free strings
58
- POLICY_KINDS = (CIDR_LIST, COUNTRY_LIST, TABLE_DAYS, NUMBER, TEXT, BOOLEAN, CHOICE, LIST)
58
+ POLICY_KINDS = (
59
+ CIDR_LIST,
60
+ COUNTRY_LIST,
61
+ TABLE_DAYS,
62
+ NUMBER,
63
+ TEXT,
64
+ BOOLEAN,
65
+ CHOICE,
66
+ LIST,
67
+ )
59
68
 
60
69
  IDENTIFIER = re.compile(r"^[a-z_][a-z0-9_]*$")
61
70
  COUNTRY_CODE = re.compile(r"^[A-Z]{2}$")
@@ -145,7 +154,9 @@ def describe(cap) -> dict:
145
154
  def catalog() -> list:
146
155
  """Every registered capability, described, ordered by label — the
147
156
  pick-list an admin UI shows."""
148
- return sorted((describe(cap) for cap in REGISTRY.values()), key=lambda d: d["label"])
157
+ return sorted(
158
+ (describe(cap) for cap in REGISTRY.values()), key=lambda d: d["label"]
159
+ )
149
160
 
150
161
 
151
162
  def load_config(raw) -> dict:
@@ -232,8 +243,7 @@ def validate_policy(name, config) -> list:
232
243
  if value in (None, "", [], {}):
233
244
  continue
234
245
  problems.extend(
235
- f"{field['label']}: {problem}"
236
- for problem in _check_field(field, value)
246
+ f"{field['label']}: {problem}" for problem in _check_field(field, value)
237
247
  )
238
248
  return problems
239
249
 
@@ -27,9 +27,9 @@ from __future__ import annotations
27
27
  import datetime
28
28
  import ipaddress
29
29
  import logging
30
- import time
31
30
 
32
31
  from velocity.aws import feature_flags
32
+ from velocity.cache import memo, read_cache
33
33
 
34
34
  from .base import (
35
35
  CIDR_LIST,
@@ -62,12 +62,10 @@ KNOBS = {
62
62
  "security_events_days": "90",
63
63
  }
64
64
 
65
- _cache: dict = {}
66
-
67
65
 
68
66
  def clear_cache():
69
67
  """Forget the cached site row AND the shared flags row (tests)."""
70
- _cache.clear()
68
+ read_cache.invalidate(SITE_TABLE)
71
69
  feature_flags.clear_cache()
72
70
 
73
71
 
@@ -174,13 +172,15 @@ def check_rate(tx, bucket, *, limit, now, table=RATE_TABLE):
174
172
 
175
173
 
176
174
  def _cached(key, loader, *, now=None):
177
- now = now or time.monotonic()
178
- hit = _cache.get(key)
179
- if hit and hit[0] > now:
180
- return hit[1]
181
- value = loader()
182
- _cache[key] = (now + CACHE_TTL_SECONDS, value)
183
- return value
175
+ """The process read cache (velocity.cache), tagged with the site table
176
+ so provisioning's own writes to it in this process drop the entry."""
177
+ return memo(
178
+ ("security",) + tuple(key),
179
+ loader,
180
+ CACHE_TTL_SECONDS,
181
+ tables=(SITE_TABLE,),
182
+ now=now,
183
+ )
184
184
 
185
185
 
186
186
  def _site_row(tx):
@@ -32,9 +32,9 @@ mechanism applied to the flags table, keyed by ``applied_system``.
32
32
  from __future__ import annotations
33
33
 
34
34
  import logging
35
- import time
36
35
 
37
36
  from velocity.aws import mirror as _mirror
37
+ from velocity.cache import memo, read_cache
38
38
 
39
39
  log = logging.getLogger(__name__)
40
40
 
@@ -46,7 +46,6 @@ CACHE_SECONDS_CONFIG_KEY = "FeatureFlagCacheSeconds"
46
46
  DEFAULT_CACHE_SECONDS = 60
47
47
 
48
48
  _alternate_engines = _mirror._alternate_engines
49
- _cache: dict = {} # (system, table) -> (expires_at, row)
50
49
  _cache_seconds: float | None = None
51
50
 
52
51
 
@@ -102,37 +101,39 @@ def cache_seconds() -> float:
102
101
  return _cache_seconds
103
102
 
104
103
 
105
- def clear_cache() -> None:
104
+ def clear_cache(table: str = DEFAULT_TABLE) -> None:
106
105
  """Forget cached rows (after a local flags write; tests)."""
107
106
  global _cache_seconds
108
- _cache.clear()
107
+ read_cache.invalidate(table)
109
108
  _cache_seconds = None
110
109
 
111
110
 
112
111
  def get_cached(
113
- tx, system: str | None = None, *, table: str = DEFAULT_TABLE, now: float | None = None
112
+ tx,
113
+ system: str | None = None,
114
+ *,
115
+ table: str = DEFAULT_TABLE,
116
+ now: float | None = None,
114
117
  ) -> dict:
115
- """:func:`get` behind a per-container TTL cache — the hot-path reader.
118
+ """:func:`get` through the process read cache (:mod:`velocity.cache`)
119
+ — the hot-path reader.
116
120
 
117
121
  The flags row is ONE row per stage, and every reader in a request
118
122
  (the handler context, the security guards, session control, any
119
123
  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
+ :func:`cache_seconds` per container, however many readers there are.
125
+ Tagged with the table, so a flags write through the table API in this
126
+ process (the mirror hook, a save) drops it at once; the other
127
+ environment sees the change within the TTL.
124
128
  """
125
129
  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))
130
+ row = memo(
131
+ ("feature_flags", table, system),
132
+ lambda: dict(get(tx, system, table=table)),
133
+ cache_seconds(),
134
+ tables=(table,),
135
+ now=now,
136
+ )
136
137
  return dict(row)
137
138
 
138
139
 
@@ -17,6 +17,7 @@ from velocity.aws.ssm_config import get_project_name as config_get_project_name
17
17
  from velocity.aws.ssm_config import get_region as config_get_region
18
18
  from velocity.aws.ssm_config import get_stage as config_get_stage
19
19
  from velocity.logging import get_logger
20
+ from velocity.cache import memo
20
21
 
21
22
  engine = velocity.db.postgres.initialize()
22
23
 
@@ -81,6 +82,40 @@ def _get_controls_vars() -> dict:
81
82
  }
82
83
 
83
84
 
85
+ # ── the signed-in user, cached per container ─────────────────────────────
86
+ USER_CACHE_CONFIG_KEY = "UserCacheSeconds"
87
+ DEFAULT_USER_CACHE_SECONDS = 60
88
+ _user_cache_seconds = None
89
+
90
+
91
+ def user_cache_seconds() -> float:
92
+ """Seconds a warm container may reuse a signed-in user's Cognito record and
93
+ user-table row: the ``UserCacheSeconds`` config key, default 60. A disabled
94
+ or deleted user keeps that container's access until it expires — accepted
95
+ policy (Paul, 2026-09-13). ``0`` looks the user up on every request."""
96
+ global _user_cache_seconds
97
+ if _user_cache_seconds is None:
98
+ raw = None
99
+ try:
100
+ raw = config_getenv(USER_CACHE_CONFIG_KEY, None)
101
+ except Exception:
102
+ raw = None
103
+ try:
104
+ _user_cache_seconds = (
105
+ float(raw)
106
+ if raw not in (None, "")
107
+ else float(DEFAULT_USER_CACHE_SECONDS)
108
+ )
109
+ except (TypeError, ValueError):
110
+ _user_cache_seconds = float(DEFAULT_USER_CACHE_SECONDS)
111
+ return _user_cache_seconds
112
+
113
+
114
+ def clear_user_cache_config() -> None:
115
+ global _user_cache_seconds
116
+ _user_cache_seconds = None
117
+
118
+
84
119
  @engine.transaction
85
120
  class Context:
86
121
 
@@ -807,63 +842,77 @@ class Context:
807
842
  "cannot resolve a Cognito user"
808
843
  ) from None
809
844
 
810
- try:
811
- self.perf.start("cognito admin_get_user")
812
- response = cognito_client.admin_get_user(
813
- UserPoolId=user_pool_id,
814
- Username=user_sub,
815
- )
816
- self.perf.log("cognito admin_get_user")
817
- resolved_username = response.get("Username")
818
- except cognito_client.exceptions.UserNotFoundException:
845
+ # one Cognito round trip per user per UserCacheSeconds per container
846
+ # (velocity.cache); a lookup that fails is never cached
847
+ def resolve():
819
848
  try:
820
- self.perf.start("cognito list_users")
821
- list_response = cognito_client.list_users(
849
+ self.perf.start("cognito admin_get_user")
850
+ response = cognito_client.admin_get_user(
822
851
  UserPoolId=user_pool_id,
823
- Filter=f'sub = "{user_sub}"',
824
- Limit=1,
852
+ Username=user_sub,
825
853
  )
826
- self.perf.log("cognito list_users")
854
+ self.perf.log("cognito admin_get_user")
855
+ resolved_username = response.get("Username")
856
+ except cognito_client.exceptions.UserNotFoundException:
857
+ try:
858
+ self.perf.start("cognito list_users")
859
+ list_response = cognito_client.list_users(
860
+ UserPoolId=user_pool_id,
861
+ Filter=f'sub = "{user_sub}"',
862
+ Limit=1,
863
+ )
864
+ self.perf.log("cognito list_users")
865
+ except ClientError as exc:
866
+ message = exc.response.get("Error", {}).get(
867
+ "Message", "Unknown error"
868
+ )
869
+ raise AlertError(
870
+ f"Failed to search Cognito user by sub: {message}"
871
+ ) from None
872
+
873
+ users = list_response.get("Users", [])
874
+ if not users:
875
+ raise AlertError("Cognito user not found") from None
876
+
877
+ user = users[0]
878
+ response = {
879
+ "Username": user.get("Username"),
880
+ "UserAttributes": user.get("Attributes", []),
881
+ "Enabled": user.get("Enabled"),
882
+ "UserStatus": user.get("UserStatus"),
883
+ "UserCreateDate": user.get("UserCreateDate"),
884
+ "UserLastModifiedDate": user.get("UserLastModifiedDate"),
885
+ }
886
+ resolved_username = response.get("Username")
827
887
  except ClientError as exc:
828
888
  message = exc.response.get("Error", {}).get("Message", "Unknown error")
829
889
  raise AlertError(
830
- f"Failed to search Cognito user by sub: {message}"
890
+ f"Failed to retrieve Cognito user: {message}"
831
891
  ) from None
832
892
 
833
- users = list_response.get("Users", [])
834
- if not users:
835
- raise AlertError("Cognito user not found") from None
836
-
837
- user = users[0]
838
- response = {
839
- "Username": user.get("Username"),
840
- "UserAttributes": user.get("Attributes", []),
841
- "Enabled": user.get("Enabled"),
842
- "UserStatus": user.get("UserStatus"),
843
- "UserCreateDate": user.get("UserCreateDate"),
844
- "UserLastModifiedDate": user.get("UserLastModifiedDate"),
893
+ attributes = {
894
+ attr["Name"]: attr.get("Value")
895
+ for attr in response.get("UserAttributes", [])
845
896
  }
846
- resolved_username = response.get("Username")
847
- except ClientError as exc:
848
- message = exc.response.get("Error", {}).get("Message", "Unknown error")
849
- raise AlertError(f"Failed to retrieve Cognito user: {message}") from None
850
-
851
- attributes = {
852
- attr["Name"]: attr.get("Value")
853
- for attr in response.get("UserAttributes", [])
854
- }
855
897
 
856
- user = {
857
- "username": resolved_username or user_sub,
858
- "email": attributes.get("email"),
859
- "attributes": attributes,
860
- "enabled": response.get("Enabled"),
861
- "user_status": response.get("UserStatus"),
862
- "sub": attributes.get("sub", user_sub),
863
- }
898
+ user = {
899
+ "username": resolved_username or user_sub,
900
+ "email": attributes.get("email"),
901
+ "attributes": attributes,
902
+ "enabled": response.get("Enabled"),
903
+ "user_status": response.get("UserStatus"),
904
+ "sub": attributes.get("sub", user_sub),
905
+ }
864
906
 
865
- self.perf.log("get_cognito_user total")
907
+ return user
866
908
 
909
+ user = memo(
910
+ ("cognito", user_pool_id, user_sub),
911
+ resolve,
912
+ user_cache_seconds(),
913
+ cache_none=False,
914
+ )
915
+ self.perf.log("get_cognito_user total")
867
916
  return user
868
917
 
869
918
  def get_cognito_user_optional(self, aws_event):
@@ -161,15 +161,13 @@ class LambdaHandler(BaseHandler):
161
161
  )
162
162
 
163
163
  context.perf.start("user lookup")
164
- row = tx.table(self.user_table).find(
165
- {"email_address": session.get("email_address")}
166
- )
164
+ user = self._cached_user(tx, session.get("email_address"))
167
165
  context.perf.log("user lookup")
168
- if not row:
166
+ if not user:
169
167
  raise Exception(
170
168
  "A valid user with permission is required to access this function [DB]"
171
169
  )
172
- self.current_user = row.to_dict()
170
+ self.current_user = user
173
171
 
174
172
  temp = copy.deepcopy(context.postdata() or {})
175
173
  payload = temp.get("payload")
@@ -181,6 +179,29 @@ class LambdaHandler(BaseHandler):
181
179
  extra={"payload": pprint.pformat(temp)},
182
180
  )
183
181
 
182
+ def _cached_user(self, tx, email_address):
183
+ """The user-table row for the signed-in user as a dict, through the
184
+ process read cache (velocity.cache) for ``UserCacheSeconds``: one
185
+ query per TTL per container instead of one per request. A miss is
186
+ never cached, so a user created a moment ago signs in at once; a
187
+ write to the user table in this process drops the entry."""
188
+ from velocity.cache import memo
189
+
190
+ from velocity.aws.handlers.context import user_cache_seconds
191
+
192
+ def load():
193
+ row = tx.table(self.user_table).find({"email_address": email_address})
194
+ return row.to_dict() if row else None
195
+
196
+ user = memo(
197
+ ("user", self.user_table, email_address),
198
+ load,
199
+ user_cache_seconds(),
200
+ tables=(self.user_table,),
201
+ cache_none=False,
202
+ )
203
+ return dict(user) if user else None
204
+
184
205
  def afterAction(self, tx, context):
185
206
  self._enhanced_after_action(tx, context)
186
207
  logger.debug("starting LamdaHandler.afterAction")
@@ -20,6 +20,10 @@ from velocity.aws.handlers.exceptions import AlertError
20
20
  from velocity.db.exceptions import DbDataIntegrityError
21
21
  from velocity.misc import export
22
22
 
23
+ # a table's column list changes only on deploy (schema sync) or a lazy ALTER
24
+ # in this process (which drops the entry): reuse it across requests
25
+ SCHEMA_CACHE_SECONDS = 300
26
+
23
27
  logger = logging.getLogger(__name__)
24
28
 
25
29
  # Postgres FK-violation text, e.g.:
@@ -522,7 +526,9 @@ class DataServiceMixin:
522
526
  order_dir = parts[1].lower()
523
527
 
524
528
  if not order_col:
525
- available = [c for c in tx.table(obj).sys_columns()]
529
+ available = [
530
+ c for c in tx.table(obj).sys_columns(cache_ttl=SCHEMA_CACHE_SECONDS)
531
+ ]
526
532
  lowered = {c.lower(): c for c in available}
527
533
  order_col = lowered.get("sys_id") or lowered.get("sys_created")
528
534
  if not order_col and available:
@@ -551,7 +557,10 @@ class DataServiceMixin:
551
557
  _inject_row_number_expression(params)
552
558
  requested_cols = _extract_requested_column_names(params.get("columns"))
553
559
  if requested_cols:
554
- available_cols = {c.lower() for c in tx.table(obj).sys_columns()}
560
+ available_cols = {
561
+ c.lower()
562
+ for c in tx.table(obj).sys_columns(cache_ttl=SCHEMA_CACHE_SECONDS)
563
+ }
555
564
  missing = sorted(
556
565
  {c for c in requested_cols if c.lower() not in available_cols},
557
566
  key=lambda v: v.lower(),
@@ -176,7 +176,9 @@ def _run(operation, context, what, verbs, notify):
176
176
  except MirrorSkip as skip:
177
177
  log.warning("%s not %s the alternate database: %s", what, done, skip)
178
178
  if notify:
179
- toast(context, f"{what} {undone} the alternate database: {skip}.", "warning")
179
+ toast(
180
+ context, f"{what} {undone} the alternate database: {skip}.", "warning"
181
+ )
180
182
  return None
181
183
  except Exception as exc: # never block the local write
182
184
  log.warning("%s: mirror to the alternate database failed", what, exc_info=True)
@@ -0,0 +1,188 @@
1
+ """Process-level read cache: reads that may be reused across requests.
2
+
3
+ velocity's transaction cache (``select(..., cache=True)``) lives and dies
4
+ with one transaction. This store outlives requests on a warm Lambda
5
+ container (or any long-lived process), so a read that is the same for
6
+ every request — a feature-flag row, a capability row, the signed-in user,
7
+ a table's column list, a Cognito lookup — costs one round trip per TTL
8
+ instead of one per request. One store, one policy, no per-module dicts.
9
+
10
+ Three ways in:
11
+
12
+ * table reads — ``tx.table(t).find(where, cache_ttl=60)``,
13
+ ``exists(cache_ttl=60)``, ``sys_columns(cache_ttl=300)``,
14
+ ``select(..., cache_ttl=60)`` — keyed by database + SQL + params;
15
+ * :func:`memo` — any loader (an API call, a composite of reads), tagged
16
+ with the tables it depends on;
17
+ * :data:`read_cache` — explicit ``invalidate`` / ``clear`` / ``stats``.
18
+
19
+ **Invalidation.** A write through the table API (insert / update / delete /
20
+ merge / upsert / truncate) or DDL on table *T* in this process drops every
21
+ entry tagged *T* at once; a raw ``tx.execute`` write is classified and does
22
+ the same. Other processes see the change when their TTL expires — that
23
+ bounded staleness is the contract a caller accepts by naming a TTL.
24
+ ``VELOCITY_READ_CACHE=0`` disables the store everywhere (tests, debugging).
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ import threading
31
+ import time
32
+ from collections import OrderedDict
33
+
34
+ ENV_KEY = "VELOCITY_READ_CACHE"
35
+ MAX_ENTRIES = 1024
36
+ # tag for entries that describe the schema (table list, columns): DDL drops
37
+ # them; ordinary writes do not
38
+ SCHEMA_TAG = "__schema__"
39
+
40
+ _MISS = object()
41
+
42
+
43
+ def _norm(table) -> str:
44
+ return str(table).strip().lower()
45
+
46
+
47
+ class ReadCache:
48
+ """A TTL + LRU store of read results, indexed by the tables they depend on."""
49
+
50
+ def __init__(self, max_entries: int = MAX_ENTRIES):
51
+ self._entries: OrderedDict = OrderedDict() # key -> (expires_at, value, tags)
52
+ self._by_table: dict = {} # tag -> set(keys)
53
+ self._lock = threading.RLock()
54
+ self._max = max_entries
55
+ self._enabled: bool | None = None
56
+ self.hits = 0
57
+ self.misses = 0
58
+
59
+ # ── switch ──────────────────────────────────────────────────────────
60
+ def enabled(self) -> bool:
61
+ if self._enabled is None:
62
+ raw = os.environ.get(ENV_KEY, "").strip().lower()
63
+ self._enabled = raw not in ("0", "false", "off", "no")
64
+ return self._enabled
65
+
66
+ def configure(self, *, enabled: bool | None) -> None:
67
+ """Force the switch (``None`` re-reads the environment)."""
68
+ self._enabled = enabled
69
+
70
+ # ── core ────────────────────────────────────────────────────────────
71
+ def get(self, key, *, now: float | None = None):
72
+ """``(hit, value)``; an expired entry is a miss and is dropped."""
73
+ if not self.enabled():
74
+ return False, None
75
+ now = time.monotonic() if now is None else now
76
+ with self._lock:
77
+ entry = self._entries.get(key)
78
+ if entry is None:
79
+ self.misses += 1
80
+ return False, None
81
+ expires_at, value, _tags = entry
82
+ if expires_at <= now:
83
+ self._drop(key)
84
+ self.misses += 1
85
+ return False, None
86
+ self._entries.move_to_end(key)
87
+ self.hits += 1
88
+ return True, value
89
+
90
+ def put(
91
+ self, key, value, ttl: float, *, tables=(), now: float | None = None
92
+ ) -> None:
93
+ if not self.enabled() or ttl is None or ttl <= 0:
94
+ return
95
+ now = time.monotonic() if now is None else now
96
+ tags = frozenset(_norm(t) for t in tables if t)
97
+ with self._lock:
98
+ if key in self._entries:
99
+ self._drop(key)
100
+ self._entries[key] = (now + ttl, value, tags)
101
+ for tag in tags:
102
+ self._by_table.setdefault(tag, set()).add(key)
103
+ while len(self._entries) > self._max:
104
+ oldest, _ = self._entries.popitem(last=False)
105
+ self._untag(oldest)
106
+
107
+ def memo(
108
+ self, key, loader, ttl: float, *, tables=(), cache_none: bool = True, now=None
109
+ ):
110
+ """``loader()`` once per ``ttl`` for ``key``. ``cache_none=False`` never
111
+ stores a ``None`` result (a "not found" that may exist a moment later)."""
112
+ hit, value = self.get(key, now=now)
113
+ if hit:
114
+ return value
115
+ value = loader()
116
+ if value is not None or cache_none:
117
+ self.put(key, value, ttl, tables=tables, now=now)
118
+ return value
119
+
120
+ # ── invalidation ────────────────────────────────────────────────────
121
+ def invalidate(self, table=None) -> int:
122
+ """Drop every entry tagged ``table`` (all entries when ``None``).
123
+ Returns how many were dropped."""
124
+ with self._lock:
125
+ if table is None:
126
+ count = len(self._entries)
127
+ self._entries.clear()
128
+ self._by_table.clear()
129
+ return count
130
+ keys = list(self._by_table.get(_norm(table), ()))
131
+ for key in keys:
132
+ self._drop(key)
133
+ return len(keys)
134
+
135
+ def invalidate_schema(self) -> int:
136
+ """DDL happened: drop the table list / column entries."""
137
+ return self.invalidate(SCHEMA_TAG)
138
+
139
+ def clear(self) -> None:
140
+ with self._lock:
141
+ self.invalidate(None)
142
+ self.hits = 0
143
+ self.misses = 0
144
+
145
+ def _drop(self, key) -> None:
146
+ self._entries.pop(key, None)
147
+ self._untag(key)
148
+
149
+ def _untag(self, key) -> None:
150
+ for tag, keys in list(self._by_table.items()):
151
+ keys.discard(key)
152
+ if not keys:
153
+ del self._by_table[tag]
154
+
155
+ # ── observability ───────────────────────────────────────────────────
156
+ def __len__(self) -> int:
157
+ return len(self._entries)
158
+
159
+ def stats(self) -> dict:
160
+ return {
161
+ "enabled": self.enabled(),
162
+ "entries": len(self._entries),
163
+ "hits": self.hits,
164
+ "misses": self.misses,
165
+ }
166
+
167
+
168
+ read_cache = ReadCache()
169
+
170
+
171
+ def memo(key, loader, ttl: float, *, tables=(), cache_none: bool = True, now=None):
172
+ """Module-level shortcut for :meth:`ReadCache.memo` on the shared store."""
173
+ return read_cache.memo(
174
+ key, loader, ttl, tables=tables, cache_none=cache_none, now=now
175
+ )
176
+
177
+
178
+ def db_identity(engine) -> str:
179
+ """A stable name for the database an engine talks to (part of every
180
+ table-read key, so two databases on one instance never share entries)."""
181
+ config = getattr(engine, "config", None)
182
+ if isinstance(config, dict):
183
+ host = config.get("host") or ""
184
+ name = config.get("dbname") or config.get("database") or ""
185
+ return f"{host}/{name}"
186
+ if isinstance(config, str):
187
+ return config
188
+ return f"engine:{id(engine)}"