velocity-python 0.0.185__tar.gz → 0.0.186__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 (143) hide show
  1. {velocity_python-0.0.185 → velocity_python-0.0.186}/PKG-INFO +1 -1
  2. {velocity_python-0.0.185 → velocity_python-0.0.186}/pyproject.toml +1 -1
  3. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/__init__.py +1 -1
  4. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/mixins/data_service.py +272 -249
  5. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity_python.egg-info/PKG-INFO +1 -1
  6. {velocity_python-0.0.185 → velocity_python-0.0.186}/LICENSE +0 -0
  7. {velocity_python-0.0.185 → velocity_python-0.0.186}/README.md +0 -0
  8. {velocity_python-0.0.185 → velocity_python-0.0.186}/setup.cfg +0 -0
  9. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/__init__.py +0 -0
  10. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/invoices.py +0 -0
  11. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/orders.py +0 -0
  12. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/payments.py +0 -0
  13. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/purchase_orders.py +0 -0
  14. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/tests/__init__.py +0 -0
  15. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/tests/test_email_processing.py +0 -0
  16. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  17. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  18. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/__init__.py +0 -0
  19. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/amplify.py +0 -0
  20. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/__init__.py +0 -0
  21. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/base_handler.py +0 -0
  22. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/context.py +0 -0
  23. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/exceptions.py +0 -0
  24. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  25. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  26. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  27. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/response.py +0 -0
  28. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  29. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/tests/__init__.py +0 -0
  30. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  31. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/aws/tests/test_response.py +0 -0
  32. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/__init__.py +0 -0
  33. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/__init__.py +0 -0
  34. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/column.py +0 -0
  35. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/database.py +0 -0
  36. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/decorators.py +0 -0
  37. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/engine.py +0 -0
  38. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/result.py +0 -0
  39. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/row.py +0 -0
  40. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/sequence.py +0 -0
  41. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/table.py +0 -0
  42. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/core/transaction.py +0 -0
  43. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/exceptions.py +0 -0
  44. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/__init__.py +0 -0
  45. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/base/__init__.py +0 -0
  46. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/base/initializer.py +0 -0
  47. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/base/operators.py +0 -0
  48. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/base/sql.py +0 -0
  49. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/base/types.py +0 -0
  50. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/mysql/__init__.py +0 -0
  51. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/mysql/operators.py +0 -0
  52. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/mysql/reserved.py +0 -0
  53. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/mysql/sql.py +0 -0
  54. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/mysql/types.py +0 -0
  55. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/postgres/__init__.py +0 -0
  56. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/postgres/operators.py +0 -0
  57. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/postgres/reserved.py +0 -0
  58. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/postgres/sql.py +0 -0
  59. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/postgres/types.py +0 -0
  60. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  61. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlite/operators.py +0 -0
  62. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  63. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlite/sql.py +0 -0
  64. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlite/types.py +0 -0
  65. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  66. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  67. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  68. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  69. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/sqlserver/types.py +0 -0
  70. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/servers/tablehelper.py +0 -0
  71. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/__init__.py +0 -0
  72. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/common_db_test.py +0 -0
  73. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/__init__.py +0 -0
  74. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/common.py +0 -0
  75. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_column.py +0 -0
  76. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  77. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_database.py +0 -0
  78. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  79. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  80. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  81. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_result.py +0 -0
  82. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_row.py +0 -0
  83. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  84. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  85. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  86. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  87. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  88. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_table.py +0 -0
  89. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  90. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  91. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/sql/__init__.py +0 -0
  92. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/sql/common.py +0 -0
  93. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  94. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  95. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  96. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_db_utils.py +0 -0
  97. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_postgres.py +0 -0
  98. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  99. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  100. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_result_caching.py +0 -0
  101. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  102. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  103. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  104. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  105. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_sql_builder.py +0 -0
  106. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/tests/test_tablehelper.py +0 -0
  107. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/db/utils.py +0 -0
  108. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/logging.py +0 -0
  109. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/__init__.py +0 -0
  110. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/conv/__init__.py +0 -0
  111. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/conv/iconv.py +0 -0
  112. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/conv/oconv.py +0 -0
  113. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/db.py +0 -0
  114. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/export.py +0 -0
  115. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/format.py +0 -0
  116. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/mail.py +0 -0
  117. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/merge.py +0 -0
  118. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/__init__.py +0 -0
  119. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_db.py +0 -0
  120. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_fix.py +0 -0
  121. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_format.py +0 -0
  122. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_iconv.py +0 -0
  123. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_merge.py +0 -0
  124. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_oconv.py +0 -0
  125. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_original_error.py +0 -0
  126. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tests/test_timer.py +0 -0
  127. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/timer.py +0 -0
  128. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/misc/tools.py +0 -0
  129. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/payment/__init__.py +0 -0
  130. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/payment/base_adapter.py +0 -0
  131. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/payment/braintree_adapter.py +0 -0
  132. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/payment/router.py +0 -0
  133. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity/payment/stripe_adapter.py +0 -0
  134. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity_python.egg-info/SOURCES.txt +0 -0
  135. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  136. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity_python.egg-info/requires.txt +0 -0
  137. {velocity_python-0.0.185 → velocity_python-0.0.186}/src/velocity_python.egg-info/top_level.txt +0 -0
  138. {velocity_python-0.0.185 → velocity_python-0.0.186}/tests/test_decorators.py +0 -0
  139. {velocity_python-0.0.185 → velocity_python-0.0.186}/tests/test_lambda_handler.py +0 -0
  140. {velocity_python-0.0.185 → velocity_python-0.0.186}/tests/test_mixins_import.py +0 -0
  141. {velocity_python-0.0.185 → velocity_python-0.0.186}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  142. {velocity_python-0.0.185 → velocity_python-0.0.186}/tests/test_table_alter.py +0 -0
  143. {velocity_python-0.0.185 → velocity_python-0.0.186}/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.185
3
+ Version: 0.0.186
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.185"
7
+ version = "0.0.186"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.185"
1
+ __version__ = version = "0.0.186"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -58,319 +58,322 @@ class DataServiceMixin:
58
58
  else self._pg_types.get(column_info["type_name"], "string")
59
59
  )
60
60
 
61
- # ========== Hook Methods (Override These) ==========
62
-
63
- def read_hook(self, tx, table_name, sys_id, row, context):
61
+ def _call_rwx_hook(self, hook_name, table, *args, **kwargs):
64
62
  """
65
- Called after reading an object. Override to add custom logic.
63
+ Call a table-specific RWX hook if it exists.
66
64
 
67
- Args:
68
- tx: Database transaction
69
- table_name: Name of the table
70
- sys_id: Record ID
71
- row: Dictionary of record data
72
- context: Request context
73
- """
74
- pass
75
-
76
- def write_hook(self, tx, table_name, sys_id, incoming, context):
77
- """
78
- Called before writing an object. Override to add validation/transformation.
65
+ This method tries to load a hook from the rwx package and call it.
66
+ If the hook doesn't exist, execution continues silently.
79
67
 
80
68
  Args:
81
- tx: Database transaction
82
- table_name: Name of the table
83
- sys_id: Record ID (or "@new" for new records)
84
- incoming: Dictionary of data to write
85
- context: Request context
69
+ hook_name: Name of the hook (e.g., 'before_write', 'after_read')
70
+ table: Table name (used to load table-specific module)
71
+ *args: Arguments to pass to the hook
72
+ **kwargs: Keyword arguments to pass to the hook
86
73
  """
87
- pass
74
+ try:
75
+ m = importlib.import_module(f".{table}", "rwx")
76
+ if hasattr(m, hook_name):
77
+ getattr(m, hook_name)(*args, **kwargs)
78
+ except ImportError:
79
+ # rwx package not available, continue without hooks
80
+ pass
88
81
 
89
- def query_hook(self, tx, table_name, params, data, context):
90
- """
91
- Called after querying objects. Override to transform results.
92
-
93
- Args:
94
- tx: Database transaction
95
- table_name: Name of the table
96
- params: Query parameters
97
- data: Query results
98
- context: Request context
99
- """
100
- pass
82
+ # ========== Hook Methods (Override These) ==========
101
83
 
102
- def delete_hook(self, tx, table_name, sys_id, context):
103
- """
104
- Called before deleting an object. Override to add authorization.
105
-
106
- Args:
107
- tx: Database transaction
108
- table_name: Name of the table
109
- sys_id: Record ID to delete
110
- context: Request context
111
- """
112
- pass
84
+ def read_hook(self, tx, table, sys_id, context):
85
+ row = {}
86
+ if not sys_id:
87
+ raise Exception("An object id was not provided for read operation")
88
+ if sys_id == "@new":
89
+ row = {}
90
+ self._call_rwx_hook("on_new", "common", tx, "common", row, context)
91
+ self._call_rwx_hook("on_new", table, tx, table, row, context)
92
+ return row
93
+ sys_id = int(sys_id)
94
+ self._call_rwx_hook("before_read", "common", tx, "common", sys_id, context)
95
+ self._call_rwx_hook("before_read", table, tx, table, sys_id, context)
96
+ row = tx.table(table).find(sys_id)
97
+ row = row.to_dict() if row else {}
98
+ self._call_rwx_hook("after_read", "common", tx, "common", sys_id, row, context)
99
+ self._call_rwx_hook("after_read", table, tx, table, sys_id, row, context)
100
+ return row
101
+
102
+ def find_hook(self, tx, table, where, context):
103
+ row = {}
104
+ if not where:
105
+ raise Exception("An query predicate was not provided for this read operation")
106
+ self._call_rwx_hook("before_find", "common", tx, "common", where, context)
107
+ self._call_rwx_hook("before_find", table, tx, table, where, context)
108
+ row = tx.table(table).find(where)
109
+ self._call_rwx_hook("after_find", "common", tx, "common", where, row, context)
110
+ self._call_rwx_hook("after_find", table, tx, table, where, row, context)
111
+ if row:
112
+ row = row.to_dict()
113
+ return row
114
+
115
+ def write_hook(self, tx, table, sys_id, incoming, context):
116
+ row = {}
117
+ incoming.pop("sys_id", None)
118
+ if sys_id == "@new":
119
+ self._call_rwx_hook("before_new", "common", tx, "common", sys_id, incoming, context)
120
+ self._call_rwx_hook("before_new", table, tx, table, sys_id, incoming, context)
121
+ row = tx.table(table).new()
122
+ sys_id = row["sys_id"]
123
+ self._call_rwx_hook("after_new", "common", tx, "common", sys_id, row, context)
124
+ self._call_rwx_hook("after_new", table, tx, table, sys_id, row, context)
125
+ elif sys_id:
126
+ sys_id = int(sys_id)
127
+ else:
128
+ raise Exception("Object sys_id was not supplied on write operation.")
129
+ self._call_rwx_hook("before_write", "common", tx, "common", sys_id, incoming, context)
130
+ self._call_rwx_hook("before_write", table, tx, table, sys_id, incoming, context)
131
+ if not row:
132
+ row = tx.table(table).get(sys_id)
133
+ row.update(incoming)
134
+ self._call_rwx_hook("after_write", "common", tx, "common", sys_id, row, context)
135
+ self._call_rwx_hook("after_write", table, tx, table, sys_id, row, context)
136
+
137
+ return row.to_dict()
138
+
139
+ def query_hook(self, tx, table, payload, context):
140
+ self._call_rwx_hook("before_query", "common", tx, "common", payload, context)
141
+ self._call_rwx_hook("before_query", table, tx, table, payload, context)
142
+ params = payload.get("params", {})
143
+ result = tx.table(payload["obj"]).select(**params)
144
+ if payload.get("result_format") == "excel":
145
+ data = {
146
+ "headers": payload.get(
147
+ "headers", [x.replace("_", " ").title() for x in result.headers]
148
+ ),
149
+ "rows": result.as_list().all(),
150
+ }
151
+ else:
152
+ data = {
153
+ "rows": result.all(),
154
+ "config": {
155
+ "lastFetch": datetime.datetime.now(),
156
+ "query": result.sql,
157
+ "format": payload.get("result_format"),
158
+ },
159
+ }
160
+ if payload.get("count"):
161
+ data["count"] = tx.table(payload["obj"]).count(where=params.get("where", None))
162
+ if payload.get("headers"):
163
+ data["columns"] = [
164
+ {
165
+ "field": x["name"],
166
+ "headerName": x["name"].replace("_", " ").title(),
167
+ "type": self._get_field_type(x),
168
+ }
169
+ for x in result.columns.values()
170
+ ]
171
+ self._call_rwx_hook("after_query", "common", tx, "common", data, payload, context)
172
+ self._call_rwx_hook("after_query", table, tx, table, data, payload, context)
173
+ return data
174
+
175
+ def delete_hook(self, tx, table, sys_id, context):
176
+ if sys_id:
177
+ sys_id = int(sys_id)
178
+ self._call_rwx_hook("before_delete", "common", tx, "common", sys_id, context)
179
+ self._call_rwx_hook("before_delete", table, tx, table, sys_id, context)
180
+ row = tx.table(table).find(sys_id)
181
+ if row:
182
+ row.clear()
183
+ self._call_rwx_hook("after_delete", "common", tx, "common", sys_id, context)
184
+ self._call_rwx_hook("after_delete", table, tx, table, sys_id, context)
185
+
113
186
 
114
187
  # ========== CRUD Action Methods ==========
115
188
 
116
189
  def OnActionReadObject(self, tx, context):
117
- """
118
- Read a single object by sys_id.
119
-
120
- Payload:
121
- tableName: str - Name of the database table
122
- object: dict - Object containing sys_id field
123
- """
124
190
  payload = context.payload()
125
-
191
+
126
192
  # Validate required parameters
127
193
  if "tableName" not in payload:
128
194
  raise ValueError("Missing required parameter 'tableName' in payload")
129
195
  if "object" not in payload:
130
196
  raise ValueError("Missing required parameter 'object' in payload")
131
-
197
+
132
198
  table_name = payload["tableName"]
133
199
  obj = payload["object"]
134
-
200
+
135
201
  if not table_name:
136
202
  raise ValueError("Parameter 'tableName' cannot be empty")
137
203
  if not obj:
138
204
  raise ValueError("Parameter 'object' cannot be empty")
205
+
206
+ row = self.read_hook(
207
+ tx,
208
+ table_name,
209
+ obj.get("sys_id"),
210
+ context,
211
+ )
139
212
 
140
- sys_id = obj.get("sys_id")
141
-
142
- if not sys_id:
143
- raise ValueError("Object must contain 'sys_id' field")
144
-
145
- if sys_id == "@new":
146
- row = {}
147
- else:
148
- sys_id = int(sys_id)
149
- row = tx.table(table_name).find(sys_id)
150
- row = row.to_dict() if row else {}
151
-
152
- # Call hook for custom logic
153
- self.read_hook(tx, table_name, sys_id, row, context)
154
-
155
- context.response().set_body({
156
- "object": row,
157
- "lastFetch": datetime.datetime.now(),
158
- })
159
-
213
+ context.response().set_body(
214
+ {
215
+ "object": row,
216
+ "lastFetch": datetime.datetime.now(),
217
+ }
218
+ )
160
219
  if row:
161
220
  context.response().load_object(row)
162
221
  else:
163
222
  message = f"Object {obj.get('sys_id')} was not found in the database. You may create it as a new object."
164
223
  context.response().toast(message, "warning")
165
-
224
+
166
225
  def OnActionFindObject(self, tx, context):
167
- """
168
- Find a single object by query predicate.
169
-
170
- Payload:
171
- tableName: str - Name of the database table
172
- query: dict - Query containing 'where' clause
173
- """
174
226
  payload = context.payload()
175
-
227
+
176
228
  # Validate required parameters
177
229
  if "tableName" not in payload:
178
230
  raise ValueError("Missing required parameter 'tableName' in payload")
179
231
  if "query" not in payload:
180
232
  raise ValueError("Missing required parameter 'query' in payload")
181
-
233
+
182
234
  table_name = payload["tableName"]
183
235
  query = payload["query"]
184
-
236
+
185
237
  if not table_name:
186
238
  raise ValueError("Parameter 'tableName' cannot be empty")
187
239
  if not query or "where" not in query:
188
240
  raise ValueError("Parameter 'query' must contain 'where' clause")
189
-
190
- row = tx.table(table_name).find(query["where"])
191
-
192
- if row:
193
- row = row.to_dict()
194
- self.read_hook(tx, table_name, None, row, context)
195
- else:
196
- row = {}
197
-
198
- context.response().set_body({
199
- "object": row,
200
- "lastFetch": datetime.datetime.now(),
201
- })
241
+
242
+ row = self.find_hook(
243
+ tx,
244
+ table_name,
245
+ query["where"],
246
+ context,
247
+ )
248
+ context.response().set_body(
249
+ {
250
+ "object": row,
251
+ "lastFetch": datetime.datetime.now(),
252
+ }
253
+ )
202
254
  context.response().load_object(row)
203
-
255
+
204
256
  def OnActionWriteObject(self, tx, context):
205
- """
206
- Write (insert or update) an object.
207
-
208
- Payload:
209
- tableName: str - Name of the database table
210
- object: dict - Object data to write
211
- """
212
257
  payload = context.payload()
213
-
258
+
214
259
  # Validate required parameters
215
260
  if "tableName" not in payload:
216
261
  raise ValueError("Missing required parameter 'tableName' in payload")
217
262
  if "object" not in payload:
218
263
  raise ValueError("Missing required parameter 'object' in payload")
219
-
264
+
220
265
  table_name = payload["tableName"]
221
266
  obj = payload["object"]
222
-
267
+
223
268
  if not table_name:
224
269
  raise ValueError("Parameter 'tableName' cannot be empty")
225
270
  if not obj or not isinstance(obj, dict):
226
271
  raise ValueError("Parameter 'object' must be a non-empty dictionary")
227
-
272
+
228
273
  # Ensure the object has at least some data
229
274
  incoming = obj.copy()
230
275
  if not any(value is not None for value in incoming.values()):
231
276
  raise ValueError("Parameter 'object' cannot contain only None values")
232
-
233
- sys_id = incoming.pop("sys_id", None)
234
-
235
- # Call hook before write
236
- self.write_hook(tx, table_name, sys_id, incoming, context)
237
-
277
+
278
+ print(
279
+ f"WRITE_LOG: Writing to table {table_name} with sys_id {incoming.get('sys_id')}"
280
+ )
281
+ print(f"WRITE_LOG: Object keys: {list(incoming.keys())}")
282
+
238
283
  try:
239
- if sys_id == "@new":
240
- row = tx.table(table_name).new()
241
- row.update(incoming)
242
- elif sys_id:
243
- sys_id = int(sys_id)
244
- row = tx.table(table_name).get(sys_id)
245
- row.update(incoming)
246
- else:
247
- raise ValueError("Object sys_id was not supplied on write operation")
248
-
249
- row_dict = row.to_dict()
250
-
251
- context.response().set_body({
252
- "object": row_dict,
253
- "lastFetch": datetime.datetime.now(),
254
- })
255
- context.response().load_object(row_dict)
256
-
284
+ row = self.write_hook(
285
+ tx,
286
+ table_name,
287
+ incoming.get("sys_id"),
288
+ incoming,
289
+ context,
290
+ )
291
+
292
+ if not row:
293
+ print(f"WARNING: self.write_hook returned empty row for table {table_name}")
294
+ row = {}
295
+
296
+ context.response().set_body(
297
+ {
298
+ "object": row,
299
+ "lastFetch": datetime.datetime.now(),
300
+ }
301
+ )
302
+ print(f"WRITE_LOG: Successfully wrote to {table_name}")
303
+
257
304
  except Exception as e:
258
- raise Exception(f"Failed to write object to {table_name}: {str(e)}")
259
-
305
+ print(f"ERROR in OnActionWriteObject: {e}")
306
+ print(f"table_name={table_name}, incoming_keys={list(incoming.keys())}")
307
+ raise
308
+ context.response().load_object(row)
309
+
310
+ # Query a table for rows. If the requested result format is excel, then return the
311
+ # data as a downloadedable excel file. If the requested result format is raw,
312
+ # then return the data as rows for the application front end to handle as it wishes.
313
+ # Otherwise, return the data as a dataset to be populated into a datatable,
314
+ # and load the data into 'store.repo' as such.
315
+ # @param self
316
+ # @param tx
317
+ # @param args
318
+ # @param postdata
319
+ # @param response
320
+ #
321
+ # Payload parameters
322
+
260
323
  def OnActionQuery(self, tx, context):
261
- """
262
- Query table for multiple rows.
263
-
264
- Payload:
265
- obj: str - Table name to query
266
- params: dict - Query parameters (where, orderby, limit, offset)
267
- result_format: str - 'excel', 'raw', or 'datatable' (default)
268
- datatable: str - Name for datatable in response (defaults to obj)
269
- count: bool - Include total count in response
270
- headers: bool - Include column headers in response
271
- """
272
324
  payload = context.payload()
273
-
325
+
274
326
  # Validate required parameters
275
327
  if "obj" not in payload:
276
328
  raise ValueError("Missing required parameter 'obj' in payload")
277
-
329
+
278
330
  table = payload["obj"]
279
331
  if not table:
280
332
  raise ValueError("Parameter 'obj' cannot be empty")
281
-
282
- params = payload.get("params", {})
283
- result = tx.table(table).select(**params)
284
-
333
+
334
+ data = self.query_hook(tx, table, payload, context)
285
335
  if payload.get("result_format") == "excel":
286
- headers = payload.get(
287
- "headers", [x.replace("_", " ").title() for x in result.headers]
288
- )
289
- rows = result.as_list().all()
290
-
291
336
  filebuffer = BytesIO()
292
- export.create_spreadsheet(headers, rows, filebuffer)
293
- context.response().file_download({
294
- "filename": payload.get("filename", "temp_file.xls"),
295
- "data": base64.b64encode(filebuffer.getvalue()).decode(),
296
- })
297
- return
298
-
299
- data = {
300
- "rows": result.all(),
301
- "config": {
302
- "lastFetch": datetime.datetime.now(),
303
- "query": result.sql,
304
- "format": payload.get("result_format"),
305
- },
306
- }
307
-
308
- if payload.get("count"):
309
- data["count"] = tx.table(table).count(where=params.get("where", None))
310
-
311
- if payload.get("headers"):
312
- data["columns"] = [
337
+ export.create_spreadsheet(data["headers"], data["rows"], filebuffer)
338
+ context.response().file_download(
313
339
  {
314
- "field": x["name"],
315
- "headerName": x["name"].replace("_", " ").title(),
316
- "type": self._get_field_type(x),
340
+ "filename": payload.get("filename", "temp_file.xls"),
341
+ "data": base64.b64encode(filebuffer.getvalue()).decode(),
317
342
  }
318
- for x in result.columns.values()
319
- ]
320
-
321
- # Call hook after query
322
- self.query_hook(tx, table, params, data, context)
323
-
343
+ )
344
+ return
324
345
  if payload.get("result_format") == "raw":
325
346
  context.response().set_body(data)
326
347
  else:
327
- context.response().set_table({
328
- payload.get("datatable", payload.get("obj")): data
329
- })
330
-
348
+ context.response().set_table(
349
+ {payload.get("datatable", payload.get("obj")): data}
350
+ )
351
+
331
352
  def OnActionDeleteObject(self, tx, context):
332
- """
333
- Delete one or more objects.
334
-
335
- Payload:
336
- tableName: str - Name of the database table
337
- deleteList: list - List of sys_id values to delete (optional)
338
- object: dict - Single object with sys_id to delete (optional)
339
- """
340
353
  payload = context.payload()
341
-
354
+
342
355
  # Validate required parameters
343
356
  if "tableName" not in payload:
344
357
  raise ValueError("Missing required parameter 'tableName' in payload")
345
-
358
+
346
359
  table_name = payload["tableName"]
347
360
  if not table_name:
348
361
  raise ValueError("Parameter 'tableName' cannot be empty")
349
-
362
+
350
363
  table = tx.table(table_name)
351
364
  deleteList = []
352
-
353
365
  if "deleteList" in payload:
354
366
  deleteList.extend(payload.get("deleteList"))
355
-
356
367
  if "object" in payload:
357
368
  obj = payload["object"]
358
369
  if obj and obj.get("sys_id"):
359
370
  deleteList.append(obj.get("sys_id"))
360
371
 
361
372
  for sys_id in deleteList:
362
- # Call hook before delete
363
373
  self.delete_hook(tx, table_name, sys_id, context)
364
374
 
365
- obj = table.find(int(sys_id))
366
- if obj:
367
- obj.clear()
368
- context.response().toast(f"Object {sys_id} deleted", "success")
369
- else:
370
- context.response().toast(f"Object {sys_id} not found", "warning")
371
-
372
- if not deleteList:
373
- context.response().toast("No items were selected.", "warning")
375
+ if not (deleteList):
376
+ context.response().toast(f"No items were selected.", "warning")
374
377
 
375
378
  def OnActionGetTables(self, tx, context):
376
379
  """Get list of all tables in the database."""
@@ -534,20 +537,63 @@ class RWXHookSystem:
534
537
  """
535
538
  RWX (Read/Write/eXecute) hook system for table-specific business logic.
536
539
 
537
- This class provides a way to dynamically load and execute table-specific
538
- hooks (before_read, after_write, etc.) from an rwx package.
539
-
540
- Usage:
541
- from velocity.aws.handlers.mixins import RWXHookSystem
540
+ This system allows you to create table-specific hook modules in an 'rwx' package
541
+ and have them automatically called during CRUD operations. This is useful for
542
+ adding custom business logic (validation, permissions, data enrichment) for
543
+ specific tables without modifying the generic DataServiceMixin.
544
+
545
+ How it works:
546
+ 1. Create a Python package called 'rwx' in your Lambda handler
547
+ 2. For each table that needs custom logic, create a module (e.g., rwx/admin_users.py)
548
+ 3. Define hook functions in the module (before_read, after_write, before_delete, etc.)
549
+ 4. Subclass RWXHookSystem and set hook_module_name = 'rwx'
550
+ 5. Use it from your handler class
551
+
552
+ Example structure:
553
+ src/
554
+ index.py
555
+ rwx/
556
+ __init__.py
557
+ admin_users.py # hooks for admin_users table
558
+ donors.py # hooks for donors table
559
+
560
+ Example hook in rwx/admin_users.py:
561
+ def before_write(tx, table, sys_id, incoming, context):
562
+ # Validate incoming data
563
+ if not incoming.get('email_address'):
564
+ raise ValueError("Email is required")
565
+ # Modify data
566
+ incoming['description'] = f"{incoming['full_name']} ({incoming['email_address']})"
567
+
568
+ def after_read(tx, table, sys_id, row, context):
569
+ # Enrich response data
570
+ context.response().set_body({'extra_data': 'value'})
571
+
572
+ Hook signatures:
573
+ - on_new(tx, table, row, context) - When creating new empty object
574
+ - before_read(tx, table, sys_id, context) - Before reading an object
575
+ - after_read(tx, table, sys_id, row, context) - After reading an object
576
+ - before_find(tx, table, query, context) - Before finding by query
577
+ - after_find(tx, table, query, row, context) - After finding by query
578
+ - before_new(tx, table, sys_id, incoming, context) - Before creating new object
579
+ - after_new(tx, table, sys_id, row, context) - After creating new object
580
+ - before_write(tx, table, sys_id, incoming, context) - Before write (insert/update)
581
+ - after_write(tx, table, sys_id, row, context) - After write (insert/update)
582
+ - before_delete(tx, table, sys_id, context) - Before deleting an object
583
+ - after_delete(tx, table, sys_id, context) - After deleting an object
584
+ - before_query(tx, table, payload, context) - Before querying objects
585
+ - after_query(tx, table, data, payload, context) - After querying objects
586
+
587
+ Usage in a Lambda handler:
588
+ from velocity.aws.handlers.mixins import DataServiceMixin, RWXHookSystem
542
589
 
543
590
  class MyRWXSystem(RWXHookSystem):
544
- hook_module_name = 'rwx' # Your rwx package
591
+ hook_module_name = 'rwx' # Name of your rwx package
545
592
 
593
+ @engine.transaction
546
594
  class HttpEventHandler(DataServiceMixin, LambdaHandler):
547
- def read_hook(self, tx, table_name, sys_id, row, context):
548
- MyRWXSystem.call_hooks(
549
- 'read', tx, table_name, sys_id, row, context
550
- )
595
+ def __init__(self, aws_event, aws_context):
596
+ super().__init__(aws_event, aws_context)
551
597
  """
552
598
 
553
599
  hook_module_name = None # Override in subclass (e.g., 'rwx')
@@ -569,29 +615,6 @@ class RWXHookSystem:
569
615
  if module and hasattr(module, hook_name):
570
616
  return getattr(module, hook_name)(*args, **kwargs)
571
617
  return None
572
-
573
- @classmethod
574
- def call_hooks(cls, operation, tx, table_name, *args, context=None):
575
- """
576
- Call before/after hooks for an operation.
577
-
578
- Args:
579
- operation: 'read', 'write', 'delete', 'query'
580
- tx: Database transaction
581
- table_name: Name of the table
582
- *args: Operation-specific arguments
583
- context: Request context (keyword only)
584
- """
585
- # Call before hook
586
- cls._call_hook(f'before_{operation}', table_name, tx, table_name, *args, context)
587
-
588
- # Note: after hooks should be called by the application after the operation
589
- # This method is just for the before hook pattern
590
-
591
- @classmethod
592
- def call_after_hook(cls, operation, tx, table_name, *args, context=None):
593
- """Call after hook for an operation"""
594
- cls._call_hook(f'after_{operation}', table_name, tx, table_name, *args, context)
595
618
 
596
619
 
597
620
  def apply_sys_modified_by(incoming, context):