velocity-python 0.1.81__tar.gz → 0.1.85__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 (243) hide show
  1. {velocity_python-0.1.81 → velocity_python-0.1.85}/PKG-INFO +17 -1
  2. {velocity_python-0.1.81 → velocity_python-0.1.85}/README.md +1 -0
  3. {velocity_python-0.1.81 → velocity_python-0.1.85}/pyproject.toml +24 -1
  4. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/__init__.py +1 -1
  5. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/amplify_build.py +50 -1
  6. velocity_python-0.1.85/src/velocity/nosql/__init__.py +20 -0
  7. velocity_python-0.1.85/src/velocity/nosql/core/__init__.py +0 -0
  8. velocity_python-0.1.85/src/velocity/nosql/core/capabilities.py +45 -0
  9. velocity_python-0.1.85/src/velocity/nosql/core/store.py +328 -0
  10. velocity_python-0.1.85/src/velocity/nosql/core/where.py +244 -0
  11. velocity_python-0.1.85/src/velocity/nosql/exceptions.py +63 -0
  12. velocity_python-0.1.85/src/velocity/nosql/servers/__init__.py +2 -0
  13. velocity_python-0.1.85/src/velocity/nosql/servers/base/__init__.py +31 -0
  14. velocity_python-0.1.85/src/velocity/nosql/servers/cassandra/__init__.py +216 -0
  15. velocity_python-0.1.85/src/velocity/nosql/servers/cassandra/translation.py +77 -0
  16. velocity_python-0.1.85/src/velocity/nosql/servers/couchbase/__init__.py +267 -0
  17. velocity_python-0.1.85/src/velocity/nosql/servers/couchbase/translation.py +93 -0
  18. velocity_python-0.1.85/src/velocity/nosql/servers/dynamodb/__init__.py +307 -0
  19. velocity_python-0.1.85/src/velocity/nosql/servers/dynamodb/translation.py +107 -0
  20. velocity_python-0.1.85/src/velocity/nosql/servers/elasticsearch/__init__.py +281 -0
  21. velocity_python-0.1.85/src/velocity/nosql/servers/elasticsearch/translation.py +101 -0
  22. velocity_python-0.1.85/src/velocity/nosql/servers/memory/__init__.py +157 -0
  23. velocity_python-0.1.85/src/velocity/nosql/servers/mongodb/__init__.py +236 -0
  24. velocity_python-0.1.85/src/velocity/nosql/servers/mongodb/translation.py +64 -0
  25. velocity_python-0.1.85/src/velocity/nosql/servers/redis/__init__.py +230 -0
  26. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity_python.egg-info/PKG-INFO +17 -1
  27. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity_python.egg-info/SOURCES.txt +26 -0
  28. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity_python.egg-info/requires.txt +20 -0
  29. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_amplify_build.py +103 -0
  30. velocity_python-0.1.85/tests/test_nosql_dynamodb.py +103 -0
  31. velocity_python-0.1.85/tests/test_nosql_memory.py +57 -0
  32. velocity_python-0.1.85/tests/test_nosql_mongodb.py +69 -0
  33. velocity_python-0.1.85/tests/test_nosql_redis.py +72 -0
  34. velocity_python-0.1.85/tests/test_nosql_translation.py +249 -0
  35. velocity_python-0.1.85/tests/test_nosql_where.py +115 -0
  36. {velocity_python-0.1.81 → velocity_python-0.1.85}/LICENSE +0 -0
  37. {velocity_python-0.1.81 → velocity_python-0.1.85}/setup.cfg +0 -0
  38. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/__init__.py +0 -0
  39. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/amplify.py +0 -0
  40. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/assets/__init__.py +0 -0
  41. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/assets/backfill.py +0 -0
  42. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/assets/indexing.py +0 -0
  43. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/assets/references.py +0 -0
  44. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/assets/service.py +0 -0
  45. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/assets/usage_index.py +0 -0
  46. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/dirty_pipeline.py +0 -0
  47. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/__init__.py +0 -0
  48. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/base_handler.py +0 -0
  49. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/context.py +0 -0
  50. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/context_factory.py +0 -0
  51. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/exceptions.py +0 -0
  52. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  53. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/masquerade.py +0 -0
  54. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  55. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  56. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  57. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/perf.py +0 -0
  58. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/response.py +0 -0
  59. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  60. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/s3.py +0 -0
  61. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/ssm_config.py +0 -0
  62. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/tests/__init__.py +0 -0
  63. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  64. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  65. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/aws/tests/test_response.py +0 -0
  66. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/__init__.py +0 -0
  67. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/__init__.py +0 -0
  68. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/async_support.py +0 -0
  69. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/column.py +0 -0
  70. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/database.py +0 -0
  71. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/decorators.py +0 -0
  72. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/engine.py +0 -0
  73. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/jsonproxy.py +0 -0
  74. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/result.py +0 -0
  75. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/row.py +0 -0
  76. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/sequence.py +0 -0
  77. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/table.py +0 -0
  78. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/transaction.py +0 -0
  79. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/core/view.py +0 -0
  80. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/exceptions.py +0 -0
  81. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/maintenance.py +0 -0
  82. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/migrations.py +0 -0
  83. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/__init__.py +0 -0
  84. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/base/__init__.py +0 -0
  85. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/base/initializer.py +0 -0
  86. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/base/operators.py +0 -0
  87. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/base/sql.py +0 -0
  88. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/base/types.py +0 -0
  89. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/mysql/__init__.py +0 -0
  90. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/mysql/operators.py +0 -0
  91. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/mysql/reserved.py +0 -0
  92. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/mysql/sql.py +0 -0
  93. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/mysql/types.py +0 -0
  94. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/postgres/__init__.py +0 -0
  95. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/postgres/operators.py +0 -0
  96. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/postgres/reserved.py +0 -0
  97. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/postgres/sql.py +0 -0
  98. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/postgres/types.py +0 -0
  99. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  100. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlite/operators.py +0 -0
  101. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  102. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlite/sql.py +0 -0
  103. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlite/types.py +0 -0
  104. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  105. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  106. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  107. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  108. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/sqlserver/types.py +0 -0
  109. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/servers/tablehelper.py +0 -0
  110. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/__init__.py +0 -0
  111. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/common_db_test.py +0 -0
  112. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/__init__.py +0 -0
  113. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/common.py +0 -0
  114. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/conftest.py +0 -0
  115. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_column.py +0 -0
  116. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  117. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_database.py +0 -0
  118. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  119. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  120. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  121. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_result.py +0 -0
  122. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_row.py +0 -0
  123. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  124. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  125. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  126. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  127. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  128. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_table.py +0 -0
  129. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  130. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  131. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/sql/__init__.py +0 -0
  132. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/sql/common.py +0 -0
  133. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  134. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  135. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  136. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_db_utils.py +0 -0
  137. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_postgres.py +0 -0
  138. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  139. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  140. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_result_caching.py +0 -0
  141. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  142. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  143. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  144. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  145. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_sql_builder.py +0 -0
  146. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_tablehelper.py +0 -0
  147. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/tests/test_view_helper.py +0 -0
  148. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/db/utils.py +0 -0
  149. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/logging.py +0 -0
  150. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/__init__.py +0 -0
  151. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/conv/__init__.py +0 -0
  152. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/conv/iconv.py +0 -0
  153. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/conv/oconv.py +0 -0
  154. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/db.py +0 -0
  155. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/export.py +0 -0
  156. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/format.py +0 -0
  157. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/mail.py +0 -0
  158. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/merge.py +0 -0
  159. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/pdf.py +0 -0
  160. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/__init__.py +0 -0
  161. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_db.py +0 -0
  162. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_fix.py +0 -0
  163. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_format.py +0 -0
  164. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_iconv.py +0 -0
  165. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_merge.py +0 -0
  166. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_oconv.py +0 -0
  167. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_original_error.py +0 -0
  168. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tests/test_timer.py +0 -0
  169. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/timer.py +0 -0
  170. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/misc/tools.py +0 -0
  171. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/__init__.py +0 -0
  172. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/authorizenet_adapter.py +0 -0
  173. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/authorizenet_mirror.py +0 -0
  174. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/base_adapter.py +0 -0
  175. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/braintree_adapter.py +0 -0
  176. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/braintree_mirror.py +0 -0
  177. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/charge_rules.py +0 -0
  178. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/stripe_adapter.py +0 -0
  179. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity/payment/stripe_mirror.py +0 -0
  180. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  181. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity_python.egg-info/entry_points.txt +0 -0
  182. {velocity_python-0.1.81 → velocity_python-0.1.85}/src/velocity_python.egg-info/top_level.txt +0 -0
  183. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_activity_tracking_aborted_tx.py +0 -0
  184. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_asset_indexing.py +0 -0
  185. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_asset_references.py +0 -0
  186. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_assets_service.py +0 -0
  187. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_async_support.py +0 -0
  188. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_batch_operations.py +0 -0
  189. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_column_tx_arg.py +0 -0
  190. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_concurrency_safety.py +0 -0
  191. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_connection_pool.py +0 -0
  192. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_connection_resilience.py +0 -0
  193. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_context_job_descriptions.py +0 -0
  194. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_db_credentials_ssm_cascade.py +0 -0
  195. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_db_maintenance.py +0 -0
  196. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_decorators.py +0 -0
  197. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_dirty_pipeline_fast_path.py +0 -0
  198. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_email_processing.py +0 -0
  199. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_enqueue_send_failures.py +0 -0
  200. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_get_cognito_user_provider.py +0 -0
  201. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_http_handler_rollback.py +0 -0
  202. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_iconv_money_to_cents.py +0 -0
  203. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_identifier_injection_guard.py +0 -0
  204. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_json_columns.py +0 -0
  205. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_jsonb_dict_adapter.py +0 -0
  206. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_lambda_handler.py +0 -0
  207. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_lambda_handler_auth.py +0 -0
  208. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_lambda_handler_masquerade.py +0 -0
  209. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_masquerade_grant.py +0 -0
  210. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_mixins_import.py +0 -0
  211. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_n_plus_one.py +0 -0
  212. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_observability.py +0 -0
  213. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_payment_authorizenet_adapter.py +0 -0
  214. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_payment_braintree_adapter.py +0 -0
  215. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_payment_braintree_mirror.py +0 -0
  216. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_payment_mirror_empty_table.py +0 -0
  217. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_payment_profile_sorting.py +0 -0
  218. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_payment_stripe_adapter.py +0 -0
  219. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_pdf.py +0 -0
  220. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_prepared_statements.py +0 -0
  221. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_psycopg3_upgrade.py +0 -0
  222. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_query_cache.py +0 -0
  223. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_restricted_direct_tables.py +0 -0
  224. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_retry_side_effect_guard.py +0 -0
  225. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_return_default_safety.py +0 -0
  226. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_row_batch_update.py +0 -0
  227. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_row_cache_staleness.py +0 -0
  228. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_schema_migrations.py +0 -0
  229. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_security_hardening.py +0 -0
  230. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_server_cursor.py +0 -0
  231. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_single_autocommit_safety.py +0 -0
  232. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_spreadsheet_functions.py +0 -0
  233. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_sqlite_backend.py +0 -0
  234. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_sqs_per_record_transactions.py +0 -0
  235. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_ssm_config.py +0 -0
  236. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_store_user_data.py +0 -0
  237. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  238. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_table_alter.py +0 -0
  239. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_transaction_class_wrapping.py +0 -0
  240. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_transaction_commit_and_ownership.py +0 -0
  241. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_transaction_edge_cases.py +0 -0
  242. {velocity_python-0.1.81 → velocity_python-0.1.85}/tests/test_where_clause_validation.py +0 -0
  243. {velocity_python-0.1.81 → velocity_python-0.1.85}/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.81
3
+ Version: 0.1.85
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.81"
7
+ version = "0.1.85"
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.81"
1
+ __version__ = version = "0.1.85"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -294,7 +294,54 @@ def _normalize_policy_document(policy_document: Any) -> Any:
294
294
  return json.loads(policy_document)
295
295
 
296
296
 
297
+ def _canonical_statement(statement: Mapping[str, Any]) -> str:
298
+ """Canonical form for statement identity: scalar Action/Resource and their
299
+ list equivalents compare equal, action order is ignored."""
300
+ normalized: Dict[str, Any] = {}
301
+ for key, value in statement.items():
302
+ if key in ("Action", "NotAction", "Resource", "NotResource"):
303
+ items = value if isinstance(value, list) else [value]
304
+ normalized[key] = sorted(str(item) for item in items)
305
+ else:
306
+ normalized[key] = value
307
+ return json.dumps(normalized, sort_keys=True)
308
+
309
+
310
+ def _merge_policy_statements(
311
+ current_document: Mapping[str, Any], desired_document: Mapping[str, Any]
312
+ ) -> Dict[str, Any]:
313
+ """Additive union of policy statements.
314
+
315
+ Every statement already on the policy is kept (manually-added grants and
316
+ other apps' statements survive); desired statements not already present are
317
+ appended. Statements are never removed here — remove manually when needed.
318
+ """
319
+ current_statements = list(current_document.get("Statement", []))
320
+ seen = {_canonical_statement(statement) for statement in current_statements}
321
+ merged = list(current_statements)
322
+ for statement in desired_document.get("Statement", []):
323
+ key = _canonical_statement(statement)
324
+ if key not in seen:
325
+ merged.append(statement)
326
+ seen.add(key)
327
+ merged_document = dict(current_document)
328
+ if "Version" not in merged_document and "Version" in desired_document:
329
+ merged_document["Version"] = desired_document["Version"]
330
+ merged_document["Statement"] = merged
331
+ return merged_document
332
+
333
+
297
334
  def _sync_managed_policy_document(iam_client, policy_arn: str, policy_document: Any) -> None:
335
+ """Ensure the managed policy contains every desired statement (additive).
336
+
337
+ The policy document is the union of what is already on the policy and the
338
+ desired statements; existing statements are never dropped. This keeps a
339
+ shared policy (e.g. lambda-vpc-execution, attached by every app's build)
340
+ stable across builds of apps with differing extra statements, and preserves
341
+ grants added out-of-band. Note: IAM managed policies cap at 6144 chars —
342
+ if the union outgrows that, create_policy_version raises and the surplus
343
+ must be pruned manually.
344
+ """
298
345
  policy = retryable_call(iam_client.get_policy, PolicyArn=policy_arn)["Policy"]
299
346
  current_version = retryable_call(
300
347
  iam_client.get_policy_version,
@@ -303,7 +350,9 @@ def _sync_managed_policy_document(iam_client, policy_arn: str, policy_document:
303
350
  )["PolicyVersion"]
304
351
 
305
352
  current_document = _normalize_policy_document(current_version["Document"])
306
- desired_document = _normalize_policy_document(policy_document)
353
+ desired_document = _merge_policy_statements(
354
+ current_document, _normalize_policy_document(policy_document)
355
+ )
307
356
  if json.dumps(current_document, sort_keys=True) == json.dumps(desired_document, sort_keys=True):
308
357
  return
309
358
 
@@ -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, []))