velocity-python 0.1.12__tar.gz → 0.1.14__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 (189) hide show
  1. {velocity_python-0.1.12/src/velocity_python.egg-info → velocity_python-0.1.14}/PKG-INFO +4 -2
  2. {velocity_python-0.1.12 → velocity_python-0.1.14}/pyproject.toml +5 -2
  3. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/engine.py +97 -75
  5. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/base/sql.py +24 -0
  6. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/mysql/sql.py +23 -0
  7. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/postgres/sql.py +36 -0
  8. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlite/sql.py +19 -0
  9. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlserver/sql.py +23 -0
  10. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_engine.py +4 -8
  11. velocity_python-0.1.14/src/velocity/db/tests/test_process_error_robustness.py +305 -0
  12. velocity_python-0.1.14/src/velocity/misc/pdf.py +199 -0
  13. {velocity_python-0.1.12 → velocity_python-0.1.14/src/velocity_python.egg-info}/PKG-INFO +4 -2
  14. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  15. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity_python.egg-info/requires.txt +4 -1
  16. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_connection_pool.py +5 -5
  17. velocity_python-0.1.14/tests/test_pdf.py +188 -0
  18. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_security_hardening.py +4 -0
  19. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_where_clause_validation.py +12 -0
  20. velocity_python-0.1.12/src/velocity/db/tests/test_process_error_robustness.py +0 -292
  21. {velocity_python-0.1.12 → velocity_python-0.1.14}/LICENSE +0 -0
  22. {velocity_python-0.1.12 → velocity_python-0.1.14}/README.md +0 -0
  23. {velocity_python-0.1.12 → velocity_python-0.1.14}/setup.cfg +0 -0
  24. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/__init__.py +0 -0
  25. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/formbuilder/__init__.py +0 -0
  26. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/formbuilder/reshaper.py +0 -0
  27. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/invoices.py +0 -0
  28. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/orders.py +0 -0
  29. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/payments.py +0 -0
  30. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/purchase_orders.py +0 -0
  31. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/tests/__init__.py +0 -0
  32. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/tests/test_email_processing.py +0 -0
  33. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  34. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  35. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/validators/__init__.py +0 -0
  36. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/app/validators/formbuilder_template.py +0 -0
  37. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/__init__.py +0 -0
  38. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/amplify.py +0 -0
  39. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/amplify_build.py +0 -0
  40. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/__init__.py +0 -0
  41. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/base_handler.py +0 -0
  42. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/context.py +0 -0
  43. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/context_factory.py +0 -0
  44. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/exceptions.py +0 -0
  45. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  46. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  47. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  48. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  49. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/perf.py +0 -0
  50. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/response.py +0 -0
  51. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  52. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/tests/__init__.py +0 -0
  53. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  54. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  55. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/aws/tests/test_response.py +0 -0
  56. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/__init__.py +0 -0
  57. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/__init__.py +0 -0
  58. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/async_support.py +0 -0
  59. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/column.py +0 -0
  60. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/database.py +0 -0
  61. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/decorators.py +0 -0
  62. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/result.py +0 -0
  63. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/row.py +0 -0
  64. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/sequence.py +0 -0
  65. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/table.py +0 -0
  66. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/transaction.py +0 -0
  67. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/core/view.py +0 -0
  68. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/exceptions.py +0 -0
  69. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/migrations.py +0 -0
  70. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/__init__.py +0 -0
  71. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/base/__init__.py +0 -0
  72. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/base/initializer.py +0 -0
  73. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/base/operators.py +0 -0
  74. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/base/types.py +0 -0
  75. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/mysql/__init__.py +0 -0
  76. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/mysql/operators.py +0 -0
  77. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/mysql/reserved.py +0 -0
  78. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/mysql/types.py +0 -0
  79. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/postgres/__init__.py +0 -0
  80. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/postgres/operators.py +0 -0
  81. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/postgres/reserved.py +0 -0
  82. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/postgres/types.py +0 -0
  83. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  84. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlite/operators.py +0 -0
  85. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  86. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlite/types.py +0 -0
  87. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  88. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  89. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  90. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/sqlserver/types.py +0 -0
  91. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/servers/tablehelper.py +0 -0
  92. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/__init__.py +0 -0
  93. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/common_db_test.py +0 -0
  94. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/__init__.py +0 -0
  95. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/common.py +0 -0
  96. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_column.py +0 -0
  97. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  98. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_database.py +0 -0
  99. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  100. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  101. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_result.py +0 -0
  102. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_row.py +0 -0
  103. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  104. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  105. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  106. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  107. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  108. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_table.py +0 -0
  109. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  110. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  111. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/sql/__init__.py +0 -0
  112. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/sql/common.py +0 -0
  113. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  114. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  115. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  116. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_db_utils.py +0 -0
  117. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_postgres.py +0 -0
  118. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  119. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_result_caching.py +0 -0
  120. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  121. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  122. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  123. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  124. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_sql_builder.py +0 -0
  125. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_tablehelper.py +0 -0
  126. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/tests/test_view_helper.py +0 -0
  127. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/db/utils.py +0 -0
  128. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/logging.py +0 -0
  129. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/__init__.py +0 -0
  130. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/conv/__init__.py +0 -0
  131. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/conv/iconv.py +0 -0
  132. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/conv/oconv.py +0 -0
  133. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/db.py +0 -0
  134. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/export.py +0 -0
  135. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/format.py +0 -0
  136. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/mail.py +0 -0
  137. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/merge.py +0 -0
  138. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/__init__.py +0 -0
  139. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_db.py +0 -0
  140. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_fix.py +0 -0
  141. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_format.py +0 -0
  142. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_iconv.py +0 -0
  143. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_merge.py +0 -0
  144. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_oconv.py +0 -0
  145. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_original_error.py +0 -0
  146. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tests/test_timer.py +0 -0
  147. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/timer.py +0 -0
  148. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/misc/tools.py +0 -0
  149. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/__init__.py +0 -0
  150. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/authorizenet_adapter.py +0 -0
  151. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/base_adapter.py +0 -0
  152. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/braintree_adapter.py +0 -0
  153. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/charge_rules.py +0 -0
  154. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/demo_profiles.py +0 -0
  155. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/profiles.py +0 -0
  156. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/router.py +0 -0
  157. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity/payment/stripe_adapter.py +0 -0
  158. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  159. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity_python.egg-info/entry_points.txt +0 -0
  160. {velocity_python-0.1.12 → velocity_python-0.1.14}/src/velocity_python.egg-info/top_level.txt +0 -0
  161. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_amplify_build.py +0 -0
  162. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_async_support.py +0 -0
  163. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_batch_operations.py +0 -0
  164. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_concurrency_safety.py +0 -0
  165. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_connection_resilience.py +0 -0
  166. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_decorators.py +0 -0
  167. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_formbuilder_reshaper.py +0 -0
  168. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_formbuilder_template_validator.py +0 -0
  169. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_iconv_money_to_cents.py +0 -0
  170. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_lambda_handler.py +0 -0
  171. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_lambda_handler_auth.py +0 -0
  172. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_mixins_import.py +0 -0
  173. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_n_plus_one.py +0 -0
  174. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_observability.py +0 -0
  175. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_payment_braintree_adapter.py +0 -0
  176. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_payment_demo_profiles.py +0 -0
  177. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_payment_profiles.py +0 -0
  178. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_payment_router.py +0 -0
  179. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_payment_stripe_adapter.py +0 -0
  180. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_prepared_statements.py +0 -0
  181. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_psycopg3_upgrade.py +0 -0
  182. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_query_cache.py +0 -0
  183. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_row_batch_update.py +0 -0
  184. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_row_cache_staleness.py +0 -0
  185. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_row_dirty_tracking.py +0 -0
  186. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_schema_migrations.py +0 -0
  187. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_sqs_per_record_transactions.py +0 -0
  188. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  189. {velocity_python-0.1.12 → velocity_python-0.1.14}/tests/test_table_alter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.1.12
3
+ Version: 0.1.14
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
@@ -29,6 +29,8 @@ Provides-Extra: excel
29
29
  Requires-Dist: openpyxl>=3.1.0; extra == "excel"
30
30
  Provides-Extra: templates
31
31
  Requires-Dist: jinja2>=3.1.0; extra == "templates"
32
+ Provides-Extra: pdf
33
+ Requires-Dist: weasyprint>=62.0; extra == "pdf"
32
34
  Provides-Extra: http
33
35
  Requires-Dist: requests>=2.32.0; extra == "http"
34
36
  Provides-Extra: mysql
@@ -41,7 +43,7 @@ Provides-Extra: payment
41
43
  Requires-Dist: stripe>=12.0.0; extra == "payment"
42
44
  Requires-Dist: braintree>=4.30.0; extra == "payment"
43
45
  Provides-Extra: all
44
- Requires-Dist: velocity-python[aws,excel,http,payment,postgres,templates]; extra == "all"
46
+ Requires-Dist: velocity-python[aws,excel,http,payment,pdf,postgres,templates]; extra == "all"
45
47
  Provides-Extra: dev
46
48
  Requires-Dist: pytest>=8.0.0; extra == "dev"
47
49
  Requires-Dist: pytest-cov>=6.0.0; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.1.12"
7
+ version = "0.1.14"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -48,6 +48,9 @@ excel = [
48
48
  templates = [
49
49
  "jinja2>=3.1.0",
50
50
  ]
51
+ pdf = [
52
+ "weasyprint>=62.0",
53
+ ]
51
54
  http = [
52
55
  "requests>=2.32.0",
53
56
  ]
@@ -65,7 +68,7 @@ payment = [
65
68
  "braintree>=4.30.0",
66
69
  ]
67
70
  all = [
68
- "velocity-python[postgres,aws,excel,templates,http,payment]",
71
+ "velocity-python[postgres,aws,excel,templates,http,payment,pdf]",
69
72
  ]
70
73
  dev = [
71
74
  "pytest>=8.0.0",
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.12"
1
+ __version__ = version = "0.1.14"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -2,6 +2,7 @@ import inspect
2
2
  import os
3
3
  import re
4
4
  import time
5
+ import traceback
5
6
  import threading
6
7
  from contextlib import contextmanager
7
8
  from functools import wraps
@@ -14,6 +15,51 @@ import logging
14
15
  logger = logging.getLogger("velocity.db.engine")
15
16
  logger.setLevel(logging.INFO) # Or DEBUG for more verbosity
16
17
 
18
+ # Map category keys (matching SQL dialect attribute name prefixes) to exception classes.
19
+ _CATEGORY_EXCEPTION_MAP = {
20
+ "Application": exceptions.DbApplicationError,
21
+ "ColumnMissing": exceptions.DbColumnMissingError,
22
+ "TableMissing": exceptions.DbTableMissingError,
23
+ "DatabaseMissing": exceptions.DbDatabaseMissingError,
24
+ "ForeignKeyMissing": exceptions.DbForeignKeyMissingError,
25
+ "Truncation": exceptions.DbTruncationError,
26
+ "DataIntegrity": exceptions.DbDataIntegrityError,
27
+ "Connection": exceptions.DbConnectionError,
28
+ "DuplicateKey": exceptions.DbDuplicateKeyError,
29
+ "ObjectExists": exceptions.DbObjectExistsError,
30
+ "LockTimeout": exceptions.DbLockTimeoutError,
31
+ "RetryTransaction": exceptions.DbRetryTransaction,
32
+ }
33
+
34
+ # Attribute names on BaseSQLDialect, in the order they should be checked.
35
+ _CODE_LIST_ATTRS = [
36
+ ("ApplicationErrorCodes", "Application"),
37
+ ("ColumnMissingErrorCodes", "ColumnMissing"),
38
+ ("TableMissingErrorCodes", "TableMissing"),
39
+ ("DatabaseMissingErrorCodes", "DatabaseMissing"),
40
+ ("ForeignKeyMissingErrorCodes", "ForeignKeyMissing"),
41
+ ("TruncationErrorCodes", "Truncation"),
42
+ ("DataIntegrityErrorCodes", "DataIntegrity"),
43
+ ("ConnectionErrorCodes", "Connection"),
44
+ ("DuplicateKeyErrorCodes", "DuplicateKey"),
45
+ ("DatabaseObjectExistsErrorCodes", "ObjectExists"),
46
+ ("LockTimeoutErrorCodes", "LockTimeout"),
47
+ ("RetryTransactionCodes", "RetryTransaction"),
48
+ ]
49
+
50
+
51
+ def _build_error_code_map(sql_dialect) -> dict:
52
+ """Build a {code: exception_class} dict from a dialect's code lists."""
53
+ code_map: dict = {}
54
+ for attr, category in _CODE_LIST_ATTRS:
55
+ exc_cls = _CATEGORY_EXCEPTION_MAP[category]
56
+ for code in getattr(sql_dialect, attr, ()):
57
+ code_map.setdefault(code, exc_cls) # first match wins
58
+ return code_map
59
+
60
+ # Path of this file — used to filter stack frames.
61
+ _ENGINE_FILE = os.path.normpath(__file__)
62
+
17
63
 
18
64
  class ConnectionPool:
19
65
  """
@@ -198,6 +244,9 @@ class Engine:
198
244
  self.__sql = sql
199
245
  self.__driver = driver
200
246
 
247
+ # Pre-build the error-code → exception-class lookup for O(1) classification.
248
+ self.__error_code_map = _build_error_code_map(sql) if sql else {}
249
+
201
250
  if connect_timeout is None:
202
251
  connect_timeout = int(os.environ.get("VELOCITY_CONNECT_TIMEOUT", "5"))
203
252
  self.__connect_timeout = connect_timeout
@@ -739,115 +788,88 @@ class Engine:
739
788
  """
740
789
  Central method to parse driver exceptions and re-raise them as our custom exceptions.
741
790
  """
742
- logger = logging.getLogger(__name__)
743
-
744
- # If it's already a velocity exception, just re-raise it
791
+ # If it's already a velocity exception, just re-raise it.
745
792
  if isinstance(exception, exceptions.DbException):
746
793
  raise exception
747
794
 
748
- # Get error code and message from the SQL driver
795
+ # Extract error code & message from the driver exception.
749
796
  try:
750
797
  error_code, error_message = self.sql.get_error(exception)
751
798
  except Exception:
752
799
  error_code, error_message = None, str(exception)
753
800
 
754
- msg = str(exception).strip().lower()
801
+ msg = str(exception)
755
802
 
756
- # Create enhanced error message with SQL query and context
757
- enhanced_message = str(exception)
803
+ # ── Build enhanced message ───────────────────────────────────
804
+ enhanced_message = msg
758
805
 
759
- # Add specific guidance for common WHERE clause errors
760
- exception_str_lower = str(exception).lower()
761
- if "argument of where must be type boolean" in exception_str_lower:
762
- enhanced_message += (
763
- "\n\n*** WHERE CLAUSE ERROR ***\n"
764
- "This error typically occurs when a WHERE clause contains a bare value "
765
- "instead of a proper boolean expression.\n"
766
- "Common fixes:\n"
767
- " - Change WHERE 1001 to WHERE sys_id = 1001\n"
768
- " - Change WHERE {'column': value} format in dictionaries\n"
769
- " - Ensure string WHERE clauses are complete SQL expressions"
770
- )
806
+ # Dialect-specific developer guidance (e.g. WHERE-clause hints).
807
+ try:
808
+ hint = self.sql.enhance_message(msg)
809
+ except (AttributeError, TypeError):
810
+ hint = None
811
+ if isinstance(hint, str):
812
+ enhanced_message += hint
771
813
 
772
814
  if sql:
773
815
  enhanced_message += (
774
816
  f"\n\nSQL Query:\n{self._format_sql_with_params(sql, parameters)}"
775
817
  )
776
818
 
777
- # Add call stack context for better debugging
778
- import traceback
779
-
780
- stack_trace = traceback.format_stack()
781
- # Get the last few frames that aren't in the error handling itself
819
+ # Append a few relevant call-stack frames for context.
782
820
  relevant_frames = [
783
- frame
784
- for frame in stack_trace
785
- if "process_error" not in frame and "logging" not in frame
821
+ frame for frame in traceback.format_stack()
822
+ if _ENGINE_FILE not in frame
786
823
  ][-3:]
787
824
  if relevant_frames:
788
825
  enhanced_message += "\n\nCall Context:\n" + "".join(relevant_frames)
789
826
 
790
- # Mask any credentials that may have leaked into driver error messages
791
- # (e.g. connection strings containing password=...).
827
+ # Mask credentials that may have leaked into driver error text.
792
828
  enhanced_message = mask_sensitive_in_string(enhanced_message)
793
829
 
794
- # Note: SQL formatting for logging is available via _format_sql_with_params,
795
- # but we intentionally avoid eager logging here.
796
-
797
- # Direct error code mapping
798
- if error_code in self.sql.ApplicationErrorCodes:
799
- raise exceptions.DbApplicationError(enhanced_message) from exception
800
- if error_code in self.sql.ColumnMissingErrorCodes:
801
- raise exceptions.DbColumnMissingError(enhanced_message) from exception
802
- if error_code in self.sql.TableMissingErrorCodes:
803
- raise exceptions.DbTableMissingError(enhanced_message) from exception
804
- if error_code in self.sql.DatabaseMissingErrorCodes:
805
- raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
806
- if error_code in self.sql.ForeignKeyMissingErrorCodes:
807
- raise exceptions.DbForeignKeyMissingError(enhanced_message) from exception
808
- if error_code in self.sql.TruncationErrorCodes:
809
- raise exceptions.DbTruncationError(enhanced_message) from exception
810
- if error_code in self.sql.DataIntegrityErrorCodes:
811
- raise exceptions.DbDataIntegrityError(enhanced_message) from exception
812
- if error_code in self.sql.ConnectionErrorCodes:
813
- raise exceptions.DbConnectionError(enhanced_message) from exception
814
- if error_code in self.sql.DuplicateKeyErrorCodes:
815
- raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
816
- if error_code in self.sql.DatabaseObjectExistsErrorCodes:
817
- raise exceptions.DbObjectExistsError(enhanced_message) from exception
818
- if error_code in self.sql.LockTimeoutErrorCodes:
819
- raise exceptions.DbLockTimeoutError(enhanced_message) from exception
820
- if error_code in self.sql.RetryTransactionCodes:
821
- raise exceptions.DbRetryTransaction(enhanced_message) from exception
822
-
823
- # Regex-based fallback patterns
824
- if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
825
- raise exceptions.DbDuplicateKeyError(enhanced_message) from exception
826
- if re.findall(r"database.*does not exist", msg, re.M):
827
- raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
828
- if re.findall(r"no such database", msg, re.M):
829
- raise exceptions.DbDatabaseMissingError(enhanced_message) from exception
830
- if re.findall(r"already exists", msg, re.M):
831
- raise exceptions.DbObjectExistsError(enhanced_message) from exception
832
- # Dialect-specific connection error message classification (fallback when no/unknown code).
833
- if getattr(self.sql, "is_connection_error_message", lambda _m: False)(msg):
834
- raise exceptions.DbConnectionError(enhanced_message) from exception
835
- if "no such table:" in msg:
836
- raise exceptions.DbTableMissingError(enhanced_message) from exception
830
+ # ── Classify by error code (O(1) dict lookup) ────────────────
831
+ code_map = getattr(self, '_Engine__error_code_map', None) or {}
832
+ exc_cls = code_map.get(error_code) if error_code else None
837
833
 
834
+ # ── Classify by message text (dialect-specific fallback) ─────
835
+ if exc_cls is None:
836
+ try:
837
+ category = self.sql.classify_by_message(msg)
838
+ except (AttributeError, TypeError):
839
+ category = None
840
+ if isinstance(category, str):
841
+ exc_cls = _CATEGORY_EXCEPTION_MAP.get(category)
842
+
843
+ # ── Raise the classified exception ───────────────────────────
844
+ if exc_cls is not None:
845
+ logger.warning(
846
+ "DB error classified: code=%s message=%s type=%s → %s",
847
+ error_code, error_message,
848
+ type(exception).__name__, exc_cls.__name__,
849
+ extra={
850
+ "error_code": error_code,
851
+ "error_msg": error_message,
852
+ "classified_as": exc_cls.__name__,
853
+ "sql_stmt": sql,
854
+ "sql_params": parameters,
855
+ },
856
+ )
857
+ raise exc_cls(enhanced_message) from exception
858
+
859
+ # ── Unclassified — log and wrap in DbException ───────────────
838
860
  logger.error(
839
861
  "Unhandled/Unknown Error in engine.process_error",
840
862
  exc_info=True,
841
863
  extra={
842
864
  "error_code": error_code,
843
865
  "error_msg": error_message,
866
+ "original_exception_type": type(exception).__name__,
867
+ "available_error_codes": list(code_map.keys()),
844
868
  "sql_stmt": sql,
845
869
  "sql_params": parameters,
846
870
  },
847
871
  )
848
-
849
- # If we can't classify it, re-raise with enhanced message
850
- raise type(exception)(enhanced_message) from exception
872
+ raise exceptions.DbException(enhanced_message) from exception
851
873
 
852
874
  def _format_sql_with_params(self, sql, parameters):
853
875
  """
@@ -79,6 +79,30 @@ class BaseSQLDialect(ABC):
79
79
  """
80
80
  return False
81
81
 
82
+ @classmethod
83
+ def classify_by_message(cls, msg: str) -> Optional[str]:
84
+ """Classify an error by its message text when no error code is available.
85
+
86
+ Returns an exception-category key understood by Engine._error_code_map,
87
+ or None if the message cannot be classified. Dialects override this to
88
+ add database-specific message patterns.
89
+
90
+ Recognised keys (matching the code-list attribute names):
91
+ "DuplicateKey", "DatabaseMissing", "TableMissing", "ColumnMissing",
92
+ "Connection", "ObjectExists", "Application", "DataIntegrity",
93
+ "Truncation", "ForeignKeyMissing", "LockTimeout", "RetryTransaction"
94
+ """
95
+ return None
96
+
97
+ @classmethod
98
+ def enhance_message(cls, msg: str) -> Optional[str]:
99
+ """Return additional guidance text for a driver error, or None.
100
+
101
+ Dialects override this to provide developer-friendly hints for common
102
+ mistakes (e.g. bare values in WHERE clauses).
103
+ """
104
+ return None
105
+
82
106
  # Core CRUD Operations
83
107
  @classmethod
84
108
  @abstractmethod
@@ -116,6 +116,29 @@ class SQL(BaseSQLDialect):
116
116
  )
117
117
  return not any(n in m for n in non_transient)
118
118
 
119
+ @classmethod
120
+ def classify_by_message(cls, msg: str):
121
+ if not msg:
122
+ return None
123
+ m = msg.strip().lower()
124
+ if "duplicate entry" in m:
125
+ return "DuplicateKey"
126
+ if "unknown database" in m:
127
+ return "DatabaseMissing"
128
+ if "doesn't exist" in m and "table" in m:
129
+ return "TableMissing"
130
+ if "unknown column" in m:
131
+ return "ColumnMissing"
132
+ if "already exists" in m:
133
+ return "ObjectExists"
134
+ if "deadlock found" in m:
135
+ return "RetryTransaction"
136
+ if "lock wait timeout" in m:
137
+ return "LockTimeout"
138
+ if cls.is_connection_error_message(msg):
139
+ return "Connection"
140
+ return None
141
+
119
142
  @classmethod
120
143
  def select(
121
144
  cls,
@@ -116,6 +116,42 @@ class SQL(BaseSQLDialect):
116
116
  # For Postgres, low-level disconnects/restarts are typically transient.
117
117
  return cls.is_connection_error_message(msg)
118
118
 
119
+ @classmethod
120
+ def classify_by_message(cls, msg: str):
121
+ if not msg:
122
+ return None
123
+ m = msg.strip().lower()
124
+ # Order matters — more specific patterns first.
125
+ if re.search(r"key \(.*?\)=\(.*?\) already exists", m):
126
+ return "DuplicateKey"
127
+ if re.search(r"duplicate key value violates unique constraint", m):
128
+ return "DuplicateKey"
129
+ if re.search(r"database.*does not exist", m):
130
+ return "DatabaseMissing"
131
+ if "no such database" in m:
132
+ return "DatabaseMissing"
133
+ if re.search(r"relation .* already exists", m):
134
+ return "ObjectExists"
135
+ if "already exists" in m:
136
+ return "ObjectExists"
137
+ if cls.is_connection_error_message(msg):
138
+ return "Connection"
139
+ return None
140
+
141
+ @classmethod
142
+ def enhance_message(cls, msg: str):
143
+ if msg and "argument of where must be type boolean" in msg.lower():
144
+ return (
145
+ "\n\n*** WHERE CLAUSE ERROR ***\n"
146
+ "This error typically occurs when a WHERE clause contains a bare value "
147
+ "instead of a proper boolean expression.\n"
148
+ "Common fixes:\n"
149
+ " - Change WHERE 1001 to WHERE sys_id = 1001\n"
150
+ " - Change WHERE {'column': value} format in dictionaries\n"
151
+ " - Ensure string WHERE clauses are complete SQL expressions"
152
+ )
153
+ return None
154
+
119
155
  @staticmethod
120
156
  def _validate_where_string(where):
121
157
  """
@@ -90,6 +90,25 @@ class SQL(BaseSQLDialect):
90
90
  # SQLite connection/file errors are typically not transient in-process.
91
91
  return False
92
92
 
93
+ @classmethod
94
+ def classify_by_message(cls, msg: str):
95
+ if not msg:
96
+ return None
97
+ m = msg.strip().lower()
98
+ if "unique constraint failed" in m or "is not unique" in m:
99
+ return "DuplicateKey"
100
+ if "no such table:" in m:
101
+ return "TableMissing"
102
+ if "no such column:" in m:
103
+ return "ColumnMissing"
104
+ if "already exists" in m:
105
+ return "ObjectExists"
106
+ if "database is locked" in m:
107
+ return "LockTimeout"
108
+ if cls.is_connection_error_message(msg):
109
+ return "Connection"
110
+ return None
111
+
93
112
  @classmethod
94
113
  def select(
95
114
  cls,
@@ -113,6 +113,29 @@ class SQL(BaseSQLDialect):
113
113
  return False
114
114
  return True
115
115
 
116
+ @classmethod
117
+ def classify_by_message(cls, msg: str):
118
+ if not msg:
119
+ return None
120
+ m = msg.strip().lower()
121
+ if "violation of primary key" in m or "violation of unique key" in m:
122
+ return "DuplicateKey"
123
+ if "cannot open database" in m:
124
+ return "DatabaseMissing"
125
+ if "invalid object name" in m:
126
+ return "TableMissing"
127
+ if "invalid column name" in m:
128
+ return "ColumnMissing"
129
+ if "already exists" in m:
130
+ return "ObjectExists"
131
+ if "deadlock victim" in m:
132
+ return "RetryTransaction"
133
+ if "lock request time out" in m:
134
+ return "LockTimeout"
135
+ if cls.is_connection_error_message(msg):
136
+ return "Connection"
137
+ return None
138
+
116
139
  @classmethod
117
140
  def select(
118
141
  cls,
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import datetime
3
3
  import unittest
4
+ from velocity.db import exceptions
4
5
  from velocity.db.core.engine import Engine
5
6
  from velocity.db.core.transaction import Transaction
6
7
  from .common import CommonPostgresTest, engine, test_db
@@ -51,14 +52,9 @@ class TestEngine(CommonPostgresTest):
51
52
  assert [] == engine.tables
52
53
 
53
54
  def test_process_error(self, tx):
54
- local_engine = Engine(
55
- None, None, None
56
- ) # Replace None with appropriate arguments
57
- with self.assertRaises(
58
- Exception
59
- ): # Replace Exception with the specific exception raised by process_error
60
- local_engine.process_error(sql_stmt=None, sql_params=None)
61
- # Add additional assertions as needed
55
+ exc = Exception("some driver error")
56
+ with self.assertRaises(exceptions.DbException):
57
+ engine.process_error(exc)
62
58
 
63
59
  def test_transaction_injection_1(self, tx):
64
60
  @engine.transaction