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.
Files changed (263) hide show
  1. {velocity_python-0.1.94 → velocity_python-0.1.98}/PKG-INFO +2 -2
  2. {velocity_python-0.1.94 → velocity_python-0.1.98}/README.md +1 -1
  3. {velocity_python-0.1.94 → velocity_python-0.1.98}/pyproject.toml +1 -1
  4. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/__init__.py +1 -1
  5. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/web_handler.py +17 -3
  6. velocity_python-0.1.98/src/velocity/aws/sms.py +237 -0
  7. velocity_python-0.1.98/src/velocity/aws/tests/test_sms.py +178 -0
  8. velocity_python-0.1.98/src/velocity/misc/mail.py +453 -0
  9. velocity_python-0.1.98/src/velocity/misc/tests/test_mail_send.py +365 -0
  10. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/PKG-INFO +2 -2
  11. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/SOURCES.txt +3 -0
  12. velocity_python-0.1.94/src/velocity/misc/mail.py +0 -86
  13. {velocity_python-0.1.94 → velocity_python-0.1.98}/LICENSE +0 -0
  14. {velocity_python-0.1.94 → velocity_python-0.1.98}/setup.cfg +0 -0
  15. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/__init__.py +0 -0
  16. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/amplify.py +0 -0
  17. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/amplify_build.py +0 -0
  18. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/__init__.py +0 -0
  19. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/backfill.py +0 -0
  20. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/indexing.py +0 -0
  21. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/references.py +0 -0
  22. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/service.py +0 -0
  23. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/assets/usage_index.py +0 -0
  24. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/__init__.py +0 -0
  25. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/config.py +0 -0
  26. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/operations.py +0 -0
  27. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/policies.py +0 -0
  28. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/reconcile.py +0 -0
  29. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/deploy/targets.py +0 -0
  30. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/dirty_pipeline.py +0 -0
  31. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/__init__.py +0 -0
  32. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/base_handler.py +0 -0
  33. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/context.py +0 -0
  34. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/context_factory.py +0 -0
  35. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/exceptions.py +0 -0
  36. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  37. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/masquerade.py +0 -0
  38. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  39. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  40. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/mixins/job_handler.py +0 -0
  41. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/perf.py +0 -0
  42. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/response.py +0 -0
  43. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  44. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/job_results.py +0 -0
  45. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/s3.py +0 -0
  46. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/ssm_config.py +0 -0
  47. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/__init__.py +0 -0
  48. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_amplify_build_policy_size.py +0 -0
  49. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_amplify_build_shim.py +0 -0
  50. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  51. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_deploy_policies.py +0 -0
  52. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_deploy_reconcile.py +0 -0
  53. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  54. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/aws/tests/test_response.py +0 -0
  55. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/__init__.py +0 -0
  56. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/__init__.py +0 -0
  57. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/async_support.py +0 -0
  58. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/column.py +0 -0
  59. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/database.py +0 -0
  60. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/decorators.py +0 -0
  61. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/engine.py +0 -0
  62. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/jsonproxy.py +0 -0
  63. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/result.py +0 -0
  64. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/row.py +0 -0
  65. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/sequence.py +0 -0
  66. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/table.py +0 -0
  67. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/transaction.py +0 -0
  68. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/core/view.py +0 -0
  69. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/exceptions.py +0 -0
  70. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/maintenance.py +0 -0
  71. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/migrations.py +0 -0
  72. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/__init__.py +0 -0
  73. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/__init__.py +0 -0
  74. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/initializer.py +0 -0
  75. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/operators.py +0 -0
  76. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/sql.py +0 -0
  77. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/base/types.py +0 -0
  78. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/__init__.py +0 -0
  79. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/operators.py +0 -0
  80. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/reserved.py +0 -0
  81. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/sql.py +0 -0
  82. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/mysql/types.py +0 -0
  83. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/__init__.py +0 -0
  84. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/operators.py +0 -0
  85. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/reserved.py +0 -0
  86. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/sql.py +0 -0
  87. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/postgres/types.py +0 -0
  88. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  89. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/operators.py +0 -0
  90. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  91. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/sql.py +0 -0
  92. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlite/types.py +0 -0
  93. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  94. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  95. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  96. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  97. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/sqlserver/types.py +0 -0
  98. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/servers/tablehelper.py +0 -0
  99. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/__init__.py +0 -0
  100. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/common_db_test.py +0 -0
  101. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/__init__.py +0 -0
  102. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/common.py +0 -0
  103. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/conftest.py +0 -0
  104. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_column.py +0 -0
  105. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  106. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_database.py +0 -0
  107. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  108. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  109. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  110. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_result.py +0 -0
  111. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_row.py +0 -0
  112. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  113. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  114. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  115. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  116. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  117. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_table.py +0 -0
  118. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  119. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  120. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/__init__.py +0 -0
  121. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/common.py +0 -0
  122. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  123. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  124. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  125. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_db_utils.py +0 -0
  126. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_postgres.py +0 -0
  127. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  128. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  129. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_result_caching.py +0 -0
  130. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  131. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  132. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  133. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  134. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_sql_builder.py +0 -0
  135. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_tablehelper.py +0 -0
  136. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/tests/test_view_helper.py +0 -0
  137. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/db/utils.py +0 -0
  138. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/logging.py +0 -0
  139. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/__init__.py +0 -0
  140. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/conv/__init__.py +0 -0
  141. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/conv/iconv.py +0 -0
  142. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/conv/oconv.py +0 -0
  143. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/db.py +0 -0
  144. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/export.py +0 -0
  145. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/format.py +0 -0
  146. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/merge.py +0 -0
  147. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/pdf.py +0 -0
  148. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/__init__.py +0 -0
  149. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_db.py +0 -0
  150. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_fix.py +0 -0
  151. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_format.py +0 -0
  152. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_iconv.py +0 -0
  153. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_merge.py +0 -0
  154. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_oconv.py +0 -0
  155. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_original_error.py +0 -0
  156. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tests/test_timer.py +0 -0
  157. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/timer.py +0 -0
  158. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/misc/tools.py +0 -0
  159. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/__init__.py +0 -0
  160. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/__init__.py +0 -0
  161. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/capabilities.py +0 -0
  162. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/store.py +0 -0
  163. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/core/where.py +0 -0
  164. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/exceptions.py +0 -0
  165. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/__init__.py +0 -0
  166. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/base/__init__.py +0 -0
  167. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/cassandra/__init__.py +0 -0
  168. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/cassandra/translation.py +0 -0
  169. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/couchbase/__init__.py +0 -0
  170. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/couchbase/translation.py +0 -0
  171. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/dynamodb/__init__.py +0 -0
  172. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/dynamodb/translation.py +0 -0
  173. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/elasticsearch/__init__.py +0 -0
  174. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/elasticsearch/translation.py +0 -0
  175. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/memory/__init__.py +0 -0
  176. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/mongodb/__init__.py +0 -0
  177. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/mongodb/translation.py +0 -0
  178. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/nosql/servers/redis/__init__.py +0 -0
  179. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/__init__.py +0 -0
  180. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/authorizenet_adapter.py +0 -0
  181. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/authorizenet_mirror.py +0 -0
  182. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/base_adapter.py +0 -0
  183. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/braintree_adapter.py +0 -0
  184. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/braintree_mirror.py +0 -0
  185. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/charge_rules.py +0 -0
  186. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/stripe_adapter.py +0 -0
  187. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity/payment/stripe_mirror.py +0 -0
  188. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  189. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/entry_points.txt +0 -0
  190. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/requires.txt +0 -0
  191. {velocity_python-0.1.94 → velocity_python-0.1.98}/src/velocity_python.egg-info/top_level.txt +0 -0
  192. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_activity_tracking_aborted_tx.py +0 -0
  193. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_amplify_build.py +0 -0
  194. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_asset_indexing.py +0 -0
  195. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_asset_references.py +0 -0
  196. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_assets_service.py +0 -0
  197. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_async_support.py +0 -0
  198. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_batch_operations.py +0 -0
  199. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_column_tx_arg.py +0 -0
  200. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_concurrency_safety.py +0 -0
  201. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_connection_pool.py +0 -0
  202. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_connection_resilience.py +0 -0
  203. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_context_job_descriptions.py +0 -0
  204. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_db_credentials_ssm_cascade.py +0 -0
  205. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_db_maintenance.py +0 -0
  206. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_decorators.py +0 -0
  207. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_delete_hook_fk_blocked.py +0 -0
  208. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_dirty_pipeline_fast_path.py +0 -0
  209. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_email_processing.py +0 -0
  210. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_enqueue_send_failures.py +0 -0
  211. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_get_cognito_user_provider.py +0 -0
  212. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_http_handler_rollback.py +0 -0
  213. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_iconv_money_to_cents.py +0 -0
  214. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_identifier_injection_guard.py +0 -0
  215. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_job_handler.py +0 -0
  216. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_job_results.py +0 -0
  217. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_json_columns.py +0 -0
  218. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_jsonb_dict_adapter.py +0 -0
  219. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_lambda_handler.py +0 -0
  220. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_lambda_handler_auth.py +0 -0
  221. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_lambda_handler_masquerade.py +0 -0
  222. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_masquerade_grant.py +0 -0
  223. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_mixins_import.py +0 -0
  224. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_n_plus_one.py +0 -0
  225. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_dynamodb.py +0 -0
  226. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_memory.py +0 -0
  227. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_mongodb.py +0 -0
  228. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_redis.py +0 -0
  229. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_translation.py +0 -0
  230. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_nosql_where.py +0 -0
  231. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_observability.py +0 -0
  232. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_on_commit.py +0 -0
  233. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_authorizenet_adapter.py +0 -0
  234. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_braintree_adapter.py +0 -0
  235. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_braintree_mirror.py +0 -0
  236. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_mirror_empty_table.py +0 -0
  237. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_profile_sorting.py +0 -0
  238. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_payment_stripe_adapter.py +0 -0
  239. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_pdf.py +0 -0
  240. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_prepared_statements.py +0 -0
  241. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_psycopg3_upgrade.py +0 -0
  242. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_query_cache.py +0 -0
  243. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_restricted_direct_tables.py +0 -0
  244. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_retry_side_effect_guard.py +0 -0
  245. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_return_default_safety.py +0 -0
  246. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_row_batch_update.py +0 -0
  247. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_row_cache_staleness.py +0 -0
  248. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_schema_migrations.py +0 -0
  249. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_security_hardening.py +0 -0
  250. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_server_cursor.py +0 -0
  251. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_single_autocommit_safety.py +0 -0
  252. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_spreadsheet_functions.py +0 -0
  253. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_sqlite_backend.py +0 -0
  254. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_sqs_per_record_transactions.py +0 -0
  255. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_ssm_config.py +0 -0
  256. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_store_user_data.py +0 -0
  257. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  258. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_table_alter.py +0 -0
  259. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_transaction_class_wrapping.py +0 -0
  260. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_transaction_commit_and_ownership.py +0 -0
  261. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_transaction_edge_cases.py +0 -0
  262. {velocity_python-0.1.94 → velocity_python-0.1.98}/tests/test_where_clause_validation.py +0 -0
  263. {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.94
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.1.94"
7
+ version = "0.1.98"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.1.94"
1
+ __version__ = version = "0.1.98"
2
2
 
3
3
  import importlib as _importlib
4
4
 
@@ -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
- return getattr(helpers, "sendmail", None)
410
+ app_sendmail = getattr(helpers, "sendmail", None)
411
+ if app_sendmail is not None:
412
+ return app_sendmail
403
413
  except ImportError:
404
- return None
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()