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.
- velocity_python-0.0.222/PKG-INFO +1479 -0
- velocity_python-0.0.222/README.md +1426 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/pyproject.toml +1 -1
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/__init__.py +1 -1
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/stripe_adapter.py +33 -24
- velocity_python-0.0.222/src/velocity_python.egg-info/PKG-INFO +1479 -0
- velocity_python-0.0.220/PKG-INFO +0 -1031
- velocity_python-0.0.220/README.md +0 -978
- velocity_python-0.0.220/src/velocity_python.egg-info/PKG-INFO +0 -1031
- {velocity_python-0.0.220 → velocity_python-0.0.222}/LICENSE +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/setup.cfg +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/invoices.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/orders.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/payments.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/purchase_orders.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/test_email_processing.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/logging.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/profiles.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity/payment/router.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/SOURCES.txt +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_decorators.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_profiles.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_router.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.0.220 → velocity_python-0.0.222}/tests/test_table_alter.py +0 -0
- {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.
|