velocity-python 0.0.198__tar.gz → 0.0.200__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 (147) hide show
  1. {velocity_python-0.0.198 → velocity_python-0.0.200}/PKG-INFO +1 -1
  2. {velocity_python-0.0.198 → velocity_python-0.0.200}/pyproject.toml +1 -1
  3. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/lambda_handler.py +92 -53
  5. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/mixins/web_handler.py +31 -4
  6. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/conv/iconv.py +25 -0
  7. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity_python.egg-info/PKG-INFO +1 -1
  8. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  9. velocity_python-0.0.200/tests/test_iconv_money_to_cents.py +20 -0
  10. velocity_python-0.0.200/tests/test_lambda_handler_auth.py +77 -0
  11. {velocity_python-0.0.198 → velocity_python-0.0.200}/LICENSE +0 -0
  12. {velocity_python-0.0.198 → velocity_python-0.0.200}/README.md +0 -0
  13. {velocity_python-0.0.198 → velocity_python-0.0.200}/setup.cfg +0 -0
  14. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/__init__.py +0 -0
  15. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/invoices.py +0 -0
  16. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/orders.py +0 -0
  17. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/payments.py +0 -0
  18. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/purchase_orders.py +0 -0
  19. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/tests/__init__.py +0 -0
  20. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/tests/test_email_processing.py +0 -0
  21. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  22. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  23. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/__init__.py +0 -0
  24. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/amplify.py +0 -0
  25. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/__init__.py +0 -0
  26. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/base_handler.py +0 -0
  27. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/context.py +0 -0
  28. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/context_factory.py +0 -0
  29. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/exceptions.py +0 -0
  30. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  31. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  32. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/perf.py +0 -0
  33. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/response.py +0 -0
  34. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  35. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/tests/__init__.py +0 -0
  36. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  37. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/aws/tests/test_response.py +0 -0
  38. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/__init__.py +0 -0
  39. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/__init__.py +0 -0
  40. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/column.py +0 -0
  41. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/database.py +0 -0
  42. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/decorators.py +0 -0
  43. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/engine.py +0 -0
  44. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/result.py +0 -0
  45. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/row.py +0 -0
  46. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/sequence.py +0 -0
  47. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/table.py +0 -0
  48. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/core/transaction.py +0 -0
  49. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/exceptions.py +0 -0
  50. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/__init__.py +0 -0
  51. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/base/__init__.py +0 -0
  52. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/base/initializer.py +0 -0
  53. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/base/operators.py +0 -0
  54. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/base/sql.py +0 -0
  55. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/base/types.py +0 -0
  56. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/mysql/__init__.py +0 -0
  57. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/mysql/operators.py +0 -0
  58. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/mysql/reserved.py +0 -0
  59. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/mysql/sql.py +0 -0
  60. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/mysql/types.py +0 -0
  61. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/postgres/__init__.py +0 -0
  62. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/postgres/operators.py +0 -0
  63. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/postgres/reserved.py +0 -0
  64. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/postgres/sql.py +0 -0
  65. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/postgres/types.py +0 -0
  66. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  67. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlite/operators.py +0 -0
  68. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  69. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlite/sql.py +0 -0
  70. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlite/types.py +0 -0
  71. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  72. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  73. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  74. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  75. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/sqlserver/types.py +0 -0
  76. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/servers/tablehelper.py +0 -0
  77. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/__init__.py +0 -0
  78. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/common_db_test.py +0 -0
  79. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/__init__.py +0 -0
  80. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/common.py +0 -0
  81. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_column.py +0 -0
  82. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  83. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_database.py +0 -0
  84. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  85. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  86. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  87. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_result.py +0 -0
  88. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_row.py +0 -0
  89. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  90. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  91. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  92. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  93. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  94. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_table.py +0 -0
  95. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  96. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  97. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/sql/__init__.py +0 -0
  98. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/sql/common.py +0 -0
  99. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  100. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  101. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  102. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_db_utils.py +0 -0
  103. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_postgres.py +0 -0
  104. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  105. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  106. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_result_caching.py +0 -0
  107. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  108. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  109. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  110. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  111. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_sql_builder.py +0 -0
  112. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/tests/test_tablehelper.py +0 -0
  113. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/db/utils.py +0 -0
  114. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/logging.py +0 -0
  115. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/__init__.py +0 -0
  116. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/conv/__init__.py +0 -0
  117. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/conv/oconv.py +0 -0
  118. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/db.py +0 -0
  119. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/export.py +0 -0
  120. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/format.py +0 -0
  121. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/mail.py +0 -0
  122. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/merge.py +0 -0
  123. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/__init__.py +0 -0
  124. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_db.py +0 -0
  125. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_fix.py +0 -0
  126. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_format.py +0 -0
  127. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_iconv.py +0 -0
  128. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_merge.py +0 -0
  129. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_oconv.py +0 -0
  130. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_original_error.py +0 -0
  131. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tests/test_timer.py +0 -0
  132. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/timer.py +0 -0
  133. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/misc/tools.py +0 -0
  134. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/payment/__init__.py +0 -0
  135. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/payment/base_adapter.py +0 -0
  136. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/payment/braintree_adapter.py +0 -0
  137. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/payment/router.py +0 -0
  138. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity/payment/stripe_adapter.py +0 -0
  139. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  140. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity_python.egg-info/requires.txt +0 -0
  141. {velocity_python-0.0.198 → velocity_python-0.0.200}/src/velocity_python.egg-info/top_level.txt +0 -0
  142. {velocity_python-0.0.198 → velocity_python-0.0.200}/tests/test_decorators.py +0 -0
  143. {velocity_python-0.0.198 → velocity_python-0.0.200}/tests/test_lambda_handler.py +0 -0
  144. {velocity_python-0.0.198 → velocity_python-0.0.200}/tests/test_mixins_import.py +0 -0
  145. {velocity_python-0.0.198 → velocity_python-0.0.200}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  146. {velocity_python-0.0.198 → velocity_python-0.0.200}/tests/test_table_alter.py +0 -0
  147. {velocity_python-0.0.198 → velocity_python-0.0.200}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.198
3
+ Version: 0.0.200
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.198"
7
+ version = "0.0.200"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.198"
1
+ __version__ = version = "0.0.200"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -18,6 +18,12 @@ logger = logging.getLogger(__name__)
18
18
 
19
19
  class LambdaHandler(BaseHandler):
20
20
  user_table: Optional[str] = None
21
+ # Auth behavior for HTTP-invoked lambdas.
22
+ # - required: must have Cognito identity and a DB user
23
+ # - optional: Cognito identity may be absent; DB user lookup is skipped unless present
24
+ # - none: no Cognito lookup; no DB user lookup
25
+ auth_mode: str = "required"
26
+ require_db_user: bool = True
21
27
  def __init__(
22
28
  self,
23
29
  aws_event,
@@ -42,18 +48,44 @@ class LambdaHandler(BaseHandler):
42
48
  logger.debug("starting LamdaHandler.beforeAction")
43
49
 
44
50
 
45
- context.perf.start("get_cognito_user")
46
- self.cognito_user = context.get_cognito_user(self.aws_event)
47
- context.perf.log("get_cognito_user")
48
51
  self.current_user = {}
49
52
 
50
- logger.debug("DEBUG: !!! cognito_user %s", self.cognito_user)
53
+ auth_mode = getattr(self, "auth_mode", "required") or "required"
54
+ require_db_user = bool(getattr(self, "require_db_user", True))
55
+
51
56
  session = context.session() or {}
57
+
58
+ public_actions = getattr(self, "public_actions", None)
52
59
  try:
53
- email_address = self.cognito_user["attributes"]["email"]
54
- session["email_address"] = email_address
60
+ current_action = context.action()
55
61
  except Exception:
56
- logger.warning("Unable to read email from Cognito user", exc_info=True)
62
+ current_action = None
63
+
64
+ if (
65
+ current_action
66
+ and isinstance(public_actions, (list, tuple, set))
67
+ and current_action in public_actions
68
+ ):
69
+ auth_mode = "none"
70
+ require_db_user = False
71
+
72
+ if auth_mode == "none":
73
+ self.cognito_user = None
74
+ else:
75
+ context.perf.start("get_cognito_user")
76
+ if auth_mode == "optional":
77
+ self.cognito_user = context.get_cognito_user_optional(self.aws_event)
78
+ else:
79
+ self.cognito_user = context.get_cognito_user(self.aws_event)
80
+ context.perf.log("get_cognito_user")
81
+
82
+ logger.debug("DEBUG: !!! cognito_user %s", self.cognito_user)
83
+ if isinstance(self.cognito_user, dict):
84
+ try:
85
+ email_address = self.cognito_user["attributes"]["email"]
86
+ session["email_address"] = email_address
87
+ except Exception:
88
+ logger.warning("Unable to read email from Cognito user", exc_info=True)
57
89
 
58
90
  logger.info(
59
91
  "Starting action",
@@ -65,27 +97,32 @@ class LambdaHandler(BaseHandler):
65
97
  },
66
98
  )
67
99
 
68
- if not self.user_table:
69
- logger.warning(
70
- "user_table not configured; skipping DB lookup for %s",
71
- session.get("email_address"),
72
- )
73
- raise Exception(
74
- "User table not configured; cannot validate user [Config]"
75
- )
76
- context.perf.start("user lookup")
77
- row = tx.table(self.user_table).find(
78
- {"email_address": session.get("email_address")}
79
- )
80
- context.perf.log("user lookup")
81
- if not row:
82
- raise Exception(
83
- "A valid user with permission is required to access this function [DB]"
100
+ if require_db_user:
101
+ if not session.get("email_address"):
102
+ raise Exception("A valid user is required to access this function [Auth]")
103
+
104
+ if not self.user_table:
105
+ logger.warning(
106
+ "user_table not configured; cannot validate user %s",
107
+ session.get("email_address"),
108
+ )
109
+ raise Exception("User table not configured; cannot validate user [Config]")
110
+
111
+ context.perf.start("user lookup")
112
+ row = tx.table(self.user_table).find(
113
+ {"email_address": session.get("email_address")}
84
114
  )
85
- self.current_user = row.to_dict()
115
+ context.perf.log("user lookup")
116
+ if not row:
117
+ raise Exception(
118
+ "A valid user with permission is required to access this function [DB]"
119
+ )
120
+ self.current_user = row.to_dict()
86
121
 
87
- temp = copy.deepcopy(context.postdata())
88
- temp["payload"].pop("cognito_user", None)
122
+ temp = copy.deepcopy(context.postdata() or {})
123
+ payload = temp.get("payload")
124
+ if isinstance(payload, dict):
125
+ payload.pop("cognito_user", None)
89
126
  logger.debug(
90
127
  "Events.OnAction %s",
91
128
  temp.get("action"),
@@ -123,9 +160,11 @@ class LambdaHandler(BaseHandler):
123
160
 
124
161
  logger.debug("starting BackOfficeEvents.onError")
125
162
 
126
- temp = copy.deepcopy(context.postdata())
163
+ temp = copy.deepcopy(context.postdata() or {})
127
164
  logger.debug("starting BackOfficeEvents.log")
128
- temp["payload"].pop("cognito_user", None)
165
+ payload = temp.get("payload")
166
+ if isinstance(payload, dict):
167
+ payload.pop("cognito_user", None)
129
168
  logger.error(
130
169
  "Events.OnError %s",
131
170
  temp.get("action"),
@@ -189,10 +228,16 @@ class LambdaHandler(BaseHandler):
189
228
  sanitized_payload["sys_modified_by"] = session.get("email_address") or "system"
190
229
 
191
230
  if not email:
192
- raise Exception(f"Tracking email could not be resolved for tracking.")
231
+ # Tracking should be best-effort and never break user flows.
232
+ logger.warning("Tracking email could not be resolved; skipping tracking write")
233
+ return
193
234
 
194
235
  table_name = context_obj.get_tracking_table(email)
195
236
 
237
+ if not table_name:
238
+ logger.warning("Tracking table could not be resolved; skipping tracking write")
239
+ return
240
+
196
241
  try:
197
242
  tx.table(table_name).insert(sanitized_payload)
198
243
  except Exception as exc: # pragma: no cover - best effort logging
@@ -200,30 +245,24 @@ class LambdaHandler(BaseHandler):
200
245
 
201
246
  def validate(self, user_required=True):
202
247
  session = self.context.session() if self.context else {}
203
- if user_required:
204
- try:
205
- attrs = self.cognito_user["attributes"]
206
- assert attrs
207
- assert attrs["email"]
208
- assert attrs["sub"]
209
- assert session["sub"]
210
- assert session["sub"] == attrs["sub"]
211
- except:
212
- raise Exception("User Authentication Error [Cognito]")
213
- else:
214
- try:
215
- if not session["sub"]:
216
- # User is not signed in. If user_required
217
- # is false, then simply return
218
- return
219
- attrs = self.cognito_user["attributes"]
220
- assert attrs
221
- assert attrs["email"]
222
- assert attrs["sub"]
223
- assert session["sub"]
224
- assert session["sub"] == attrs["sub"]
225
- except:
226
- raise Exception("User Authentication Error [Cognito]")
248
+ session_sub = session.get("sub")
249
+ cognito_user = getattr(self, "cognito_user", None)
250
+
251
+ if not user_required:
252
+ # Optional-auth flows: if there's no user in session, treat as anonymous.
253
+ if not session_sub:
254
+ return
255
+
256
+ try:
257
+ if not isinstance(cognito_user, dict):
258
+ raise Exception("Missing Cognito user")
259
+ attrs = cognito_user.get("attributes") or {}
260
+ email = attrs.get("email")
261
+ sub = attrs.get("sub")
262
+ if not email or not sub or not session_sub or session_sub != sub:
263
+ raise Exception("User mismatch")
264
+ except Exception:
265
+ raise Exception("User Authentication Error [Cognito]")
227
266
 
228
267
  def _summarize_postdata(self, postdata):
229
268
  """Extract key fields from postdata for logging without storing entire payload"""
@@ -484,7 +484,7 @@ class ButtonHandler:
484
484
  module_name = None # Override in subclass (e.g., 'buttons')
485
485
 
486
486
  @classmethod
487
- def _get_button_module(cls, tx, handler):
487
+ def _get_button_module(cls, *args, **kwargs):
488
488
  """
489
489
  Load button-specific handler module if it exists.
490
490
 
@@ -495,6 +495,19 @@ class ButtonHandler:
495
495
  The loaded module or None if not found
496
496
 
497
497
  """
498
+ # NOTE: This helper is called from Lambda handlers that are often wrapped by
499
+ # velocity's transaction decorators. Those wrappers can inject extra positional
500
+ # arguments (including duplicate tx objects). To stay robust across versions,
501
+ # we accept *args and locate the handler name from strings.
502
+
503
+ handler = kwargs.get("handler")
504
+ if not handler:
505
+ string_args = [value for value in args if isinstance(value, str)]
506
+ handler = string_args[-1] if string_args else None
507
+
508
+ if not handler:
509
+ raise AlertError("Button handler name missing in request payload")
510
+
498
511
  if not cls.module_name:
499
512
  raise AlertError("ButtonHandler.module_name not set in subclass. This is a configuration issue that needs to be handled by the developer.")
500
513
  try:
@@ -503,7 +516,7 @@ class ButtonHandler:
503
516
  raise AlertError(f"Unable to import ButtonHandler module `{handler}`. Please ensure the code has been deployed.")
504
517
 
505
518
  @classmethod
506
- def _get_button_function(cls, tx, handler, action):
519
+ def _get_button_function(cls, *args, **kwargs):
507
520
  """
508
521
  Get a specific function from a button handler module.
509
522
 
@@ -514,7 +527,21 @@ class ButtonHandler:
514
527
  Returns:
515
528
  The function object or None if not found
516
529
  """
517
- module = cls._get_button_module(tx, handler)
530
+ handler = kwargs.get("handler")
531
+ action = kwargs.get("action")
532
+
533
+ if not handler or not action:
534
+ string_args = [value for value in args if isinstance(value, str)]
535
+ if len(string_args) >= 2:
536
+ handler = handler or string_args[-2]
537
+ action = action or string_args[-1]
538
+
539
+ if not handler:
540
+ raise AlertError("Button handler name missing in request payload")
541
+ if not action:
542
+ raise AlertError("Button action missing in request payload")
543
+
544
+ module = cls._get_button_module(handler=handler)
518
545
 
519
546
  # Convert action name: "refresh-data" -> "RefreshData"
520
547
  func_name = action.replace("-", " ").title().replace(" ", "")
@@ -558,7 +585,7 @@ class ButtonHandler:
558
585
  action = dataset["action"]
559
586
 
560
587
  # Resolve the function using the helper
561
- func = self._get_button_function(tx, handler, action)
588
+ func = self.__class__._get_button_function(tx, handler, action)
562
589
 
563
590
  # Execute the button handler
564
591
  # Don't return its result to avoid triggering base handler warnings
@@ -229,6 +229,31 @@ def money(data: str) -> Optional[Decimal]:
229
229
  return None
230
230
 
231
231
 
232
+ def money_to_cents(value) -> int:
233
+ """Convert a dollars-like value to integer cents.
234
+
235
+ This is intentionally permissive and mirrors common app-layer behavior:
236
+ - None/"" => 0
237
+ - int/float/Decimal/str => dollars * 100, truncated toward 0
238
+
239
+ Notes:
240
+ - If you already have cents, do not pass them here.
241
+ """
242
+ if value is None:
243
+ return 0
244
+ if isinstance(value, int):
245
+ # Ambiguous (could be dollars or cents). Many apps pass dollars.
246
+ # Treat as dollars to preserve historical behavior.
247
+ return int(Decimal(str(value)) * 100)
248
+ if isinstance(value, (float, Decimal)):
249
+ return int(Decimal(str(value)) * 100)
250
+
251
+ raw = str(value).strip()
252
+ if raw == "":
253
+ return 0
254
+ return int(Decimal(raw) * 100)
255
+
256
+
232
257
  def round_to(
233
258
  precision: int, data: Optional[Union[str, float, Decimal]] = None
234
259
  ) -> Union[Decimal, Callable[[Union[str, float, Decimal]], Optional[Decimal]]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.198
3
+ Version: 0.0.200
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
@@ -136,7 +136,9 @@ src/velocity_python.egg-info/dependency_links.txt
136
136
  src/velocity_python.egg-info/requires.txt
137
137
  src/velocity_python.egg-info/top_level.txt
138
138
  tests/test_decorators.py
139
+ tests/test_iconv_money_to_cents.py
139
140
  tests/test_lambda_handler.py
141
+ tests/test_lambda_handler_auth.py
140
142
  tests/test_mixins_import.py
141
143
  tests/test_sys_modified_count_postgres_demo.py
142
144
  tests/test_table_alter.py
@@ -0,0 +1,20 @@
1
+ import decimal
2
+
3
+ from velocity.misc.conv import iconv
4
+
5
+
6
+ def test_money_to_cents_none_and_empty():
7
+ assert iconv.money_to_cents(None) == 0
8
+ assert iconv.money_to_cents("") == 0
9
+ assert iconv.money_to_cents(" ") == 0
10
+
11
+
12
+ def test_money_to_cents_numeric_types():
13
+ assert iconv.money_to_cents(10) == 1000
14
+ assert iconv.money_to_cents(10.5) == 1050
15
+ assert iconv.money_to_cents(decimal.Decimal("10.50")) == 1050
16
+
17
+
18
+ def test_money_to_cents_string_values():
19
+ assert iconv.money_to_cents("10") == 1000
20
+ assert iconv.money_to_cents("10.50") == 1050
@@ -0,0 +1,77 @@
1
+ import unittest
2
+
3
+ from velocity.aws.handlers.lambda_handler import LambdaHandler
4
+
5
+
6
+ class _Perf:
7
+ def start(self, *args, **kwargs):
8
+ return None
9
+
10
+ def log(self, *args, **kwargs):
11
+ return None
12
+
13
+
14
+ class _Context:
15
+ def __init__(self, action=None, postdata=None, session=None):
16
+ self._action = action
17
+ self._postdata = postdata
18
+ self._session = session if isinstance(session, dict) else {}
19
+ self.perf = _Perf()
20
+
21
+ def action(self):
22
+ return self._action
23
+
24
+ def args(self):
25
+ return {}
26
+
27
+ def postdata(self):
28
+ return self._postdata
29
+
30
+ def session(self):
31
+ return self._session
32
+
33
+ def get_cognito_user(self, aws_event): # pragma: no cover
34
+ raise AssertionError("get_cognito_user should not be called")
35
+
36
+ def get_cognito_user_optional(self, aws_event): # pragma: no cover
37
+ raise AssertionError("get_cognito_user_optional should not be called")
38
+
39
+
40
+ class _Handler(LambdaHandler):
41
+ def _enhanced_before_action(self, tx, context):
42
+ return False
43
+
44
+ def _enhanced_error_handler(self, tx, context, exc, tb):
45
+ return False
46
+
47
+
48
+ class TestLambdaHandlerAuthModes(unittest.TestCase):
49
+ def test_auth_mode_none_skips_cognito(self):
50
+ h = _Handler(aws_event={}, aws_context=type("C", (), {"aws_request_id": "rid"})())
51
+ h.auth_mode = "none"
52
+ h.require_db_user = False
53
+ h.beforeAction(tx=object(), context=_Context(action="anything", postdata={}))
54
+
55
+ def test_public_actions_bypass_auth_and_db_user(self):
56
+ h = _Handler(aws_event={}, aws_context=type("C", (), {"aws_request_id": "rid"})())
57
+ h.auth_mode = "required"
58
+ h.require_db_user = True
59
+ h.user_table = "users"
60
+ h.public_actions = ["public-action"]
61
+
62
+ # Should not call cognito, should not require session email
63
+ h.beforeAction(tx=object(), context=_Context(action="public-action", postdata={}))
64
+
65
+ def test_onerror_does_not_require_payload(self):
66
+ h = _Handler(aws_event={}, aws_context=type("C", (), {"aws_request_id": "rid"})())
67
+ h.onError(tx=object(), context=_Context(action="anything", postdata={}), exc="E", tb="TB")
68
+ h.onError(
69
+ tx=object(),
70
+ context=_Context(action="anything", postdata={"payload": "not-a-dict"}),
71
+ exc="E",
72
+ tb="TB",
73
+ )
74
+
75
+
76
+ if __name__ == "__main__":
77
+ unittest.main()