velocity-python 0.1.94__tar.gz → 0.1.98__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.94 → velocity_python-0.1.98}/PKG-INFO +2 -2
- {velocity_python-0.1.94 → velocity_python-0.1.98}/README.md +1 -1
- {velocity_python-0.1.94 → velocity_python-0.1.98}/pyproject.toml +1 -1
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/__init__.py +1 -1
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/web_handler.py +17 -3
- velocity_python-0.1.98/src/velocity/aws/sms.py +237 -0
- velocity_python-0.1.98/src/velocity/aws/tests/test_sms.py +178 -0
- velocity_python-0.1.98/src/velocity/misc/mail.py +453 -0
- velocity_python-0.1.98/src/velocity/misc/tests/test_mail_send.py +365 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/PKG-INFO +2 -2
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/SOURCES.txt +3 -0
- velocity_python-0.1.94/src/velocity/misc/mail.py +0 -86
- {velocity_python-0.1.94 → velocity_python-0.1.98}/LICENSE +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/setup.cfg +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/amplify.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/amplify_build.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/backfill.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/indexing.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/references.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/service.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/usage_index.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/config.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/operations.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/policies.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/reconcile.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/targets.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/dirty_pipeline.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/base_handler.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/context.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/context_factory.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/exceptions.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/lambda_handler.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/masquerade.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/perf.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/response.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/sqs_handler.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/job_results.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/s3.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/ssm_config.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_response.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/async_support.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/column.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/database.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/decorators.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/engine.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/jsonproxy.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/result.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/row.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/sequence.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/table.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/transaction.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/view.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/exceptions.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/maintenance.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/migrations.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/initializer.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/operators.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/sql.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/types.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/operators.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/reserved.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/sql.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/types.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/operators.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/reserved.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/sql.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/types.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/operators.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/reserved.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/sql.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/types.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/operators.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/sql.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/types.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/tablehelper.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/common_db_test.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/common.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/conftest.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_column.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_connections.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_database.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_engine.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_imports.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_result.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_row.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_table.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/common.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_db_utils.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_postgres.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_result_caching.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_sql_builder.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_tablehelper.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_view_helper.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/utils.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/logging.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/conv/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/conv/iconv.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/conv/oconv.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/db.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/export.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/format.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/merge.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/pdf.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_db.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_fix.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_format.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_iconv.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_merge.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_oconv.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_original_error.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_timer.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/timer.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tools.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/capabilities.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/store.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/where.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/exceptions.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/base/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/memory/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/redis/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/__init__.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/authorizenet_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/authorizenet_mirror.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/base_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/braintree_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/braintree_mirror.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/charge_rules.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/stripe_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/stripe_mirror.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/dependency_links.txt +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/entry_points.txt +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/requires.txt +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/top_level.txt +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_activity_tracking_aborted_tx.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_amplify_build.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_asset_indexing.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_asset_references.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_assets_service.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_async_support.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_batch_operations.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_column_tx_arg.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_concurrency_safety.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_connection_pool.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_connection_resilience.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_context_job_descriptions.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_db_credentials_ssm_cascade.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_db_maintenance.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_decorators.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_delete_hook_fk_blocked.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_dirty_pipeline_fast_path.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_email_processing.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_enqueue_send_failures.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_get_cognito_user_provider.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_http_handler_rollback.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_iconv_money_to_cents.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_identifier_injection_guard.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_job_handler.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_job_results.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_json_columns.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_jsonb_dict_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_lambda_handler.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_lambda_handler_auth.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_lambda_handler_masquerade.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_masquerade_grant.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_mixins_import.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_n_plus_one.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_dynamodb.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_memory.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_mongodb.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_redis.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_translation.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_where.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_observability.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_on_commit.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_authorizenet_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_braintree_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_braintree_mirror.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_mirror_empty_table.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_profile_sorting.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_stripe_adapter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_pdf.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_prepared_statements.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_psycopg3_upgrade.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_query_cache.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_restricted_direct_tables.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_retry_side_effect_guard.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_return_default_safety.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_row_batch_update.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_row_cache_staleness.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_schema_migrations.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_security_hardening.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_server_cursor.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_single_autocommit_safety.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_spreadsheet_functions.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_sqlite_backend.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_sqs_per_record_transactions.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_ssm_config.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_store_user_data.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_sys_modified_count_postgres_demo.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_table_alter.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_transaction_class_wrapping.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_transaction_commit_and_ownership.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_transaction_edge_cases.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_where_clause_validation.py +0 -0
- {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_write_hook_create_flow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: velocity-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.98
|
|
4
4
|
Summary: A rapid application development library for interfacing with data storage
|
|
5
5
|
Author-email: Velocity Team <info@codeclubs.org>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -253,7 +253,7 @@ Full documentation is included in the `docs/` directory of the source distributi
|
|
|
253
253
|
| AWS Lambda Handlers | `docs/aws-handlers.md` | LambdaHandler, SqsHandler, auth modes, per-record transactions, async jobs (JobHandler + job result stores) |
|
|
254
254
|
| NoSQL Adapters | `docs/nosql.md` | Redis, DynamoDB, MongoDB, Elasticsearch, Cassandra, Couchbase behind one capability-honest contract |
|
|
255
255
|
| Payment Processing | `docs/payment.md` | Stripe and Braintree adapters, payment lifecycle |
|
|
256
|
-
| Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing |
|
|
256
|
+
| Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing and SMTP sending |
|
|
257
257
|
| Testing Guide | `docs/TESTING.md` | Running tests, markers, coverage |
|
|
258
258
|
| Security | `docs/SECURITY.md` | Pre-commit hooks, credential scanning |
|
|
259
259
|
|
|
@@ -159,7 +159,7 @@ Full documentation is included in the `docs/` directory of the source distributi
|
|
|
159
159
|
| AWS Lambda Handlers | `docs/aws-handlers.md` | LambdaHandler, SqsHandler, auth modes, per-record transactions, async jobs (JobHandler + job result stores) |
|
|
160
160
|
| NoSQL Adapters | `docs/nosql.md` | Redis, DynamoDB, MongoDB, Elasticsearch, Cassandra, Couchbase behind one capability-honest contract |
|
|
161
161
|
| Payment Processing | `docs/payment.md` | Stripe and Braintree adapters, payment lifecycle |
|
|
162
|
-
| Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing |
|
|
162
|
+
| Utilities | `docs/utilities.md` | Excel export, data conversion, formatting, timers, email parsing and SMTP sending |
|
|
163
163
|
| Testing Guide | `docs/TESTING.md` | Running tests, markers, coverage |
|
|
164
164
|
| Security | `docs/SECURITY.md` | Pre-commit hooks, credential scanning |
|
|
165
165
|
|
{velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/web_handler.py
RENAMED
|
@@ -395,13 +395,27 @@ Request Details:
|
|
|
395
395
|
return ""
|
|
396
396
|
|
|
397
397
|
def _get_sendmail_callable(self):
|
|
398
|
-
"""Resolve notification sender callable without hard dependency.
|
|
398
|
+
"""Resolve notification sender callable without hard dependency.
|
|
399
|
+
|
|
400
|
+
An application-supplied ``support.app.helpers.sendmail`` wins so
|
|
401
|
+
existing projects keep their own delivery rules (recipient whitelists,
|
|
402
|
+
non-production rewriting, send logging). Projects without that package
|
|
403
|
+
fall back to velocity's own SMTP sender, which resolves configuration
|
|
404
|
+
from an ``email_settings`` row or the ``Mail*`` config keys — before
|
|
405
|
+
this fallback existed they silently sent nothing.
|
|
406
|
+
"""
|
|
399
407
|
try:
|
|
400
408
|
from support.app import helpers
|
|
401
409
|
|
|
402
|
-
|
|
410
|
+
app_sendmail = getattr(helpers, "sendmail", None)
|
|
411
|
+
if app_sendmail is not None:
|
|
412
|
+
return app_sendmail
|
|
403
413
|
except ImportError:
|
|
404
|
-
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
from velocity.misc.mail import sendmail
|
|
417
|
+
|
|
418
|
+
return sendmail
|
|
405
419
|
|
|
406
420
|
def _get_error_context(self, context) -> str:
|
|
407
421
|
"""Sanitize request context for reporting"""
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""AWS End User Messaging (two-way SMS/MMS) helpers.
|
|
2
|
+
|
|
3
|
+
Two halves, both business-agnostic:
|
|
4
|
+
|
|
5
|
+
``parse_inbound_event(event)``
|
|
6
|
+
Normalize inbound two-way SMS deliveries. AWS End User Messaging publishes
|
|
7
|
+
each inbound message to the SNS topic configured on the receiving phone
|
|
8
|
+
number; in a Lambda subscribed to that topic the payload arrives as
|
|
9
|
+
``event["Records"][n]["Sns"]["Message"]`` (a JSON string). This accepts a
|
|
10
|
+
full Lambda SNS event, a single SNS record, a decoded payload dict, or the
|
|
11
|
+
payload's JSON string, and returns a list of :class:`InboundSms`.
|
|
12
|
+
|
|
13
|
+
``SmsClient``
|
|
14
|
+
Thin wrapper over the ``pinpoint-sms-voice-v2`` API (the SDK name for AWS
|
|
15
|
+
End User Messaging): ``send_text`` / ``send_media``. The boto3 client is
|
|
16
|
+
injectable so callers and tests never need network access.
|
|
17
|
+
|
|
18
|
+
Neither half reads configuration on its own: the origination identity (phone
|
|
19
|
+
number, pool ARN, or sender id) and optional configuration set are constructor
|
|
20
|
+
or call arguments, so applications keep ownership of where those values come
|
|
21
|
+
from (env, SSM, database).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Any, Dict, List, Optional
|
|
27
|
+
|
|
28
|
+
from velocity.logging import get_logger
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class InboundSms:
|
|
35
|
+
"""One inbound SMS/MMS message, normalized from the SNS delivery payload.
|
|
36
|
+
|
|
37
|
+
``keyword`` is AWS's ``messageKeyword`` (the registered keyword the
|
|
38
|
+
carrier matched, e.g. an opt-in keyword) — it is *not* the first word of
|
|
39
|
+
the body; use :attr:`first_word` for command routing.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
origination_number: str = ""
|
|
43
|
+
destination_number: str = ""
|
|
44
|
+
body: str = ""
|
|
45
|
+
message_id: str = ""
|
|
46
|
+
keyword: str = ""
|
|
47
|
+
media_urls: List[str] = field(default_factory=list)
|
|
48
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def first_word(self) -> str:
|
|
52
|
+
"""First whitespace-delimited token of the body, lowercased."""
|
|
53
|
+
parts = (self.body or "").split()
|
|
54
|
+
return parts[0].lower() if parts else ""
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def remainder(self) -> str:
|
|
58
|
+
"""The body with the first word removed (original casing preserved)."""
|
|
59
|
+
parts = (self.body or "").split(None, 1)
|
|
60
|
+
return parts[1] if len(parts) > 1 else ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _coerce_payload(value) -> Optional[Dict[str, Any]]:
|
|
64
|
+
"""A payload dict from a dict or JSON string; None when neither."""
|
|
65
|
+
if isinstance(value, str):
|
|
66
|
+
try:
|
|
67
|
+
value = json.loads(value)
|
|
68
|
+
except (ValueError, TypeError):
|
|
69
|
+
return None
|
|
70
|
+
return value if isinstance(value, dict) else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _payload_to_inbound(payload: Dict[str, Any]) -> Optional[InboundSms]:
|
|
74
|
+
"""Map one End User Messaging inbound payload onto InboundSms.
|
|
75
|
+
|
|
76
|
+
Requires at least an origination number — anything else (delivery status
|
|
77
|
+
events, unrelated SNS traffic on a shared topic) is not an inbound message
|
|
78
|
+
and returns None.
|
|
79
|
+
"""
|
|
80
|
+
origination = str(payload.get("originationNumber") or "").strip()
|
|
81
|
+
if not origination:
|
|
82
|
+
return None
|
|
83
|
+
media = payload.get("mediaUrls")
|
|
84
|
+
if isinstance(media, str):
|
|
85
|
+
media = [media]
|
|
86
|
+
if not isinstance(media, list):
|
|
87
|
+
media = []
|
|
88
|
+
return InboundSms(
|
|
89
|
+
origination_number=origination,
|
|
90
|
+
destination_number=str(payload.get("destinationNumber") or "").strip(),
|
|
91
|
+
body=str(payload.get("messageBody") or ""),
|
|
92
|
+
message_id=str(payload.get("inboundMessageId") or "").strip(),
|
|
93
|
+
keyword=str(payload.get("messageKeyword") or "").strip(),
|
|
94
|
+
media_urls=[str(u) for u in media if u],
|
|
95
|
+
raw=payload,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_inbound_event(event) -> List[InboundSms]:
|
|
100
|
+
"""Normalize an inbound two-way SMS delivery into InboundSms objects.
|
|
101
|
+
|
|
102
|
+
Accepts, in order of most to least wrapped:
|
|
103
|
+
|
|
104
|
+
- a Lambda SNS event: ``{"Records": [{"Sns": {"Message": "<json>"}}]}``
|
|
105
|
+
- a single SNS record: ``{"Sns": {"Message": "<json>"}}``
|
|
106
|
+
- the decoded payload dict (or its JSON string) as published by
|
|
107
|
+
AWS End User Messaging
|
|
108
|
+
|
|
109
|
+
Records that do not look like inbound messages are skipped with a warning
|
|
110
|
+
rather than raising — one malformed record in a batch must not lose the
|
|
111
|
+
others.
|
|
112
|
+
"""
|
|
113
|
+
if event is None:
|
|
114
|
+
return []
|
|
115
|
+
payload = _coerce_payload(event)
|
|
116
|
+
if payload is None:
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
records = payload.get("Records")
|
|
120
|
+
if not isinstance(records, list):
|
|
121
|
+
# Single record or bare payload.
|
|
122
|
+
if "Sns" in payload:
|
|
123
|
+
records = [payload]
|
|
124
|
+
else:
|
|
125
|
+
inbound = _payload_to_inbound(payload)
|
|
126
|
+
return [inbound] if inbound else []
|
|
127
|
+
|
|
128
|
+
messages = []
|
|
129
|
+
for record in records:
|
|
130
|
+
if not isinstance(record, dict):
|
|
131
|
+
continue
|
|
132
|
+
sns = record.get("Sns")
|
|
133
|
+
message = _coerce_payload(sns.get("Message")) if isinstance(sns, dict) else None
|
|
134
|
+
inbound = _payload_to_inbound(message) if message else None
|
|
135
|
+
if inbound:
|
|
136
|
+
messages.append(inbound)
|
|
137
|
+
else:
|
|
138
|
+
logger.warning(
|
|
139
|
+
"parse_inbound_event: skipping record that is not an inbound "
|
|
140
|
+
"SMS payload",
|
|
141
|
+
extra={"record_keys": sorted(record.keys())},
|
|
142
|
+
)
|
|
143
|
+
return messages
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SmsClient:
|
|
147
|
+
"""Outbound SMS/MMS via AWS End User Messaging.
|
|
148
|
+
|
|
149
|
+
``origination_identity`` may be a phone number in E.164 form, a phone
|
|
150
|
+
number ARN, a pool ARN, or a sender ID — whatever the account has
|
|
151
|
+
provisioned. A per-call ``origination_identity`` overrides the default.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
origination_identity: Optional[str] = None,
|
|
157
|
+
configuration_set: Optional[str] = None,
|
|
158
|
+
client=None,
|
|
159
|
+
region: Optional[str] = None,
|
|
160
|
+
):
|
|
161
|
+
self.origination_identity = origination_identity
|
|
162
|
+
self.configuration_set = configuration_set
|
|
163
|
+
self.region = region
|
|
164
|
+
self._client = client
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def client(self):
|
|
168
|
+
if self._client is None:
|
|
169
|
+
import boto3
|
|
170
|
+
|
|
171
|
+
kwargs = {"region_name": self.region} if self.region else {}
|
|
172
|
+
self._client = boto3.client("pinpoint-sms-voice-v2", **kwargs)
|
|
173
|
+
return self._client
|
|
174
|
+
|
|
175
|
+
def _resolve(self, destination, origination_identity):
|
|
176
|
+
destination = str(destination or "").strip()
|
|
177
|
+
origination = str(origination_identity or self.origination_identity or "").strip()
|
|
178
|
+
if not destination:
|
|
179
|
+
raise ValueError("SmsClient: destination phone number is required")
|
|
180
|
+
if not origination:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
"SmsClient: origination_identity is required (constructor "
|
|
183
|
+
"default or per-call argument)"
|
|
184
|
+
)
|
|
185
|
+
return destination, origination
|
|
186
|
+
|
|
187
|
+
def send_text(
|
|
188
|
+
self,
|
|
189
|
+
destination: str,
|
|
190
|
+
body: str,
|
|
191
|
+
origination_identity: Optional[str] = None,
|
|
192
|
+
message_type: str = "TRANSACTIONAL",
|
|
193
|
+
configuration_set: Optional[str] = None,
|
|
194
|
+
context: Optional[Dict[str, str]] = None,
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Send an SMS; returns the AWS message id."""
|
|
197
|
+
destination, origination = self._resolve(destination, origination_identity)
|
|
198
|
+
params = {
|
|
199
|
+
"DestinationPhoneNumber": destination,
|
|
200
|
+
"OriginationIdentity": origination,
|
|
201
|
+
"MessageBody": str(body or ""),
|
|
202
|
+
"MessageType": message_type,
|
|
203
|
+
}
|
|
204
|
+
configuration_set = configuration_set or self.configuration_set
|
|
205
|
+
if configuration_set:
|
|
206
|
+
params["ConfigurationSetName"] = configuration_set
|
|
207
|
+
if context:
|
|
208
|
+
params["Context"] = context
|
|
209
|
+
response = self.client.send_text_message(**params)
|
|
210
|
+
return response.get("MessageId", "")
|
|
211
|
+
|
|
212
|
+
def send_media(
|
|
213
|
+
self,
|
|
214
|
+
destination: str,
|
|
215
|
+
body: Optional[str] = None,
|
|
216
|
+
media_urls: Optional[List[str]] = None,
|
|
217
|
+
origination_identity: Optional[str] = None,
|
|
218
|
+
configuration_set: Optional[str] = None,
|
|
219
|
+
context: Optional[Dict[str, str]] = None,
|
|
220
|
+
) -> str:
|
|
221
|
+
"""Send an MMS (``media_urls`` are s3:// URIs); returns the message id."""
|
|
222
|
+
destination, origination = self._resolve(destination, origination_identity)
|
|
223
|
+
params = {
|
|
224
|
+
"DestinationPhoneNumber": destination,
|
|
225
|
+
"OriginationIdentity": origination,
|
|
226
|
+
}
|
|
227
|
+
if body:
|
|
228
|
+
params["MessageBody"] = str(body)
|
|
229
|
+
if media_urls:
|
|
230
|
+
params["MediaUrls"] = [str(u) for u in media_urls]
|
|
231
|
+
configuration_set = configuration_set or self.configuration_set
|
|
232
|
+
if configuration_set:
|
|
233
|
+
params["ConfigurationSetName"] = configuration_set
|
|
234
|
+
if context:
|
|
235
|
+
params["Context"] = context
|
|
236
|
+
response = self.client.send_media_message(**params)
|
|
237
|
+
return response.get("MessageId", "")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Unit tests for velocity.aws.sms (AWS End User Messaging helpers)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from velocity.aws.sms import InboundSms, SmsClient, parse_inbound_event
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _payload(**overrides):
|
|
10
|
+
payload = {
|
|
11
|
+
"originationNumber": "+14255550182",
|
|
12
|
+
"destinationNumber": "+12125550101",
|
|
13
|
+
"messageKeyword": "KEYWORD_JOIN",
|
|
14
|
+
"messageBody": "receipt 42.50 office supplies",
|
|
15
|
+
"inboundMessageId": "cae173d2-66b9-564c-8309-21f858e9fb84",
|
|
16
|
+
"previousPublishedMessageId": "prev-id",
|
|
17
|
+
}
|
|
18
|
+
payload.update(overrides)
|
|
19
|
+
return payload
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sns_event(*payloads):
|
|
23
|
+
return {
|
|
24
|
+
"Records": [
|
|
25
|
+
{
|
|
26
|
+
"EventSource": "aws:sns",
|
|
27
|
+
"Sns": {"Message": json.dumps(p)},
|
|
28
|
+
}
|
|
29
|
+
for p in payloads
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestParseInboundEvent(unittest.TestCase):
|
|
35
|
+
def test_full_lambda_sns_event(self):
|
|
36
|
+
event = _sns_event(_payload())
|
|
37
|
+
messages = parse_inbound_event(event)
|
|
38
|
+
self.assertEqual(len(messages), 1)
|
|
39
|
+
msg = messages[0]
|
|
40
|
+
self.assertEqual(msg.origination_number, "+14255550182")
|
|
41
|
+
self.assertEqual(msg.destination_number, "+12125550101")
|
|
42
|
+
self.assertEqual(msg.body, "receipt 42.50 office supplies")
|
|
43
|
+
self.assertEqual(msg.message_id, "cae173d2-66b9-564c-8309-21f858e9fb84")
|
|
44
|
+
self.assertEqual(msg.keyword, "KEYWORD_JOIN")
|
|
45
|
+
self.assertEqual(msg.media_urls, [])
|
|
46
|
+
self.assertEqual(msg.raw["previousPublishedMessageId"], "prev-id")
|
|
47
|
+
|
|
48
|
+
def test_multiple_records_including_mms(self):
|
|
49
|
+
event = _sns_event(
|
|
50
|
+
_payload(),
|
|
51
|
+
_payload(
|
|
52
|
+
inboundMessageId="mms-1",
|
|
53
|
+
mediaUrls=["s3://bucket/media/one.jpg", "s3://bucket/media/two.jpg"],
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
messages = parse_inbound_event(event)
|
|
57
|
+
self.assertEqual(len(messages), 2)
|
|
58
|
+
self.assertEqual(messages[1].media_urls, [
|
|
59
|
+
"s3://bucket/media/one.jpg",
|
|
60
|
+
"s3://bucket/media/two.jpg",
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
def test_bare_payload_dict(self):
|
|
64
|
+
messages = parse_inbound_event(_payload())
|
|
65
|
+
self.assertEqual(len(messages), 1)
|
|
66
|
+
self.assertEqual(messages[0].origination_number, "+14255550182")
|
|
67
|
+
|
|
68
|
+
def test_payload_json_string(self):
|
|
69
|
+
messages = parse_inbound_event(json.dumps(_payload()))
|
|
70
|
+
self.assertEqual(len(messages), 1)
|
|
71
|
+
|
|
72
|
+
def test_single_sns_record(self):
|
|
73
|
+
messages = parse_inbound_event({"Sns": {"Message": json.dumps(_payload())}})
|
|
74
|
+
self.assertEqual(len(messages), 1)
|
|
75
|
+
|
|
76
|
+
def test_malformed_record_skipped_others_kept(self):
|
|
77
|
+
event = _sns_event(_payload())
|
|
78
|
+
event["Records"].insert(0, {"Sns": {"Message": "not json"}})
|
|
79
|
+
event["Records"].insert(0, "not a dict")
|
|
80
|
+
messages = parse_inbound_event(event)
|
|
81
|
+
self.assertEqual(len(messages), 1)
|
|
82
|
+
|
|
83
|
+
def test_non_inbound_payload_returns_empty(self):
|
|
84
|
+
# A delivery-status style event has no originationNumber.
|
|
85
|
+
self.assertEqual(parse_inbound_event({"eventType": "TEXT_DELIVERED"}), [])
|
|
86
|
+
self.assertEqual(parse_inbound_event(None), [])
|
|
87
|
+
self.assertEqual(parse_inbound_event("not json"), [])
|
|
88
|
+
self.assertEqual(parse_inbound_event(42), [])
|
|
89
|
+
|
|
90
|
+
def test_media_urls_string_coerced_to_list(self):
|
|
91
|
+
messages = parse_inbound_event(_payload(mediaUrls="s3://bucket/only.jpg"))
|
|
92
|
+
self.assertEqual(messages[0].media_urls, ["s3://bucket/only.jpg"])
|
|
93
|
+
|
|
94
|
+
def test_first_word_and_remainder(self):
|
|
95
|
+
msg = InboundSms(body="Receipt 42.50 office supplies")
|
|
96
|
+
self.assertEqual(msg.first_word, "receipt")
|
|
97
|
+
self.assertEqual(msg.remainder, "42.50 office supplies")
|
|
98
|
+
self.assertEqual(InboundSms(body="").first_word, "")
|
|
99
|
+
self.assertEqual(InboundSms(body="help").remainder, "")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FakeBotoClient:
|
|
103
|
+
def __init__(self):
|
|
104
|
+
self.text_calls = []
|
|
105
|
+
self.media_calls = []
|
|
106
|
+
|
|
107
|
+
def send_text_message(self, **kwargs):
|
|
108
|
+
self.text_calls.append(kwargs)
|
|
109
|
+
return {"MessageId": "text-msg-id"}
|
|
110
|
+
|
|
111
|
+
def send_media_message(self, **kwargs):
|
|
112
|
+
self.media_calls.append(kwargs)
|
|
113
|
+
return {"MessageId": "media-msg-id"}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestSmsClient(unittest.TestCase):
|
|
117
|
+
def setUp(self):
|
|
118
|
+
self.boto = FakeBotoClient()
|
|
119
|
+
self.client = SmsClient(
|
|
120
|
+
origination_identity="+18885550100",
|
|
121
|
+
configuration_set="default-set",
|
|
122
|
+
client=self.boto,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def test_send_text_uses_defaults(self):
|
|
126
|
+
message_id = self.client.send_text("+14255550182", "hello")
|
|
127
|
+
self.assertEqual(message_id, "text-msg-id")
|
|
128
|
+
call = self.boto.text_calls[0]
|
|
129
|
+
self.assertEqual(call["DestinationPhoneNumber"], "+14255550182")
|
|
130
|
+
self.assertEqual(call["OriginationIdentity"], "+18885550100")
|
|
131
|
+
self.assertEqual(call["MessageBody"], "hello")
|
|
132
|
+
self.assertEqual(call["MessageType"], "TRANSACTIONAL")
|
|
133
|
+
self.assertEqual(call["ConfigurationSetName"], "default-set")
|
|
134
|
+
self.assertNotIn("Context", call)
|
|
135
|
+
|
|
136
|
+
def test_send_text_overrides(self):
|
|
137
|
+
self.client.send_text(
|
|
138
|
+
"+14255550182",
|
|
139
|
+
"hello",
|
|
140
|
+
origination_identity="+18885550999",
|
|
141
|
+
configuration_set="other-set",
|
|
142
|
+
context={"job_id": "abc"},
|
|
143
|
+
)
|
|
144
|
+
call = self.boto.text_calls[0]
|
|
145
|
+
self.assertEqual(call["OriginationIdentity"], "+18885550999")
|
|
146
|
+
self.assertEqual(call["ConfigurationSetName"], "other-set")
|
|
147
|
+
self.assertEqual(call["Context"], {"job_id": "abc"})
|
|
148
|
+
|
|
149
|
+
def test_send_text_requires_destination_and_origination(self):
|
|
150
|
+
with self.assertRaises(ValueError):
|
|
151
|
+
self.client.send_text("", "hello")
|
|
152
|
+
bare = SmsClient(client=self.boto)
|
|
153
|
+
with self.assertRaises(ValueError):
|
|
154
|
+
bare.send_text("+14255550182", "hello")
|
|
155
|
+
|
|
156
|
+
def test_send_media(self):
|
|
157
|
+
message_id = self.client.send_media(
|
|
158
|
+
"+14255550182",
|
|
159
|
+
body="see attached",
|
|
160
|
+
media_urls=["s3://bucket/receipt.jpg"],
|
|
161
|
+
)
|
|
162
|
+
self.assertEqual(message_id, "media-msg-id")
|
|
163
|
+
call = self.boto.media_calls[0]
|
|
164
|
+
self.assertEqual(call["MediaUrls"], ["s3://bucket/receipt.jpg"])
|
|
165
|
+
self.assertEqual(call["MessageBody"], "see attached")
|
|
166
|
+
|
|
167
|
+
def test_send_media_body_optional(self):
|
|
168
|
+
self.client.send_media("+14255550182", media_urls=["s3://bucket/a.jpg"])
|
|
169
|
+
self.assertNotIn("MessageBody", self.boto.media_calls[0])
|
|
170
|
+
|
|
171
|
+
def test_no_configuration_set_when_unset(self):
|
|
172
|
+
bare = SmsClient(origination_identity="+18885550100", client=self.boto)
|
|
173
|
+
bare.send_text("+14255550182", "hi")
|
|
174
|
+
self.assertNotIn("ConfigurationSetName", self.boto.text_calls[0])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
unittest.main()
|