velocity-python 0.1.100__tar.gz → 0.1.102__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.1.100 → velocity_python-0.1.102}/PKG-INFO +1 -1
- {velocity_python-0.1.100 → velocity_python-0.1.102}/pyproject.toml +1 -1
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/__init__.py +1 -1
- velocity_python-0.1.102/src/velocity/aws/blobs.py +91 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/data_service.py +213 -4
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/response.py +27 -1
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/ssm_config.py +9 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/braintree_adapter.py +29 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/stripe_adapter.py +149 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/SOURCES.txt +6 -0
- velocity_python-0.1.102/tests/test_blob_store.py +64 -0
- velocity_python-0.1.102/tests/test_data_service_query_cap.py +115 -0
- velocity_python-0.1.102/tests/test_data_service_renumber.py +93 -0
- velocity_python-0.1.102/tests/test_data_service_save_as.py +190 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_braintree_adapter.py +38 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_stripe_adapter.py +139 -0
- velocity_python-0.1.102/tests/test_response.py +32 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_ssm_config.py +13 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/LICENSE +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/README.md +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/setup.cfg +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_job_handler.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_job_results.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_observability.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_query_missing_table_unlocked.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.100 → velocity_python-0.1.102}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Content-addressed S3 blob store.
|
|
2
|
+
|
|
3
|
+
For offloading large row payloads (raw emails, documents, exports) out of
|
|
4
|
+
Postgres while the row keeps only the content hash. Keys are derived from
|
|
5
|
+
the sha256 of the content, so storage is idempotent and deduplicating by
|
|
6
|
+
construction: the same bytes always land on the same key, and a re-upload
|
|
7
|
+
of an existing object is skipped.
|
|
8
|
+
|
|
9
|
+
store = BlobStore("my-bucket", prefix="demo/gmail-raw", suffix=".eml")
|
|
10
|
+
sha, key = store.put_bytes(raw) # no-op if already stored
|
|
11
|
+
raw = store.get_bytes(sha) # or get_bytes(key)
|
|
12
|
+
url = store.presigned_get(sha) # hand-off to a browser
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
from typing import Optional, Tuple
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BlobStore:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
bucket: str,
|
|
25
|
+
prefix: str = "",
|
|
26
|
+
suffix: str = "",
|
|
27
|
+
region: Optional[str] = None,
|
|
28
|
+
client=None,
|
|
29
|
+
):
|
|
30
|
+
if not bucket:
|
|
31
|
+
raise ValueError("BlobStore requires a bucket name")
|
|
32
|
+
self.bucket = bucket
|
|
33
|
+
self.prefix = prefix.strip("/")
|
|
34
|
+
self.suffix = suffix
|
|
35
|
+
self.region = region
|
|
36
|
+
self._client = client
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def client(self):
|
|
40
|
+
if self._client is None:
|
|
41
|
+
import boto3
|
|
42
|
+
|
|
43
|
+
kwargs = {"region_name": self.region} if self.region else {}
|
|
44
|
+
self._client = boto3.client("s3", **kwargs)
|
|
45
|
+
return self._client
|
|
46
|
+
|
|
47
|
+
def key_for(self, sha256_hex: str) -> str:
|
|
48
|
+
name = f"{sha256_hex}{self.suffix}"
|
|
49
|
+
return f"{self.prefix}/{name}" if self.prefix else name
|
|
50
|
+
|
|
51
|
+
def _resolve_key(self, sha_or_key: str) -> str:
|
|
52
|
+
# A bare 64-hex-char sha resolves through key_for; anything else is
|
|
53
|
+
# treated as a full key (lets callers store either form).
|
|
54
|
+
value = str(sha_or_key)
|
|
55
|
+
if len(value) == 64 and all(c in "0123456789abcdef" for c in value.lower()):
|
|
56
|
+
return self.key_for(value.lower())
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
def exists(self, sha_or_key: str) -> bool:
|
|
60
|
+
try:
|
|
61
|
+
self.client.head_object(
|
|
62
|
+
Bucket=self.bucket, Key=self._resolve_key(sha_or_key)
|
|
63
|
+
)
|
|
64
|
+
return True
|
|
65
|
+
except Exception:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def put_bytes(self, data: bytes, content_type: Optional[str] = None) -> Tuple[str, str]:
|
|
69
|
+
"""Store ``data`` (idempotent); returns (sha256_hex, key)."""
|
|
70
|
+
payload = bytes(data or b"")
|
|
71
|
+
sha = hashlib.sha256(payload).hexdigest()
|
|
72
|
+
key = self.key_for(sha)
|
|
73
|
+
if not self.exists(key):
|
|
74
|
+
params = {"Bucket": self.bucket, "Key": key, "Body": payload}
|
|
75
|
+
if content_type:
|
|
76
|
+
params["ContentType"] = content_type
|
|
77
|
+
self.client.put_object(**params)
|
|
78
|
+
return sha, key
|
|
79
|
+
|
|
80
|
+
def get_bytes(self, sha_or_key: str) -> bytes:
|
|
81
|
+
response = self.client.get_object(
|
|
82
|
+
Bucket=self.bucket, Key=self._resolve_key(sha_or_key)
|
|
83
|
+
)
|
|
84
|
+
return response["Body"].read()
|
|
85
|
+
|
|
86
|
+
def presigned_get(self, sha_or_key: str, expires_seconds: int = 900) -> str:
|
|
87
|
+
return self.client.generate_presigned_url(
|
|
88
|
+
"get_object",
|
|
89
|
+
Params={"Bucket": self.bucket, "Key": self._resolve_key(sha_or_key)},
|
|
90
|
+
ExpiresIn=expires_seconds,
|
|
91
|
+
)
|
{velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
@@ -15,7 +15,7 @@ import re
|
|
|
15
15
|
import xml.etree.ElementTree as ET
|
|
16
16
|
from io import BytesIO, StringIO
|
|
17
17
|
|
|
18
|
-
from velocity.aws import dirty_pipeline
|
|
18
|
+
from velocity.aws import dirty_pipeline, ssm_config
|
|
19
19
|
from velocity.aws.handlers.exceptions import AlertError
|
|
20
20
|
from velocity.db.exceptions import DbDataIntegrityError
|
|
21
21
|
from velocity.misc import export
|
|
@@ -48,6 +48,85 @@ def _fk_blocked_delete_message(table, exc):
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _bump_sys_id_sequence(tx, table):
|
|
55
|
+
"""Keep the table's serial/identity sequence ahead of MAX(sys_id) so a
|
|
56
|
+
future auto-generated id can never collide with an explicitly chosen one
|
|
57
|
+
(renumber, Save-As)."""
|
|
58
|
+
if not _IDENTIFIER_RE.match(table or ""):
|
|
59
|
+
raise AlertError(f"'{table}' is not a valid table name.")
|
|
60
|
+
tx.execute(
|
|
61
|
+
f"""select setval(s.seq, greatest((select coalesce(max(sys_id), 1) from "{table}"), 1))
|
|
62
|
+
from (select pg_get_serial_sequence(%s, 'sys_id') as seq) s
|
|
63
|
+
where s.seq is not null""",
|
|
64
|
+
(table,),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def renumber_row(tx, table, old_sys_id, new_sys_id, overwrite=False):
|
|
69
|
+
"""Assign a CHOSEN sys_id to an existing row (admin surgery).
|
|
70
|
+
|
|
71
|
+
Rules (deliberately simple):
|
|
72
|
+
- refused when any real foreign-key constraint references this table's
|
|
73
|
+
sys_id (renumbering under FKs is a cascade problem for another day);
|
|
74
|
+
- when the target sys_id already exists: refused unless ``overwrite`` —
|
|
75
|
+
which DELETES the occupying row first;
|
|
76
|
+
- the table's sys_id sequence is bumped past MAX(sys_id) afterwards so
|
|
77
|
+
future inserts can never collide with the chosen id.
|
|
78
|
+
|
|
79
|
+
Returns "renumbered" on success, or "exists" when the target id is
|
|
80
|
+
occupied and overwrite was not requested (the caller asks the user).
|
|
81
|
+
Raises AlertError for every refusal.
|
|
82
|
+
"""
|
|
83
|
+
if not _IDENTIFIER_RE.match(table or ""):
|
|
84
|
+
raise AlertError(f"'{table}' is not a valid table name.")
|
|
85
|
+
try:
|
|
86
|
+
old_id = int(old_sys_id)
|
|
87
|
+
new_id = int(new_sys_id)
|
|
88
|
+
except (TypeError, ValueError):
|
|
89
|
+
raise AlertError("sys_id values must be integers.")
|
|
90
|
+
if new_id <= 0:
|
|
91
|
+
raise AlertError("The new sys_id must be a positive integer.")
|
|
92
|
+
if new_id == old_id:
|
|
93
|
+
raise AlertError("That is already this record's sys_id.")
|
|
94
|
+
|
|
95
|
+
if not tx.table(table).find(old_id):
|
|
96
|
+
raise AlertError(f"Record {old_id} was not found in {table}.")
|
|
97
|
+
|
|
98
|
+
referencing = tx.execute(
|
|
99
|
+
"""select con.conrelid::regclass::text as referencing_table
|
|
100
|
+
from pg_constraint con
|
|
101
|
+
join pg_class rel on rel.oid = con.confrelid
|
|
102
|
+
join pg_namespace nsp on nsp.oid = rel.relnamespace
|
|
103
|
+
where con.contype = 'f' and nsp.nspname = 'public'
|
|
104
|
+
and rel.relname = %s
|
|
105
|
+
limit 5""",
|
|
106
|
+
(table,),
|
|
107
|
+
).all()
|
|
108
|
+
if referencing:
|
|
109
|
+
tables = ", ".join(sorted({r["referencing_table"] for r in referencing}))
|
|
110
|
+
raise AlertError(
|
|
111
|
+
f"Renumbering is not allowed: {tables} reference(s) {table} "
|
|
112
|
+
"through a foreign key. (Denied for now — reassigning "
|
|
113
|
+
"references is not implemented.)"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if tx.table(table).find(new_id):
|
|
117
|
+
if not overwrite:
|
|
118
|
+
return "exists"
|
|
119
|
+
tx.execute(f'DELETE FROM "{table}" WHERE sys_id = %s', (new_id,))
|
|
120
|
+
|
|
121
|
+
tx.execute(
|
|
122
|
+
f'UPDATE "{table}" SET sys_id = %s WHERE sys_id = %s',
|
|
123
|
+
(new_id, old_id),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
_bump_sys_id_sequence(tx, table)
|
|
127
|
+
return "renumbered"
|
|
128
|
+
|
|
129
|
+
|
|
51
130
|
class DataServiceMixin:
|
|
52
131
|
"""
|
|
53
132
|
Mixin providing generic CRUD operations for Lambda handlers.
|
|
@@ -77,6 +156,12 @@ class DataServiceMixin:
|
|
|
77
156
|
# which DO run hooks — remain the supported path for them.
|
|
78
157
|
restricted_direct_tables = frozenset()
|
|
79
158
|
|
|
159
|
+
# Ceiling on rows a single un-paginated `query` may materialize (None =
|
|
160
|
+
# no cap). Apps set the class attribute; the `QueryMaxRows` config key
|
|
161
|
+
# overrides it per environment. Promoted 2026-09-04 from BackOffice's
|
|
162
|
+
# full query_hook override, which existed largely to carry this.
|
|
163
|
+
query_max_rows = None
|
|
164
|
+
|
|
80
165
|
# PostgreSQL type mappings for frontend display
|
|
81
166
|
_pg_types = {
|
|
82
167
|
"bool": "string",
|
|
@@ -94,6 +179,22 @@ class DataServiceMixin:
|
|
|
94
179
|
"timestamp": "string",
|
|
95
180
|
}
|
|
96
181
|
|
|
182
|
+
def _resolve_query_max_rows(self):
|
|
183
|
+
"""Effective query row ceiling: `QueryMaxRows` config key first, then
|
|
184
|
+
the class attribute. Returns a positive int or None (no cap)."""
|
|
185
|
+
raw = None
|
|
186
|
+
try:
|
|
187
|
+
raw = ssm_config.getenv("QueryMaxRows", None)
|
|
188
|
+
except Exception:
|
|
189
|
+
raw = None
|
|
190
|
+
if raw in (None, ""):
|
|
191
|
+
raw = self.query_max_rows
|
|
192
|
+
try:
|
|
193
|
+
value = int(raw) if raw not in (None, "") else None
|
|
194
|
+
except (TypeError, ValueError):
|
|
195
|
+
return None
|
|
196
|
+
return value if value and value > 0 else None
|
|
197
|
+
|
|
97
198
|
def _assert_direct_access_allowed(self, table):
|
|
98
199
|
"""Reject hook-bypassing direct access to a restricted table."""
|
|
99
200
|
if table in self.restricted_direct_tables:
|
|
@@ -185,7 +286,7 @@ class DataServiceMixin:
|
|
|
185
286
|
row = row.to_dict()
|
|
186
287
|
return row
|
|
187
288
|
|
|
188
|
-
def write_hook(self, tx, table, sys_id, incoming, context):
|
|
289
|
+
def write_hook(self, tx, table, sys_id, incoming, context, save_as=False):
|
|
189
290
|
row = {}
|
|
190
291
|
incoming.pop("sys_id", None)
|
|
191
292
|
if sys_id == "@new":
|
|
@@ -223,9 +324,23 @@ class DataServiceMixin:
|
|
|
223
324
|
# error rather than a phantom row inserted with a client-chosen PK.
|
|
224
325
|
row = tx.table(table).find(sys_id)
|
|
225
326
|
if not row:
|
|
226
|
-
|
|
227
|
-
|
|
327
|
+
if not save_as:
|
|
328
|
+
raise AlertError(
|
|
329
|
+
f"Cannot update {table}: record {sys_id} no longer exists."
|
|
330
|
+
)
|
|
331
|
+
# Save-As: the user explicitly chose this id — claim it, then
|
|
332
|
+
# fall through to the shared update tail. before_new is skipped
|
|
333
|
+
# (the data comes from a populated form, not a blank create);
|
|
334
|
+
# after_new still fires so create-time defaults apply. The
|
|
335
|
+
# sequence is bumped past MAX(sys_id) so future auto-ids can
|
|
336
|
+
# never collide with the chosen one.
|
|
337
|
+
tx.table(table).insert({"sys_id": sys_id})
|
|
338
|
+
_bump_sys_id_sequence(tx, table)
|
|
339
|
+
row = tx.table(table).find(sys_id)
|
|
340
|
+
self._call_rwx_hook(
|
|
341
|
+
"after_new", "common", tx, table, sys_id, row, context
|
|
228
342
|
)
|
|
343
|
+
self._call_rwx_hook("after_new", table, tx, table, sys_id, row, context)
|
|
229
344
|
else:
|
|
230
345
|
raise Exception("Object sys_id was not supplied on write operation.")
|
|
231
346
|
row.update(incoming)
|
|
@@ -436,6 +551,23 @@ class DataServiceMixin:
|
|
|
436
551
|
},
|
|
437
552
|
}
|
|
438
553
|
|
|
554
|
+
# Optional ceiling on un-paginated queries: fetch one row past the cap
|
|
555
|
+
# so truncation is detectable and reportable rather than silent.
|
|
556
|
+
max_rows = self._resolve_query_max_rows()
|
|
557
|
+
requested_qty = params.get("qty")
|
|
558
|
+
try:
|
|
559
|
+
requested_qty = (
|
|
560
|
+
int(requested_qty) if requested_qty not in (None, "") else None
|
|
561
|
+
)
|
|
562
|
+
except (TypeError, ValueError):
|
|
563
|
+
requested_qty = None
|
|
564
|
+
cap_applied = bool(max_rows) and (
|
|
565
|
+
requested_qty is None or requested_qty > max_rows
|
|
566
|
+
)
|
|
567
|
+
if cap_applied:
|
|
568
|
+
params = dict(params)
|
|
569
|
+
params["qty"] = max_rows + 1
|
|
570
|
+
|
|
439
571
|
result = tx.table(obj).select(**params)
|
|
440
572
|
|
|
441
573
|
swallowed_error = getattr(tx, "_last_return_default_error", None)
|
|
@@ -514,6 +646,23 @@ class DataServiceMixin:
|
|
|
514
646
|
},
|
|
515
647
|
}
|
|
516
648
|
)
|
|
649
|
+
if (
|
|
650
|
+
cap_applied
|
|
651
|
+
and isinstance(data.get("rows"), list)
|
|
652
|
+
and len(data["rows"]) > max_rows
|
|
653
|
+
):
|
|
654
|
+
data["rows"] = data["rows"][:max_rows]
|
|
655
|
+
truncation = {"truncated": True, "maxRows": max_rows}
|
|
656
|
+
if isinstance(data.get("config"), dict):
|
|
657
|
+
data["config"]["truncated"] = truncation
|
|
658
|
+
context.response().toast(
|
|
659
|
+
f"Showing the first {max_rows:,} rows — refine the search "
|
|
660
|
+
"or paginate to see the rest.",
|
|
661
|
+
"warning",
|
|
662
|
+
)
|
|
663
|
+
else:
|
|
664
|
+
data["truncated"] = truncation
|
|
665
|
+
|
|
517
666
|
if payload.get("count"):
|
|
518
667
|
data["count"] = tx.table(payload["obj"]).count(
|
|
519
668
|
where=params.get("where", None)
|
|
@@ -640,6 +789,29 @@ class DataServiceMixin:
|
|
|
640
789
|
if not any(value is not None for value in incoming.values()):
|
|
641
790
|
raise ValueError("Parameter 'object' cannot contain only None values")
|
|
642
791
|
|
|
792
|
+
# Save-As: the client chose the target sys_id. When that id is already
|
|
793
|
+
# taken, answer "exists" so the UI can confirm the overwrite instead of
|
|
794
|
+
# silently clobbering; with saveAsOverwrite the normal update path
|
|
795
|
+
# writes the full object onto the existing row. A free id is created
|
|
796
|
+
# by write_hook (save_as=True).
|
|
797
|
+
save_as = bool(payload.get("saveAs"))
|
|
798
|
+
if save_as:
|
|
799
|
+
target_sys_id = incoming.get("sys_id")
|
|
800
|
+
if target_sys_id not in (None, "", "@new"):
|
|
801
|
+
try:
|
|
802
|
+
chosen = int(target_sys_id)
|
|
803
|
+
except (TypeError, ValueError):
|
|
804
|
+
raise AlertError("Save-As ID must be a positive integer.")
|
|
805
|
+
if chosen <= 0:
|
|
806
|
+
raise AlertError("Save-As ID must be a positive integer.")
|
|
807
|
+
if not payload.get("saveAsOverwrite") and tx.table(table_name).find(
|
|
808
|
+
chosen
|
|
809
|
+
):
|
|
810
|
+
context.response().set_body(
|
|
811
|
+
{"saveAsStatus": "exists", "sys_id": chosen}
|
|
812
|
+
)
|
|
813
|
+
return
|
|
814
|
+
|
|
643
815
|
logger.debug(
|
|
644
816
|
"Writing to table",
|
|
645
817
|
extra={
|
|
@@ -656,6 +828,7 @@ class DataServiceMixin:
|
|
|
656
828
|
incoming.get("sys_id"),
|
|
657
829
|
incoming,
|
|
658
830
|
context,
|
|
831
|
+
save_as=save_as,
|
|
659
832
|
)
|
|
660
833
|
|
|
661
834
|
if not row:
|
|
@@ -964,6 +1137,42 @@ class DataServiceMixin:
|
|
|
964
1137
|
if not deleteList:
|
|
965
1138
|
context.response().toast("No items were selected.", "warning")
|
|
966
1139
|
|
|
1140
|
+
def OnActionRenumberObject(self, tx, context):
|
|
1141
|
+
"""Assign a chosen sys_id to one row.
|
|
1142
|
+
|
|
1143
|
+
Payload: tableName, sys_id (current), new_sys_id, overwrite (bool).
|
|
1144
|
+
Responds {status: "exists"} when the target id is occupied and
|
|
1145
|
+
overwrite was not requested; on success reloads the renumbered row
|
|
1146
|
+
into the form (load-object) and reports the change.
|
|
1147
|
+
"""
|
|
1148
|
+
payload = context.payload()
|
|
1149
|
+
for field in ("tableName", "sys_id", "new_sys_id"):
|
|
1150
|
+
if field not in payload:
|
|
1151
|
+
raise ValueError(f"Missing required parameter '{field}' in payload")
|
|
1152
|
+
table = payload["tableName"]
|
|
1153
|
+
self._assert_direct_access_allowed(table)
|
|
1154
|
+
|
|
1155
|
+
result = renumber_row(
|
|
1156
|
+
tx,
|
|
1157
|
+
table,
|
|
1158
|
+
payload["sys_id"],
|
|
1159
|
+
payload["new_sys_id"],
|
|
1160
|
+
overwrite=bool(payload.get("overwrite")),
|
|
1161
|
+
)
|
|
1162
|
+
if result == "exists":
|
|
1163
|
+
context.response().set_body(
|
|
1164
|
+
{"status": "exists", "new_sys_id": payload["new_sys_id"]}
|
|
1165
|
+
)
|
|
1166
|
+
return
|
|
1167
|
+
|
|
1168
|
+
row = tx.table(table).find(int(payload["new_sys_id"]))
|
|
1169
|
+
context.response().set_body({"status": "renumbered", "object": dict(row)})
|
|
1170
|
+
context.response().load_object(dict(row))
|
|
1171
|
+
context.response().toast(
|
|
1172
|
+
f"Record renumbered: {payload['sys_id']} → {payload['new_sys_id']}.",
|
|
1173
|
+
"success",
|
|
1174
|
+
)
|
|
1175
|
+
|
|
967
1176
|
def OnActionGetTables(self, tx, context):
|
|
968
1177
|
"""Get list of all tables in the database."""
|
|
969
1178
|
context.response().set_body({"tables": tx.tables()})
|
|
@@ -36,9 +36,16 @@ class Response:
|
|
|
36
36
|
"""
|
|
37
37
|
Finalize the response body as JSON and return the complete response dictionary.
|
|
38
38
|
|
|
39
|
+
When a raw body has been set via :meth:`set_raw_body`, it is returned
|
|
40
|
+
verbatim (no JSON encoding, no actions envelope) — the escape hatch
|
|
41
|
+
for non-JSON responses like TwiML or plain text.
|
|
42
|
+
|
|
39
43
|
Returns:
|
|
40
|
-
Dict[str, Any]: The complete HTTP response with headers, status code, and
|
|
44
|
+
Dict[str, Any]: The complete HTTP response with headers, status code, and body.
|
|
41
45
|
"""
|
|
46
|
+
if getattr(self, "_raw_body", None) is not None:
|
|
47
|
+
self.raw["body"] = self._raw_body
|
|
48
|
+
return self.raw
|
|
42
49
|
if self.actions:
|
|
43
50
|
self.body["actions"] = self.actions
|
|
44
51
|
if self.jobs.events:
|
|
@@ -46,6 +53,25 @@ class Response:
|
|
|
46
53
|
self.raw["body"] = to_json(self.body)
|
|
47
54
|
return self.raw
|
|
48
55
|
|
|
56
|
+
def set_raw_body(self, body: str, content_type: str = "text/plain") -> "Response":
|
|
57
|
+
"""
|
|
58
|
+
Send a non-JSON body verbatim (e.g. TwiML, plain text, XML).
|
|
59
|
+
|
|
60
|
+
Bypasses the JSON envelope entirely: render() returns this body
|
|
61
|
+
unencoded and skips actions/jobs. Added 2026-09-04 — apps previously
|
|
62
|
+
had to override serve() to do this.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
body (str): The exact body string to return.
|
|
66
|
+
content_type (str): Content-Type header value. Defaults to "text/plain".
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Response: The current Response object, allowing method chaining.
|
|
70
|
+
"""
|
|
71
|
+
self._raw_body = body
|
|
72
|
+
self.raw["headers"]["Content-Type"] = content_type
|
|
73
|
+
return self
|
|
74
|
+
|
|
49
75
|
def alert(self, message: str, title: str = "Notification") -> "Response":
|
|
50
76
|
"""
|
|
51
77
|
Add an alert action to the response.
|
|
@@ -122,6 +122,15 @@ def get_stage(default: Optional[str] = None) -> Optional[str]:
|
|
|
122
122
|
return _first_nonempty_env(*_STAGE_ENV_KEYS) or default
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
def is_production() -> bool:
|
|
126
|
+
"""True when the resolved stage is the production branch.
|
|
127
|
+
|
|
128
|
+
The missing primitive behind six per-app ``runtime_config`` copies: apps
|
|
129
|
+
kept re-wrapping :func:`get_stage` just to ask this one question.
|
|
130
|
+
"""
|
|
131
|
+
return (get_stage() or "").strip().lower() == "production"
|
|
132
|
+
|
|
133
|
+
|
|
125
134
|
def get_region(default: Optional[str] = "us-east-1") -> Optional[str]:
|
|
126
135
|
return _first_nonempty_env("AWS_REGION", "REGION", "USER_REGION") or default
|
|
127
136
|
|
{velocity_python-0.1.100 → velocity_python-0.1.102}/src/velocity/payment/braintree_adapter.py
RENAMED
|
@@ -14,6 +14,8 @@ Key Features:
|
|
|
14
14
|
- Authorization with later capture support
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
import time
|
|
18
|
+
|
|
17
19
|
import braintree
|
|
18
20
|
from typing import Dict, Any, Optional
|
|
19
21
|
from datetime import datetime, timezone, timedelta
|
|
@@ -66,6 +68,33 @@ class BraintreeAdapter(PaymentProcessorAdapter):
|
|
|
66
68
|
self.environment = environment_name
|
|
67
69
|
self.platform = config.get("platform")
|
|
68
70
|
|
|
71
|
+
def generate_client_token(
|
|
72
|
+
self,
|
|
73
|
+
customer_id: Optional[str] = None,
|
|
74
|
+
attempts: int = 3,
|
|
75
|
+
backoff_seconds: float = 0.25,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Generate a Braintree client token, retrying transient failures.
|
|
78
|
+
|
|
79
|
+
The retry policy lives here, once — apps were hand-rolling identical
|
|
80
|
+
backoff loops around raw ``client_token.generate()`` calls (promoted
|
|
81
|
+
2026-09-04 from DonateMain / BackOffice copies).
|
|
82
|
+
"""
|
|
83
|
+
params: Dict[str, Any] = {"customer_id": customer_id} if customer_id else {}
|
|
84
|
+
last_exc: Optional[Exception] = None
|
|
85
|
+
for attempt in range(max(1, attempts)):
|
|
86
|
+
try:
|
|
87
|
+
return self.gateway.client_token.generate(params)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
last_exc = exc
|
|
90
|
+
if attempt < attempts - 1:
|
|
91
|
+
time.sleep(backoff_seconds * (attempt + 1))
|
|
92
|
+
raise ProcessorError(
|
|
93
|
+
"braintree",
|
|
94
|
+
"client_token_unavailable",
|
|
95
|
+
f"Client token unavailable after {attempts} attempts: {last_exc}",
|
|
96
|
+
) from last_exc
|
|
97
|
+
|
|
69
98
|
# ========================================================================
|
|
70
99
|
# ACCOUNT MANAGEMENT
|
|
71
100
|
# ========================================================================
|
|
@@ -853,6 +853,155 @@ class StripeAdapter(PaymentProcessorAdapter):
|
|
|
853
853
|
metadata={"charge_id": processor_charge_id},
|
|
854
854
|
)
|
|
855
855
|
|
|
856
|
+
# ========================================================================
|
|
857
|
+
# CHECKOUT SESSIONS (hosted payment page)
|
|
858
|
+
# ========================================================================
|
|
859
|
+
|
|
860
|
+
def create_checkout_session(
|
|
861
|
+
self, tx, session_data: Dict[str, Any]
|
|
862
|
+
) -> Dict[str, Any]:
|
|
863
|
+
"""
|
|
864
|
+
Create a hosted Stripe Checkout Session for a one-off payment.
|
|
865
|
+
|
|
866
|
+
The buyer is redirected to Stripe's hosted page, so the application
|
|
867
|
+
never touches card data (minimal PCI scope). Works on a direct
|
|
868
|
+
account by default; pass ``processor_account_id`` (and optionally
|
|
869
|
+
``application_fee_amount``) for a Connect destination charge.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
tx: Database transaction (reserved for mirror hooks; may be None)
|
|
873
|
+
session_data: Dictionary containing:
|
|
874
|
+
- amount: Amount in cents (required)
|
|
875
|
+
- name: Line-item name shown on the Stripe page (required)
|
|
876
|
+
- success_url: Redirect after payment (required; include
|
|
877
|
+
``{CHECKOUT_SESSION_ID}`` to receive the session id back)
|
|
878
|
+
- cancel_url: Redirect when the buyer backs out (required)
|
|
879
|
+
- currency: default 'usd'
|
|
880
|
+
- customer_email: prefills the Stripe page (optional)
|
|
881
|
+
- metadata: dict stamped on session AND payment intent
|
|
882
|
+
- processor_account_id / application_fee_amount: Connect only
|
|
883
|
+
- idempotency_key: collapses double-submits (optional)
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
{"success": True, "session_id", "url", "status", "payment_status"}
|
|
887
|
+
"""
|
|
888
|
+
del tx # no mirror writes at create time; nothing has settled yet
|
|
889
|
+
try:
|
|
890
|
+
metadata = dict(session_data.get("metadata") or {})
|
|
891
|
+
metadata.setdefault("environment", self.environment)
|
|
892
|
+
if self.platform:
|
|
893
|
+
metadata.setdefault("platform", str(self.platform))
|
|
894
|
+
|
|
895
|
+
params: Dict[str, Any] = {
|
|
896
|
+
"mode": "payment",
|
|
897
|
+
"line_items": [
|
|
898
|
+
{
|
|
899
|
+
"quantity": 1,
|
|
900
|
+
"price_data": {
|
|
901
|
+
"currency": session_data.get("currency", "usd"),
|
|
902
|
+
"unit_amount": int(session_data["amount"]),
|
|
903
|
+
"product_data": {"name": session_data["name"]},
|
|
904
|
+
},
|
|
905
|
+
}
|
|
906
|
+
],
|
|
907
|
+
"success_url": session_data["success_url"],
|
|
908
|
+
"cancel_url": session_data["cancel_url"],
|
|
909
|
+
"metadata": metadata,
|
|
910
|
+
"payment_intent_data": {"metadata": metadata},
|
|
911
|
+
"api_key": self._api_key,
|
|
912
|
+
}
|
|
913
|
+
if session_data.get("customer_email"):
|
|
914
|
+
params["customer_email"] = session_data["customer_email"]
|
|
915
|
+
processor_account_id = session_data.get("processor_account_id")
|
|
916
|
+
if processor_account_id:
|
|
917
|
+
transfer: Dict[str, Any] = {
|
|
918
|
+
"transfer_data": {"destination": processor_account_id}
|
|
919
|
+
}
|
|
920
|
+
fee = session_data.get("application_fee_amount")
|
|
921
|
+
if fee is not None:
|
|
922
|
+
transfer["application_fee_amount"] = int(fee)
|
|
923
|
+
params["payment_intent_data"] = {
|
|
924
|
+
**params["payment_intent_data"],
|
|
925
|
+
**transfer,
|
|
926
|
+
}
|
|
927
|
+
idempotency_key = session_data.get("idempotency_key")
|
|
928
|
+
if idempotency_key:
|
|
929
|
+
params["idempotency_key"] = str(idempotency_key)
|
|
930
|
+
|
|
931
|
+
session = stripe.checkout.Session.create(**params)
|
|
932
|
+
return {
|
|
933
|
+
"success": True,
|
|
934
|
+
"session_id": session.id,
|
|
935
|
+
"url": session.url,
|
|
936
|
+
"status": session.status,
|
|
937
|
+
"payment_status": session.payment_status,
|
|
938
|
+
}
|
|
939
|
+
except KeyError as e:
|
|
940
|
+
raise ProcessorError(
|
|
941
|
+
processor="stripe",
|
|
942
|
+
error_code="missing_field",
|
|
943
|
+
error_message=f"checkout session requires {e}",
|
|
944
|
+
metadata={},
|
|
945
|
+
)
|
|
946
|
+
except stripe.error.StripeError as e:
|
|
947
|
+
raise ProcessorError(
|
|
948
|
+
processor="stripe",
|
|
949
|
+
error_code=getattr(e, "code", None) or "unknown",
|
|
950
|
+
error_message=str(e),
|
|
951
|
+
metadata={},
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
def retrieve_checkout_session(self, tx, session_id: str) -> Dict[str, Any]:
|
|
955
|
+
"""
|
|
956
|
+
Retrieve a Checkout Session and report whether it is paid.
|
|
957
|
+
|
|
958
|
+
Server-side verification for the success redirect: the client hands
|
|
959
|
+
back only the session id; the truth (``payment_status``) comes from
|
|
960
|
+
Stripe. A settled session's PaymentIntent artifacts are mirrored
|
|
961
|
+
when a transaction is supplied.
|
|
962
|
+
|
|
963
|
+
Returns:
|
|
964
|
+
{"success": True, "session_id", "status", "payment_status",
|
|
965
|
+
"paid": bool, "amount_total", "currency",
|
|
966
|
+
"payment_intent_id", "customer_email", "metadata"}
|
|
967
|
+
"""
|
|
968
|
+
try:
|
|
969
|
+
# latest_charge stays an ID on purpose: the mirror helper
|
|
970
|
+
# re-retrieves it with balance_transaction expanded (real fees).
|
|
971
|
+
session = stripe.checkout.Session.retrieve(
|
|
972
|
+
session_id,
|
|
973
|
+
expand=["payment_intent"],
|
|
974
|
+
api_key=self._api_key,
|
|
975
|
+
)
|
|
976
|
+
payment_intent = session.payment_intent
|
|
977
|
+
paid = session.payment_status == "paid"
|
|
978
|
+
if paid and tx is not None and payment_intent is not None:
|
|
979
|
+
self._mirror_payment_intent_artifacts(
|
|
980
|
+
tx,
|
|
981
|
+
payment_intent,
|
|
982
|
+
source="api:stripe_adapter.retrieve_checkout_session",
|
|
983
|
+
)
|
|
984
|
+
details = session.get("customer_details") or {}
|
|
985
|
+
return {
|
|
986
|
+
"success": True,
|
|
987
|
+
"session_id": session.id,
|
|
988
|
+
"status": session.status,
|
|
989
|
+
"payment_status": session.payment_status,
|
|
990
|
+
"paid": paid,
|
|
991
|
+
"amount_total": session.amount_total,
|
|
992
|
+
"currency": session.currency,
|
|
993
|
+
"payment_intent_id": getattr(payment_intent, "id", payment_intent),
|
|
994
|
+
"customer_email": session.get("customer_email") or details.get("email"),
|
|
995
|
+
"metadata": dict(session.metadata or {}),
|
|
996
|
+
}
|
|
997
|
+
except stripe.error.StripeError as e:
|
|
998
|
+
raise ProcessorError(
|
|
999
|
+
processor="stripe",
|
|
1000
|
+
error_code=getattr(e, "code", None) or "unknown",
|
|
1001
|
+
error_message=str(e),
|
|
1002
|
+
metadata={"session_id": session_id},
|
|
1003
|
+
)
|
|
1004
|
+
|
|
856
1005
|
# ========================================================================
|
|
857
1006
|
# UTILITY METHODS
|
|
858
1007
|
# ========================================================================
|