velocity-python 0.0.220__tar.gz → 0.0.222__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 (160) hide show
  1. velocity_python-0.0.222/PKG-INFO +1479 -0
  2. velocity_python-0.0.222/README.md +1426 -0
  3. {velocity_python-0.0.220 → velocity_python-0.0.222}/pyproject.toml +1 -1
  4. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/__init__.py +1 -1
  5. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/stripe_adapter.py +33 -24
  6. velocity_python-0.0.222/src/velocity_python.egg-info/PKG-INFO +1479 -0
  7. velocity_python-0.0.220/PKG-INFO +0 -1031
  8. velocity_python-0.0.220/README.md +0 -978
  9. velocity_python-0.0.220/src/velocity_python.egg-info/PKG-INFO +0 -1031
  10. {velocity_python-0.0.220 → velocity_python-0.0.222}/LICENSE +0 -0
  11. {velocity_python-0.0.220 → velocity_python-0.0.222}/setup.cfg +0 -0
  12. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/__init__.py +0 -0
  13. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/invoices.py +0 -0
  14. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/orders.py +0 -0
  15. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/payments.py +0 -0
  16. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/purchase_orders.py +0 -0
  17. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/__init__.py +0 -0
  18. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/test_email_processing.py +0 -0
  19. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  20. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  21. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/__init__.py +0 -0
  22. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/amplify.py +0 -0
  23. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/amplify_build.py +0 -0
  24. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/__init__.py +0 -0
  25. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/base_handler.py +0 -0
  26. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/context.py +0 -0
  27. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/context_factory.py +0 -0
  28. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/exceptions.py +0 -0
  29. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  30. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  31. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  32. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  33. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/perf.py +0 -0
  34. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/response.py +0 -0
  35. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  36. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/__init__.py +0 -0
  37. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  38. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  39. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/test_response.py +0 -0
  40. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/__init__.py +0 -0
  41. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/__init__.py +0 -0
  42. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/column.py +0 -0
  43. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/database.py +0 -0
  44. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/decorators.py +0 -0
  45. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/engine.py +0 -0
  46. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/result.py +0 -0
  47. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/row.py +0 -0
  48. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/sequence.py +0 -0
  49. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/table.py +0 -0
  50. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/transaction.py +0 -0
  51. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/view.py +0 -0
  52. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/exceptions.py +0 -0
  53. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/__init__.py +0 -0
  54. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/__init__.py +0 -0
  55. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/initializer.py +0 -0
  56. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/operators.py +0 -0
  57. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/sql.py +0 -0
  58. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/types.py +0 -0
  59. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/__init__.py +0 -0
  60. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/operators.py +0 -0
  61. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/reserved.py +0 -0
  62. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/sql.py +0 -0
  63. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/types.py +0 -0
  64. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/__init__.py +0 -0
  65. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/operators.py +0 -0
  66. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/reserved.py +0 -0
  67. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/sql.py +0 -0
  68. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/types.py +0 -0
  69. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  70. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/operators.py +0 -0
  71. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  72. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/sql.py +0 -0
  73. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/types.py +0 -0
  74. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  75. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  76. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  77. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  78. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/types.py +0 -0
  79. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/tablehelper.py +0 -0
  80. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/__init__.py +0 -0
  81. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/common_db_test.py +0 -0
  82. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/__init__.py +0 -0
  83. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/common.py +0 -0
  84. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_column.py +0 -0
  85. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  86. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_database.py +0 -0
  87. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  88. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  89. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  90. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_result.py +0 -0
  91. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_row.py +0 -0
  92. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  93. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  94. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  95. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  96. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  97. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_table.py +0 -0
  98. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  99. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  100. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/__init__.py +0 -0
  101. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/common.py +0 -0
  102. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  103. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  104. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  105. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_db_utils.py +0 -0
  106. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_postgres.py +0 -0
  107. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  108. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  109. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_result_caching.py +0 -0
  110. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  111. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  112. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  113. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  114. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_sql_builder.py +0 -0
  115. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_tablehelper.py +0 -0
  116. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_view_helper.py +0 -0
  117. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/utils.py +0 -0
  118. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/logging.py +0 -0
  119. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/__init__.py +0 -0
  120. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/conv/__init__.py +0 -0
  121. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/conv/iconv.py +0 -0
  122. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/conv/oconv.py +0 -0
  123. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/db.py +0 -0
  124. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/export.py +0 -0
  125. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/format.py +0 -0
  126. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/mail.py +0 -0
  127. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/merge.py +0 -0
  128. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/__init__.py +0 -0
  129. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_db.py +0 -0
  130. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_fix.py +0 -0
  131. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_format.py +0 -0
  132. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_iconv.py +0 -0
  133. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_merge.py +0 -0
  134. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_oconv.py +0 -0
  135. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_original_error.py +0 -0
  136. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_timer.py +0 -0
  137. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/timer.py +0 -0
  138. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tools.py +0 -0
  139. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/__init__.py +0 -0
  140. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/base_adapter.py +0 -0
  141. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/braintree_adapter.py +0 -0
  142. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/profiles.py +0 -0
  143. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/router.py +0 -0
  144. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  145. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  146. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/requires.txt +0 -0
  147. {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/top_level.txt +0 -0
  148. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_amplify_build.py +0 -0
  149. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_decorators.py +0 -0
  150. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_iconv_money_to_cents.py +0 -0
  151. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_lambda_handler.py +0 -0
  152. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_lambda_handler_auth.py +0 -0
  153. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_mixins_import.py +0 -0
  154. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_braintree_adapter.py +0 -0
  155. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_profiles.py +0 -0
  156. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_router.py +0 -0
  157. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_stripe_adapter.py +0 -0
  158. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  159. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_table_alter.py +0 -0
  160. {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_where_clause_validation.py +0 -0
@@ -0,0 +1,1479 @@
1
+ Metadata-Version: 2.4
2
+ Name: velocity-python
3
+ Version: 0.0.222
4
+ Summary: A rapid application development library for interfacing with data storage
5
+ Author-email: Velocity Team <info@codeclubs.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://codeclubs.org/projects/velocity
8
+ Keywords: database,orm,sql,rapid-development,data-storage
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Database
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Operating System :: OS Independent
20
+ Requires-Python: >=3.7
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: boto3>=1.26.0
24
+ Requires-Dist: requests>=2.25.0
25
+ Requires-Dist: jinja2>=3.0.0
26
+ Requires-Dist: xlrd>=2.0.0
27
+ Requires-Dist: openpyxl>=3.0.0
28
+ Requires-Dist: sqlparse>=0.4.0
29
+ Provides-Extra: mysql
30
+ Requires-Dist: mysql-connector-python>=8.0.0; extra == "mysql"
31
+ Provides-Extra: sqlserver
32
+ Requires-Dist: python-tds>=1.10.0; extra == "sqlserver"
33
+ Provides-Extra: postgres
34
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
35
+ Provides-Extra: payment
36
+ Requires-Dist: stripe>=8.0.0; extra == "payment"
37
+ Requires-Dist: braintree>=4.0.0; extra == "payment"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
40
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
41
+ Requires-Dist: black>=23.0.0; extra == "dev"
42
+ Requires-Dist: flake8>=6.0.0; extra == "dev"
43
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
44
+ Requires-Dist: pre-commit>=3.0.0; extra == "dev"
45
+ Provides-Extra: test
46
+ Requires-Dist: pytest>=7.0.0; extra == "test"
47
+ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
48
+ Requires-Dist: pytest-mock>=3.10.0; extra == "test"
49
+ Provides-Extra: docs
50
+ Requires-Dist: sphinx>=5.0.0; extra == "docs"
51
+ Requires-Dist: sphinx-rtd-theme>=1.2.0; extra == "docs"
52
+ Dynamic: license-file
53
+
54
+ # Velocity
55
+
56
+ A rapid application development library for Python that eliminates boilerplate across the entire backend stack. Velocity provides a Pythonic database ORM where rows behave like dictionaries, a serverless handler framework for AWS Lambda, processor-agnostic payment integration, and a collection of everyday utilities — all designed so you write business logic instead of infrastructure code.
57
+
58
+ ## Table of Contents
59
+
60
+ - [Why Velocity?](#why-velocity)
61
+ - [Installation](#installation)
62
+ - [Quick Start](#quick-start)
63
+ - [Velocity.DB — Database ORM](#velocitydb--database-orm)
64
+ - [Connecting to a Database](#connecting-to-a-database)
65
+ - [Transactions](#transactions)
66
+ - [Rows as Dictionaries](#rows-as-dictionaries)
67
+ - [Tables — CRUD Operations](#tables--crud-operations)
68
+ - [Advanced Queries](#advanced-queries)
69
+ - [Result Objects](#result-objects)
70
+ - [Automatic Schema Evolution](#automatic-schema-evolution)
71
+ - [Views, Sequences, and Databases](#views-sequences-and-databases)
72
+ - [Schema Locking](#schema-locking)
73
+ - [Error Handling](#error-handling)
74
+ - [Exception Hierarchy](#exception-hierarchy)
75
+ - [Utility Functions](#utility-functions)
76
+ - [Velocity.AWS — Serverless Framework](#velocityaws--serverless-framework)
77
+ - [Lambda Handlers](#lambda-handlers)
78
+ - [SQS Handlers](#sqs-handlers)
79
+ - [Context and Response Objects](#context-and-response-objects)
80
+ - [Amplify Integration](#amplify-integration)
81
+ - [Velocity.Payment — Payment Processing](#velocitypayment--payment-processing)
82
+ - [Adapter Pattern](#adapter-pattern)
83
+ - [Payment Routing](#payment-routing)
84
+ - [Stripe and Braintree](#stripe-and-braintree)
85
+ - [Customer Profiles](#customer-profiles)
86
+ - [Velocity.App — Business Domain Objects](#velocityapp--business-domain-objects)
87
+ - [Velocity.Misc — Utilities](#velocitymisc--utilities)
88
+ - [Data Conversion (iconv / oconv)](#data-conversion-iconv--oconv)
89
+ - [Spreadsheet Export](#spreadsheet-export)
90
+ - [Email Parsing](#email-parsing)
91
+ - [Formatting and Serialization](#formatting-and-serialization)
92
+ - [Deep Merge](#deep-merge)
93
+ - [Timer](#timer)
94
+ - [Project Structure](#project-structure)
95
+ - [Development](#development)
96
+ - [License](#license)
97
+
98
+ ---
99
+
100
+ ## Why Velocity?
101
+
102
+ Most Python backend projects cobble together an ORM, a web/serverless framework, a payment library, spreadsheet tooling, and assorted glue code. Velocity replaces all of that with one cohesive library built around two principles:
103
+
104
+ 1. **Convention over configuration** — sensible defaults everywhere. Tables auto-create columns, transactions auto-commit, Lambda handlers auto-route actions.
105
+ 2. **Python-native interfaces** — database rows are real `MutableMapping` objects you can `dict()`, `**unpack`, iterate, and compare. No new query language to learn.
106
+
107
+ **What you get:**
108
+
109
+ | Layer | What it does | Without Velocity |
110
+ |-------|-------------|-----------------|
111
+ | `velocity.db` | Multi-database ORM with dict-like rows, auto-schema, query builder | SQLAlchemy + Alembic + custom code |
112
+ | `velocity.aws` | Lambda handler framework with auth, routing, SQS | Custom event parsing per function |
113
+ | `velocity.payment` | Processor-agnostic payments (Stripe, Braintree) | Per-processor integration code |
114
+ | `velocity.misc` | Excel export, email parsing, data conversion, formatting | openpyxl + email libs + helpers |
115
+ | `velocity.app` | Order / invoice / payment domain objects | Custom per-project |
116
+
117
+ ## Installation
118
+
119
+ ```bash
120
+ pip install velocity-python
121
+ ```
122
+
123
+ With database-specific drivers:
124
+
125
+ ```bash
126
+ pip install velocity-python[postgres] # PostgreSQL (psycopg2)
127
+ pip install velocity-python[mysql] # MySQL (mysql-connector-python)
128
+ pip install velocity-python[sqlserver] # SQL Server (python-tds)
129
+ pip install velocity-python[payment] # Stripe + Braintree
130
+ pip install velocity-python[all] # Everything
131
+ ```
132
+
133
+ SQLite support is built-in (uses Python's `sqlite3` module).
134
+
135
+ **Requirements:** Python 3.7+
136
+
137
+ ---
138
+
139
+ ## Quick Start
140
+
141
+ ```python
142
+ import velocity.db
143
+
144
+ # 1. Connect
145
+ engine = velocity.db.postgres.initialize() # reads DB* env vars
146
+
147
+ # 2. Write business logic — the decorator handles transactions
148
+ @engine.transaction
149
+ def onboard_customer(tx, name, email):
150
+ customer = tx.table('customers').new() # table auto-created if missing
151
+ customer['name'] = name # column auto-created if missing
152
+ customer['email'] = email
153
+ customer['status'] = 'active'
154
+ return customer['sys_id']
155
+
156
+ # 3. Call it — tx is injected automatically
157
+ customer_id = onboard_customer('Acme Corp', 'hello@acme.com')
158
+ ```
159
+
160
+ That's it. No model definitions, no migration files, no manual commits.
161
+
162
+ ---
163
+
164
+ ## Velocity.DB — Database ORM
165
+
166
+ ### Connecting to a Database
167
+
168
+ Each database engine has an `initialize()` function that returns a configured `Engine` object:
169
+
170
+ ```python
171
+ import velocity.db
172
+
173
+ # PostgreSQL — reads from environment variables by default:
174
+ # DBHost, DBPort, DBDatabase, DBUser, DBPassword
175
+ engine = velocity.db.postgres.initialize()
176
+
177
+ # Or pass config explicitly:
178
+ engine = velocity.db.postgres.initialize(config={
179
+ 'host': 'localhost',
180
+ 'port': 5432,
181
+ 'database': 'mydb',
182
+ 'user': 'myuser',
183
+ 'password': 'secret',
184
+ })
185
+
186
+ # MySQL
187
+ engine = velocity.db.mysql.initialize(config={...})
188
+
189
+ # SQLite
190
+ engine = velocity.db.sqlite.initialize(config={'database': '/path/to/db.sqlite'})
191
+
192
+ # SQL Server
193
+ engine = velocity.db.sqlserver.initialize(config={...})
194
+ ```
195
+
196
+ The `Engine` object exposes metadata about the server:
197
+
198
+ ```python
199
+ engine.version # Server version string
200
+ engine.current_database # Active database name
201
+ engine.current_schema # Active schema
202
+ engine.tables # List of table names
203
+ engine.views # List of view names
204
+ engine.databases # List of all databases
205
+ ```
206
+
207
+ ---
208
+
209
+ ### Transactions
210
+
211
+ Every database operation runs inside a transaction. The `@engine.transaction` decorator manages the full lifecycle — connect, begin, commit on success, rollback on exception, close.
212
+
213
+ #### Basic Usage
214
+
215
+ Declare a `tx` parameter and the engine provides it automatically:
216
+
217
+ ```python
218
+ @engine.transaction
219
+ def create_user(tx, name, email):
220
+ user = tx.table('users').new()
221
+ user['name'] = name
222
+ user['email'] = email
223
+ return user['sys_id']
224
+
225
+ # Call without passing tx — the decorator injects it:
226
+ user_id = create_user('Alice', 'alice@example.com')
227
+ ```
228
+
229
+ #### Transaction Reuse
230
+
231
+ Pass `tx` explicitly to share a single transaction across multiple decorated functions:
232
+
233
+ ```python
234
+ @engine.transaction
235
+ def create_user(tx, name, email):
236
+ user = tx.table('users').new()
237
+ user['name'] = name
238
+ user['email'] = email
239
+ return user['sys_id']
240
+
241
+ @engine.transaction
242
+ def create_profile(tx, user_id, bio):
243
+ profile = tx.table('profiles').new()
244
+ profile['user_id'] = user_id
245
+ profile['bio'] = bio
246
+
247
+ @engine.transaction
248
+ def onboard(tx, name, email, bio):
249
+ # Both calls share the same transaction — atomic
250
+ uid = create_user(tx, name, email)
251
+ create_profile(tx, uid, bio)
252
+ return uid
253
+
254
+ onboard('Alice', 'alice@example.com', 'Engineer')
255
+ ```
256
+
257
+ If you omit `tx` when calling a decorated function, it gets its own independent transaction.
258
+
259
+ #### Class-Level Decoration
260
+
261
+ Apply `@engine.transaction` to an entire class to auto-wrap every method that has a `tx` parameter:
262
+
263
+ ```python
264
+ @engine.transaction
265
+ class UserService:
266
+ def create(self, tx, name, email):
267
+ user = tx.table('users').new()
268
+ user['name'] = name
269
+ user['email'] = email
270
+ return user['sys_id']
271
+
272
+ def deactivate(self, tx, user_id):
273
+ user = tx.table('users').find(user_id)
274
+ user['status'] = 'inactive'
275
+
276
+ def validate_email(self, email):
277
+ # No tx parameter — not wrapped
278
+ return '@' in email
279
+
280
+ service = UserService()
281
+ uid = service.create('Alice', 'alice@example.com')
282
+ service.deactivate(uid)
283
+ ```
284
+
285
+ #### Savepoints
286
+
287
+ For partial rollback within a transaction:
288
+
289
+ ```python
290
+ @engine.transaction
291
+ def risky_operation(tx):
292
+ sp = tx.create_savepoint()
293
+ try:
294
+ tx.table('ledger').insert({'amount': 100})
295
+ except Exception:
296
+ tx.rollback_savepoint(sp) # undo just this part
297
+ else:
298
+ tx.release_savepoint(sp)
299
+ ```
300
+
301
+ #### Automatic Retries
302
+
303
+ The engine automatically retries on transient errors:
304
+ - `DbRetryTransaction` — signals explicit retry
305
+ - `DbLockTimeoutError` — lock contention
306
+ - `DbConnectionError` — transient connection drops
307
+
308
+ Up to 100 retries with backoff.
309
+
310
+ #### Key Rules
311
+
312
+ | Rule | Detail |
313
+ |------|--------|
314
+ | Declare `tx` | Your function signature must include a `tx` parameter |
315
+ | Don't pass `tx` from outside | Let the decorator create it for new transactions |
316
+ | Pass `tx` to share | Explicitly pass `tx` to keep calls in the same transaction |
317
+ | `_tx` is reserved | Do not use `_tx` as a parameter name |
318
+
319
+ ---
320
+
321
+ ### Rows as Dictionaries
322
+
323
+ `Row` implements `collections.abc.MutableMapping`. A row retrieved from the database behaves exactly like a Python dictionary — because it *is* one.
324
+
325
+ ```python
326
+ @engine.transaction
327
+ def demonstrate_row(tx):
328
+ user = tx.table('users').find(42)
329
+
330
+ # Dictionary access
331
+ name = user['name']
332
+ user['status'] = 'active' # writes through to DB immediately
333
+
334
+ # Attribute access
335
+ email = user.email
336
+ user.phone = '+1-555-1234'
337
+
338
+ # Standard dict operations
339
+ data = dict(user) # full shallow copy
340
+ merged = {**user, 'extra': True} # unpack into new dict
341
+ keys = list(user) # column names
342
+ length = len(user) # number of columns
343
+ has_phone = 'phone' in user # membership test (case-insensitive)
344
+
345
+ # .get() with default
346
+ bio = user.get('bio', 'No bio')
347
+
348
+ # Iteration
349
+ for key in user:
350
+ print(key, user[key])
351
+
352
+ # Equality and hashing (by table name + primary key)
353
+ other = tx.table('users').find(42)
354
+ assert user == other
355
+ assert hash(user) == hash(other)
356
+ s = {user, other} # set with one element
357
+ ```
358
+
359
+ #### Caching Behaviour
360
+
361
+ Row data is **lazy-loaded** on first access and cached locally. Subsequent reads hit the cache — not the database.
362
+
363
+ ```python
364
+ user['name'] # fetches all columns from DB, caches result
365
+ user['email'] # served from cache — no DB call
366
+ user.invalidate() # clear cache; next access re-fetches
367
+ user.refresh() # re-fetch immediately
368
+ ```
369
+
370
+ Writes are **write-through**: `user['name'] = 'New'` issues an `UPDATE` and refreshes the cache.
371
+
372
+ #### Row API Reference
373
+
374
+ | Method / Property | Description |
375
+ |-------------------|-------------|
376
+ | `user['col']` | Get column value (lazy-loads from DB on first access) |
377
+ | `user['col'] = val` | Set column value (writes through to DB + updates cache) |
378
+ | `user.col` / `user.col = val` | Attribute-style access (same as bracket notation) |
379
+ | `dict(user)` / `user.to_dict()` | Full row as a plain `dict` |
380
+ | `user.extract('a', 'b')` | Subset of columns as a `dict` |
381
+ | `user.update({'a': 1, 'b': 2})` | Bulk update multiple columns |
382
+ | `user.get(key, default)` | Safe get with default |
383
+ | `'col' in user` | Case-insensitive membership test |
384
+ | `len(user)` | Number of columns |
385
+ | `list(user)` | Column names |
386
+ | `user.refresh()` | Re-fetch all data from DB |
387
+ | `user.invalidate()` | Clear cache (next access re-fetches) |
388
+ | `user.copy()` | Clone row with a new `sys_id` (strips system columns) |
389
+ | `user.delete()` / `user.clear()` | Delete the row from the database |
390
+ | `user.lock()` | `SELECT ... FOR UPDATE` |
391
+ | `user.touch()` | Update `sys_modified` timestamp |
392
+ | `user.split()` | Returns `(data_without_sys_columns, pk)` |
393
+ | `user.match(other_dict)` | True if other dict's keys all match this row's values |
394
+ | `user.row('fk_column')` | Follow a foreign key to get the referenced Row |
395
+ | `user.sys_id` | Primary key value |
396
+
397
+ ---
398
+
399
+ ### Tables — CRUD Operations
400
+
401
+ `tx.table('name')` returns a `Table` object with a full CRUD and DDL interface.
402
+
403
+ #### Insert
404
+
405
+ ```python
406
+ @engine.transaction
407
+ def insert_examples(tx):
408
+ users = tx.table('users')
409
+
410
+ # Dict-style: create + populate a Row
411
+ user = users.new()
412
+ user['name'] = 'Alice'
413
+ user['email'] = 'alice@example.com'
414
+ # Row is inserted when created; updates write through
415
+
416
+ # Direct insert from a dict
417
+ users.insert({'name': 'Bob', 'email': 'bob@example.com'})
418
+
419
+ # Insert-if-not-exists
420
+ users.insert_if_not_exists(
421
+ {'name': 'Carol', 'email': 'carol@example.com'},
422
+ where={'email': 'carol@example.com'}
423
+ )
424
+ ```
425
+
426
+ #### Select
427
+
428
+ ```python
429
+ @engine.transaction
430
+ def select_examples(tx):
431
+ users = tx.table('users')
432
+
433
+ # All rows (returns list of dicts via Result.all())
434
+ all_users = users.select().all()
435
+
436
+ # Filtered + ordered + limited
437
+ recent = users.select(
438
+ columns=['name', 'email'],
439
+ where={'status': 'active'},
440
+ orderby='sys_created DESC',
441
+ qty=10
442
+ ).all()
443
+
444
+ # Shorthand — .list() is select().all()
445
+ active = users.list(where={'status': 'active'})
446
+
447
+ # Single row lookups return Row objects
448
+ user = users.find(42) # by primary key
449
+ user = users.find({'email': 'a@b.c'}) # by condition
450
+ user = users.one({'email': 'a@b.c'}) # alias for find()
451
+ user = users.first(where={'status': 'active'}, orderby='name')
452
+
453
+ # get() — find-or-create
454
+ user = users.get({'email': 'a@b.c'}) # creates row if not found
455
+
456
+ # Iterate Row objects
457
+ for row in users.rows(where={'status': 'active'}):
458
+ print(row['name'])
459
+
460
+ # Iterate primary keys only
461
+ for sys_id in users.ids(where={'status': 'active'}):
462
+ print(sys_id)
463
+
464
+ # Batched iteration
465
+ for batch in users.batch(size=100, where={'status': 'active'}):
466
+ process(batch)
467
+
468
+ # Aggregates
469
+ total = users.count()
470
+ total_active = users.count(where={'status': 'active'})
471
+ revenue = tx.table('orders').sum('amount', where={'status': 'paid'})
472
+ ```
473
+
474
+ #### Update
475
+
476
+ ```python
477
+ @engine.transaction
478
+ def update_examples(tx):
479
+ users = tx.table('users')
480
+
481
+ # Row-level (write-through)
482
+ user = users.find(42)
483
+ user['name'] = 'New Name'
484
+
485
+ # Bulk update
486
+ users.update(
487
+ {'status': 'inactive'},
488
+ where={'<last_login': '2024-01-01'}
489
+ )
490
+
491
+ # Upsert (INSERT ON CONFLICT UPDATE)
492
+ users.upsert({'name': 'Alice', 'status': 'active'}, pk='email')
493
+ # or equivalently:
494
+ users.merge({'name': 'Alice', 'status': 'active'}, pk='email')
495
+
496
+ # Update-or-insert (try UPDATE first, fallback to INSERT IF NOT EXISTS)
497
+ users.update_or_insert(
498
+ {'status': 'active'},
499
+ insert_data={'name': 'Alice', 'email': 'a@b.c', 'status': 'active'},
500
+ where={'email': 'a@b.c'}
501
+ )
502
+ ```
503
+
504
+ #### Delete
505
+
506
+ ```python
507
+ @engine.transaction
508
+ def delete_examples(tx):
509
+ users = tx.table('users')
510
+
511
+ # Single row
512
+ user = users.find(42)
513
+ user.delete()
514
+
515
+ # Bulk delete (where is required — prevents accidental full deletes)
516
+ users.delete(where={'status': 'inactive'})
517
+
518
+ # Truncate (remove all rows)
519
+ users.truncate()
520
+
521
+ # Drop table entirely
522
+ users.drop()
523
+ ```
524
+
525
+ #### DDL (Schema Management)
526
+
527
+ ```python
528
+ @engine.transaction
529
+ def schema_examples(tx):
530
+ users = tx.table('users')
531
+
532
+ # Create table (with velocity system columns auto-added)
533
+ users.create({'name': str, 'email': str, 'age': int})
534
+
535
+ # Check existence
536
+ if users.exists():
537
+ print('Table exists')
538
+
539
+ # Add columns (smart mode: skips existing columns)
540
+ users.alter({'phone': str, 'bio': str})
541
+
542
+ # Change column type
543
+ users.alter_type('age', 'BIGINT')
544
+
545
+ # Indexes
546
+ users.create_index(['email'], unique=True)
547
+ users.create_index(['name', 'status'])
548
+
549
+ # Foreign keys
550
+ users.create_foreign_key(['department_id'], 'departments')
551
+
552
+ # Rename
553
+ users.rename('app_users')
554
+
555
+ # Column inspection
556
+ cols = users.columns() # non-system columns
557
+ all_cols = users.sys_columns() # all columns including sys_*
558
+ pks = users.primary_keys()
559
+ fks = users.foreign_keys()
560
+
561
+ # Column object
562
+ col = users.column('email')
563
+ col.py_type # Python type
564
+ col.sql_type # SQL type
565
+ col.is_nullable # nullable?
566
+ col.distinct() # distinct values
567
+ col.max() # max value
568
+ col.rename('contact_email')
569
+ ```
570
+
571
+ #### System Columns
572
+
573
+ Every table managed by Velocity automatically includes:
574
+
575
+ | Column | Type | Description |
576
+ |--------|------|-------------|
577
+ | `sys_id` | `BIGINT` | Auto-incrementing primary key |
578
+ | `sys_created` | `TIMESTAMP` | Row creation time |
579
+ | `sys_modified` | `TIMESTAMP` | Last modification time (trigger-maintained) |
580
+ | `sys_modified_by` | `TEXT` | Last modifier identifier |
581
+ | `sys_modified_row` | `TEXT` | Session tracking |
582
+ | `sys_modified_count` | `BIGINT` | Modification counter (trigger-maintained) |
583
+ | `sys_dirty` | `BOOLEAN` | Dirty flag for sync workflows |
584
+ | `sys_table` | `TEXT` | Table name (self-referential) |
585
+ | `sys_keywords` | `TEXT` | Full-text search keywords |
586
+
587
+ ---
588
+
589
+ ### Advanced Queries
590
+
591
+ #### Where Clause Formats
592
+
593
+ Velocity supports three where clause formats:
594
+
595
+ **1. Dictionary with operator prefixes** (most common):
596
+
597
+ ```python
598
+ users.select(where={
599
+ 'status': 'active', # = (default)
600
+ '>age': 18, # >
601
+ '<=score': 100, # <=
602
+ '>=created_at': '2024-01-01', # >=
603
+ '!status': 'deleted', # <> (not equal)
604
+ '%email': '@company.com', # LIKE
605
+ '!%name': 'test%', # NOT LIKE
606
+ '><age': [18, 65], # BETWEEN
607
+ '!><score': [0, 50], # NOT BETWEEN
608
+ }).all()
609
+ ```
610
+
611
+ | Prefix | SQL | Description |
612
+ |--------|-----|-------------|
613
+ | *(none)* | `=` | Equals (default) |
614
+ | `>` | `>` | Greater than |
615
+ | `<` | `<` | Less than |
616
+ | `>=` | `>=` | Greater than or equal |
617
+ | `<=` | `<=` | Less than or equal |
618
+ | `!` or `!=` or `<>` | `<>` | Not equal |
619
+ | `%` | `LIKE` | Pattern match |
620
+ | `!%` | `NOT LIKE` | Negated pattern match |
621
+ | `><` | `BETWEEN` | Inclusive range (value must be `[low, high]`) |
622
+ | `!><` | `NOT BETWEEN` | Negated range |
623
+
624
+ **2. List of tuples** (for complex predicates):
625
+
626
+ ```python
627
+ users.select(where=[
628
+ ('status = %s', 'active'),
629
+ ('priority = %s OR urgency = %s', ('high', 'critical'))
630
+ ]).all()
631
+ ```
632
+
633
+ **3. Raw string** (use with care):
634
+
635
+ ```python
636
+ users.select(where="status = 'active' AND age >= 18").all()
637
+ ```
638
+
639
+ #### Foreign Key Pointer Syntax
640
+
641
+ The `>` pointer syntax automatically resolves foreign keys into JOINs:
642
+
643
+ ```python
644
+ # local_column>foreign_column auto-joins through the FK relationship
645
+ orders = tx.table('orders')
646
+ result = orders.select(
647
+ columns=['order_number', 'customer_id>name', 'customer_id>email'],
648
+ where={'status': 'pending'}
649
+ ).all()
650
+ # Generates: SELECT A.order_number, B.name, B.email
651
+ # FROM orders A JOIN customers B ON A.customer_id = B.sys_id
652
+ # WHERE A.status = 'pending'
653
+ ```
654
+
655
+ Multi-level pointers chain through multiple tables:
656
+
657
+ ```python
658
+ # order → customer → region
659
+ orders.select(columns=[
660
+ 'order_number',
661
+ 'customer_id>name',
662
+ 'customer_id>region_id>region_name'
663
+ ]).all()
664
+ ```
665
+
666
+ #### Aggregations
667
+
668
+ ```python
669
+ stats = tx.table('orders').select(
670
+ columns=['customer_id', 'COUNT(*) as total', 'SUM(amount) as revenue'],
671
+ where={'status': 'completed'},
672
+ groupby='customer_id',
673
+ having='COUNT(*) > 5'
674
+ ).all()
675
+ ```
676
+
677
+ #### Server-Side Cursors
678
+
679
+ For very large result sets, use server-side cursors to avoid loading everything into memory:
680
+
681
+ ```python
682
+ result = users.server_select(where={'status': 'active'})
683
+ for row in result:
684
+ process(row)
685
+ result.close()
686
+ ```
687
+
688
+ #### Raw SQL
689
+
690
+ ```python
691
+ @engine.transaction
692
+ def raw_query(tx):
693
+ result = tx.execute("""
694
+ SELECT u.name, COUNT(o.sys_id) as order_count
695
+ FROM users u
696
+ LEFT JOIN orders o ON u.sys_id = o.user_id
697
+ WHERE u.status = %s
698
+ GROUP BY u.name
699
+ HAVING COUNT(o.sys_id) > %s
700
+ """, ['active', 5])
701
+
702
+ for row in result:
703
+ print(row['name'], row['order_count'])
704
+
705
+ # Single value
706
+ total = tx.execute("SELECT COUNT(*) FROM users").scalar()
707
+ ```
708
+
709
+ ---
710
+
711
+ ### Result Objects
712
+
713
+ Every `select()`, `execute()`, or query method returns a `Result` object with chainable data transformations.
714
+
715
+ #### Output Formats
716
+
717
+ ```python
718
+ result = users.select(columns=['name', 'email'])
719
+
720
+ result.as_dict().all() # [{'name': 'Alice', 'email': 'a@b'}, ...] (default)
721
+ result.as_tuple().all() # [('Alice', 'a@b'), ...]
722
+ result.as_list().all() # [['Alice', 'a@b'], ...]
723
+ result.as_json().all() # ['{"name":"Alice","email":"a@b"}', ...]
724
+ result.as_pairs().all() # [[('name','Alice'),('email','a@b')], ...]
725
+ result.as_simple_list().all() # ['Alice', ...] (first column only)
726
+ result.as_simple_list(1).all() # ['a@b', ...] (column at position 1)
727
+ result.strings().all() # all values coerced to strings
728
+ ```
729
+
730
+ #### Retrieval Methods
731
+
732
+ | Method | Returns | Use case |
733
+ |--------|---------|----------|
734
+ | `.all()` | `list` | Get all rows at once |
735
+ | `.one(default=None)` | single row or default | Expect exactly one result |
736
+ | `.scalar(default=None)` | single value or default | `SELECT COUNT(*)`, `SELECT MAX(...)` |
737
+ | `.batch(qty=100)` | generator of lists | Process in chunks |
738
+ | `.enum()` | `(index, row)` tuples | Numbered iteration |
739
+
740
+ #### Inspection
741
+
742
+ ```python
743
+ result.headers # ['name', 'email'] — column names
744
+ result.columns # detailed column metadata from cursor.description
745
+ bool(result) # True if there are rows (pre-fetches first row)
746
+ result.has_results() # same as bool()
747
+ result.is_empty() # opposite of has_results()
748
+ ```
749
+
750
+ #### Table Data Export
751
+
752
+ ```python
753
+ # 2D list with header row — useful for spreadsheet export
754
+ table_data = result.get_table_data(headers=True)
755
+ # [['name', 'email'], ['Alice', 'a@b'], ['Bob', 'c@d']]
756
+ ```
757
+
758
+ ---
759
+
760
+ ### Automatic Schema Evolution
761
+
762
+ Velocity automatically creates missing tables and columns when you write data. This is powered by the `@create_missing` decorator on CRUD methods.
763
+
764
+ ```python
765
+ @engine.transaction
766
+ def evolve_schema(tx):
767
+ users = tx.table('users') # table may or may not exist
768
+
769
+ # First run: creates table with name, email columns + system columns
770
+ users.insert({'name': 'Alice', 'email': 'alice@a.com'})
771
+
772
+ # Later: phone column doesn't exist yet — auto-added
773
+ users.insert({'name': 'Bob', 'email': 'bob@b.com', 'phone': '555-1234'})
774
+ ```
775
+
776
+ #### How It Works
777
+
778
+ 1. Operation attempted (e.g., `INSERT`)
779
+ 2. If `DbTableMissingError` → create the table from the data's keys/types, retry
780
+ 3. If `DbColumnMissingError` → `ALTER TABLE ADD COLUMN` for missing columns, retry
781
+
782
+ #### Type Inference
783
+
784
+ | Python Type | PostgreSQL | MySQL | SQLite |
785
+ |-------------|-----------|-------|--------|
786
+ | `str` | `TEXT` | `TEXT` | `TEXT` |
787
+ | `int` | `BIGINT` | `BIGINT` | `INTEGER` |
788
+ | `float` | `NUMERIC(19,6)` | `DECIMAL(19,6)` | `REAL` |
789
+ | `bool` | `BOOLEAN` | `BOOLEAN` | `INTEGER` |
790
+ | `datetime` | `TIMESTAMP` | `DATETIME` | `TEXT` |
791
+ | `date` | `DATE` | `DATE` | `TEXT` |
792
+
793
+ #### Protected Operations
794
+
795
+ Auto-creation applies to: `insert()`, `update()`, `merge()` / `upsert()`, `alter()`, `alter_type()`.
796
+
797
+ #### Preview Without Executing
798
+
799
+ ```python
800
+ sql, vals = users.insert({'name': 'Test', 'new_col': 'val'}, sql_only=True)
801
+ print(sql) # Shows the ALTER + INSERT that would run
802
+ ```
803
+
804
+ ---
805
+
806
+ ### Views, Sequences, and Databases
807
+
808
+ #### Views
809
+
810
+ ```python
811
+ @engine.transaction
812
+ def view_examples(tx):
813
+ v = tx.view('active_users')
814
+
815
+ v.create_or_replace("SELECT * FROM users WHERE status = 'active'")
816
+ v.grant('SELECT', 'readonly_role')
817
+
818
+ # Idempotent ensure — creates if missing, applies grants
819
+ v.ensure(
820
+ "SELECT * FROM users WHERE status = 'active'",
821
+ grants=[('SELECT', 'readonly_role')],
822
+ grant_public_select=True
823
+ )
824
+
825
+ if v.exists():
826
+ print('View exists')
827
+ ```
828
+
829
+ #### Sequences (PostgreSQL)
830
+
831
+ ```python
832
+ @engine.transaction
833
+ def sequence_examples(tx):
834
+ seq = tx.sequence('invoice_number')
835
+
836
+ seq.create(start=1000)
837
+ next_val = seq.next() # nextval()
838
+ curr_val = seq.current() # currval()
839
+ safe_val = seq.safe_current() # returns None if uninitialized (no exception)
840
+
841
+ seq.set_value(5000) # ALTER SEQUENCE RESTART WITH 5000
842
+ seq.configure(increment=10, minvalue=1, maxvalue=999999, cycle=True)
843
+
844
+ info = seq.info() # metadata from pg_sequences
845
+ seq.rename('order_number')
846
+ seq.drop()
847
+ ```
848
+
849
+ #### Database Management
850
+
851
+ ```python
852
+ @engine.transaction
853
+ def database_examples(tx):
854
+ db = tx.database('analytics')
855
+
856
+ if not db.exists():
857
+ db.create()
858
+
859
+ db.tables # list of schema.table strings
860
+ db.vacuum(analyze=True)
861
+ db.reindex()
862
+ db.switch() # switch active database
863
+ ```
864
+
865
+ ---
866
+
867
+ ### Schema Locking
868
+
869
+ In production, disable auto-creation to prevent accidental schema changes:
870
+
871
+ ```python
872
+ engine = velocity.db.postgres.initialize(schema_locked=True)
873
+
874
+ # Or toggle at runtime:
875
+ engine.lock_schema()
876
+ engine.unlock_schema()
877
+
878
+ # Temporary unlock for migrations:
879
+ with engine.unlocked_schema():
880
+ migrate(engine)
881
+ ```
882
+
883
+ When schema is locked, `@create_missing` raises `DbSchemaLockedError` instead of auto-creating.
884
+
885
+ ---
886
+
887
+ ### Error Handling
888
+
889
+ Transactions automatically roll back on any exception:
890
+
891
+ ```python
892
+ @engine.transaction
893
+ def safe_transfer(tx, from_id, to_id, amount):
894
+ from_acct = tx.table('accounts').find(from_id)
895
+ to_acct = tx.table('accounts').find(to_id)
896
+
897
+ if from_acct['balance'] < amount:
898
+ raise ValueError("Insufficient funds")
899
+
900
+ from_acct['balance'] -= amount # both changes are atomic
901
+ to_acct['balance'] += amount
902
+ # auto-committed on success; auto-rolled-back on exception
903
+ ```
904
+
905
+ The `@return_default` decorator on internal methods swallows specified exceptions and logs them:
906
+
907
+ ```python
908
+ # Example: table.count() returns 0 if the table doesn't exist
909
+ # because count() is decorated with @return_default(0, (DbTableMissingError,))
910
+ user_count = users.count() # returns 0 for missing table, not an exception
911
+ ```
912
+
913
+ The `@reset_id_on_dup_key` decorator handles primary key collisions by bumping the sequence and retrying (up to 3 times).
914
+
915
+ ---
916
+
917
+ ### Exception Hierarchy
918
+
919
+ All database exceptions inherit from `DbException`:
920
+
921
+ | Exception | When |
922
+ |-----------|------|
923
+ | `DbConnectionError` | Connection failure (transient — auto-retried) |
924
+ | `DbDatabaseMissingError` | Database doesn't exist |
925
+ | `DbTableMissingError` | Table doesn't exist (triggers auto-create) |
926
+ | `DbColumnMissingError` | Column doesn't exist (triggers auto-add) |
927
+ | `DbDuplicateKeyError` | Unique constraint violation |
928
+ | `DbForeignKeyMissingError` | FK constraint violation |
929
+ | `DbDataIntegrityError` | General integrity violation |
930
+ | `DbTruncationError` | Data too long for column |
931
+ | `DbLockTimeoutError` | Lock wait timeout (auto-retried) |
932
+ | `DbSchemaLockedError` | Schema modification blocked |
933
+ | `DbObjectExistsError` | Object already exists |
934
+ | `DbQueryError` | SQL syntax or execution error |
935
+ | `DbTransactionError` | Transaction state error |
936
+ | `DbRetryTransaction` | Signals transaction retry |
937
+ | `DuplicateRowsFoundError` | Multiple rows found when single expected |
938
+
939
+ ---
940
+
941
+ ### Utility Functions
942
+
943
+ `velocity.db.utils` provides helpers for working with row data:
944
+
945
+ ```python
946
+ from velocity.db.utils import safe_sort_rows, group_by_fields
947
+
948
+ # Sort rows with None-safe handling
949
+ sorted_rows = safe_sort_rows(rows, 'last_name', none_handling='last')
950
+
951
+ # Group rows by field values
952
+ groups = group_by_fields(rows, 'department', 'status')
953
+ # {('Engineering', 'active'): [...], ('Marketing', 'active'): [...]}
954
+ ```
955
+
956
+ | Function | Description |
957
+ |----------|-------------|
958
+ | `safe_sort_rows(rows, field, none_handling='last')` | Sort dicts by field, placing `None` values first or last |
959
+ | `group_by_fields(rows, *fields)` | Group rows into `{tuple: [rows]}` by field values |
960
+ | `mask_config_for_display(config)` | Redact passwords/secrets in config dicts for logging |
961
+
962
+ ---
963
+
964
+ ## Velocity.AWS — Serverless Framework
965
+
966
+ A complete framework for building AWS Lambda functions with action-based routing, Cognito authentication, and SQS message processing.
967
+
968
+ ### Lambda Handlers
969
+
970
+ Subclass `LambdaHandler` to create API Gateway-backed Lambda functions:
971
+
972
+ ```python
973
+ from velocity.aws.handlers import LambdaHandler
974
+
975
+ class MyHandler(LambdaHandler):
976
+ auth_mode = 'required' # 'required' | 'optional' | 'none'
977
+ user_table = 'app_users' # table for DB user lookup
978
+ public_actions = ['get-status'] # actions that skip auth
979
+
980
+ def OnActionGetUsers(self, tx, context):
981
+ users = tx.table('users').list(where={'status': 'active'})
982
+ context.response().load_object({'users': users})
983
+
984
+ def OnActionCreateUser(self, tx, context):
985
+ data = context.payload()
986
+ user = tx.table('users').new()
987
+ user['name'] = data['name']
988
+ user['email'] = data['email']
989
+ context.response().toast('User created', 'success')
990
+
991
+ def OnActionGetStatus(self, tx, context):
992
+ context.response().load_object({'status': 'ok'})
993
+
994
+ handler = MyHandler()
995
+
996
+ def lambda_handler(event, context):
997
+ return handler.serve(event, context)
998
+ ```
999
+
1000
+ **Action routing**: A POST with `{"action": "get-users"}` calls `OnActionGetUsers`. The action name is converted to PascalCase with `OnAction` prefix.
1001
+
1002
+ **Authentication flow** (`beforeAction`):
1003
+ 1. Extract Cognito user from API Gateway event
1004
+ 2. Look up user in `user_table` by Cognito subject
1005
+ 3. Attach `current_user` to context
1006
+ 4. Log activity to `aws_api_activity` table
1007
+
1008
+ **Error handling**: Exceptions in handlers are caught by `onError`, logged, and returned as structured error responses. `AlertError` surfaces user-facing messages.
1009
+
1010
+ ### SQS Handlers
1011
+
1012
+ For Lambda functions triggered by SQS queues:
1013
+
1014
+ ```python
1015
+ from velocity.aws.handlers import SqsHandler
1016
+
1017
+ class MyQueueHandler(SqsHandler):
1018
+ def OnActionProcessOrder(self, tx, context):
1019
+ data = context.payload()
1020
+ order = tx.table('orders').find(data['order_id'])
1021
+ order['status'] = 'processed'
1022
+
1023
+ def OnActionSendNotification(self, tx, context):
1024
+ data = context.payload()
1025
+ # send notification logic...
1026
+
1027
+ handler = MyQueueHandler()
1028
+
1029
+ def lambda_handler(event, context):
1030
+ return handler.serve(event, context)
1031
+ ```
1032
+
1033
+ Each SQS record's message body is parsed and routed by its `action` field.
1034
+
1035
+ ### Context and Response Objects
1036
+
1037
+ **Context** — request data accessor:
1038
+
1039
+ ```python
1040
+ def OnActionExample(self, tx, context):
1041
+ action = context.action() # 'example'
1042
+ body = context.postdata() # full parsed request body
1043
+ payload = context.payload() # body['payload'] shortcut
1044
+ name = context.payload('name') # body['payload']['name']
1045
+ nested = context.payload('a', 'b') # body['payload']['a']['b']
1046
+ ```
1047
+
1048
+ **Response** — action-based response builder:
1049
+
1050
+ ```python
1051
+ def OnActionExample(self, tx, context):
1052
+ resp = context.response()
1053
+
1054
+ resp.load_object({'users': [...]}) # send data to frontend store
1055
+ resp.update_store({'count': 42}) # partial store update
1056
+ resp.toast('Saved!', 'success') # toast notification
1057
+ resp.alert('Are you sure?', 'Confirm') # alert dialog
1058
+ resp.file_download({'url': '...', 'filename': 'report.xlsx'})
1059
+
1060
+ resp.set_status(201)
1061
+ resp.set_headers({'X-Custom': 'value'})
1062
+ ```
1063
+
1064
+ **AlertError** — surface errors as UI notifications:
1065
+
1066
+ ```python
1067
+ from velocity.aws.handlers import AlertError
1068
+
1069
+ raise AlertError('Email already exists', title='Duplicate', toast=True, variant='warning')
1070
+ ```
1071
+
1072
+ **PerfTimer** — optional request timing:
1073
+
1074
+ ```python
1075
+ def OnActionHeavyOperation(self, tx, context):
1076
+ context.configure_perf(context.postdata()) # enables if perf=True in request
1077
+
1078
+ context.perf.start('fetch')
1079
+ data = tx.table('big_table').list()
1080
+ context.perf.time('fetch')
1081
+
1082
+ context.perf.start('process')
1083
+ result = process(data)
1084
+ context.perf.time('process')
1085
+ # Timing logged automatically
1086
+ ```
1087
+
1088
+ ### Amplify Integration
1089
+
1090
+ `AmplifyProject` wraps AWS Amplify, Lambda, and SQS APIs:
1091
+
1092
+ ```python
1093
+ from velocity.aws.amplify import AmplifyProject
1094
+
1095
+ app = AmplifyProject('d1234abcde')
1096
+
1097
+ app.get_app_name() # Amplify app name
1098
+ app.list_backend_branches() # ['main', 'staging']
1099
+ app.get_merged_env_vars('main') # env vars (global + branch)
1100
+ app.set_environment_variable('KEY', 'val') # set env var
1101
+
1102
+ # Update Lambda functions
1103
+ for fn in app.list_lambda_functions_filtered('main'):
1104
+ app.update_lambda_function(fn, env_vars={'NEW_KEY': 'val'})
1105
+ ```
1106
+
1107
+ `amplify_build` provides full deployment automation:
1108
+
1109
+ ```python
1110
+ from velocity.aws.amplify_build import run_backend_deployment, BackendDeploymentConfig
1111
+
1112
+ config = BackendDeploymentConfig(queue_names=['notifications'])
1113
+ run_backend_deployment(app, 'main', config)
1114
+ ```
1115
+
1116
+ ---
1117
+
1118
+ ## Velocity.Payment — Payment Processing
1119
+
1120
+ Processor-agnostic payment system using the Adapter pattern. Currently supports **Stripe** (Connect with Express accounts) and **Braintree** (sub-merchant accounts).
1121
+
1122
+ ### Adapter Pattern
1123
+
1124
+ All adapters implement `PaymentProcessorAdapter`:
1125
+
1126
+ ```python
1127
+ from velocity.payment.base_adapter import PaymentProcessorAdapter
1128
+
1129
+ # Adapters provide:
1130
+ adapter.create_account(tx, client_data) # create merchant account
1131
+ adapter.get_account_status(tx, account_id) # account status
1132
+ adapter.create_onboarding_link(tx, id, return_url, refresh_url)
1133
+ adapter.authorize_payment(tx, payment_data) # pre-authorize (hold)
1134
+ adapter.capture_payment(tx, transaction_id, amount) # capture authorized payment
1135
+ adapter.cancel_payment(tx, transaction_id, reason) # void authorization
1136
+ adapter.charge_stored_payment_method(tx, payment_data) # charge vaulted method
1137
+ ```
1138
+
1139
+ ### Payment Routing
1140
+
1141
+ The router selects the correct adapter using a 3-level hierarchy:
1142
+
1143
+ 1. **Form-level** override (specific donation form)
1144
+ 2. **Client-level** setting (organization preference)
1145
+ 3. **Platform default** (system-wide feature flag)
1146
+
1147
+ ```python
1148
+ from velocity.payment.router import get_processor_adapter, charge_stored_payment_method
1149
+
1150
+ # Get the right adapter for a client
1151
+ adapter = get_processor_adapter(tx, client_id=42, form_sys_id=100)
1152
+
1153
+ # Or use the convenience function
1154
+ charge_stored_payment_method(
1155
+ tx,
1156
+ client_id=42,
1157
+ payment_profile=profile_data,
1158
+ amount=50.00,
1159
+ description='Monthly donation'
1160
+ )
1161
+ ```
1162
+
1163
+ Revenue split percentages follow a 4-level hierarchy:
1164
+
1165
+ ```python
1166
+ from velocity.payment.router import get_revenue_split_percentage
1167
+
1168
+ split = get_revenue_split_percentage(tx, client_id=42) # e.g., 15.00
1169
+ ```
1170
+
1171
+ ### Stripe and Braintree
1172
+
1173
+ **Stripe** — Express Connected Accounts with destination charges:
1174
+ - Hosted onboarding via AccountLinks
1175
+ - Destination charges with `application_fee_amount`
1176
+ - Two-phase auth (manual capture)
1177
+ - Full customer profile management (create, attach/detach payment methods, delete)
1178
+
1179
+ **Braintree** — Sub-merchant accounts:
1180
+ - Platform-centric model (all payments to master account)
1181
+ - Manual settlement to clients
1182
+ - Sub-merchant account management
1183
+
1184
+ ### Customer Profiles
1185
+
1186
+ ```python
1187
+ from velocity.payment.router import (
1188
+ get_or_create_customer_profile,
1189
+ attach_payment_method,
1190
+ detach_payment_method,
1191
+ )
1192
+
1193
+ profile = get_or_create_customer_profile(tx, client_id=42, customer_data={...})
1194
+ attach_payment_method(tx, client_id=42, customer_id=cid, payment_method_id=pm_id)
1195
+ detach_payment_method(tx, client_id=42, payment_method_id=pm_id)
1196
+ ```
1197
+
1198
+ ```python
1199
+ from velocity.payment.profiles import money_to_cents, build_stripe_payment_profile
1200
+
1201
+ cents = money_to_cents(49.99) # 4999
1202
+ profile = build_stripe_payment_profile(customer_id, payment_method, email)
1203
+ ```
1204
+
1205
+ ---
1206
+
1207
+ ## Velocity.App — Business Domain Objects
1208
+
1209
+ Pre-built, transaction-aware domain objects for common business operations.
1210
+
1211
+ ### Orders
1212
+
1213
+ ```python
1214
+ from velocity.app.orders import Order
1215
+
1216
+ @engine.transaction
1217
+ class OrderService:
1218
+ def create_order(self, tx, customer_id, items):
1219
+ order = Order()
1220
+ order.update_header('customer_id', customer_id)
1221
+ order.update_header('status', 'pending')
1222
+
1223
+ for item in items:
1224
+ order.add_lineitem(
1225
+ {'product_id': item['id'], 'qty': item['qty'], 'price': item['price']},
1226
+ supp_data={'notes': item.get('notes')}
1227
+ )
1228
+
1229
+ order.persist(tx)
1230
+ return order.to_dict()
1231
+ ```
1232
+
1233
+ `Order` manages headers, line items, and supplemental data with schema validation (`SCHEMA`), defaults (`DEFAULTS`), and full CRUD (`load`, `persist`, `add_lineitem`, `update_lineitem`, `delete_lineitem`).
1234
+
1235
+ Invoices, Payments, and Purchase Orders modules are available as extension points.
1236
+
1237
+ ---
1238
+
1239
+ ## Velocity.Misc — Utilities
1240
+
1241
+ ### Data Conversion (iconv / oconv)
1242
+
1243
+ Two-way data conversion for transforming between frontend/API formats and database storage:
1244
+
1245
+ **`iconv`** — Input Conversion (frontend → database):
1246
+
1247
+ ```python
1248
+ from velocity.misc.conv import iconv
1249
+
1250
+ iconv.phone('5551234567') # '5551234567' (normalized to 10 digits)
1251
+ iconv.email('John@Example.COM') # 'john@example.com'
1252
+ iconv.date_conv('01/15/2024') # datetime.date(2024, 1, 15)
1253
+ iconv.boolean('yes') # True
1254
+ iconv.integer('42') # 42
1255
+ iconv.none('null') # None
1256
+ iconv.pointer('123') # 123
1257
+ ```
1258
+
1259
+ **`oconv`** — Output Conversion (database → frontend):
1260
+
1261
+ ```python
1262
+ from velocity.misc.conv import oconv
1263
+
1264
+ oconv.phone('5551234567') # '(555) 123-4567'
1265
+ oconv.money(1234.5) # '$1,234.50'
1266
+ oconv.date_conv(date.today()) # '01/15/2024'
1267
+ oconv.day_of_week(1) # 'Monday'
1268
+ oconv.ein('123456789') # '12-3456789'
1269
+ oconv.boolean(1) # True
1270
+ oconv.title('john doe') # 'John Doe'
1271
+ oconv.padding(10, '0') # returns a right-pad function
1272
+ oconv.pprint('{"a":1}') # formatted JSON string
1273
+ ```
1274
+
1275
+ ### Spreadsheet Export
1276
+
1277
+ Generate Excel files with headers, styles, merged cells, and auto-sized columns:
1278
+
1279
+ ```python
1280
+ from velocity.misc.export import create_spreadsheet
1281
+
1282
+ headers = ['Name', 'Email', 'Amount']
1283
+ rows = [
1284
+ ['Alice', 'alice@a.com', 150.00],
1285
+ ['Bob', 'bob@b.com', 275.50],
1286
+ ]
1287
+
1288
+ create_spreadsheet(
1289
+ headers=headers,
1290
+ rows=rows,
1291
+ fileorbuffer='report.xlsx',
1292
+ styles={
1293
+ 'col_header': True, # bold header row
1294
+ 'align_right': [2], # right-align Amount column
1295
+ },
1296
+ freeze_panes='A2', # freeze header row
1297
+ auto_size=True # auto-fit column widths
1298
+ )
1299
+ ```
1300
+
1301
+ Available styles: `col_header`, `bold`, `sum_total`, `sub_total`, `align_right`. Supports named styles, merge cells, number formats, and dimension overrides.
1302
+
1303
+ ### Email Parsing
1304
+
1305
+ Parse MIME email content into structured data:
1306
+
1307
+ ```python
1308
+ from velocity.misc.mail import parse
1309
+
1310
+ result = parse(raw_email_content)
1311
+ result['body'] # plain text body
1312
+ result['html'] # HTML body
1313
+ result['attachments'] # [Attachment(name, data, ctype, size, hash), ...]
1314
+ ```
1315
+
1316
+ ### Formatting and Serialization
1317
+
1318
+ ```python
1319
+ from velocity.misc.format import currency, human_delta, to_json
1320
+ from datetime import timedelta
1321
+ from decimal import Decimal
1322
+
1323
+ currency(1234.5) # '1,234.50'
1324
+ human_delta(timedelta(hours=3, minutes=2)) # '3 hr(s) 2 min'
1325
+
1326
+ # JSON serializer that handles Decimal, datetime, date, time, timedelta
1327
+ to_json({'amount': Decimal('19.99'), 'created': datetime.now()})
1328
+ ```
1329
+
1330
+ ### Deep Merge
1331
+
1332
+ Recursively merge dictionaries:
1333
+
1334
+ ```python
1335
+ from velocity.misc.merge import deep_merge
1336
+
1337
+ base = {'a': 1, 'nested': {'x': 10, 'y': 20}}
1338
+ override = {'b': 2, 'nested': {'y': 99, 'z': 30}}
1339
+
1340
+ result = deep_merge(base, override)
1341
+ # {'a': 1, 'b': 2, 'nested': {'x': 10, 'y': 99, 'z': 30}}
1342
+
1343
+ # Mutate first dict in-place:
1344
+ deep_merge(base, override, update=True)
1345
+ ```
1346
+
1347
+ ### Timer
1348
+
1349
+ Simple stopwatch for profiling:
1350
+
1351
+ ```python
1352
+ from velocity.misc.timer import Timer
1353
+
1354
+ t = Timer('DB query')
1355
+ # ... do work ...
1356
+ elapsed = t.stop()
1357
+ print(t) # "DB query: 0.1234 s"
1358
+ ```
1359
+
1360
+ ---
1361
+
1362
+ ## Project Structure
1363
+
1364
+ ```
1365
+ velocity-python/
1366
+ ├── src/velocity/
1367
+ │ ├── db/ # Database ORM
1368
+ │ │ ├── core/
1369
+ │ │ │ ├── engine.py # Engine — connection + transaction decorator
1370
+ │ │ │ ├── transaction.py # Transaction — connection lifecycle
1371
+ │ │ │ ├── table.py # Table — full CRUD + DDL
1372
+ │ │ │ ├── row.py # Row — MutableMapping dict-like row
1373
+ │ │ │ ├── result.py # Result — cursor wrapper + transforms
1374
+ │ │ │ ├── view.py # View — DB view management
1375
+ │ │ │ ├── column.py # Column — column metadata + operations
1376
+ │ │ │ ├── database.py # Database — DB-level operations
1377
+ │ │ │ ├── sequence.py # Sequence — PostgreSQL sequences
1378
+ │ │ │ └── decorators.py # @create_missing, @return_default, etc.
1379
+ │ │ ├── servers/
1380
+ │ │ │ ├── postgres/ # PostgreSQL dialect
1381
+ │ │ │ ├── mysql/ # MySQL dialect
1382
+ │ │ │ ├── sqlite/ # SQLite dialect
1383
+ │ │ │ ├── sqlserver/ # SQL Server dialect
1384
+ │ │ │ ├── base/ # Base dialect (abstract)
1385
+ │ │ │ └── tablehelper.py # Query builder + FK pointer resolution
1386
+ │ │ ├── exceptions.py # Exception hierarchy
1387
+ │ │ └── utils.py # Sorting, grouping, masking utilities
1388
+ │ ├── aws/ # AWS integrations
1389
+ │ │ ├── handlers/ # Lambda handler framework
1390
+ │ │ │ ├── base_handler.py # BaseHandler — action routing
1391
+ │ │ │ ├── lambda_handler.py # LambdaHandler — API Gateway
1392
+ │ │ │ ├── sqs_handler.py # SqsHandler — SQS events
1393
+ │ │ │ ├── context.py # Context — request accessor
1394
+ │ │ │ ├── response.py # Response — action-based response builder
1395
+ │ │ │ ├── data_service.py # DataServiceMixin — generic CRUD + export
1396
+ │ │ │ ├── web_handler.py # WebHandler — activity tracking mixin
1397
+ │ │ │ └── perf.py # PerfTimer — request timing
1398
+ │ │ ├── amplify.py # AmplifyProject — Amplify/Lambda/SQS API
1399
+ │ │ └── amplify_build.py # Deployment automation
1400
+ │ ├── payment/ # Payment processing
1401
+ │ │ ├── base_adapter.py # PaymentProcessorAdapter (ABC)
1402
+ │ │ ├── stripe_adapter.py # Stripe Connect adapter
1403
+ │ │ ├── braintree_adapter.py # Braintree adapter
1404
+ │ │ ├── router.py # Processor routing + convenience functions
1405
+ │ │ └── profiles.py # Payment profile utilities
1406
+ │ ├── app/ # Business domain objects
1407
+ │ │ ├── orders.py # Order management
1408
+ │ │ ├── invoices.py # Invoice management
1409
+ │ │ ├── payments.py # Payment records
1410
+ │ │ └── purchase_orders.py # Purchase order management
1411
+ │ ├── misc/ # Utilities
1412
+ │ │ ├── conv/
1413
+ │ │ │ ├── iconv.py # Input conversion (frontend → DB)
1414
+ │ │ │ └── oconv.py # Output conversion (DB → frontend)
1415
+ │ │ ├── export.py # Excel spreadsheet generation
1416
+ │ │ ├── mail.py # Email MIME parsing
1417
+ │ │ ├── format.py # Currency, JSON, timedelta formatting
1418
+ │ │ ├── merge.py # Deep dict merge
1419
+ │ │ ├── timer.py # Stopwatch utility
1420
+ │ │ └── tools.py # run_once and misc helpers
1421
+ │ └── logging.py # Structured logging (CloudWatch JSON)
1422
+ ├── tests/ # Test suite
1423
+ ├── scripts/ # Utility and demo scripts
1424
+ ├── docs/ # Additional documentation
1425
+ ├── pyproject.toml # Package configuration
1426
+ ├── Makefile # Development commands
1427
+ └── README.md # This file
1428
+ ```
1429
+
1430
+ ---
1431
+
1432
+ ## Development
1433
+
1434
+ ### Setup
1435
+
1436
+ ```bash
1437
+ git clone <repository-url>
1438
+ cd velocity-python
1439
+ pip install -e ".[dev]"
1440
+ ```
1441
+
1442
+ ### Running Tests
1443
+
1444
+ ```bash
1445
+ # All tests
1446
+ pytest
1447
+
1448
+ # With coverage
1449
+ pytest --cov=velocity
1450
+
1451
+ # Unit tests only (no database required)
1452
+ make test-unit
1453
+
1454
+ # Integration tests (requires database)
1455
+ make test-integration
1456
+
1457
+ # Clean caches
1458
+ make clean
1459
+ ```
1460
+
1461
+ ### Code Quality
1462
+
1463
+ ```bash
1464
+ black src/ # format
1465
+ mypy src/ # type check
1466
+ flake8 src/ # lint
1467
+ ```
1468
+
1469
+ ### Version Management
1470
+
1471
+ ```bash
1472
+ python scripts/bump.py
1473
+ ```
1474
+
1475
+ ---
1476
+
1477
+ ## License
1478
+
1479
+ MIT License — see [LICENSE](LICENSE) for details.