velocity-python 0.1.83__tar.gz → 0.1.86__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 (244) hide show
  1. {velocity_python-0.1.83/src/velocity_python.egg-info → velocity_python-0.1.86}/PKG-INFO +17 -1
  2. {velocity_python-0.1.83 → velocity_python-0.1.86}/README.md +1 -0
  3. {velocity_python-0.1.83 → velocity_python-0.1.86}/pyproject.toml +24 -1
  4. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/__init__.py +1 -1
  5. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/mixins/data_service.py +36 -1
  6. velocity_python-0.1.86/src/velocity/nosql/__init__.py +20 -0
  7. velocity_python-0.1.86/src/velocity/nosql/core/__init__.py +0 -0
  8. velocity_python-0.1.86/src/velocity/nosql/core/capabilities.py +45 -0
  9. velocity_python-0.1.86/src/velocity/nosql/core/store.py +328 -0
  10. velocity_python-0.1.86/src/velocity/nosql/core/where.py +244 -0
  11. velocity_python-0.1.86/src/velocity/nosql/exceptions.py +63 -0
  12. velocity_python-0.1.86/src/velocity/nosql/servers/__init__.py +2 -0
  13. velocity_python-0.1.86/src/velocity/nosql/servers/base/__init__.py +31 -0
  14. velocity_python-0.1.86/src/velocity/nosql/servers/cassandra/__init__.py +216 -0
  15. velocity_python-0.1.86/src/velocity/nosql/servers/cassandra/translation.py +77 -0
  16. velocity_python-0.1.86/src/velocity/nosql/servers/couchbase/__init__.py +267 -0
  17. velocity_python-0.1.86/src/velocity/nosql/servers/couchbase/translation.py +93 -0
  18. velocity_python-0.1.86/src/velocity/nosql/servers/dynamodb/__init__.py +307 -0
  19. velocity_python-0.1.86/src/velocity/nosql/servers/dynamodb/translation.py +107 -0
  20. velocity_python-0.1.86/src/velocity/nosql/servers/elasticsearch/__init__.py +281 -0
  21. velocity_python-0.1.86/src/velocity/nosql/servers/elasticsearch/translation.py +101 -0
  22. velocity_python-0.1.86/src/velocity/nosql/servers/memory/__init__.py +157 -0
  23. velocity_python-0.1.86/src/velocity/nosql/servers/mongodb/__init__.py +236 -0
  24. velocity_python-0.1.86/src/velocity/nosql/servers/mongodb/translation.py +64 -0
  25. velocity_python-0.1.86/src/velocity/nosql/servers/redis/__init__.py +230 -0
  26. {velocity_python-0.1.83 → velocity_python-0.1.86/src/velocity_python.egg-info}/PKG-INFO +17 -1
  27. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity_python.egg-info/SOURCES.txt +27 -0
  28. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity_python.egg-info/requires.txt +20 -0
  29. velocity_python-0.1.86/tests/test_delete_hook_fk_blocked.py +154 -0
  30. velocity_python-0.1.86/tests/test_nosql_dynamodb.py +103 -0
  31. velocity_python-0.1.86/tests/test_nosql_memory.py +57 -0
  32. velocity_python-0.1.86/tests/test_nosql_mongodb.py +69 -0
  33. velocity_python-0.1.86/tests/test_nosql_redis.py +72 -0
  34. velocity_python-0.1.86/tests/test_nosql_translation.py +249 -0
  35. velocity_python-0.1.86/tests/test_nosql_where.py +115 -0
  36. {velocity_python-0.1.83 → velocity_python-0.1.86}/LICENSE +0 -0
  37. {velocity_python-0.1.83 → velocity_python-0.1.86}/setup.cfg +0 -0
  38. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/__init__.py +0 -0
  39. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/amplify.py +0 -0
  40. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/amplify_build.py +0 -0
  41. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/assets/__init__.py +0 -0
  42. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/assets/backfill.py +0 -0
  43. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/assets/indexing.py +0 -0
  44. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/assets/references.py +0 -0
  45. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/assets/service.py +0 -0
  46. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/assets/usage_index.py +0 -0
  47. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/dirty_pipeline.py +0 -0
  48. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/__init__.py +0 -0
  49. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/base_handler.py +0 -0
  50. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/context.py +0 -0
  51. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/context_factory.py +0 -0
  52. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/exceptions.py +0 -0
  53. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  54. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/masquerade.py +0 -0
  55. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  56. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  57. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/perf.py +0 -0
  58. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/response.py +0 -0
  59. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  60. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/s3.py +0 -0
  61. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/ssm_config.py +0 -0
  62. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/tests/__init__.py +0 -0
  63. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  64. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  65. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/aws/tests/test_response.py +0 -0
  66. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/__init__.py +0 -0
  67. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/__init__.py +0 -0
  68. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/async_support.py +0 -0
  69. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/column.py +0 -0
  70. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/database.py +0 -0
  71. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/decorators.py +0 -0
  72. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/engine.py +0 -0
  73. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/jsonproxy.py +0 -0
  74. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/result.py +0 -0
  75. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/row.py +0 -0
  76. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/sequence.py +0 -0
  77. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/table.py +0 -0
  78. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/transaction.py +0 -0
  79. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/core/view.py +0 -0
  80. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/exceptions.py +0 -0
  81. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/maintenance.py +0 -0
  82. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/migrations.py +0 -0
  83. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/__init__.py +0 -0
  84. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/base/__init__.py +0 -0
  85. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/base/initializer.py +0 -0
  86. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/base/operators.py +0 -0
  87. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/base/sql.py +0 -0
  88. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/base/types.py +0 -0
  89. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/mysql/__init__.py +0 -0
  90. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/mysql/operators.py +0 -0
  91. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/mysql/reserved.py +0 -0
  92. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/mysql/sql.py +0 -0
  93. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/mysql/types.py +0 -0
  94. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/postgres/__init__.py +0 -0
  95. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/postgres/operators.py +0 -0
  96. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/postgres/reserved.py +0 -0
  97. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/postgres/sql.py +0 -0
  98. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/postgres/types.py +0 -0
  99. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  100. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlite/operators.py +0 -0
  101. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  102. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlite/sql.py +0 -0
  103. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlite/types.py +0 -0
  104. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  105. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  106. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  107. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  108. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/sqlserver/types.py +0 -0
  109. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/servers/tablehelper.py +0 -0
  110. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/__init__.py +0 -0
  111. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/common_db_test.py +0 -0
  112. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/__init__.py +0 -0
  113. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/common.py +0 -0
  114. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/conftest.py +0 -0
  115. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_column.py +0 -0
  116. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  117. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_database.py +0 -0
  118. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  119. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  120. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  121. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_result.py +0 -0
  122. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_row.py +0 -0
  123. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  124. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  125. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  126. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  127. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  128. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_table.py +0 -0
  129. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  130. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  131. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/sql/__init__.py +0 -0
  132. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/sql/common.py +0 -0
  133. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  134. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  135. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  136. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_db_utils.py +0 -0
  137. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_postgres.py +0 -0
  138. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  139. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  140. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_result_caching.py +0 -0
  141. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  142. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  143. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  144. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  145. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_sql_builder.py +0 -0
  146. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_tablehelper.py +0 -0
  147. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/tests/test_view_helper.py +0 -0
  148. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/db/utils.py +0 -0
  149. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/logging.py +0 -0
  150. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/__init__.py +0 -0
  151. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/conv/__init__.py +0 -0
  152. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/conv/iconv.py +0 -0
  153. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/conv/oconv.py +0 -0
  154. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/db.py +0 -0
  155. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/export.py +0 -0
  156. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/format.py +0 -0
  157. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/mail.py +0 -0
  158. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/merge.py +0 -0
  159. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/pdf.py +0 -0
  160. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/__init__.py +0 -0
  161. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_db.py +0 -0
  162. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_fix.py +0 -0
  163. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_format.py +0 -0
  164. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_iconv.py +0 -0
  165. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_merge.py +0 -0
  166. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_oconv.py +0 -0
  167. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_original_error.py +0 -0
  168. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tests/test_timer.py +0 -0
  169. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/timer.py +0 -0
  170. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/misc/tools.py +0 -0
  171. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/__init__.py +0 -0
  172. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/authorizenet_adapter.py +0 -0
  173. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/authorizenet_mirror.py +0 -0
  174. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/base_adapter.py +0 -0
  175. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/braintree_adapter.py +0 -0
  176. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/braintree_mirror.py +0 -0
  177. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/charge_rules.py +0 -0
  178. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/stripe_adapter.py +0 -0
  179. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity/payment/stripe_mirror.py +0 -0
  180. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  181. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity_python.egg-info/entry_points.txt +0 -0
  182. {velocity_python-0.1.83 → velocity_python-0.1.86}/src/velocity_python.egg-info/top_level.txt +0 -0
  183. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_activity_tracking_aborted_tx.py +0 -0
  184. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_amplify_build.py +0 -0
  185. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_asset_indexing.py +0 -0
  186. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_asset_references.py +0 -0
  187. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_assets_service.py +0 -0
  188. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_async_support.py +0 -0
  189. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_batch_operations.py +0 -0
  190. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_column_tx_arg.py +0 -0
  191. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_concurrency_safety.py +0 -0
  192. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_connection_pool.py +0 -0
  193. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_connection_resilience.py +0 -0
  194. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_context_job_descriptions.py +0 -0
  195. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_db_credentials_ssm_cascade.py +0 -0
  196. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_db_maintenance.py +0 -0
  197. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_decorators.py +0 -0
  198. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_dirty_pipeline_fast_path.py +0 -0
  199. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_email_processing.py +0 -0
  200. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_enqueue_send_failures.py +0 -0
  201. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_get_cognito_user_provider.py +0 -0
  202. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_http_handler_rollback.py +0 -0
  203. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_iconv_money_to_cents.py +0 -0
  204. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_identifier_injection_guard.py +0 -0
  205. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_json_columns.py +0 -0
  206. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_jsonb_dict_adapter.py +0 -0
  207. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_lambda_handler.py +0 -0
  208. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_lambda_handler_auth.py +0 -0
  209. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_lambda_handler_masquerade.py +0 -0
  210. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_masquerade_grant.py +0 -0
  211. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_mixins_import.py +0 -0
  212. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_n_plus_one.py +0 -0
  213. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_observability.py +0 -0
  214. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_payment_authorizenet_adapter.py +0 -0
  215. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_payment_braintree_adapter.py +0 -0
  216. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_payment_braintree_mirror.py +0 -0
  217. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_payment_mirror_empty_table.py +0 -0
  218. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_payment_profile_sorting.py +0 -0
  219. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_payment_stripe_adapter.py +0 -0
  220. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_pdf.py +0 -0
  221. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_prepared_statements.py +0 -0
  222. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_psycopg3_upgrade.py +0 -0
  223. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_query_cache.py +0 -0
  224. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_restricted_direct_tables.py +0 -0
  225. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_retry_side_effect_guard.py +0 -0
  226. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_return_default_safety.py +0 -0
  227. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_row_batch_update.py +0 -0
  228. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_row_cache_staleness.py +0 -0
  229. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_schema_migrations.py +0 -0
  230. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_security_hardening.py +0 -0
  231. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_server_cursor.py +0 -0
  232. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_single_autocommit_safety.py +0 -0
  233. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_spreadsheet_functions.py +0 -0
  234. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_sqlite_backend.py +0 -0
  235. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_sqs_per_record_transactions.py +0 -0
  236. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_ssm_config.py +0 -0
  237. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_store_user_data.py +0 -0
  238. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  239. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_table_alter.py +0 -0
  240. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_transaction_class_wrapping.py +0 -0
  241. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_transaction_commit_and_ownership.py +0 -0
  242. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_transaction_edge_cases.py +0 -0
  243. {velocity_python-0.1.83 → velocity_python-0.1.86}/tests/test_where_clause_validation.py +0 -0
  244. {velocity_python-0.1.83 → velocity_python-0.1.86}/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.83
3
+ Version: 0.1.86
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
@@ -39,6 +39,16 @@ Provides-Extra: http
39
39
  Requires-Dist: requests>=2.32.0; extra == "http"
40
40
  Provides-Extra: mysql
41
41
  Requires-Dist: mysql-connector-python>=9.0.0; extra == "mysql"
42
+ Provides-Extra: redis
43
+ Requires-Dist: redis>=5.0.0; extra == "redis"
44
+ Provides-Extra: mongodb
45
+ Requires-Dist: pymongo>=4.6.0; extra == "mongodb"
46
+ Provides-Extra: elasticsearch
47
+ Requires-Dist: elasticsearch>=8.13.0; extra == "elasticsearch"
48
+ Provides-Extra: cassandra
49
+ Requires-Dist: cassandra-driver>=3.29.0; extra == "cassandra"
50
+ Provides-Extra: couchbase
51
+ Requires-Dist: couchbase>=4.2.0; extra == "couchbase"
42
52
  Provides-Extra: sqlserver
43
53
  Requires-Dist: python-tds>=1.15.0; extra == "sqlserver"
44
54
  Provides-Extra: postgres
@@ -70,6 +80,11 @@ Requires-Dist: braintree>=4.30.0; extra == "test"
70
80
  Requires-Dist: xmltodict>=0.13.0; extra == "test"
71
81
  Requires-Dist: authorizenet>=1.1.3; extra == "test"
72
82
  Requires-Dist: openpyxl>=3.1.0; extra == "test"
83
+ Requires-Dist: redis>=5.0.0; extra == "test"
84
+ Requires-Dist: fakeredis>=2.23.0; extra == "test"
85
+ Requires-Dist: pymongo>=4.6.0; extra == "test"
86
+ Requires-Dist: mongomock>=4.1.0; extra == "test"
87
+ Requires-Dist: moto[dynamodb]>=5.0.0; extra == "test"
73
88
  Provides-Extra: docs
74
89
  Requires-Dist: sphinx>=8.0.0; extra == "docs"
75
90
  Requires-Dist: sphinx-rtd-theme>=3.0.0; extra == "docs"
@@ -234,6 +249,7 @@ Full documentation is included in the `docs/` directory of the source distributi
234
249
  | Async Support | `docs/async.md` | AsyncTransaction, AsyncTable, AsyncResult, parallel queries with gather() |
235
250
  | Configuration Reference | `docs/configuration.md` | All environment variables, engine options, and connection settings |
236
251
  | AWS Lambda Handlers | `docs/aws-handlers.md` | LambdaHandler, SqsHandler, auth modes, per-record transactions |
252
+ | NoSQL Adapters | `docs/nosql.md` | Redis, DynamoDB, MongoDB, Elasticsearch, Cassandra, Couchbase behind one capability-honest contract |
237
253
  | Payment Processing | `docs/payment.md` | Stripe and Braintree adapters, payment lifecycle |
238
254
  | Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing |
239
255
  | Testing Guide | `docs/TESTING.md` | Running tests, markers, coverage |
@@ -157,6 +157,7 @@ Full documentation is included in the `docs/` directory of the source distributi
157
157
  | Async Support | `docs/async.md` | AsyncTransaction, AsyncTable, AsyncResult, parallel queries with gather() |
158
158
  | Configuration Reference | `docs/configuration.md` | All environment variables, engine options, and connection settings |
159
159
  | AWS Lambda Handlers | `docs/aws-handlers.md` | LambdaHandler, SqsHandler, auth modes, per-record transactions |
160
+ | NoSQL Adapters | `docs/nosql.md` | Redis, DynamoDB, MongoDB, Elasticsearch, Cassandra, Couchbase behind one capability-honest contract |
160
161
  | Payment Processing | `docs/payment.md` | Stripe and Braintree adapters, payment lifecycle |
161
162
  | Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing |
162
163
  | Testing Guide | `docs/TESTING.md` | Running tests, markers, coverage |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.1.83"
7
+ version = "0.1.86"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -63,6 +63,21 @@ http = [
63
63
  mysql = [
64
64
  "mysql-connector-python>=9.0.0",
65
65
  ]
66
+ redis = [
67
+ "redis>=5.0.0",
68
+ ]
69
+ mongodb = [
70
+ "pymongo>=4.6.0",
71
+ ]
72
+ elasticsearch = [
73
+ "elasticsearch>=8.13.0",
74
+ ]
75
+ cassandra = [
76
+ "cassandra-driver>=3.29.0",
77
+ ]
78
+ couchbase = [
79
+ "couchbase>=4.2.0",
80
+ ]
66
81
  sqlserver = [
67
82
  "python-tds>=1.15.0",
68
83
  ]
@@ -102,6 +117,14 @@ test = [
102
117
  "xmltodict>=0.13.0",
103
118
  "authorizenet>=1.1.3",
104
119
  "openpyxl>=3.1.0",
120
+ # NoSQL adapter unit tests run against in-process fakes (real clients for
121
+ # the wire formats, fakes for the servers). ES/Cassandra/Couchbase have
122
+ # translation-only unit tests and need no driver here.
123
+ "redis>=5.0.0",
124
+ "fakeredis>=2.23.0",
125
+ "pymongo>=4.6.0",
126
+ "mongomock>=4.1.0",
127
+ "moto[dynamodb]>=5.0.0",
105
128
  ]
106
129
  docs = [
107
130
  "sphinx>=8.0.0",
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.83"
1
+ __version__ = version = "0.1.86"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -17,10 +17,36 @@ from io import BytesIO, StringIO
17
17
 
18
18
  from velocity.aws import dirty_pipeline
19
19
  from velocity.aws.handlers.exceptions import AlertError
20
+ from velocity.db.exceptions import DbDataIntegrityError
20
21
  from velocity.misc import export
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
25
+ # Postgres FK-violation text, e.g.:
26
+ # ... violates foreign key constraint "billing_formula_client_sys_id_fkey"
27
+ # on table "billing_formula"
28
+ # DETAIL: Key (sys_id)=(1364) is still referenced from table "billing_formula".
29
+ _REFERENCED_FROM_RE = re.compile(r'still referenced from table "([^"]+)"')
30
+ _FK_ON_TABLE_RE = re.compile(r'foreign key constraint "[^"]+" on table "([^"]+)"')
31
+
32
+
33
+ def _fk_blocked_delete_message(table, exc):
34
+ """User-facing message for a delete blocked by a foreign key reference."""
35
+ text = str(exc)
36
+ match = _REFERENCED_FROM_RE.search(text) or _FK_ON_TABLE_RE.search(text)
37
+ label = table.replace("_", " ")
38
+ if match:
39
+ referencing = match.group(1).replace("_", " ")
40
+ return (
41
+ f"This {label} record can't be deleted because {referencing} "
42
+ f"records still reference it. Remove or reassign those related "
43
+ f"records first."
44
+ )
45
+ return (
46
+ f"This {label} record can't be deleted because related records "
47
+ f"still reference it."
48
+ )
49
+
24
50
 
25
51
  class DataServiceMixin:
26
52
  """
@@ -463,7 +489,16 @@ class DataServiceMixin:
463
489
  self._call_rwx_hook("before_delete", table, tx, table, sys_id, context)
464
490
  row = tx.table(table).find(sys_id)
465
491
  if row:
466
- row.clear()
492
+ # A failed DELETE aborts the whole postgres transaction; the
493
+ # savepoint confines the abort so the outer tx (activity
494
+ # tracking, response writes) stays usable.
495
+ try:
496
+ with tx.savepoint():
497
+ row.clear()
498
+ except DbDataIntegrityError as exc:
499
+ raise AlertError(
500
+ _fk_blocked_delete_message(table, exc)
501
+ ) from exc
467
502
  self._call_rwx_hook("after_delete", "common", tx, table, sys_id, context)
468
503
  self._call_rwx_hook("after_delete", table, tx, table, sys_id, context)
469
504
 
@@ -0,0 +1,20 @@
1
+ """
2
+ velocity.nosql — a thin, capability-honest layer over NoSQL backends.
3
+
4
+ Same idiom as velocity.db: collections are just names, documents are just
5
+ dicts, where-dicts use the familiar operator-prefix convention. Unlike
6
+ velocity.db there is no transaction contract — see docs/nosql.md.
7
+
8
+ Usage:
9
+ from velocity.nosql.servers import redis
10
+ store = redis.initialize()
11
+ col = store.collection("sessions")
12
+ col.put("abc", {"user": "paul"}, ttl=3600)
13
+ col.get("abc")
14
+ """
15
+
16
+ from velocity.nosql.core.capabilities import Capabilities
17
+ from velocity.nosql.core.store import KEY_FIELD, Collection, Store
18
+ from velocity.nosql import exceptions
19
+
20
+ __all__ = ["Capabilities", "Collection", "Store", "KEY_FIELD", "exceptions"]
@@ -0,0 +1,45 @@
1
+ """
2
+ Capability declaration for NoSQL adapters.
3
+
4
+ Every adapter declares what it can do; shared code checks the flags and the
5
+ Collection base class enforces them, so unsupported operations fail loudly
6
+ with NoSqlUnsupportedOperationError instead of degrading silently.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Capabilities:
14
+ """What a NoSQL adapter supports.
15
+
16
+ Attributes:
17
+ name: Adapter identifier ("memory", "redis", "dynamodb", ...).
18
+ query: ``find()`` with a where-dict is supported at all.
19
+ indexed_query: ad-hoc where-dicts are served efficiently without an
20
+ explicit index (Mongo/ES/Couchbase style). When False, ``find()``
21
+ beyond a key lookup requires ``allow_scan=True``.
22
+ scan: full-collection scans are possible (with ``allow_scan=True``).
23
+ ttl: per-document time-to-live on ``put()``.
24
+ lazy_ttl: expired documents may still exist physically and are
25
+ filtered out client-side on read (DynamoDB-style lazy expiry).
26
+ conditional_put: ``put_if_absent()`` supported.
27
+ cas: compare-and-swap via ``get_versioned()`` / ``cas()``.
28
+ transactions: backend has some atomic multi-write construct exposed
29
+ through the adapter's native handle (never through the shared
30
+ contract).
31
+ search: full-text relevance search via an adapter extension.
32
+ sort: server-side sort in ``find()``.
33
+ """
34
+
35
+ name: str
36
+ query: bool = False
37
+ indexed_query: bool = False
38
+ scan: bool = False
39
+ ttl: bool = False
40
+ lazy_ttl: bool = False
41
+ conditional_put: bool = False
42
+ cas: bool = False
43
+ transactions: bool = False
44
+ search: bool = False
45
+ sort: bool = False
@@ -0,0 +1,328 @@
1
+ """
2
+ Store / Collection — the shared NoSQL contract.
3
+
4
+ Deliberately *not* part of velocity.db's transactional world: there is no
5
+ ``tx`` here, and the shared contract makes no atomicity promises beyond the
6
+ single operation. Backends with atomic multi-write constructs expose them
7
+ via ``store.native`` only.
8
+
9
+ Tiered contract:
10
+ Tier 1 (every adapter): get / put / delete / exists / put_if_absent,
11
+ plus get_versioned / cas where Capabilities.cas is set.
12
+ Tier 2 (Capabilities.query): find(where) using velocity's where-dict
13
+ convention (operator prefixes: {">=age": 21}, {"%name": "Smi%"}).
14
+ Tier 3: store/collection ``.native`` and adapter-specific extensions.
15
+
16
+ Honesty rules enforced here, not left to adapters:
17
+ - Operations a backend can't support raise NoSqlUnsupportedOperationError.
18
+ - On backends without efficient ad-hoc queries (Capabilities.indexed_query
19
+ False), a find() beyond key lookups raises NoSqlUnindexedQueryError
20
+ unless the caller passes allow_scan=True.
21
+ """
22
+
23
+ import os
24
+ from abc import ABC, abstractmethod
25
+ from typing import Any, Dict, List, Optional, Tuple, Union
26
+
27
+ from velocity.nosql.core.capabilities import Capabilities
28
+ from velocity.nosql.core.where import EQ, IN, Condition, parse_where
29
+ from velocity.nosql.exceptions import (
30
+ NoSqlInvalidKeyError,
31
+ NoSqlSchemaLockedError,
32
+ NoSqlUnindexedQueryError,
33
+ NoSqlUnsupportedOperationError,
34
+ )
35
+
36
+ # Reserved field carrying the document key in find() results.
37
+ KEY_FIELD = "_key"
38
+ # Internal version field used by adapters that emulate CAS.
39
+ VERSION_FIELD = "_vel_version"
40
+ # Internal expiry marker used by adapters with lazy TTL (DynamoDB, Mongo).
41
+ EXPIRES_FIELD = "_expires_at"
42
+
43
+ MAX_KEY_LENGTH = 1024
44
+
45
+ SortSpec = Union[str, List[str], None]
46
+
47
+
48
+ def _schema_locked_from_env() -> bool:
49
+ return os.environ.get("VELOCITY_SCHEMA_LOCKED", "").lower() in ("true", "1", "yes")
50
+
51
+
52
+ def apply_sort(
53
+ results: List[Dict[str, Any]], sort: List[Tuple[str, bool]]
54
+ ) -> List[Dict[str, Any]]:
55
+ """Client-side sort used by adapters that filter in Python (memory,
56
+ Redis/Cassandra scans). None sorts last in both directions; stable sorts
57
+ applied in reverse order keep earlier sort keys dominant."""
58
+ for field, ascending in reversed(sort):
59
+ nulls = [d for d in results if d.get(field) is None]
60
+ rest = [d for d in results if d.get(field) is not None]
61
+ rest.sort(key=lambda d, f=field: d.get(f), reverse=not ascending)
62
+ results = rest + nulls
63
+ return results
64
+
65
+
66
+ def parse_sort(sort: SortSpec) -> List[Tuple[str, bool]]:
67
+ """Normalize a sort spec into [(field, ascending), ...].
68
+
69
+ Accepts "field", "-field" (descending), or a list of those.
70
+ """
71
+ if not sort:
72
+ return []
73
+ if isinstance(sort, str):
74
+ sort = [sort]
75
+ result = []
76
+ for item in sort:
77
+ item = item.strip()
78
+ if item.startswith("-"):
79
+ result.append((item[1:].strip(), False))
80
+ else:
81
+ result.append((item, True))
82
+ return result
83
+
84
+
85
+ class Store(ABC):
86
+ """A connection to one NoSQL backend; hands out Collections."""
87
+
88
+ capabilities: Capabilities
89
+
90
+ def __init__(self, schema_locked: bool = False):
91
+ self.schema_locked = schema_locked or _schema_locked_from_env()
92
+ self._collections: Dict[str, "Collection"] = {}
93
+
94
+ def collection(self, name: str) -> "Collection":
95
+ if not isinstance(name, str) or not name.strip():
96
+ raise NoSqlInvalidKeyError(
97
+ f"Collection name must be a non-empty string: {name!r}"
98
+ )
99
+ name = name.strip()
100
+ if name not in self._collections:
101
+ self._collections[name] = self._make_collection(name)
102
+ return self._collections[name]
103
+
104
+ def ensure_collection(self, name: str, **options) -> None:
105
+ """Create the backing object (table/index/...) if missing. Idempotent.
106
+
107
+ Honors VELOCITY_SCHEMA_LOCKED, mirroring velocity.db schema locking.
108
+ """
109
+ if self.schema_locked:
110
+ raise NoSqlSchemaLockedError(
111
+ f"Cannot ensure collection {name!r}: schema is locked"
112
+ )
113
+ self._ensure_collection(name, **options)
114
+
115
+ def drop_collection(self, name: str) -> None:
116
+ if self.schema_locked:
117
+ raise NoSqlSchemaLockedError(
118
+ f"Cannot drop collection {name!r}: schema is locked"
119
+ )
120
+ self._drop_collection(name)
121
+ self._collections.pop(name, None)
122
+
123
+ def close(self) -> None:
124
+ """Release backend resources. Default: no-op."""
125
+
126
+ @property
127
+ def native(self) -> Any:
128
+ """The raw backend client, for Tier-3 use. Never required for Tier 1/2."""
129
+ raise NoSqlUnsupportedOperationError(
130
+ f"{self.capabilities.name} store does not expose a native client"
131
+ )
132
+
133
+ @abstractmethod
134
+ def _make_collection(self, name: str) -> "Collection": ...
135
+
136
+ @abstractmethod
137
+ def _ensure_collection(self, name: str, **options) -> None: ...
138
+
139
+ @abstractmethod
140
+ def _drop_collection(self, name: str) -> None: ...
141
+
142
+
143
+ class Collection(ABC):
144
+ """One named document collection. Documents are dicts; keys are strings."""
145
+
146
+ def __init__(self, store: Store, name: str):
147
+ self.store = store
148
+ self.name = name
149
+
150
+ # ------------------------------------------------------------------ Tier 1
151
+
152
+ def get(self, key: str) -> Optional[Dict[str, Any]]:
153
+ """Return the document, or None if absent (or expired)."""
154
+ return self._get(self._check_key(key))
155
+
156
+ def put(self, key: str, doc: Dict[str, Any], ttl: Optional[int] = None) -> None:
157
+ """Write (create or replace) the document. ``ttl`` is seconds."""
158
+ self._put(self._check_key(key), self._check_doc(doc), self._check_ttl(ttl))
159
+
160
+ def put_if_absent(
161
+ self, key: str, doc: Dict[str, Any], ttl: Optional[int] = None
162
+ ) -> bool:
163
+ """Atomically create the document only if the key is absent.
164
+
165
+ Returns True if this call created it. This is the locking primitive.
166
+ """
167
+ self._require(self.store.capabilities.conditional_put, "put_if_absent")
168
+ return self._put_if_absent(
169
+ self._check_key(key), self._check_doc(doc), self._check_ttl(ttl)
170
+ )
171
+
172
+ def get_versioned(self, key: str) -> Tuple[Optional[Dict[str, Any]], Optional[Any]]:
173
+ """Return (document, version-token) for use with cas(). (None, None) if absent."""
174
+ self._require(self.store.capabilities.cas, "get_versioned")
175
+ return self._get_versioned(self._check_key(key))
176
+
177
+ def cas(
178
+ self, key: str, doc: Dict[str, Any], version: Any, ttl: Optional[int] = None
179
+ ) -> bool:
180
+ """Replace the document only if it still carries ``version``.
181
+
182
+ Returns True on success, False if the document changed (or vanished)
183
+ since ``get_versioned``. The version token is adapter-opaque.
184
+ """
185
+ self._require(self.store.capabilities.cas, "cas")
186
+ if version is None:
187
+ return False
188
+ return self._cas(
189
+ self._check_key(key), self._check_doc(doc), version, self._check_ttl(ttl)
190
+ )
191
+
192
+ def delete(self, key: str) -> bool:
193
+ """Delete the document. Returns True if it existed."""
194
+ return self._delete(self._check_key(key))
195
+
196
+ def exists(self, key: str) -> bool:
197
+ return self._exists(self._check_key(key))
198
+
199
+ # ------------------------------------------------------------------ Tier 2
200
+
201
+ def find(
202
+ self,
203
+ where: Optional[Dict[str, Any]] = None,
204
+ limit: Optional[int] = None,
205
+ sort: SortSpec = None,
206
+ allow_scan: bool = False,
207
+ ) -> List[Dict[str, Any]]:
208
+ """Query the collection with a velocity where-dict.
209
+
210
+ Returned documents include their key under ``_key``. Conditions are
211
+ AND-combined. ``sort`` is "field" / "-field" / a list of those.
212
+ """
213
+ conditions = self._gate_query(where, allow_scan)
214
+ sort_spec = parse_sort(sort)
215
+ if sort_spec:
216
+ self._require(self.store.capabilities.sort, "sort")
217
+ return self._find(conditions, limit, sort_spec)
218
+
219
+ def count(
220
+ self, where: Optional[Dict[str, Any]] = None, allow_scan: bool = False
221
+ ) -> int:
222
+ conditions = self._gate_query(where, allow_scan)
223
+ return self._count(conditions)
224
+
225
+ # ------------------------------------------------------------------ Tier 3
226
+
227
+ @property
228
+ def native(self) -> Any:
229
+ """The adapter's native handle for this collection (falls back to the store's)."""
230
+ return self.store.native
231
+
232
+ # ---------------------------------------------------------------- plumbing
233
+
234
+ def _gate_query(
235
+ self, where: Optional[Dict[str, Any]], allow_scan: bool
236
+ ) -> List[Condition]:
237
+ caps = self.store.capabilities
238
+ if not caps.query and not caps.scan:
239
+ raise NoSqlUnsupportedOperationError(
240
+ f"{caps.name} does not support find()/count()"
241
+ )
242
+ conditions = parse_where(where)
243
+ key_only = all(
244
+ c.field == KEY_FIELD and c.op in (EQ, IN) for c in conditions
245
+ ) and bool(conditions)
246
+ needs_scan = not caps.indexed_query and not key_only
247
+ if needs_scan and not caps.scan:
248
+ raise NoSqlUnsupportedOperationError(
249
+ f"{caps.name} can only query by {KEY_FIELD}"
250
+ )
251
+ if needs_scan and not allow_scan:
252
+ raise NoSqlUnindexedQueryError(
253
+ f"This query is a full scan on {caps.name}. Pass allow_scan=True "
254
+ f"to accept the cost, or restructure the query around {KEY_FIELD}."
255
+ )
256
+ return conditions
257
+
258
+ def _require(self, flag: bool, operation: str) -> None:
259
+ if not flag:
260
+ raise NoSqlUnsupportedOperationError(
261
+ f"{self.store.capabilities.name} does not support {operation}"
262
+ )
263
+
264
+ @staticmethod
265
+ def _check_key(key: str) -> str:
266
+ if not isinstance(key, str) or not key or len(key) > MAX_KEY_LENGTH:
267
+ raise NoSqlInvalidKeyError(
268
+ f"Keys must be non-empty strings up to {MAX_KEY_LENGTH} chars, got: {key!r}"
269
+ )
270
+ return key
271
+
272
+ @staticmethod
273
+ def _check_doc(doc: Dict[str, Any]) -> Dict[str, Any]:
274
+ if not isinstance(doc, dict):
275
+ raise TypeError(f"Documents must be dicts, got {type(doc)}")
276
+ # Reserved fields never round-trip into storage.
277
+ return {
278
+ k: v
279
+ for k, v in doc.items()
280
+ if k not in (KEY_FIELD, VERSION_FIELD, EXPIRES_FIELD)
281
+ }
282
+
283
+ def _check_ttl(self, ttl: Optional[int]) -> Optional[int]:
284
+ if ttl is None:
285
+ return None
286
+ self._require(self.store.capabilities.ttl, "ttl")
287
+ ttl = int(ttl)
288
+ if ttl <= 0:
289
+ raise ValueError(f"ttl must be a positive number of seconds, got {ttl}")
290
+ return ttl
291
+
292
+ # ------------------------------------------------------- adapter interface
293
+
294
+ @abstractmethod
295
+ def _get(self, key: str) -> Optional[Dict[str, Any]]: ...
296
+
297
+ @abstractmethod
298
+ def _put(self, key: str, doc: Dict[str, Any], ttl: Optional[int]) -> None: ...
299
+
300
+ def _put_if_absent(self, key: str, doc: Dict[str, Any], ttl: Optional[int]) -> bool:
301
+ raise NoSqlUnsupportedOperationError("put_if_absent not implemented")
302
+
303
+ def _get_versioned(
304
+ self, key: str
305
+ ) -> Tuple[Optional[Dict[str, Any]], Optional[Any]]:
306
+ raise NoSqlUnsupportedOperationError("get_versioned not implemented")
307
+
308
+ def _cas(
309
+ self, key: str, doc: Dict[str, Any], version: Any, ttl: Optional[int]
310
+ ) -> bool:
311
+ raise NoSqlUnsupportedOperationError("cas not implemented")
312
+
313
+ @abstractmethod
314
+ def _delete(self, key: str) -> bool: ...
315
+
316
+ def _exists(self, key: str) -> bool:
317
+ return self._get(key) is not None
318
+
319
+ def _find(
320
+ self,
321
+ conditions: List[Condition],
322
+ limit: Optional[int],
323
+ sort: List[Tuple[str, bool]],
324
+ ) -> List[Dict[str, Any]]:
325
+ raise NoSqlUnsupportedOperationError("find not implemented")
326
+
327
+ def _count(self, conditions: List[Condition]) -> int:
328
+ return len(self._find(conditions, None, []))