velocity-python 0.1.99__tar.gz → 0.1.101__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.99 → velocity_python-0.1.101}/PKG-INFO +1 -1
- {velocity_python-0.1.99 → velocity_python-0.1.101}/pyproject.toml +1 -1
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/__init__.py +1 -1
- velocity_python-0.1.101/src/velocity/aws/blobs.py +91 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/mixins/data_service.py +14 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/stripe_adapter.py +149 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity_python.egg-info/PKG-INFO +1 -1
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity_python.egg-info/SOURCES.txt +3 -0
- velocity_python-0.1.101/tests/test_blob_store.py +64 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_payment_stripe_adapter.py +139 -0
- velocity_python-0.1.101/tests/test_query_missing_table_unlocked.py +98 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/LICENSE +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/README.md +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/setup.cfg +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/email.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/sms.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_email.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/aws/tests/test_sms.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/mail.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_mail_send.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_job_handler.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_job_results.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_observability.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.99 → velocity_python-0.1.101}/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.99 → velocity_python-0.1.101}/src/velocity/aws/handlers/mixins/data_service.py
RENAMED
|
@@ -442,6 +442,20 @@ class DataServiceMixin:
|
|
|
442
442
|
if swallowed_error:
|
|
443
443
|
tx._last_return_default_error = None
|
|
444
444
|
|
|
445
|
+
# A missing table under UNLOCKED auto-schema is a legitimate empty
|
|
446
|
+
# state, not an error: velocity creates tables on first write, so a
|
|
447
|
+
# table nothing has written to yet (user_data on a fresh site / for a
|
|
448
|
+
# new user) is simply empty — answer quietly instead of toasting a
|
|
449
|
+
# scary "relation does not exist" at the user. Under a LOCKED schema
|
|
450
|
+
# (production) tables exist only via ensure_schema, so a missing one
|
|
451
|
+
# is a real deploy bug and stays loud.
|
|
452
|
+
if (
|
|
453
|
+
swallowed_error
|
|
454
|
+
and swallowed_error.get("type") == "DbTableMissingError"
|
|
455
|
+
and not tx.engine.schema_locked
|
|
456
|
+
):
|
|
457
|
+
swallowed_error = None
|
|
458
|
+
|
|
445
459
|
_file_export_formats = {
|
|
446
460
|
"excel",
|
|
447
461
|
"csv",
|
|
@@ -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
|
# ========================================================================
|
|
@@ -6,6 +6,7 @@ src/velocity/logging.py
|
|
|
6
6
|
src/velocity/aws/__init__.py
|
|
7
7
|
src/velocity/aws/amplify.py
|
|
8
8
|
src/velocity/aws/amplify_build.py
|
|
9
|
+
src/velocity/aws/blobs.py
|
|
9
10
|
src/velocity/aws/dirty_pipeline.py
|
|
10
11
|
src/velocity/aws/email.py
|
|
11
12
|
src/velocity/aws/job_results.py
|
|
@@ -195,6 +196,7 @@ tests/test_asset_references.py
|
|
|
195
196
|
tests/test_assets_service.py
|
|
196
197
|
tests/test_async_support.py
|
|
197
198
|
tests/test_batch_operations.py
|
|
199
|
+
tests/test_blob_store.py
|
|
198
200
|
tests/test_column_tx_arg.py
|
|
199
201
|
tests/test_concurrency_safety.py
|
|
200
202
|
tests/test_connection_pool.py
|
|
@@ -239,6 +241,7 @@ tests/test_pdf.py
|
|
|
239
241
|
tests/test_prepared_statements.py
|
|
240
242
|
tests/test_psycopg3_upgrade.py
|
|
241
243
|
tests/test_query_cache.py
|
|
244
|
+
tests/test_query_missing_table_unlocked.py
|
|
242
245
|
tests/test_restricted_direct_tables.py
|
|
243
246
|
tests/test_retry_side_effect_guard.py
|
|
244
247
|
tests/test_return_default_safety.py
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Content-addressed BlobStore: idempotent puts, sha/key duality."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
from velocity.aws.blobs import BlobStore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def make_client(existing_keys=()):
|
|
11
|
+
client = MagicMock()
|
|
12
|
+
existing = set(existing_keys)
|
|
13
|
+
|
|
14
|
+
def head_object(Bucket, Key):
|
|
15
|
+
if Key in existing:
|
|
16
|
+
return {}
|
|
17
|
+
raise Exception("404")
|
|
18
|
+
|
|
19
|
+
client.head_object.side_effect = head_object
|
|
20
|
+
return client
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
PAYLOAD = b"raw email bytes"
|
|
24
|
+
SHA = hashlib.sha256(PAYLOAD).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestBlobStore(unittest.TestCase):
|
|
28
|
+
def test_put_is_content_addressed_and_uploads_once(self):
|
|
29
|
+
client = make_client()
|
|
30
|
+
store = BlobStore("bkt", prefix="demo/gmail-raw", suffix=".eml", client=client)
|
|
31
|
+
sha, key = store.put_bytes(PAYLOAD)
|
|
32
|
+
self.assertEqual(sha, SHA)
|
|
33
|
+
self.assertEqual(key, f"demo/gmail-raw/{SHA}.eml")
|
|
34
|
+
client.put_object.assert_called_once()
|
|
35
|
+
self.assertEqual(client.put_object.call_args.kwargs["Key"], key)
|
|
36
|
+
|
|
37
|
+
def test_put_skips_upload_when_object_exists(self):
|
|
38
|
+
key = f"demo/gmail-raw/{SHA}.eml"
|
|
39
|
+
client = make_client(existing_keys=[key])
|
|
40
|
+
store = BlobStore("bkt", prefix="demo/gmail-raw", suffix=".eml", client=client)
|
|
41
|
+
sha, got_key = store.put_bytes(PAYLOAD)
|
|
42
|
+
self.assertEqual((sha, got_key), (SHA, key))
|
|
43
|
+
client.put_object.assert_not_called()
|
|
44
|
+
|
|
45
|
+
def test_get_accepts_bare_sha_or_full_key(self):
|
|
46
|
+
client = make_client()
|
|
47
|
+
client.get_object.return_value = {"Body": MagicMock(read=lambda: PAYLOAD)}
|
|
48
|
+
store = BlobStore("bkt", prefix="p", suffix=".eml", client=client)
|
|
49
|
+
self.assertEqual(store.get_bytes(SHA), PAYLOAD)
|
|
50
|
+
self.assertEqual(
|
|
51
|
+
client.get_object.call_args.kwargs["Key"], f"p/{SHA}.eml"
|
|
52
|
+
)
|
|
53
|
+
store.get_bytes("explicit/key.eml")
|
|
54
|
+
self.assertEqual(
|
|
55
|
+
client.get_object.call_args.kwargs["Key"], "explicit/key.eml"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_requires_bucket(self):
|
|
59
|
+
with self.assertRaises(ValueError):
|
|
60
|
+
BlobStore("")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|
|
@@ -597,3 +597,142 @@ def test_create_account_upserts_account_mirror_before_business_sync(
|
|
|
597
597
|
},
|
|
598
598
|
)
|
|
599
599
|
]
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class FakeCheckoutSession(dict):
|
|
603
|
+
"""Attribute+item access like a Stripe object."""
|
|
604
|
+
|
|
605
|
+
def __init__(self, **kwargs):
|
|
606
|
+
super().__init__(**kwargs)
|
|
607
|
+
self.__dict__.update(kwargs)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def test_create_checkout_session_builds_hosted_payment(adapter, monkeypatch):
|
|
611
|
+
captured = {}
|
|
612
|
+
|
|
613
|
+
def fake_create(**params):
|
|
614
|
+
captured.update(params)
|
|
615
|
+
return FakeCheckoutSession(
|
|
616
|
+
id="cs_test_1",
|
|
617
|
+
url="https://checkout.stripe.com/pay/cs_test_1",
|
|
618
|
+
status="open",
|
|
619
|
+
payment_status="unpaid",
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
monkeypatch.setattr(
|
|
623
|
+
"velocity.payment.stripe_adapter.stripe.checkout.Session.create", fake_create
|
|
624
|
+
)
|
|
625
|
+
result = adapter.create_checkout_session(
|
|
626
|
+
None,
|
|
627
|
+
{
|
|
628
|
+
"amount": 12500,
|
|
629
|
+
"name": "2026/2027 School Year Registration",
|
|
630
|
+
"success_url": "https://x/register?session_id={CHECKOUT_SESSION_ID}",
|
|
631
|
+
"cancel_url": "https://x/register",
|
|
632
|
+
"customer_email": "ada@example.com",
|
|
633
|
+
"metadata": {"registration_sys_id": "55"},
|
|
634
|
+
"idempotency_key": "reg-55",
|
|
635
|
+
},
|
|
636
|
+
)
|
|
637
|
+
assert result["success"] is True
|
|
638
|
+
assert result["url"].startswith("https://checkout.stripe.com/")
|
|
639
|
+
assert captured["mode"] == "payment"
|
|
640
|
+
item = captured["line_items"][0]["price_data"]
|
|
641
|
+
assert item["unit_amount"] == 12500
|
|
642
|
+
assert item["product_data"]["name"] == "2026/2027 School Year Registration"
|
|
643
|
+
assert captured["customer_email"] == "ada@example.com"
|
|
644
|
+
# metadata rides on the session AND the payment intent, env stamped
|
|
645
|
+
assert captured["metadata"]["registration_sys_id"] == "55"
|
|
646
|
+
assert captured["metadata"]["environment"] == "test"
|
|
647
|
+
assert captured["payment_intent_data"]["metadata"]["registration_sys_id"] == "55"
|
|
648
|
+
assert captured["idempotency_key"] == "reg-55"
|
|
649
|
+
assert "transfer_data" not in captured["payment_intent_data"] # direct account
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def test_create_checkout_session_connect_destination(adapter, monkeypatch):
|
|
653
|
+
captured = {}
|
|
654
|
+
monkeypatch.setattr(
|
|
655
|
+
"velocity.payment.stripe_adapter.stripe.checkout.Session.create",
|
|
656
|
+
lambda **p: captured.update(p)
|
|
657
|
+
or FakeCheckoutSession(
|
|
658
|
+
id="cs", url="u", status="open", payment_status="unpaid"
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
adapter.create_checkout_session(
|
|
662
|
+
None,
|
|
663
|
+
{
|
|
664
|
+
"amount": 1000,
|
|
665
|
+
"name": "x",
|
|
666
|
+
"success_url": "https://x/s",
|
|
667
|
+
"cancel_url": "https://x/c",
|
|
668
|
+
"processor_account_id": "acct_123",
|
|
669
|
+
"application_fee_amount": 250,
|
|
670
|
+
},
|
|
671
|
+
)
|
|
672
|
+
intent_data = captured["payment_intent_data"]
|
|
673
|
+
assert intent_data["transfer_data"] == {"destination": "acct_123"}
|
|
674
|
+
assert intent_data["application_fee_amount"] == 250
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def test_create_checkout_session_missing_field_is_processor_error(adapter):
|
|
678
|
+
with pytest.raises(ProcessorError) as err:
|
|
679
|
+
adapter.create_checkout_session(None, {"amount": 100, "name": "x"})
|
|
680
|
+
assert err.value.error_code == "missing_field"
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def test_retrieve_checkout_session_verifies_paid_and_mirrors(adapter, monkeypatch):
|
|
684
|
+
intent = SimpleNamespace(id="pi_9", charges=None, latest_charge=None)
|
|
685
|
+
session = FakeCheckoutSession(
|
|
686
|
+
id="cs_test_1",
|
|
687
|
+
status="complete",
|
|
688
|
+
payment_status="paid",
|
|
689
|
+
amount_total=12500,
|
|
690
|
+
currency="usd",
|
|
691
|
+
payment_intent=intent,
|
|
692
|
+
metadata={"registration_sys_id": "55"},
|
|
693
|
+
customer_email=None,
|
|
694
|
+
customer_details={"email": "ada@example.com"},
|
|
695
|
+
)
|
|
696
|
+
monkeypatch.setattr(
|
|
697
|
+
"velocity.payment.stripe_adapter.stripe.checkout.Session.retrieve",
|
|
698
|
+
lambda sid, **kw: session,
|
|
699
|
+
)
|
|
700
|
+
mirrored = {}
|
|
701
|
+
monkeypatch.setattr(
|
|
702
|
+
adapter,
|
|
703
|
+
"_mirror_payment_intent_artifacts",
|
|
704
|
+
lambda tx, pi, source: mirrored.update({"pi": pi.id, "source": source}),
|
|
705
|
+
)
|
|
706
|
+
result = adapter.retrieve_checkout_session(object(), "cs_test_1")
|
|
707
|
+
assert result["paid"] is True
|
|
708
|
+
assert result["amount_total"] == 12500
|
|
709
|
+
assert result["payment_intent_id"] == "pi_9"
|
|
710
|
+
assert result["customer_email"] == "ada@example.com"
|
|
711
|
+
assert result["metadata"] == {"registration_sys_id": "55"}
|
|
712
|
+
assert mirrored["pi"] == "pi_9"
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def test_retrieve_checkout_session_unpaid_never_mirrors(adapter, monkeypatch):
|
|
716
|
+
session = FakeCheckoutSession(
|
|
717
|
+
id="cs_test_1",
|
|
718
|
+
status="open",
|
|
719
|
+
payment_status="unpaid",
|
|
720
|
+
amount_total=12500,
|
|
721
|
+
currency="usd",
|
|
722
|
+
payment_intent=None,
|
|
723
|
+
metadata={},
|
|
724
|
+
customer_email=None,
|
|
725
|
+
customer_details=None,
|
|
726
|
+
)
|
|
727
|
+
monkeypatch.setattr(
|
|
728
|
+
"velocity.payment.stripe_adapter.stripe.checkout.Session.retrieve",
|
|
729
|
+
lambda sid, **kw: session,
|
|
730
|
+
)
|
|
731
|
+
monkeypatch.setattr(
|
|
732
|
+
adapter,
|
|
733
|
+
"_mirror_payment_intent_artifacts",
|
|
734
|
+
lambda *a, **k: (_ for _ in ()).throw(AssertionError("must not mirror")),
|
|
735
|
+
)
|
|
736
|
+
result = adapter.retrieve_checkout_session(object(), "cs_test_1")
|
|
737
|
+
assert result["paid"] is False
|
|
738
|
+
assert result["payment_intent_id"] is None
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""A missing table under UNLOCKED auto-schema is empty, not an error.
|
|
2
|
+
|
|
3
|
+
velocity creates tables on first write, so a table nothing has written to
|
|
4
|
+
yet (``user_data`` on a fresh factory site / for a brand-new user) is
|
|
5
|
+
semantically an empty table. The query path must answer with empty rows and
|
|
6
|
+
NO error toast in that mode — the 2026-09-01 dance.10sp.us first sign-in
|
|
7
|
+
toasted 'relation "user_data" does not exist' at the operator. Under a
|
|
8
|
+
LOCKED schema (production), tables exist only via ensure_schema, so a
|
|
9
|
+
missing one is a real deploy bug and must stay loud.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import unittest
|
|
13
|
+
from unittest.mock import MagicMock
|
|
14
|
+
|
|
15
|
+
from velocity.aws.handlers.mixins.data_service import DataServiceMixin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Service(DataServiceMixin):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _harness(schema_locked):
|
|
23
|
+
service = _Service()
|
|
24
|
+
service._call_rwx_hook = MagicMock()
|
|
25
|
+
|
|
26
|
+
result = MagicMock()
|
|
27
|
+
result.all.return_value = []
|
|
28
|
+
result.sql = None
|
|
29
|
+
result.headers = []
|
|
30
|
+
|
|
31
|
+
table = MagicMock()
|
|
32
|
+
table.select.return_value = result
|
|
33
|
+
|
|
34
|
+
tx = MagicMock()
|
|
35
|
+
tx.table.return_value = table
|
|
36
|
+
tx.engine.schema_locked = schema_locked
|
|
37
|
+
# what @return_default records when the select swallowed 42P01
|
|
38
|
+
tx._last_return_default_error = None
|
|
39
|
+
|
|
40
|
+
def select_with_swallow(**params):
|
|
41
|
+
tx._last_return_default_error = {
|
|
42
|
+
"type": "DbTableMissingError",
|
|
43
|
+
"message": 'relation "user_data" does not exist',
|
|
44
|
+
"function": "velocity.db.core.table.Table.select",
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
table.select.side_effect = select_with_swallow
|
|
49
|
+
|
|
50
|
+
context = MagicMock()
|
|
51
|
+
return service, tx, context
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MissingTableUnlockedTest(unittest.TestCase):
|
|
55
|
+
def test_unlocked_missing_table_is_quiet_empty(self):
|
|
56
|
+
service, tx, context = _harness(schema_locked=False)
|
|
57
|
+
data = service.query_hook(
|
|
58
|
+
tx, "user_data", {"obj": "user_data", "params": {}}, context
|
|
59
|
+
)
|
|
60
|
+
self.assertEqual(data["rows"], [])
|
|
61
|
+
self.assertNotIn("error", data["config"])
|
|
62
|
+
context.response().toast.assert_not_called()
|
|
63
|
+
|
|
64
|
+
def test_locked_missing_table_stays_loud(self):
|
|
65
|
+
service, tx, context = _harness(schema_locked=True)
|
|
66
|
+
data = service.query_hook(
|
|
67
|
+
tx, "user_data", {"obj": "user_data", "params": {}}, context
|
|
68
|
+
)
|
|
69
|
+
self.assertEqual(data["rows"], [])
|
|
70
|
+
self.assertEqual(data["config"]["error"]["type"], "DbTableMissingError")
|
|
71
|
+
context.response().toast.assert_called()
|
|
72
|
+
|
|
73
|
+
def test_other_swallowed_errors_still_surface_when_unlocked(self):
|
|
74
|
+
service, tx, context = _harness(schema_locked=False)
|
|
75
|
+
tx.table.return_value.select.side_effect = None
|
|
76
|
+
|
|
77
|
+
def swallow_column_error(**params):
|
|
78
|
+
tx._last_return_default_error = {
|
|
79
|
+
"type": "DbColumnMissingError",
|
|
80
|
+
"message": 'column "bogus" does not exist',
|
|
81
|
+
"function": "velocity.db.core.table.Table.select",
|
|
82
|
+
}
|
|
83
|
+
result = MagicMock()
|
|
84
|
+
result.all.return_value = []
|
|
85
|
+
result.sql = None
|
|
86
|
+
result.headers = []
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
tx.table.return_value.select.side_effect = swallow_column_error
|
|
90
|
+
data = service.query_hook(
|
|
91
|
+
tx, "user_data", {"obj": "user_data", "params": {}}, context
|
|
92
|
+
)
|
|
93
|
+
self.assertEqual(data["config"]["error"]["type"], "DbColumnMissingError")
|
|
94
|
+
context.response().toast.assert_called()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|