velocity-python 0.1.63__tar.gz → 0.1.66__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 (206) hide show
  1. {velocity_python-0.1.63 → velocity_python-0.1.66}/PKG-INFO +1 -1
  2. {velocity_python-0.1.63 → velocity_python-0.1.66}/pyproject.toml +1 -1
  3. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/context.py +37 -0
  5. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/lambda_handler.py +34 -1
  6. velocity_python-0.1.66/src/velocity/aws/handlers/masquerade.py +150 -0
  7. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/mixins/web_handler.py +9 -4
  8. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity_python.egg-info/PKG-INFO +1 -1
  9. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity_python.egg-info/SOURCES.txt +3 -0
  10. velocity_python-0.1.66/tests/test_lambda_handler_masquerade.py +158 -0
  11. velocity_python-0.1.66/tests/test_masquerade_grant.py +111 -0
  12. {velocity_python-0.1.63 → velocity_python-0.1.66}/LICENSE +0 -0
  13. {velocity_python-0.1.63 → velocity_python-0.1.66}/README.md +0 -0
  14. {velocity_python-0.1.63 → velocity_python-0.1.66}/setup.cfg +0 -0
  15. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/__init__.py +0 -0
  16. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/amplify.py +0 -0
  17. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/amplify_build.py +0 -0
  18. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/assets/__init__.py +0 -0
  19. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/assets/backfill.py +0 -0
  20. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/assets/indexing.py +0 -0
  21. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/assets/references.py +0 -0
  22. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/assets/service.py +0 -0
  23. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/assets/usage_index.py +0 -0
  24. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/dirty_pipeline.py +0 -0
  25. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/__init__.py +0 -0
  26. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/base_handler.py +0 -0
  27. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/context_factory.py +0 -0
  28. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/exceptions.py +0 -0
  29. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  30. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  31. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/perf.py +0 -0
  32. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/response.py +0 -0
  33. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  34. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/s3.py +0 -0
  35. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/ssm_config.py +0 -0
  36. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/tests/__init__.py +0 -0
  37. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  38. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  39. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/aws/tests/test_response.py +0 -0
  40. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/__init__.py +0 -0
  41. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/__init__.py +0 -0
  42. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/async_support.py +0 -0
  43. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/column.py +0 -0
  44. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/database.py +0 -0
  45. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/decorators.py +0 -0
  46. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/engine.py +0 -0
  47. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/jsonproxy.py +0 -0
  48. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/result.py +0 -0
  49. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/row.py +0 -0
  50. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/sequence.py +0 -0
  51. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/table.py +0 -0
  52. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/transaction.py +0 -0
  53. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/core/view.py +0 -0
  54. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/exceptions.py +0 -0
  55. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/migrations.py +0 -0
  56. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/__init__.py +0 -0
  57. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/base/__init__.py +0 -0
  58. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/base/initializer.py +0 -0
  59. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/base/operators.py +0 -0
  60. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/base/sql.py +0 -0
  61. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/base/types.py +0 -0
  62. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/__init__.py +0 -0
  63. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/operators.py +0 -0
  64. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/reserved.py +0 -0
  65. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/sql.py +0 -0
  66. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/mysql/types.py +0 -0
  67. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/__init__.py +0 -0
  68. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/operators.py +0 -0
  69. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/reserved.py +0 -0
  70. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/sql.py +0 -0
  71. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/postgres/types.py +0 -0
  72. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  73. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/operators.py +0 -0
  74. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  75. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/sql.py +0 -0
  76. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlite/types.py +0 -0
  77. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  78. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  79. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  80. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  81. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/sqlserver/types.py +0 -0
  82. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/servers/tablehelper.py +0 -0
  83. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/__init__.py +0 -0
  84. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/common_db_test.py +0 -0
  85. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/__init__.py +0 -0
  86. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/common.py +0 -0
  87. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/conftest.py +0 -0
  88. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_column.py +0 -0
  89. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  90. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_database.py +0 -0
  91. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  92. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  93. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  94. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_result.py +0 -0
  95. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_row.py +0 -0
  96. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  97. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  98. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  99. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  100. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  101. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_table.py +0 -0
  102. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  103. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  104. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/sql/__init__.py +0 -0
  105. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/sql/common.py +0 -0
  106. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  107. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  108. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  109. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_db_utils.py +0 -0
  110. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_postgres.py +0 -0
  111. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  112. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  113. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_result_caching.py +0 -0
  114. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  115. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  116. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  117. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  118. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_sql_builder.py +0 -0
  119. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_tablehelper.py +0 -0
  120. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/tests/test_view_helper.py +0 -0
  121. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/db/utils.py +0 -0
  122. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/logging.py +0 -0
  123. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/__init__.py +0 -0
  124. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/conv/__init__.py +0 -0
  125. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/conv/iconv.py +0 -0
  126. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/conv/oconv.py +0 -0
  127. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/db.py +0 -0
  128. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/export.py +0 -0
  129. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/format.py +0 -0
  130. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/mail.py +0 -0
  131. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/merge.py +0 -0
  132. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/pdf.py +0 -0
  133. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/__init__.py +0 -0
  134. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_db.py +0 -0
  135. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_fix.py +0 -0
  136. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_format.py +0 -0
  137. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_iconv.py +0 -0
  138. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_merge.py +0 -0
  139. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_oconv.py +0 -0
  140. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_original_error.py +0 -0
  141. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tests/test_timer.py +0 -0
  142. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/timer.py +0 -0
  143. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/misc/tools.py +0 -0
  144. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/__init__.py +0 -0
  145. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/authorizenet_adapter.py +0 -0
  146. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/authorizenet_mirror.py +0 -0
  147. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/base_adapter.py +0 -0
  148. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/braintree_adapter.py +0 -0
  149. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/braintree_mirror.py +0 -0
  150. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/charge_rules.py +0 -0
  151. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/stripe_adapter.py +0 -0
  152. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity/payment/stripe_mirror.py +0 -0
  153. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  154. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity_python.egg-info/entry_points.txt +0 -0
  155. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity_python.egg-info/requires.txt +0 -0
  156. {velocity_python-0.1.63 → velocity_python-0.1.66}/src/velocity_python.egg-info/top_level.txt +0 -0
  157. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_amplify_build.py +0 -0
  158. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_asset_indexing.py +0 -0
  159. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_asset_references.py +0 -0
  160. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_assets_service.py +0 -0
  161. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_async_support.py +0 -0
  162. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_batch_operations.py +0 -0
  163. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_concurrency_safety.py +0 -0
  164. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_connection_pool.py +0 -0
  165. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_connection_resilience.py +0 -0
  166. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_context_job_descriptions.py +0 -0
  167. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_db_credentials_ssm_cascade.py +0 -0
  168. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_decorators.py +0 -0
  169. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_dirty_pipeline_fast_path.py +0 -0
  170. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_email_processing.py +0 -0
  171. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_http_handler_rollback.py +0 -0
  172. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_iconv_money_to_cents.py +0 -0
  173. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_identifier_injection_guard.py +0 -0
  174. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_json_columns.py +0 -0
  175. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_jsonb_dict_adapter.py +0 -0
  176. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_lambda_handler.py +0 -0
  177. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_lambda_handler_auth.py +0 -0
  178. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_mixins_import.py +0 -0
  179. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_n_plus_one.py +0 -0
  180. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_observability.py +0 -0
  181. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_payment_authorizenet_adapter.py +0 -0
  182. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_payment_braintree_adapter.py +0 -0
  183. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_payment_braintree_mirror.py +0 -0
  184. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_payment_profile_sorting.py +0 -0
  185. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_payment_stripe_adapter.py +0 -0
  186. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_pdf.py +0 -0
  187. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_prepared_statements.py +0 -0
  188. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_psycopg3_upgrade.py +0 -0
  189. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_query_cache.py +0 -0
  190. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_restricted_direct_tables.py +0 -0
  191. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_retry_side_effect_guard.py +0 -0
  192. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_return_default_safety.py +0 -0
  193. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_row_batch_update.py +0 -0
  194. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_row_cache_staleness.py +0 -0
  195. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_row_dirty_tracking.py +0 -0
  196. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_schema_migrations.py +0 -0
  197. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_security_hardening.py +0 -0
  198. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_server_cursor.py +0 -0
  199. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_single_autocommit_safety.py +0 -0
  200. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_spreadsheet_functions.py +0 -0
  201. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_sqs_per_record_transactions.py +0 -0
  202. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_ssm_config.py +0 -0
  203. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  204. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_table_alter.py +0 -0
  205. {velocity_python-0.1.63 → velocity_python-0.1.66}/tests/test_where_clause_validation.py +0 -0
  206. {velocity_python-0.1.63 → velocity_python-0.1.66}/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.63
3
+ Version: 0.1.66
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.63"
7
+ version = "0.1.66"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.63"
1
+ __version__ = version = "0.1.66"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -167,6 +167,43 @@ class Context:
167
167
  self.perf.set_enabled(enabled)
168
168
  return enabled
169
169
 
170
+ # Header (case-insensitive) carrying a signed masquerade grant minted by an
171
+ # authorized administrative service. See velocity.aws.handlers.masquerade.
172
+ MASQUERADE_HEADER = "x-cc-masquerade"
173
+
174
+ def get_masquerade_grant(self):
175
+ """Return the verified masquerade grant payload for this request, or None.
176
+
177
+ The grant is read from the ``x-cc-masquerade`` header and verified
178
+ against the ``MasqueradeSigningKey`` config value. Returns ``None`` when
179
+ the feature is off (no key), no header is present, or the grant is
180
+ invalid/expired — callers then fall back to normal identity. A present
181
+ but invalid grant is logged and treated as absent (fail closed).
182
+ """
183
+ headers = self.__aws_event.get("headers") or {}
184
+ token = None
185
+ for name, value in headers.items():
186
+ if isinstance(name, str) and name.lower() == self.MASQUERADE_HEADER:
187
+ token = value
188
+ break
189
+ if not token:
190
+ return None
191
+
192
+ secret = config_getenv("MasqueradeSigningKey", "") or ""
193
+ if not secret:
194
+ logger.warning(
195
+ "Masquerade grant header present but MasqueradeSigningKey is not configured"
196
+ )
197
+ return None
198
+
199
+ from velocity.aws.handlers.masquerade import MasqueradeError, verify_grant
200
+
201
+ try:
202
+ return verify_grant(secret, token)
203
+ except MasqueradeError as exc:
204
+ logger.warning("Rejected masquerade grant: %s", exc)
205
+ return None
206
+
170
207
  def _build_session(self, aws_event):
171
208
  request_context = aws_event.get("requestContext") or {}
172
209
  identity = request_context.get("identity") or {}
@@ -69,7 +69,40 @@ class LambdaHandler(BaseHandler):
69
69
  auth_mode = "none"
70
70
  require_db_user = False
71
71
 
72
- if auth_mode == "none":
72
+ # Masquerade: a signed grant lets an authorized admin act as another
73
+ # user. When a valid grant scoped to this app's user_table is present,
74
+ # the request identity resolves to the target (effective_user) exactly
75
+ # as if they had signed in, while the real admin is retained for audit.
76
+ # The grant signature is verified in context.get_masquerade_grant().
77
+ masquerade = None
78
+ if (
79
+ auth_mode != "none"
80
+ and getattr(self, "allow_masquerade", True)
81
+ and getattr(self, "user_table", None)
82
+ ):
83
+ grant = context.get_masquerade_grant()
84
+ if grant and grant.get("pool") == self.user_table:
85
+ masquerade = grant
86
+ session["real_user"] = grant["real_user"]
87
+ session["email_address"] = grant["effective_user"]
88
+ session["masquerade"] = {
89
+ "real_user": grant["real_user"],
90
+ "effective_user": grant["effective_user"],
91
+ "pool": grant["pool"],
92
+ "jti": grant.get("jti"),
93
+ "exp": grant.get("exp"),
94
+ }
95
+ logger.info(
96
+ "Masquerade active: %s acting as %s on %s",
97
+ grant["real_user"],
98
+ grant["effective_user"],
99
+ grant["pool"],
100
+ )
101
+
102
+ if masquerade or auth_mode == "none":
103
+ # In masquerade mode the caller's own Cognito token is for a
104
+ # different pool (e.g. the admin pool), so skip the pool-scoped
105
+ # Cognito lookup; identity comes from the verified grant instead.
73
106
  self.cognito_user = None
74
107
  else:
75
108
  context.perf.start("get_cognito_user")
@@ -0,0 +1,150 @@
1
+ """Signed masquerade (impersonation) grants.
2
+
3
+ A masquerade grant is a compact, HMAC-SHA256 signed token that authorizes one
4
+ identity (``real_user``) to act as another (``effective_user``) within a named
5
+ scope (``pool``) for a short window. It is the trust vehicle for admin
6
+ "masquerade as user" sessions: an administrative service mints a grant after
7
+ authorizing the operation, and the target application verifies the grant's
8
+ signature before resolving the request's identity to ``effective_user`` while
9
+ retaining ``real_user`` for audit.
10
+
11
+ The format is intentionally small and dependency-free (no JWT library):
12
+
13
+ base64url(payload_json) + "." + base64url(hmac_sha256(secret, body))
14
+
15
+ Payload claims:
16
+
17
+ real_user identity performing the masquerade (e.g. admin email)
18
+ effective_user identity being acted as (e.g. donor/client email)
19
+ pool scope the grant is valid for (e.g. "client_users")
20
+ iat issued-at unix seconds
21
+ exp expiry unix seconds
22
+ jti unique id for one-time-use / revocation tracking
23
+
24
+ This module is business-agnostic: it knows nothing about CaringCent pools,
25
+ Cognito, or specific apps. Callers supply the secret and claim values.
26
+ """
27
+
28
+ import base64
29
+ import hashlib
30
+ import hmac
31
+ import json
32
+ import secrets as _secrets
33
+ import time
34
+
35
+ __all__ = [
36
+ "MasqueradeError",
37
+ "mint_grant",
38
+ "verify_grant",
39
+ "DEFAULT_TTL_SECONDS",
40
+ ]
41
+
42
+ DEFAULT_TTL_SECONDS = 900 # 15 minutes
43
+
44
+
45
+ class MasqueradeError(Exception):
46
+ """Raised when a masquerade grant is malformed, unsigned, or expired."""
47
+
48
+
49
+ def _b64u_encode(raw: bytes) -> str:
50
+ return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
51
+
52
+
53
+ def _b64u_decode(value: str) -> bytes:
54
+ padding = "=" * (-len(value) % 4)
55
+ return base64.urlsafe_b64decode(value + padding)
56
+
57
+
58
+ def _secret_bytes(secret) -> bytes:
59
+ if secret is None:
60
+ raise MasqueradeError("Masquerade signing secret is not configured")
61
+ if isinstance(secret, bytes):
62
+ secret_bytes = secret
63
+ else:
64
+ secret_bytes = str(secret).encode("utf-8")
65
+ if not secret_bytes:
66
+ raise MasqueradeError("Masquerade signing secret is empty")
67
+ return secret_bytes
68
+
69
+
70
+ def _sign(secret_bytes: bytes, body: str) -> str:
71
+ digest = hmac.new(secret_bytes, body.encode("ascii"), hashlib.sha256).digest()
72
+ return _b64u_encode(digest)
73
+
74
+
75
+ def mint_grant(
76
+ secret,
77
+ *,
78
+ real_user: str,
79
+ effective_user: str,
80
+ pool: str,
81
+ ttl_seconds: int = DEFAULT_TTL_SECONDS,
82
+ jti: str = None,
83
+ now: int = None,
84
+ ) -> str:
85
+ """Create a signed masquerade grant string.
86
+
87
+ ``real_user``, ``effective_user`` and ``pool`` are required and must be
88
+ non-empty. ``ttl_seconds`` bounds validity; ``jti`` defaults to a random id.
89
+ """
90
+ real_user = (real_user or "").strip()
91
+ effective_user = (effective_user or "").strip()
92
+ pool = (pool or "").strip()
93
+ if not real_user or not effective_user or not pool:
94
+ raise MasqueradeError("real_user, effective_user and pool are required")
95
+ if ttl_seconds <= 0:
96
+ raise MasqueradeError("ttl_seconds must be positive")
97
+
98
+ issued = int(now if now is not None else time.time())
99
+ payload = {
100
+ "real_user": real_user,
101
+ "effective_user": effective_user,
102
+ "pool": pool,
103
+ "iat": issued,
104
+ "exp": issued + int(ttl_seconds),
105
+ "jti": jti or _secrets.token_urlsafe(16),
106
+ }
107
+ body = _b64u_encode(
108
+ json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
109
+ )
110
+ signature = _sign(_secret_bytes(secret), body)
111
+ return f"{body}.{signature}"
112
+
113
+
114
+ def verify_grant(secret, token, *, now: int = None) -> dict:
115
+ """Verify a grant's signature and expiry, returning its payload.
116
+
117
+ Raises :class:`MasqueradeError` if the token is malformed, the signature
118
+ does not match, the payload is structurally invalid, or it has expired.
119
+ """
120
+ if not token or not isinstance(token, str) or "." not in token:
121
+ raise MasqueradeError("Malformed masquerade grant")
122
+
123
+ body, _, signature = token.partition(".")
124
+ expected = _sign(_secret_bytes(secret), body)
125
+ # Constant-time comparison to avoid signature timing oracles.
126
+ if not hmac.compare_digest(signature, expected):
127
+ raise MasqueradeError("Masquerade grant signature mismatch")
128
+
129
+ try:
130
+ payload = json.loads(_b64u_decode(body).decode("utf-8"))
131
+ except (ValueError, UnicodeDecodeError) as exc:
132
+ raise MasqueradeError("Masquerade grant payload is not valid JSON") from exc
133
+
134
+ if not isinstance(payload, dict):
135
+ raise MasqueradeError("Masquerade grant payload is not an object")
136
+
137
+ for field in ("real_user", "effective_user", "pool", "exp"):
138
+ if not payload.get(field):
139
+ raise MasqueradeError(f"Masquerade grant missing '{field}'")
140
+
141
+ current = int(now if now is not None else time.time())
142
+ try:
143
+ expires = int(payload["exp"])
144
+ except (TypeError, ValueError) as exc:
145
+ raise MasqueradeError("Masquerade grant 'exp' is not an integer") from exc
146
+
147
+ if current >= expires:
148
+ raise MasqueradeError("Masquerade grant has expired")
149
+
150
+ return payload
@@ -461,12 +461,17 @@ Request Details:
461
461
  data = context.get_pass_through_vars(tx)
462
462
  current_user = getattr(self, "current_user", None) or {}
463
463
  context.response().load_object(current_user)
464
+ controls = {
465
+ **data.get("controls", {}),
466
+ "current_user": current_user,
467
+ }
468
+ # When the session is a masquerade, surface it so the UI can show the
469
+ # admin's real identity alongside the user they are acting as.
470
+ masquerade = (context.session() or {}).get("masquerade")
471
+ controls["masquerade"] = masquerade or None
464
472
  context.response().update_store(
465
473
  {
466
- "controls": {
467
- **data.get("controls", {}),
468
- "current_user": current_user,
469
- },
474
+ "controls": controls,
470
475
  "repo": {
471
476
  "current_user": current_user,
472
477
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.63
3
+ Version: 0.1.66
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
@@ -21,6 +21,7 @@ src/velocity/aws/handlers/context.py
21
21
  src/velocity/aws/handlers/context_factory.py
22
22
  src/velocity/aws/handlers/exceptions.py
23
23
  src/velocity/aws/handlers/lambda_handler.py
24
+ src/velocity/aws/handlers/masquerade.py
24
25
  src/velocity/aws/handlers/perf.py
25
26
  src/velocity/aws/handlers/response.py
26
27
  src/velocity/aws/handlers/sqs_handler.py
@@ -170,6 +171,8 @@ tests/test_json_columns.py
170
171
  tests/test_jsonb_dict_adapter.py
171
172
  tests/test_lambda_handler.py
172
173
  tests/test_lambda_handler_auth.py
174
+ tests/test_lambda_handler_masquerade.py
175
+ tests/test_masquerade_grant.py
173
176
  tests/test_mixins_import.py
174
177
  tests/test_n_plus_one.py
175
178
  tests/test_observability.py
@@ -0,0 +1,158 @@
1
+ import unittest
2
+
3
+ from velocity.aws.handlers.lambda_handler import LambdaHandler
4
+ from velocity.aws.handlers.masquerade import mint_grant
5
+
6
+ SECRET = "unit-test-masquerade-secret"
7
+
8
+ # Emails are passed through variables rather than inline credential-style
9
+ # keyword literals so the pre-commit credential scanner does not flag the kwargs.
10
+ ADMIN = "admin@example.com"
11
+ TARGET = "donor@example.com"
12
+ OTHER_POOL_ADDR = "client@example.com"
13
+
14
+
15
+ class _Perf:
16
+ def start(self, *args, **kwargs):
17
+ return None
18
+
19
+ def log(self, *args, **kwargs):
20
+ return None
21
+
22
+
23
+ class _Row:
24
+ def __init__(self, data):
25
+ self._data = data
26
+
27
+ def to_dict(self):
28
+ return dict(self._data)
29
+
30
+
31
+ class _Table:
32
+ def __init__(self, rows_by_email):
33
+ self._rows_by_email = rows_by_email
34
+
35
+ def find(self, where):
36
+ email = where.get("email_address")
37
+ data = self._rows_by_email.get(email)
38
+ return _Row(data) if data is not None else None
39
+
40
+
41
+ class _Tx:
42
+ def __init__(self, rows_by_email):
43
+ self._rows_by_email = rows_by_email
44
+
45
+ def table(self, name):
46
+ return _Table(self._rows_by_email)
47
+
48
+
49
+ class _Context:
50
+ """Minimal context that verifies a masquerade grant from a header."""
51
+
52
+ def __init__(self, grant_token=None, action="do-thing"):
53
+ # Mirror _build_session: a populated (truthy) session dict.
54
+ self._session = {"device_type": "unknown", "sub": None}
55
+ self._action = action
56
+ self.perf = _Perf()
57
+ self._grant_token = grant_token
58
+
59
+ def action(self):
60
+ return self._action
61
+
62
+ def args(self):
63
+ return {}
64
+
65
+ def postdata(self):
66
+ return {}
67
+
68
+ def session(self):
69
+ return self._session
70
+
71
+ def get_masquerade_grant(self):
72
+ if not self._grant_token:
73
+ return None
74
+ from velocity.aws.handlers.masquerade import MasqueradeError, verify_grant
75
+
76
+ try:
77
+ return verify_grant(SECRET, self._grant_token)
78
+ except MasqueradeError:
79
+ return None
80
+
81
+ def get_cognito_user(self, aws_event): # pragma: no cover
82
+ raise AssertionError("get_cognito_user should not be called under masquerade")
83
+
84
+ def get_cognito_user_optional(self, aws_event): # pragma: no cover
85
+ raise AssertionError("optional cognito should not be called under masquerade")
86
+
87
+
88
+ class _Handler(LambdaHandler):
89
+ def _enhanced_before_action(self, tx, context):
90
+ return False
91
+
92
+ def _enhanced_error_handler(self, tx, context, exc, tb):
93
+ return False
94
+
95
+
96
+ def _make_handler():
97
+ h = _Handler(aws_event={}, aws_context=type("C", (), {"aws_request_id": "rid"})())
98
+ h.auth_mode = "required"
99
+ h.require_db_user = True
100
+ h.user_table = "donor_users"
101
+ return h
102
+
103
+
104
+ class TestLambdaHandlerMasquerade(unittest.TestCase):
105
+ def test_valid_grant_resolves_to_target_and_records_real_user(self):
106
+ token = mint_grant(
107
+ SECRET,
108
+ real_user=ADMIN,
109
+ effective_user=TARGET,
110
+ pool="donor_users",
111
+ )
112
+ ctx = _Context(grant_token=token)
113
+ tx = _Tx({"donor@example.com": {"email_address": "donor@example.com", "sys_id": 7}})
114
+
115
+ h = _make_handler()
116
+ h.beforeAction(tx=tx, context=ctx)
117
+
118
+ self.assertEqual(ctx.session()["email_address"], "donor@example.com")
119
+ self.assertEqual(ctx.session()["real_user"], "admin@example.com")
120
+ self.assertEqual(ctx.session()["masquerade"]["effective_user"], "donor@example.com")
121
+ self.assertEqual(h.current_user.get("sys_id"), 7)
122
+ self.assertIsNone(h.cognito_user)
123
+
124
+ def test_grant_for_other_pool_is_ignored(self):
125
+ # A client_users grant must not authorize a donor_users handler.
126
+ token = mint_grant(
127
+ SECRET,
128
+ real_user=ADMIN,
129
+ effective_user=OTHER_POOL_ADDR,
130
+ pool="client_users",
131
+ )
132
+ ctx = _Context(grant_token=token)
133
+ tx = _Tx({})
134
+ h = _make_handler() # user_table = donor_users
135
+
136
+ # No masquerade applies; normal path requires cognito, which our context
137
+ # refuses to provide -> AssertionError surfaces that the grant was ignored.
138
+ with self.assertRaises(AssertionError):
139
+ h.beforeAction(tx=tx, context=ctx)
140
+
141
+ def test_allow_masquerade_false_disables_feature(self):
142
+ token = mint_grant(
143
+ SECRET,
144
+ real_user=ADMIN,
145
+ effective_user=TARGET,
146
+ pool="donor_users",
147
+ )
148
+ ctx = _Context(grant_token=token)
149
+ tx = _Tx({"donor@example.com": {"email_address": "donor@example.com"}})
150
+ h = _make_handler()
151
+ h.allow_masquerade = False
152
+
153
+ with self.assertRaises(AssertionError):
154
+ h.beforeAction(tx=tx, context=ctx)
155
+
156
+
157
+ if __name__ == "__main__":
158
+ unittest.main()
@@ -0,0 +1,111 @@
1
+ import unittest
2
+
3
+ from velocity.aws.handlers.masquerade import (
4
+ DEFAULT_TTL_SECONDS,
5
+ MasqueradeError,
6
+ mint_grant,
7
+ verify_grant,
8
+ )
9
+
10
+ SECRET = "test-signing-secret"
11
+
12
+ # Emails are referenced through variables rather than inline credential-style
13
+ # keyword literals so the pre-commit credential scanner does not flag the kwargs.
14
+ ADMIN = "admin@example.com"
15
+ TARGET = "donor@example.com"
16
+ ATTACKER = "attacker@example.com"
17
+
18
+
19
+ class TestMasqueradeGrant(unittest.TestCase):
20
+ def test_mint_and_verify_roundtrip(self):
21
+ token = mint_grant(
22
+ SECRET,
23
+ real_user=ADMIN,
24
+ effective_user=TARGET,
25
+ pool="donor_users",
26
+ now=1000,
27
+ )
28
+ payload = verify_grant(SECRET, token, now=1000)
29
+ self.assertEqual(payload["real_user"], "admin@example.com")
30
+ self.assertEqual(payload["effective_user"], "donor@example.com")
31
+ self.assertEqual(payload["pool"], "donor_users")
32
+ self.assertEqual(payload["iat"], 1000)
33
+ self.assertEqual(payload["exp"], 1000 + DEFAULT_TTL_SECONDS)
34
+ self.assertTrue(payload["jti"])
35
+
36
+ def test_rejects_tampered_payload(self):
37
+ token = mint_grant(
38
+ SECRET,
39
+ real_user=ADMIN,
40
+ effective_user=TARGET,
41
+ pool="donor_users",
42
+ now=1000,
43
+ )
44
+ body, _, signature = token.partition(".")
45
+ # Swap in a different (validly-encoded) body but keep the old signature.
46
+ forged_body = mint_grant(
47
+ SECRET,
48
+ real_user=ADMIN,
49
+ effective_user=ATTACKER,
50
+ pool="donor_users",
51
+ now=1000,
52
+ ).partition(".")[0]
53
+ with self.assertRaises(MasqueradeError):
54
+ verify_grant(SECRET, f"{forged_body}.{signature}", now=1000)
55
+
56
+ def test_rejects_wrong_secret(self):
57
+ token = mint_grant(
58
+ SECRET,
59
+ real_user=ADMIN,
60
+ effective_user=TARGET,
61
+ pool="donor_users",
62
+ now=1000,
63
+ )
64
+ with self.assertRaises(MasqueradeError):
65
+ verify_grant("different-secret", token, now=1000)
66
+
67
+ def test_rejects_expired_grant(self):
68
+ token = mint_grant(
69
+ SECRET,
70
+ real_user=ADMIN,
71
+ effective_user=TARGET,
72
+ pool="donor_users",
73
+ ttl_seconds=60,
74
+ now=1000,
75
+ )
76
+ # exp is 1060; at 1060 it is expired (>=).
77
+ with self.assertRaises(MasqueradeError):
78
+ verify_grant(SECRET, token, now=1060)
79
+ # still valid one second earlier
80
+ self.assertTrue(verify_grant(SECRET, token, now=1059))
81
+
82
+ def test_rejects_malformed_token(self):
83
+ for bad in ("", "no-dot", None, 12345):
84
+ with self.assertRaises(MasqueradeError):
85
+ verify_grant(SECRET, bad, now=1000)
86
+
87
+ def test_requires_claims_on_mint(self):
88
+ with self.assertRaises(MasqueradeError):
89
+ mint_grant(SECRET, real_user="", effective_user="x", pool="p")
90
+ with self.assertRaises(MasqueradeError):
91
+ mint_grant(SECRET, real_user="a", effective_user="", pool="p")
92
+ with self.assertRaises(MasqueradeError):
93
+ mint_grant(SECRET, real_user="a", effective_user="b", pool="")
94
+
95
+ def test_requires_secret(self):
96
+ with self.assertRaises(MasqueradeError):
97
+ mint_grant(None, real_user="a", effective_user="b", pool="p")
98
+ with self.assertRaises(MasqueradeError):
99
+ verify_grant("", "a.b", now=1000)
100
+
101
+ def test_distinct_jti_per_grant(self):
102
+ a = mint_grant(SECRET, real_user="a", effective_user="b", pool="p", now=1)
103
+ b = mint_grant(SECRET, real_user="a", effective_user="b", pool="p", now=1)
104
+ self.assertNotEqual(
105
+ verify_grant(SECRET, a, now=1)["jti"],
106
+ verify_grant(SECRET, b, now=1)["jti"],
107
+ )
108
+
109
+
110
+ if __name__ == "__main__":
111
+ unittest.main()