velocity-python 0.0.195__tar.gz → 0.0.197__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 (145) hide show
  1. {velocity_python-0.0.195 → velocity_python-0.0.197}/PKG-INFO +1 -1
  2. {velocity_python-0.0.195 → velocity_python-0.0.197}/pyproject.toml +1 -1
  3. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/base_handler.py +28 -27
  5. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/context.py +88 -19
  6. velocity_python-0.0.197/src/velocity/aws/handlers/context_factory.py +27 -0
  7. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/lambda_handler.py +48 -81
  8. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/mixins/web_handler.py +50 -49
  9. velocity_python-0.0.197/src/velocity/aws/handlers/perf.py +42 -0
  10. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/sqs_handler.py +12 -5
  11. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity_python.egg-info/PKG-INFO +1 -1
  12. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity_python.egg-info/SOURCES.txt +2 -0
  13. {velocity_python-0.0.195 → velocity_python-0.0.197}/LICENSE +0 -0
  14. {velocity_python-0.0.195 → velocity_python-0.0.197}/README.md +0 -0
  15. {velocity_python-0.0.195 → velocity_python-0.0.197}/setup.cfg +0 -0
  16. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/__init__.py +0 -0
  17. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/invoices.py +0 -0
  18. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/orders.py +0 -0
  19. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/payments.py +0 -0
  20. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/purchase_orders.py +0 -0
  21. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/tests/__init__.py +0 -0
  22. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/tests/test_email_processing.py +0 -0
  23. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  24. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  25. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/__init__.py +0 -0
  26. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/amplify.py +0 -0
  27. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/__init__.py +0 -0
  28. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/exceptions.py +0 -0
  29. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  30. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  31. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/handlers/response.py +0 -0
  32. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/tests/__init__.py +0 -0
  33. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  34. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/aws/tests/test_response.py +0 -0
  35. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/__init__.py +0 -0
  36. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/__init__.py +0 -0
  37. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/column.py +0 -0
  38. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/database.py +0 -0
  39. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/decorators.py +0 -0
  40. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/engine.py +0 -0
  41. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/result.py +0 -0
  42. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/row.py +0 -0
  43. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/sequence.py +0 -0
  44. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/table.py +0 -0
  45. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/core/transaction.py +0 -0
  46. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/exceptions.py +0 -0
  47. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/__init__.py +0 -0
  48. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/base/__init__.py +0 -0
  49. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/base/initializer.py +0 -0
  50. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/base/operators.py +0 -0
  51. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/base/sql.py +0 -0
  52. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/base/types.py +0 -0
  53. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/mysql/__init__.py +0 -0
  54. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/mysql/operators.py +0 -0
  55. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/mysql/reserved.py +0 -0
  56. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/mysql/sql.py +0 -0
  57. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/mysql/types.py +0 -0
  58. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/postgres/__init__.py +0 -0
  59. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/postgres/operators.py +0 -0
  60. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/postgres/reserved.py +0 -0
  61. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/postgres/sql.py +0 -0
  62. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/postgres/types.py +0 -0
  63. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  64. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlite/operators.py +0 -0
  65. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  66. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlite/sql.py +0 -0
  67. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlite/types.py +0 -0
  68. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  69. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  70. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  71. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  72. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/sqlserver/types.py +0 -0
  73. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/servers/tablehelper.py +0 -0
  74. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/__init__.py +0 -0
  75. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/common_db_test.py +0 -0
  76. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/__init__.py +0 -0
  77. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/common.py +0 -0
  78. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_column.py +0 -0
  79. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  80. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_database.py +0 -0
  81. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  82. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  83. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  84. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_result.py +0 -0
  85. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_row.py +0 -0
  86. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  87. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  88. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  89. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  90. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  91. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_table.py +0 -0
  92. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  93. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  94. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/sql/__init__.py +0 -0
  95. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/sql/common.py +0 -0
  96. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  97. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  98. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  99. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_db_utils.py +0 -0
  100. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_postgres.py +0 -0
  101. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  102. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  103. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_result_caching.py +0 -0
  104. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  105. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  106. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  107. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  108. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_sql_builder.py +0 -0
  109. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/tests/test_tablehelper.py +0 -0
  110. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/db/utils.py +0 -0
  111. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/logging.py +0 -0
  112. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/__init__.py +0 -0
  113. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/conv/__init__.py +0 -0
  114. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/conv/iconv.py +0 -0
  115. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/conv/oconv.py +0 -0
  116. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/db.py +0 -0
  117. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/export.py +0 -0
  118. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/format.py +0 -0
  119. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/mail.py +0 -0
  120. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/merge.py +0 -0
  121. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/__init__.py +0 -0
  122. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_db.py +0 -0
  123. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_fix.py +0 -0
  124. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_format.py +0 -0
  125. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_iconv.py +0 -0
  126. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_merge.py +0 -0
  127. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_oconv.py +0 -0
  128. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_original_error.py +0 -0
  129. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tests/test_timer.py +0 -0
  130. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/timer.py +0 -0
  131. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/misc/tools.py +0 -0
  132. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/payment/__init__.py +0 -0
  133. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/payment/base_adapter.py +0 -0
  134. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/payment/braintree_adapter.py +0 -0
  135. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/payment/router.py +0 -0
  136. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity/payment/stripe_adapter.py +0 -0
  137. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  138. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity_python.egg-info/requires.txt +0 -0
  139. {velocity_python-0.0.195 → velocity_python-0.0.197}/src/velocity_python.egg-info/top_level.txt +0 -0
  140. {velocity_python-0.0.195 → velocity_python-0.0.197}/tests/test_decorators.py +0 -0
  141. {velocity_python-0.0.195 → velocity_python-0.0.197}/tests/test_lambda_handler.py +0 -0
  142. {velocity_python-0.0.195 → velocity_python-0.0.197}/tests/test_mixins_import.py +0 -0
  143. {velocity_python-0.0.195 → velocity_python-0.0.197}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  144. {velocity_python-0.0.195 → velocity_python-0.0.197}/tests/test_table_alter.py +0 -0
  145. {velocity_python-0.0.195 → velocity_python-0.0.197}/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.195
3
+ Version: 0.0.197
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.195"
7
+ version = "0.0.197"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.195"
1
+ __version__ = version = "0.0.197"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -8,11 +8,11 @@ It includes common functionality shared between LambdaHandler and SqsHandler.
8
8
  import logging
9
9
  import os
10
10
  import sys
11
- import time
12
11
  import traceback
13
12
  from typing import Any, Dict, List, Optional
14
13
 
15
14
  from velocity.aws.handlers import context as VelocityContext
15
+ from velocity.aws.handlers.context_factory import ContextFactory
16
16
  logger = logging.getLogger(__name__)
17
17
 
18
18
 
@@ -28,6 +28,7 @@ class BaseHandler:
28
28
  self,
29
29
  aws_event: Dict[str, Any],
30
30
  aws_context: Any,
31
+ context_factory: Optional[ContextFactory] = None,
31
32
  context_class=VelocityContext.Context,
32
33
  ):
33
34
  """
@@ -42,10 +43,21 @@ class BaseHandler:
42
43
  self.aws_context = aws_context
43
44
  self.serve_action_default = True # Set to False to disable OnActionDefault
44
45
  self.skip_action = False # Set to True to skip all actions
45
- self.ContextClass = context_class
46
-
47
- # Configure SSL certificates for HTTPS requests
48
- self._update_lambda_ca_certificates()
46
+ if context_factory is None:
47
+ context_factory = ContextFactory(context_class=context_class)
48
+ self.context_factory = context_factory
49
+ self.context = None
50
+
51
+ def create_context(self, *, args, postdata, response, session):
52
+ self.context = self.context_factory.create(
53
+ aws_event=self.aws_event,
54
+ aws_context=self.aws_context,
55
+ args=args,
56
+ postdata=postdata,
57
+ response=response,
58
+ session=session,
59
+ )
60
+ return self.context
49
61
 
50
62
  def _update_lambda_ca_certificates(self):
51
63
  """Configure SSL certificates for HTTPS requests in Lambda environments."""
@@ -131,16 +143,14 @@ class BaseHandler:
131
143
  Returns:
132
144
  True if an action was executed, False otherwise
133
145
  """
134
- total_start = time.perf_counter()
146
+ local_context.perf.start("execute_actions total")
135
147
 
136
148
  # Execute beforeAction hook if available
137
149
  if hasattr(self, "beforeAction"):
138
- before_start = time.perf_counter()
150
+ local_context.perf.start("beforeAction")
139
151
  self.beforeAction(tx, local_context)
140
- before_elapsed = (time.perf_counter() - before_start) * 1000
141
- logger.info(
142
- "Timing: beforeAction %.2f ms",
143
- before_elapsed,
152
+ local_context.perf.log(
153
+ "beforeAction",
144
154
  extra={"action": getattr(local_context, "action", lambda: None)()},
145
155
  )
146
156
 
@@ -152,13 +162,10 @@ class BaseHandler:
152
162
  break
153
163
 
154
164
  if hasattr(self, action):
155
- action_start = time.perf_counter()
165
+ local_context.perf.start(action)
156
166
  result = getattr(self, action)(tx, local_context)
157
- action_elapsed = (time.perf_counter() - action_start) * 1000
158
- logger.info(
159
- "Timing: %s %.2f ms",
167
+ local_context.perf.log(
160
168
  action,
161
- action_elapsed,
162
169
  extra={"action": getattr(local_context, "action", lambda: None)()},
163
170
  )
164
171
  if result is not None:
@@ -179,8 +186,6 @@ class BaseHandler:
179
186
  diagnostic_info["dataset"] = local_context.dataset()
180
187
  except Exception as e:
181
188
  diagnostic_info["diagnostic_error"] = str(e)
182
- """
183
- Get the list of actions to execute.
184
189
  logger.warning(
185
190
  "Action %s returned an unexpected non-None result (%r).",
186
191
  action,
@@ -192,19 +197,15 @@ class BaseHandler:
192
197
 
193
198
  # Execute afterAction hook if available
194
199
  if hasattr(self, "afterAction"):
195
- after_start = time.perf_counter()
200
+ local_context.perf.start("afterAction")
196
201
  self.afterAction(tx, local_context)
197
- after_elapsed = (time.perf_counter() - after_start) * 1000
198
- logger.info(
199
- "Timing: afterAction %.2f ms",
200
- after_elapsed,
202
+ local_context.perf.log(
203
+ "afterAction",
201
204
  extra={"action": getattr(local_context, "action", lambda: None)()},
202
205
  )
203
206
 
204
- total_elapsed = (time.perf_counter() - total_start) * 1000
205
- logger.info(
206
- "Timing: execute_actions total %.2f ms",
207
- total_elapsed,
207
+ local_context.perf.log(
208
+ "execute_actions total",
208
209
  extra={"action": getattr(local_context, "action", lambda: None)()},
209
210
  )
210
211
 
@@ -8,6 +8,7 @@ from velocity.misc.format import to_json
8
8
  from velocity.misc.merge import deep_merge
9
9
  from datetime import datetime
10
10
  from velocity.aws.handlers.exceptions import AlertError
11
+ from velocity.aws.handlers.perf import PerfTimer
11
12
  from botocore.exceptions import ClientError
12
13
  import hashlib
13
14
  import velocity
@@ -24,16 +25,16 @@ logger = get_logger("velocity.aws.handlers.context")
24
25
  @engine.transaction
25
26
  class Context:
26
27
 
27
- def __init__(
28
- self, aws_event, aws_context, args, postdata, response, session, log=None
29
- ):
28
+ def __init__(self, aws_event, aws_context, args, postdata, response, session):
30
29
  self.__args = args
31
30
  self.__postdata = postdata
32
31
  self.__response = response
32
+ if session is None:
33
+ session = self._build_session(aws_event)
33
34
  self.__session = {} if session is None else session
34
35
  self.__aws_event = aws_event
35
36
  self.__aws_context = aws_context
36
- self.__log = log
37
+ self.perf = PerfTimer(False, logger)
37
38
  self._job_record_cache = {}
38
39
  self._job_cancelled_flag = False
39
40
  self._feature_flags_cache = None
@@ -81,13 +82,84 @@ class Context:
81
82
  def dataset(self):
82
83
  return self.payload().get("dataset", {})
83
84
 
84
- def log(self, message, function=None):
85
- if self.__log:
86
- return self.__log(message, function)
87
- if function:
88
- logger.info("%s: %s", function, message)
85
+ def update_postdata(self, postdata):
86
+ self.__postdata = postdata
87
+
88
+ def parse_postdata(self):
89
+ body = self.__aws_event.get("body")
90
+ postdata = {}
91
+ if isinstance(body, str) and len(body) > 0:
92
+ try:
93
+ postdata = json.loads(body)
94
+ except (json.JSONDecodeError, TypeError):
95
+ postdata = {"raw_body": body}
96
+ elif isinstance(body, dict):
97
+ postdata = body
98
+ elif isinstance(body, list) and len(body) > 0:
99
+ try:
100
+ new = "\n".join(body)
101
+ postdata = json.loads(new)
102
+ except (json.JSONDecodeError, TypeError):
103
+ postdata = {"raw_body": body}
104
+ return postdata
105
+
106
+ def _is_performance_timing_enabled(self, postdata=None) -> bool:
107
+ if postdata is None:
108
+ postdata = self.__postdata
109
+
110
+ if not isinstance(postdata, dict):
111
+ return False
112
+
113
+ def normalize(val):
114
+ if isinstance(val, bool):
115
+ return val
116
+ if val is None:
117
+ return False
118
+ return str(val).strip().lower() in {"true", "1", "yes", "y"}
119
+
120
+ if normalize(postdata.get("log_performance_timing")):
121
+ return True
122
+ payload = postdata.get("payload")
123
+ if isinstance(payload, dict) and normalize(payload.get("log_performance_timing")):
124
+ return True
125
+ return False
126
+
127
+ def configure_perf(self, postdata=None):
128
+ enabled = self._is_performance_timing_enabled(postdata=postdata)
129
+ self.perf.set_enabled(enabled)
130
+ return enabled
131
+
132
+ def _build_session(self, aws_event):
133
+ request_context = aws_event.get("requestContext") or {}
134
+ identity = request_context.get("identity") or {}
135
+ headers = aws_event.get("headers") or {}
136
+ auth = identity.get("cognitoAuthenticationProvider")
137
+ session = {
138
+ "authentication_provider": identity.get("cognitoAuthenticationProvider"),
139
+ "authentication_type": identity.get("cognitoAuthenticationType"),
140
+ "cognito_user": identity.get("user"),
141
+ "is_desktop": headers.get("CloudFront-Is-Desktop-Viewer") == "true",
142
+ "is_mobile": headers.get("CloudFront-Is-Mobile-Viewer") == "true",
143
+ "is_smart_tv": headers.get("CloudFront-Is-SmartTV-Viewer") == "true",
144
+ "is_tablet": headers.get("CloudFront-Is-Tablet-Viewer") == "true",
145
+ "origin": headers.get("origin"),
146
+ "path": aws_event.get("path"),
147
+ "referer": headers.get("Referer"),
148
+ "source_ip": identity.get("sourceIp"),
149
+ "user_agent": identity.get("userAgent"),
150
+ "sub": auth.split(":")[-1] if auth else None,
151
+ }
152
+ if session.get("is_mobile"):
153
+ session["device_type"] = "mobile"
154
+ elif session.get("is_desktop"):
155
+ session["device_type"] = "desktop"
156
+ elif session.get("is_tablet"):
157
+ session["device_type"] = "tablet"
158
+ elif session.get("is_smart_tv"):
159
+ session["device_type"] = "smart_tv"
89
160
  else:
90
- logger.info("%s", message)
161
+ session["device_type"] = "unknown"
162
+ return session
91
163
 
92
164
  def update_job(self, tx, data=None):
93
165
  """Update job status and message in aws_job_activity table.
@@ -478,7 +550,7 @@ class Context:
478
550
  return None
479
551
 
480
552
  def get_cognito_user(self, aws_event):
481
- overall_start = time.perf_counter()
553
+ self.perf.start("get_cognito_user total")
482
554
  provider = (
483
555
  aws_event.get("requestContext", {})
484
556
  .get("identity", {})
@@ -504,24 +576,22 @@ class Context:
504
576
  raise AlertError("Incomplete Cognito identity provider data") from None
505
577
 
506
578
  try:
507
- admin_start = time.perf_counter()
579
+ self.perf.start("cognito admin_get_user")
508
580
  response = cognito_client.admin_get_user(
509
581
  UserPoolId=user_pool_id,
510
582
  Username=user_sub,
511
583
  )
512
- admin_elapsed = (time.perf_counter() - admin_start) * 1000
513
- logger.info("Timing: cognito admin_get_user %.2f ms", admin_elapsed)
584
+ self.perf.log("cognito admin_get_user")
514
585
  resolved_username = response.get("Username")
515
586
  except cognito_client.exceptions.UserNotFoundException:
516
587
  try:
517
- list_start = time.perf_counter()
588
+ self.perf.start("cognito list_users")
518
589
  list_response = cognito_client.list_users(
519
590
  UserPoolId=user_pool_id,
520
591
  Filter=f'sub = "{user_sub}"',
521
592
  Limit=1,
522
593
  )
523
- list_elapsed = (time.perf_counter() - list_start) * 1000
524
- logger.info("Timing: cognito list_users %.2f ms", list_elapsed)
594
+ self.perf.log("cognito list_users")
525
595
  except ClientError as exc:
526
596
  message = exc.response.get("Error", {}).get("Message", "Unknown error")
527
597
  raise AlertError(
@@ -560,8 +630,7 @@ class Context:
560
630
  "sub": attributes.get("sub", user_sub),
561
631
  }
562
632
 
563
- overall_elapsed = (time.perf_counter() - overall_start) * 1000
564
- logger.info("Timing: get_cognito_user total %.2f ms", overall_elapsed)
633
+ self.perf.log("get_cognito_user total")
565
634
 
566
635
  return user
567
636
 
@@ -0,0 +1,27 @@
1
+ from typing import Any, Dict, Optional, Type
2
+
3
+ from velocity.aws.handlers import context as VelocityContext
4
+
5
+
6
+ class ContextFactory:
7
+ def __init__(self, context_class: Optional[Type[VelocityContext.Context]] = None):
8
+ self.context_class = context_class or VelocityContext.Context
9
+
10
+ def create(
11
+ self,
12
+ *,
13
+ aws_event: Dict[str, Any],
14
+ aws_context: Any,
15
+ args: Dict[str, Any],
16
+ postdata: Dict[str, Any],
17
+ response: Any,
18
+ session: Optional[Dict[str, Any]],
19
+ ) -> VelocityContext.Context:
20
+ return self.context_class(
21
+ aws_event=aws_event,
22
+ aws_context=aws_context,
23
+ args=args,
24
+ postdata=postdata,
25
+ response=response,
26
+ session=session,
27
+ )
@@ -4,9 +4,10 @@ import logging
4
4
  import os
5
5
  import pprint
6
6
  import time
7
- from typing import Optional, Type
7
+ from typing import Optional
8
8
 
9
9
  from velocity.aws.handlers.base_handler import BaseHandler
10
+ from velocity.aws.handlers.context_factory import ContextFactory
10
11
  from velocity.aws.handlers.response import Response
11
12
  from velocity.logging import configure_logging
12
13
  from . import context
@@ -21,44 +22,16 @@ class LambdaHandler(BaseHandler):
21
22
  self,
22
23
  aws_event,
23
24
  aws_context,
24
- context_class: Type[context.Context] = context.Context
25
+ context_factory: Optional[ContextFactory] = None,
26
+ context_class: type = context.Context,
25
27
  ):
26
28
  self.start = time.time()
27
- super().__init__(aws_event, aws_context, context_class)
28
- self.session = self.create_session(aws_event, aws_context)
29
-
30
- def create_session(self, aws_event, aws_context):
31
- # LambdaHandler-specific initialization
32
- requestContext = aws_event.get("requestContext") or {}
33
- identity = requestContext.get("identity") or {}
34
- headers = aws_event.get("headers") or {}
35
- auth = identity.get("cognitoAuthenticationProvider")
36
- session = {
37
- "authentication_provider": identity.get("cognitoAuthenticationProvider"),
38
- "authentication_type": identity.get("cognitoAuthenticationType"),
39
- "cognito_user": identity.get("user"),
40
- "is_desktop": headers.get("CloudFront-Is-Desktop-Viewer") == "true",
41
- "is_mobile": headers.get("CloudFront-Is-Mobile-Viewer") == "true",
42
- "is_smart_tv": headers.get("CloudFront-Is-SmartTV-Viewer") == "true",
43
- "is_tablet": headers.get("CloudFront-Is-Tablet-Viewer") == "true",
44
- "origin": headers.get("origin"),
45
- "path": aws_event.get("path"),
46
- "referer": headers.get("Referer"),
47
- "source_ip": identity.get("sourceIp"),
48
- "user_agent": identity.get("userAgent"),
49
- "sub": auth.split(":")[-1] if auth else None,
50
- }
51
- if session.get("is_mobile"):
52
- session["device_type"] = "mobile"
53
- elif session.get("is_desktop"):
54
- session["device_type"] = "desktop"
55
- elif session.get("is_tablet"):
56
- session["device_type"] = "tablet"
57
- elif session.get("is_smart_tv"):
58
- session["device_type"] = "smart_tv"
59
- else:
60
- session["device_type"] = "unknown"
61
- return session
29
+ super().__init__(
30
+ aws_event,
31
+ aws_context,
32
+ context_factory=context_factory,
33
+ context_class=context_class,
34
+ )
62
35
 
63
36
 
64
37
  def beforeAction(self, tx, context):
@@ -69,16 +42,16 @@ class LambdaHandler(BaseHandler):
69
42
  logger.debug("starting LamdaHandler.beforeAction")
70
43
 
71
44
 
72
- cognito_start = time.perf_counter()
45
+ context.perf.start("get_cognito_user")
73
46
  self.cognito_user = context.get_cognito_user(self.aws_event)
74
- cognito_elapsed = (time.perf_counter() - cognito_start) * 1000
75
- logger.info("Timing: get_cognito_user %.2f ms", cognito_elapsed)
47
+ context.perf.log("get_cognito_user")
76
48
  self.current_user = {}
77
49
 
78
50
  logger.debug("DEBUG: !!! cognito_user %s", self.cognito_user)
51
+ session = context.session() or {}
79
52
  try:
80
53
  email_address = self.cognito_user["attributes"]["email"]
81
- self.session["email_address"] = email_address
54
+ session["email_address"] = email_address
82
55
  except Exception:
83
56
  logger.warning("Unable to read email from Cognito user", exc_info=True)
84
57
 
@@ -88,24 +61,23 @@ class LambdaHandler(BaseHandler):
88
61
  "action": context.action(),
89
62
  "context_args": context.args(),
90
63
  "summary": self._summarize_postdata(context.postdata()),
91
- "session_email": self.session.get("email_address"),
64
+ "session_email": session.get("email_address"),
92
65
  },
93
66
  )
94
67
 
95
68
  if not self.user_table:
96
69
  logger.warning(
97
70
  "user_table not configured; skipping DB lookup for %s",
98
- self.session.get("email_address"),
71
+ session.get("email_address"),
99
72
  )
100
73
  raise Exception(
101
74
  "User table not configured; cannot validate user [Config]"
102
75
  )
103
- lookup_start = time.perf_counter()
76
+ context.perf.start("user lookup")
104
77
  row = tx.table(self.user_table).find(
105
- {"email_address": self.session.get("email_address")}
78
+ {"email_address": session.get("email_address")}
106
79
  )
107
- lookup_elapsed = (time.perf_counter() - lookup_start) * 1000
108
- logger.info("Timing: user lookup %.2f ms", lookup_elapsed)
80
+ context.perf.log("user lookup")
109
81
  if not row:
110
82
  raise Exception(
111
83
  "A valid user with permission is required to access this function [DB]"
@@ -163,32 +135,18 @@ class LambdaHandler(BaseHandler):
163
135
 
164
136
  def serve(self, tx):
165
137
  response = Response()
166
- body = self.aws_event.get("body")
167
- postdata = {}
168
- if isinstance(body, str) and len(body) > 0:
169
- try:
170
- postdata = json.loads(body)
171
- except (json.JSONDecodeError, TypeError):
172
- postdata = {"raw_body": body}
173
- elif isinstance(body, dict):
174
- postdata = body
175
- elif isinstance(body, list) and len(body) > 0:
176
- try:
177
- new = "\n".join(body)
178
- postdata = json.loads(new)
179
- except (json.JSONDecodeError, TypeError):
180
- postdata = {"raw_body": body}
181
-
182
138
  req_params = self.aws_event.get("queryStringParameters") or {}
183
- local_context = self.ContextClass(
184
- aws_event=self.aws_event,
185
- aws_context=self.aws_context,
139
+ local_context = self.create_context(
186
140
  args=req_params,
187
- postdata=postdata,
141
+ postdata={},
188
142
  response=response,
189
- session=self.session,
190
- log=lambda message, function=None: self.log(tx, message, function),
143
+ session=None,
191
144
  )
145
+ local_context.perf.start("parse body")
146
+ postdata = local_context.parse_postdata()
147
+ local_context.update_postdata(postdata)
148
+ local_context.configure_perf(postdata=postdata)
149
+ local_context.perf.log("parse body")
192
150
 
193
151
  # Determine action from postdata or query parameters
194
152
  action = postdata.get("action") or req_params.get("action")
@@ -197,21 +155,29 @@ class LambdaHandler(BaseHandler):
197
155
  actions = self.get_actions_to_execute(action)
198
156
 
199
157
  # Use BaseHandler's execute_actions method
158
+ local_context.perf.start("execute_actions total (serve)")
200
159
  try:
201
160
  self.execute_actions(tx, local_context, actions)
202
161
  except Exception as e:
203
162
  self.handle_error(tx, local_context, e)
204
-
205
- return local_context.response().render()
163
+ local_context.perf.log("execute_actions total (serve)")
164
+
165
+ local_context.perf.start("response.render")
166
+ rendered = local_context.response().render()
167
+ local_context.perf.log("response.render")
168
+
169
+ return rendered
206
170
 
207
171
  def track(self, tx, data=None, user=None, context_obj=None):
172
+ context_obj = context_obj or self.context
173
+ session = context_obj.session() if context_obj else {}
208
174
  sanitized_payload = context.sanitize_tracking_payload(data or {})
209
175
  sanitized_payload.update(
210
176
  {
211
- "source_ip": self.session.get("source_ip"),
212
- "referer": self.session.get("referer"),
213
- "user_agent": self.session.get("user_agent"),
214
- "device_type": self.session.get("device_type"),
177
+ "source_ip": session.get("source_ip"),
178
+ "referer": session.get("referer"),
179
+ "user_agent": session.get("user_agent"),
180
+ "device_type": session.get("device_type"),
215
181
  }
216
182
  )
217
183
 
@@ -220,7 +186,7 @@ class LambdaHandler(BaseHandler):
220
186
  if email:
221
187
  sanitized_payload["sys_modified_by"] = email
222
188
  elif not sanitized_payload.get("sys_modified_by"):
223
- sanitized_payload["sys_modified_by"] = self.session.get("email_address") or "system"
189
+ sanitized_payload["sys_modified_by"] = session.get("email_address") or "system"
224
190
 
225
191
  if not email:
226
192
  raise Exception(f"Tracking email could not be resolved for tracking.")
@@ -233,19 +199,20 @@ class LambdaHandler(BaseHandler):
233
199
  self.log(tx, f"Failed to write tracking record: {exc}", "track")
234
200
 
235
201
  def validate(self, user_required=True):
202
+ session = self.context.session() if self.context else {}
236
203
  if user_required:
237
204
  try:
238
205
  attrs = self.cognito_user["attributes"]
239
206
  assert attrs
240
207
  assert attrs["email"]
241
208
  assert attrs["sub"]
242
- assert self.session["sub"]
243
- assert self.session["sub"] == attrs["sub"]
209
+ assert session["sub"]
210
+ assert session["sub"] == attrs["sub"]
244
211
  except:
245
212
  raise Exception("User Authentication Error [Cognito]")
246
213
  else:
247
214
  try:
248
- if not self.session["sub"]:
215
+ if not session["sub"]:
249
216
  # User is not signed in. If user_required
250
217
  # is false, then simply return
251
218
  return
@@ -253,8 +220,8 @@ class LambdaHandler(BaseHandler):
253
220
  assert attrs
254
221
  assert attrs["email"]
255
222
  assert attrs["sub"]
256
- assert self.session["sub"]
257
- assert self.session["sub"] == attrs["sub"]
223
+ assert session["sub"]
224
+ assert session["sub"] == attrs["sub"]
258
225
  except:
259
226
  raise Exception("User Authentication Error [Cognito]")
260
227