velocity-python 0.1.109__tar.gz → 0.1.110__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.110}/PKG-INFO +1 -1
  2. {velocity_python-0.1.109 → velocity_python-0.1.110}/pyproject.toml +1 -1
  3. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/security.py +5 -12
  5. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/feature_flags.py +16 -19
  6. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/context.py +91 -48
  7. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/lambda_handler.py +26 -5
  8. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/mixins/data_service.py +10 -2
  9. velocity_python-0.1.110/src/velocity/cache.py +182 -0
  10. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/table.py +131 -19
  11. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/transaction.py +7 -1
  12. {velocity_python-0.1.109 → velocity_python-0.1.110/src/velocity_python.egg-info}/PKG-INFO +1 -1
  13. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity_python.egg-info/SOURCES.txt +5 -0
  14. velocity_python-0.1.110/tests/test_cognito_user_cache.py +81 -0
  15. velocity_python-0.1.110/tests/test_lambda_handler_user_cache.py +87 -0
  16. velocity_python-0.1.110/tests/test_read_cache.py +119 -0
  17. velocity_python-0.1.110/tests/test_table_find_cache.py +172 -0
  18. {velocity_python-0.1.109 → velocity_python-0.1.110}/LICENSE +0 -0
  19. {velocity_python-0.1.109 → velocity_python-0.1.110}/README.md +0 -0
  20. {velocity_python-0.1.109 → velocity_python-0.1.110}/setup.cfg +0 -0
  21. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/__init__.py +0 -0
  22. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/amplify.py +0 -0
  23. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/amplify_build.py +0 -0
  24. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/assets/__init__.py +0 -0
  25. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/assets/backfill.py +0 -0
  26. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/assets/indexing.py +0 -0
  27. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/assets/references.py +0 -0
  28. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/assets/service.py +0 -0
  29. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/assets/usage_index.py +0 -0
  30. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/blobs.py +0 -0
  31. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/__init__.py +0 -0
  32. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/base.py +0 -0
  33. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/dispatch.py +0 -0
  34. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/infra.py +0 -0
  35. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/jobs.py +0 -0
  36. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/provisioning.py +0 -0
  37. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/retention.py +0 -0
  38. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/capabilities/single_session.py +0 -0
  39. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/deploy/__init__.py +0 -0
  40. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/deploy/config.py +0 -0
  41. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/deploy/operations.py +0 -0
  42. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/deploy/policies.py +0 -0
  43. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/deploy/reconcile.py +0 -0
  44. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/deploy/targets.py +0 -0
  45. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/dirty_pipeline.py +0 -0
  46. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/email.py +0 -0
  47. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/__init__.py +0 -0
  48. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/base_handler.py +0 -0
  49. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/context_factory.py +0 -0
  50. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/exceptions.py +0 -0
  51. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/masquerade.py +0 -0
  52. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  53. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  54. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/mixins/session_control.py +0 -0
  55. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  56. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/perf.py +0 -0
  57. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/response.py +0 -0
  58. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  59. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/job_lifecycle.py +0 -0
  60. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/job_results.py +0 -0
  61. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/mirror.py +0 -0
  62. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/s3.py +0 -0
  63. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/sms.py +0 -0
  64. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/ssm_config.py +0 -0
  65. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/__init__.py +0 -0
  66. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  67. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  68. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  69. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  70. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  71. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_email.py +0 -0
  72. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  73. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_response.py +0 -0
  74. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/aws/tests/test_sms.py +0 -0
  75. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/__init__.py +0 -0
  76. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/__init__.py +0 -0
  77. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/async_support.py +0 -0
  78. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/column.py +0 -0
  79. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/database.py +0 -0
  80. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/decorators.py +0 -0
  81. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/engine.py +0 -0
  82. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/jsonproxy.py +0 -0
  83. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/result.py +0 -0
  84. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/row.py +0 -0
  85. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/sequence.py +0 -0
  86. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/core/view.py +0 -0
  87. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/exceptions.py +0 -0
  88. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/maintenance.py +0 -0
  89. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/migrations.py +0 -0
  90. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/__init__.py +0 -0
  91. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/base/__init__.py +0 -0
  92. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/base/initializer.py +0 -0
  93. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/base/operators.py +0 -0
  94. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/base/sql.py +0 -0
  95. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/base/types.py +0 -0
  96. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/mysql/__init__.py +0 -0
  97. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/mysql/operators.py +0 -0
  98. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/mysql/reserved.py +0 -0
  99. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/mysql/sql.py +0 -0
  100. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/mysql/types.py +0 -0
  101. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/postgres/__init__.py +0 -0
  102. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/postgres/operators.py +0 -0
  103. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/postgres/reserved.py +0 -0
  104. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/postgres/sql.py +0 -0
  105. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/postgres/types.py +0 -0
  106. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  107. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlite/operators.py +0 -0
  108. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  109. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlite/sql.py +0 -0
  110. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlite/types.py +0 -0
  111. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  112. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  113. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  114. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  115. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/sqlserver/types.py +0 -0
  116. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/servers/tablehelper.py +0 -0
  117. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/__init__.py +0 -0
  118. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/common_db_test.py +0 -0
  119. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/__init__.py +0 -0
  120. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/common.py +0 -0
  121. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/conftest.py +0 -0
  122. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_column.py +0 -0
  123. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  124. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_database.py +0 -0
  125. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  126. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  127. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  128. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_result.py +0 -0
  129. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_row.py +0 -0
  130. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  131. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  132. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  133. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  134. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  135. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_table.py +0 -0
  136. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  137. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  138. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/sql/__init__.py +0 -0
  139. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/sql/common.py +0 -0
  140. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  141. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  142. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  143. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_db_utils.py +0 -0
  144. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_postgres.py +0 -0
  145. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  146. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  147. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_result_caching.py +0 -0
  148. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  149. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  150. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  151. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  152. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_sql_builder.py +0 -0
  153. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_tablehelper.py +0 -0
  154. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/tests/test_view_helper.py +0 -0
  155. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/db/utils.py +0 -0
  156. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/logging.py +0 -0
  157. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/__init__.py +0 -0
  158. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/conv/__init__.py +0 -0
  159. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/conv/iconv.py +0 -0
  160. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/conv/oconv.py +0 -0
  161. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/db.py +0 -0
  162. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/export.py +0 -0
  163. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/format.py +0 -0
  164. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/mail.py +0 -0
  165. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/merge.py +0 -0
  166. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/pdf.py +0 -0
  167. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/__init__.py +0 -0
  168. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_db.py +0 -0
  169. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_fix.py +0 -0
  170. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_format.py +0 -0
  171. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_iconv.py +0 -0
  172. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_mail_send.py +0 -0
  173. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_merge.py +0 -0
  174. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_oconv.py +0 -0
  175. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_original_error.py +0 -0
  176. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tests/test_timer.py +0 -0
  177. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/timer.py +0 -0
  178. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/misc/tools.py +0 -0
  179. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/__init__.py +0 -0
  180. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/core/__init__.py +0 -0
  181. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/core/capabilities.py +0 -0
  182. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/core/store.py +0 -0
  183. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/core/where.py +0 -0
  184. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/exceptions.py +0 -0
  185. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/__init__.py +0 -0
  186. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/base/__init__.py +0 -0
  187. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  188. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  189. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  190. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  191. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  192. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  193. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  194. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  195. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  196. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  197. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  198. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  199. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/__init__.py +0 -0
  200. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/authorizenet_adapter.py +0 -0
  201. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/authorizenet_mirror.py +0 -0
  202. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/base_adapter.py +0 -0
  203. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/braintree_adapter.py +0 -0
  204. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/braintree_mirror.py +0 -0
  205. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/charge_rules.py +0 -0
  206. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/stripe_adapter.py +0 -0
  207. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity/payment/stripe_mirror.py +0 -0
  208. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  209. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity_python.egg-info/entry_points.txt +0 -0
  210. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity_python.egg-info/requires.txt +0 -0
  211. {velocity_python-0.1.109 → velocity_python-0.1.110}/src/velocity_python.egg-info/top_level.txt +0 -0
  212. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_access_error_403.py +0 -0
  213. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_activity_tracking_aborted_tx.py +0 -0
  214. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_amplify_build.py +0 -0
  215. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_asset_indexing.py +0 -0
  216. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_asset_references.py +0 -0
  217. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_assets_service.py +0 -0
  218. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_async_support.py +0 -0
  219. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_batch_operations.py +0 -0
  220. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_blob_store.py +0 -0
  221. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_capabilities.py +0 -0
  222. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_capability_catalog.py +0 -0
  223. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_column_tx_arg.py +0 -0
  224. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_concurrency_safety.py +0 -0
  225. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_connection_pool.py +0 -0
  226. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_connection_resilience.py +0 -0
  227. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_context_flags_loader.py +0 -0
  228. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_context_job_descriptions.py +0 -0
  229. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_data_service_query_cap.py +0 -0
  230. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_data_service_renumber.py +0 -0
  231. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_data_service_save_as.py +0 -0
  232. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_db_credentials_ssm_cascade.py +0 -0
  233. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_db_maintenance.py +0 -0
  234. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_decorators.py +0 -0
  235. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_delete_hook_fk_blocked.py +0 -0
  236. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_dirty_pipeline_fast_path.py +0 -0
  237. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_email_processing.py +0 -0
  238. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_enqueue_send_failures.py +0 -0
  239. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_feature_flags.py +0 -0
  240. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_get_cognito_user_provider.py +0 -0
  241. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_http_handler_rollback.py +0 -0
  242. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_iconv_money_to_cents.py +0 -0
  243. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_identifier_injection_guard.py +0 -0
  244. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_job_handler.py +0 -0
  245. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_job_lifecycle.py +0 -0
  246. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_job_results.py +0 -0
  247. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_json_columns.py +0 -0
  248. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_jsonb_dict_adapter.py +0 -0
  249. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_lambda_handler.py +0 -0
  250. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_lambda_handler_auth.py +0 -0
  251. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_lambda_handler_masquerade.py +0 -0
  252. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_masquerade_grant.py +0 -0
  253. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_mirror.py +0 -0
  254. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_mixins_import.py +0 -0
  255. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_n_plus_one.py +0 -0
  256. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_nosql_dynamodb.py +0 -0
  257. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_nosql_memory.py +0 -0
  258. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_nosql_mongodb.py +0 -0
  259. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_nosql_redis.py +0 -0
  260. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_nosql_translation.py +0 -0
  261. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_nosql_where.py +0 -0
  262. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_observability.py +0 -0
  263. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_on_commit.py +0 -0
  264. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_payment_authorizenet_adapter.py +0 -0
  265. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_payment_braintree_adapter.py +0 -0
  266. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_payment_braintree_mirror.py +0 -0
  267. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_payment_mirror_empty_table.py +0 -0
  268. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_payment_profile_sorting.py +0 -0
  269. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_payment_stripe_adapter.py +0 -0
  270. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_pdf.py +0 -0
  271. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_prepared_statements.py +0 -0
  272. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_psycopg3_upgrade.py +0 -0
  273. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_query_cache.py +0 -0
  274. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_query_missing_table_unlocked.py +0 -0
  275. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_response.py +0 -0
  276. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_restricted_direct_tables.py +0 -0
  277. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_retry_side_effect_guard.py +0 -0
  278. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_return_default_safety.py +0 -0
  279. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_row_batch_update.py +0 -0
  280. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_row_cache_staleness.py +0 -0
  281. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_schema_migrations.py +0 -0
  282. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_security_capability.py +0 -0
  283. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_security_hardening.py +0 -0
  284. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_server_cursor.py +0 -0
  285. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_session_control.py +0 -0
  286. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_single_autocommit_safety.py +0 -0
  287. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_single_session_capability.py +0 -0
  288. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_spreadsheet_functions.py +0 -0
  289. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_sqlite_backend.py +0 -0
  290. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_sqs_per_record_transactions.py +0 -0
  291. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_ssm_config.py +0 -0
  292. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_store_user_data.py +0 -0
  293. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  294. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_table_alter.py +0 -0
  295. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_table_purge.py +0 -0
  296. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_transaction_class_wrapping.py +0 -0
  297. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_transaction_commit_and_ownership.py +0 -0
  298. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_transaction_edge_cases.py +0 -0
  299. {velocity_python-0.1.109 → velocity_python-0.1.110}/tests/test_where_clause_validation.py +0 -0
  300. {velocity_python-0.1.109 → velocity_python-0.1.110}/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.110
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.110"
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.110"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -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,9 @@ KNOBS = {
62
62
  "security_events_days": "90",
63
63
  }
64
64
 
65
- _cache: dict = {}
66
-
67
-
68
65
  def clear_cache():
69
66
  """Forget the cached site row AND the shared flags row (tests)."""
70
- _cache.clear()
67
+ read_cache.invalidate(SITE_TABLE)
71
68
  feature_flags.clear_cache()
72
69
 
73
70
 
@@ -174,13 +171,9 @@ def check_rate(tx, bucket, *, limit, now, table=RATE_TABLE):
174
171
 
175
172
 
176
173
  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
174
+ """The process read cache (velocity.cache), tagged with the site table
175
+ so provisioning's own writes to it in this process drop the entry."""
176
+ return memo(("security",) + tuple(key), loader, CACHE_TTL_SECONDS, tables=(SITE_TABLE,), now=now)
184
177
 
185
178
 
186
179
  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,35 @@ 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
112
  tx, system: str | None = None, *, table: str = DEFAULT_TABLE, now: float | None = None
114
113
  ) -> dict:
115
- """:func:`get` behind a per-container TTL cache — the hot-path reader.
114
+ """:func:`get` through the process read cache (:mod:`velocity.cache`)
115
+ — the hot-path reader.
116
116
 
117
117
  The flags row is ONE row per stage, and every reader in a request
118
118
  (the handler context, the security guards, session control, any
119
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.
120
+ :func:`cache_seconds` per container, however many readers there are.
121
+ Tagged with the table, so a flags write through the table API in this
122
+ process (the mirror hook, a save) drops it at once; the other
123
+ environment sees the change within the TTL.
124
124
  """
125
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))
126
+ row = memo(
127
+ ("feature_flags", table, system),
128
+ lambda: dict(get(tx, system, table=table)),
129
+ cache_seconds(),
130
+ tables=(table,),
131
+ now=now,
132
+ )
136
133
  return dict(row)
137
134
 
138
135
 
@@ -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,38 @@ 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) if raw not in (None, "") else float(DEFAULT_USER_CACHE_SECONDS)
106
+ )
107
+ except (TypeError, ValueError):
108
+ _user_cache_seconds = float(DEFAULT_USER_CACHE_SECONDS)
109
+ return _user_cache_seconds
110
+
111
+
112
+ def clear_user_cache_config() -> None:
113
+ global _user_cache_seconds
114
+ _user_cache_seconds = None
115
+
116
+
84
117
  @engine.transaction
85
118
  class Context:
86
119
 
@@ -807,63 +840,73 @@ class Context:
807
840
  "cannot resolve a Cognito user"
808
841
  ) from None
809
842
 
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:
843
+ # one Cognito round trip per user per UserCacheSeconds per container
844
+ # (velocity.cache); a lookup that fails is never cached
845
+ def resolve():
819
846
  try:
820
- self.perf.start("cognito list_users")
821
- list_response = cognito_client.list_users(
847
+ self.perf.start("cognito admin_get_user")
848
+ response = cognito_client.admin_get_user(
822
849
  UserPoolId=user_pool_id,
823
- Filter=f'sub = "{user_sub}"',
824
- Limit=1,
850
+ Username=user_sub,
825
851
  )
826
- self.perf.log("cognito list_users")
852
+ self.perf.log("cognito admin_get_user")
853
+ resolved_username = response.get("Username")
854
+ except cognito_client.exceptions.UserNotFoundException:
855
+ try:
856
+ self.perf.start("cognito list_users")
857
+ list_response = cognito_client.list_users(
858
+ UserPoolId=user_pool_id,
859
+ Filter=f'sub = "{user_sub}"',
860
+ Limit=1,
861
+ )
862
+ self.perf.log("cognito list_users")
863
+ except ClientError as exc:
864
+ message = exc.response.get("Error", {}).get("Message", "Unknown error")
865
+ raise AlertError(
866
+ f"Failed to search Cognito user by sub: {message}"
867
+ ) from None
868
+
869
+ users = list_response.get("Users", [])
870
+ if not users:
871
+ raise AlertError("Cognito user not found") from None
872
+
873
+ user = users[0]
874
+ response = {
875
+ "Username": user.get("Username"),
876
+ "UserAttributes": user.get("Attributes", []),
877
+ "Enabled": user.get("Enabled"),
878
+ "UserStatus": user.get("UserStatus"),
879
+ "UserCreateDate": user.get("UserCreateDate"),
880
+ "UserLastModifiedDate": user.get("UserLastModifiedDate"),
881
+ }
882
+ resolved_username = response.get("Username")
827
883
  except ClientError as exc:
828
884
  message = exc.response.get("Error", {}).get("Message", "Unknown error")
829
- raise AlertError(
830
- f"Failed to search Cognito user by sub: {message}"
831
- ) from None
832
-
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"),
885
+ raise AlertError(f"Failed to retrieve Cognito user: {message}") from None
886
+
887
+ attributes = {
888
+ attr["Name"]: attr.get("Value")
889
+ for attr in response.get("UserAttributes", [])
845
890
  }
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
891
 
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
- }
892
+ user = {
893
+ "username": resolved_username or user_sub,
894
+ "email": attributes.get("email"),
895
+ "attributes": attributes,
896
+ "enabled": response.get("Enabled"),
897
+ "user_status": response.get("UserStatus"),
898
+ "sub": attributes.get("sub", user_sub),
899
+ }
864
900
 
865
- self.perf.log("get_cognito_user total")
901
+ return user
866
902
 
903
+ user = memo(
904
+ ("cognito", user_pool_id, user_sub),
905
+ resolve,
906
+ user_cache_seconds(),
907
+ cache_none=False,
908
+ )
909
+ self.perf.log("get_cognito_user total")
867
910
  return user
868
911
 
869
912
  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,9 @@ 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() for c in tx.table(obj).sys_columns(cache_ttl=SCHEMA_CACHE_SECONDS)
562
+ }
555
563
  missing = sorted(
556
564
  {c for c in requested_cols if c.lower() not in available_cols},
557
565
  key=lambda v: v.lower(),
@@ -0,0 +1,182 @@
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(self, key, value, ttl: float, *, tables=(), now: float | None = None) -> None:
91
+ if not self.enabled() or ttl is None or ttl <= 0:
92
+ return
93
+ now = time.monotonic() if now is None else now
94
+ tags = frozenset(_norm(t) for t in tables if t)
95
+ with self._lock:
96
+ if key in self._entries:
97
+ self._drop(key)
98
+ self._entries[key] = (now + ttl, value, tags)
99
+ for tag in tags:
100
+ self._by_table.setdefault(tag, set()).add(key)
101
+ while len(self._entries) > self._max:
102
+ oldest, _ = self._entries.popitem(last=False)
103
+ self._untag(oldest)
104
+
105
+ def memo(self, key, loader, ttl: float, *, tables=(), cache_none: bool = True, now=None):
106
+ """``loader()`` once per ``ttl`` for ``key``. ``cache_none=False`` never
107
+ stores a ``None`` result (a "not found" that may exist a moment later)."""
108
+ hit, value = self.get(key, now=now)
109
+ if hit:
110
+ return value
111
+ value = loader()
112
+ if value is not None or cache_none:
113
+ self.put(key, value, ttl, tables=tables, now=now)
114
+ return value
115
+
116
+ # ── invalidation ────────────────────────────────────────────────────
117
+ def invalidate(self, table=None) -> int:
118
+ """Drop every entry tagged ``table`` (all entries when ``None``).
119
+ Returns how many were dropped."""
120
+ with self._lock:
121
+ if table is None:
122
+ count = len(self._entries)
123
+ self._entries.clear()
124
+ self._by_table.clear()
125
+ return count
126
+ keys = list(self._by_table.get(_norm(table), ()))
127
+ for key in keys:
128
+ self._drop(key)
129
+ return len(keys)
130
+
131
+ def invalidate_schema(self) -> int:
132
+ """DDL happened: drop the table list / column entries."""
133
+ return self.invalidate(SCHEMA_TAG)
134
+
135
+ def clear(self) -> None:
136
+ with self._lock:
137
+ self.invalidate(None)
138
+ self.hits = 0
139
+ self.misses = 0
140
+
141
+ def _drop(self, key) -> None:
142
+ self._entries.pop(key, None)
143
+ self._untag(key)
144
+
145
+ def _untag(self, key) -> None:
146
+ for tag, keys in list(self._by_table.items()):
147
+ keys.discard(key)
148
+ if not keys:
149
+ del self._by_table[tag]
150
+
151
+ # ── observability ───────────────────────────────────────────────────
152
+ def __len__(self) -> int:
153
+ return len(self._entries)
154
+
155
+ def stats(self) -> dict:
156
+ return {
157
+ "enabled": self.enabled(),
158
+ "entries": len(self._entries),
159
+ "hits": self.hits,
160
+ "misses": self.misses,
161
+ }
162
+
163
+
164
+ read_cache = ReadCache()
165
+
166
+
167
+ def memo(key, loader, ttl: float, *, tables=(), cache_none: bool = True, now=None):
168
+ """Module-level shortcut for :meth:`ReadCache.memo` on the shared store."""
169
+ return read_cache.memo(key, loader, ttl, tables=tables, cache_none=cache_none, now=now)
170
+
171
+
172
+ def db_identity(engine) -> str:
173
+ """A stable name for the database an engine talks to (part of every
174
+ table-read key, so two databases on one instance never share entries)."""
175
+ config = getattr(engine, "config", None)
176
+ if isinstance(config, dict):
177
+ host = config.get("host") or ""
178
+ name = config.get("dbname") or config.get("database") or ""
179
+ return f"{host}/{name}"
180
+ if isinstance(config, str):
181
+ return config
182
+ return f"engine:{id(engine)}"